9

问题不在于 Linux 内核。这也不是 C 与 C++ 的辩论。

我做了一项研究,在我看来,C++ 在嵌入式系统的异常处理和内存分配方面缺乏工具支持:

为什么linux内核没有用C++实现? 除了公认的答案,另请参阅Ben Collins 的答案

C++ 上的 Linus Torvalds

“[...] 任何为 C++ 设计内核模块的人都是 [...]
(b) 一个看不到自己在写什么的 C++ 顽固分子,反正就是 C”

“ - 整个 C++ 异常处理的东西从根本上被破坏了。对于内核来说尤其如此
。 - 任何喜欢将内存分配之类的东西隐藏在背后的编译器或语言对于内核来说都不是一个好的选择。”

联合打击战斗机 C++ 编码标准

“不得使用 AV Rule 208 C++ 异常”


  1. 异常处理和内存分配是 C++ 显然缺乏工具支持的唯一点(在这种情况下)吗?

  2. 要解决异常处理问题,是否必须在异常被抛出后直到被捕获的时间提供限制?

  3. 你能解释一下为什么内存分配是个问题吗?如何克服这个问题,必须做什么?

正如我所看到的,在这两种情况下,都必须在编译时为发生的不平凡的事情提供上限,并且取决于运行时的事情。


回答:

  1. 不,动态演员表也是一个问题,但它已经解决了

  2. 基本上的。必须通过分析所有抛出路径来限制处理异常所需的时间。

  3. 请参阅嵌入式系统编程中幻灯片“如何在没有新事物的情况下生活”中的解决方案。简而言之:预分配(全局对象、堆栈、池)。

4

3 回答 3

14

嗯,有几件事。首先,您必须记住,STL 完全建立在 OS 例程、C 标准库和动态分配之上。当您编写内核时,没有动态内存分配给您(您正在提供它),没有 C 标准库(您必须提供一个构建在内核之上的库),并且您正在提供系统调用。还有一个事实是,C 与程序集的互操作非常好且容易,而 C++ 与程序集的交互非常困难,因为 ABI 不一定是常量,名称也不一定。由于名称修饰,您会获得全新的复杂性。

然后,您必须记住,在构建操作系统时,您需要了解和控制内核使用的内存的各个方面。在 C++ 中,有很多隐藏结构是您无法控制的(vtables、RTTI、异常),它们会严重干扰您的工作。

换句话说,Linus 的意思是,使用 C 语言,您可以轻松理解正在生成的程序集,并且非常简单,可以直接在机器上运行。尽管 C++ 可以,但您总是需要设置相当多的上下文,并且仍然需要做一些 C 来接口程序集和 C。另一个原因是在系统编程中,您需要确切地知道方法是如何被调用的。C 具有非常详细的 C 调用约定,但在 C++ 中,您必须this处理名称修改等。

简而言之,这是因为 C++ 无需您请求即可完成工作。

根据@Josh 的评论,C++ 在你背后做的另一件事是构造函数和析构函数。它们增加了进入和退出堆栈帧的开销,最重要的是,使程序集互操作更加困难,因为当您销毁 C++ 堆栈帧时,您必须调用其中每个对象的析构函数。这很快就会变得丑陋。

于 2012-08-18T17:06:47.017 回答
9

为什么某些内核在其代码库中拒绝 C++ 代码?政治和偏好,但我离题了。

现代操作系统内核的某些部分是用 C++ 的某些子集编写的。在这些子集中,主要是异常和 RTTI 被禁用(有时也不允许多重继承和模板)。

在 C 语言中也是如此。某些特性不应该在内核环境中使用(例如 VLA)。

当我们谈论内核代码(或嵌入式代码)时,除了异常和 RTTI 之外,C++ 中的某些特性受到了严厉的批评。这些是 vtable 和构造函数/析构函数。他们在引擎盖下带来了一些代码,这似乎被认为是“坏的”。如果你不想要一个构造函数,那么就不要实现一个。如果您担心使用带有构造函数的类,那么也要担心必须用来初始化结构的函数。C++ 的好处是,除了忘记释放内存之外,您真的不能忘记使用 dtor。

但是 vtables 呢?

当您实现一个包含扩展点的对象时(例如,一个 linux 文件系统驱动程序),您实现的就是一个带有虚拟方法的类。那么,为什么有一个 vtable 这么糟糕呢?当您对 vtable 所在的页面有特定要求时,您必须控制此 vtable 的放置。据我回忆,这与linux无关,但在windows下,代码页可以被分页,当你从一个太高的irql调用一个分页函数时,你会崩溃。但是你真的必须注意你调用了什么函数,当你处于高 irql 时,不管它是什么函数。如果您在这种情况下不使用虚拟呼叫,您也不必担心。在嵌入式软件中,这可能会更糟,因为(很少)您需要直接控制代码所在的代码页,

那么为什么这么多人如此坚持“在内核中使用 C”呢?

因为他们要么被工具链问题烧毁,要么被过度热情的开发人员在内核模式下使用最新的东西烧毁。

可能内核模式的开发者比较保守,C++是个太新奇的东西……


为什么内核模式代码中不使用异常?

因为他们需要为每个函数生成一些代码,在代码路径中引入复杂性并且不处理异常对于内核模式组件是不利的,因为它会杀死系统。

在 C++ 中,当抛出异常时,堆栈必须展开,并且必须调用相应的析构函数。这至少涉及一些开销。这几乎可以忽略不计,但确实会产生成本,这可能不是您想要的。(请注意,我不知道展开表的实际成本是多少,我想我读到没有异常运行时没有成本,但是......我想我必须查一下)。

不能抛出异常的代码路径可以更容易推理,然后可能。所以 :

int f( int a )
{
   if( a == 0 )
      return -1;

   if( g() < 0 )
      return -2;
   f3(); 

   return h();
}

在这个函数中,我们可以推断每条退出路径,因为我们可以很容易地看到所有返回,但是当启用异常时,函数可能会抛出,我们不能保证函数采用的实际路径是什么。这是代码的确切点可能会做一些我们无法立即看到的事情。(当启用异常时,这是糟糕的 C++ 代码)。

第三点是,您希望用户模式应用程序崩溃,当发生意外情况时(例如内存耗尽),用户模式应用程序应该崩溃(释放资源后)以允许开发人员调试问题或至少获得一个好的错误信息。您永远不应该在内核模式模块中有未捕获的异常。

请注意,这一切都可以克服,Windows 内核中存在 SEH 异常,因此第 2+3 点在 NT 内核中并不是真正的好点。


内核中的 C++ 没有内存管理问题。例如,NT 内核头文件为 new 和 delete 提供重载,它允许您指定分配的池类型,但在其他方面与用户模式应用程序中的 new 和 delete 完全相同。

于 2012-08-18T20:14:00.777 回答
3

我真的不喜欢语言战争,并且投票决定再次关闭它。但不管怎么说...

嗯,有几件事。首先,您必须记住,STL 完全建立在 OS 例程、C 标准库和动态分配之上。当您编写内核时,没有动态内存分配给您(您正在提供它),没有 C 标准库(您必须提供一个构建在内核之上的库),并且您正在提供系统调用。还有一个事实,即 C 与程序集的互操作非常好且容易,而 C++ 与程序集的交互非常困难,因为 ABI 不一定是恒定的,名称也不一定是恒定的。由于名称修饰,您会获得全新的复杂性。

不,使用 C++,您可以声明具有extern "C"(或可选extern "assembly")调用约定的函数。这使得名称与同一平台上的其他所有内容兼容。

然后,您必须记住,在构建操作系统时,您需要了解和控制内核使用的内存的各个方面。在 C++ 中,有很多隐藏结构是您无法控制的(vtables、RTTI、异常),它们会严重干扰您的工作。

编写内核功能时必须小心,但这不仅限于 C++。当然,您不能将std::vector<byte>其用作内存分配的基础,但也不能malloc用于此目的。您不必为所有 C++ 类使用虚函数、多重继承和动态分配,对吗

换句话说,Linus 的意思是,使用 C 语言,您可以轻松理解正在生成的程序集,并且非常简单,可以直接在机器上运行。尽管 C++ 可以,但您总是需要设置相当多的上下文,并且仍然需要做一些 C 来接口程序集和 C。另一个原因是在系统编程中,您需要确切地知道方法是如何被调用的。C 有非常详细的 C 调用约定,但是在 C++ 中,你需要处理这个,名称修改等。

Linus 可能声称他可以发现每个调用f(x)并立即看到它调用g(x),h(x)q(x)20 层深度。仍然MyClass M(x);是一个很大的谜,因为它可能在他背后调用一些未知的代码。把我弄丢了。

简而言之,这是因为 C++ 无需您请求即可完成工作。

如何?如果我为一个类编写构造函数和析构函数,那是因为我要求执行代码。不要告诉我C可以在执行一些代码的情况下神奇地复制一个对象!

根据@Josh 的评论,C++ 在你背后做的另一件事是构造函数和析构函数。它们增加了进入和退出堆栈帧的开销,最重要的是,使程序集互操作更加困难,因为当您销毁 C++ 堆栈帧时,您必须调用其中每个对象的析构函数。这很快就会变得丑陋。

构造函数和析构函数不会在你背后添加代码,它们仅在需要时才会出现。只有在需要时才调用析构函数,例如在需要释放动态内存时。不要告诉我没有这个 C 代码可以工作。


Linux 和 Windows 都缺乏对 C++ 的支持的一个原因是,很多从事内核工作的人早在 C++ 可用之前就已经在这样做了。我看到来自 Windows 内核开发人员的帖子认为实际上并不需要 C++ 支持,因为用 C++ 编写的设备驱动程序非常少。第 22 条军规!


异常处理和内存分配是 C++ 显然缺乏工具支持的唯一点(在这种情况下)吗?

在没有妥善处理的地方,不要使用它。您不必到处使用多重继承、动态分配和抛出异常。如果返回错误代码有效,那很好。去做!

要解决异常处理问题,是否必须在异常被抛出后直到被捕获的时间提供限制?

不,但是您不能在内核中使用应用程序级别的功能。使用 a 实现动态内存std::vector<byte>不是一个好主意,但谁会真正尝试呢?

你能解释一下为什么内存分配是个问题吗?如何克服这个问题,必须做什么?

在实现内存管理的函数下面的层上使用取决于内存分配的标准库功能 将是一个问题。malloc使用调用来 实现malloc同样愚蠢。但谁会尝试呢?

于 2012-08-18T19:57:10.243 回答