Linux 内核:为什么 'subclass' 结构将基础 class 信息放在末尾?

Linux kernel: why do 'subclass' structs put base class info at end?

我正在阅读 Beautiful Code 中关于 Linux 内核的章节,作者讨论了 Linux 内核如何在 C 语言中实现继承(以及其他主题)。简而言之,定义了一个 'base' 结构,为了从它继承,'subclass' 结构将基础 的副本放置在子类结构的末尾 定义。然后作者用了几页解释了一个巧妙而复杂的宏,以计算出要返回多少字节才能从对象的基类部分转换为对象的子类部分。

我的问题:在子类结构中,为什么不将基结构声明为结构中的第一个,而不是最后件事?

将基类结构放在首位的主要优点是,当从基类转换到子类时,您根本不需要移动指针 - 本质上,进行转换只是意味着告诉编译器让您的代码使用子类结构放置在基础定义的内容之后的 'extra' 字段。

为了澄清我的问题,让我抛出一些代码:

struct device { // this is the 'base class' struct
     int a;
     int b;
     //etc
}
struct usb_device { // this is the 'subclass' struct
    int usb_a;
    int usb_b;
    struct device dev; // This is what confuses me - 
                       // why put this here, rather than before usb_a?
}

如果碰巧有一个指向 usb_device 对象内部的 "dev" 字段的指针,那么为了将其转换回那个 usb_device 对象,需要从中减去 8那个指针。但是,如果 "dev" 是 usb_device 中的第一件事,则根本不需要移动指针。

如有任何帮助,我们将不胜感激。即使是关于在哪里可以找到答案的建议,我们也将不胜感激 - 我不确定如何 Google 出于这样的决定背后的架构原因。我在 Whosebug 上能找到的最接近的是: why to use these weird nesting structure

而且,要明确一点——我知道很多聪明人已经在 Linux 内核上工作了很长时间,所以很明显,这样做是有充分理由的,我就是可以'不知道是什么。

我是 Linux 内核代码的新手,所以请对我的胡言乱语持保留态度。据我所知,对于 "subclass" 结构的放置位置没有要求。这正是宏所提供的:您可以强制转换为 "subclass" 结构,而不管其布局如何。这为您的代码提供了健壮性(可以更改结构的布局,而无需更改您的代码。 也许有将 "base class" 结构放在末尾的约定,但我不知道。我在驱动程序中看到很多代码,其中使用不同的 "base class" 结构来转换回相同的 "subclass" 结构(当然来自 "subclass" 中的不同字段)。

我对 Linux 内核没有新的经验,但对其他内核有新的经验。我会说这根本不重要。

你不应该从一个转换到另一个。允许这样的强制转换只应在非常特殊的情况下进行。在大多数情况下,它降低了代码的健壮性和灵活性,被认为是相当草率的。因此,您正在寻找的最深 "architectural reason" 可能只是 "because that's the order someone happened to write it in"。或者,这就是基准测试显示的最适合该代码中某些重要代码路径的性能。或者,编写它的人认为它看起来很漂亮(如果没有其他限制,我总是在我的变量声明和结构中构建倒金字塔)。或者 20 年前有人碰巧这样写,从那以后其他人一直在复制它。

这背后可能有更深层次的设计,但我对此表示怀疑。根本没有理由设计这些东西。如果你想从权威来源找出为什么这样做,只需向 linux 提交一个补丁来改变它,看看谁对你大喊大叫。

Amiga OS 在很多地方使用了这个 "common header" 技巧,这在当时看起来是个好主意:通过简单地转换指针类型来进行子类化。但是也有缺点。

亲:

  • 您可以扩展现有的数据结构
  • 你可以在所有需要基类型的地方使用同一个指针,不需要指针运算,节省宝贵的周期
  • 感觉很自然

缺点:

  • 不同的编译器倾向于以不同的方式对齐数据结构。如果基本结构以 char a; 结尾,那么在子类的下一个字段开始之前,您可以在之后有 0、1 或 3 个填充字节。这导致了非常严重的错误,尤其是当你必须保持向后兼容性时(即出于某种原因,你必须有一定的填充,因为一个古老的编译器版本有一个错误,现在,有很多代码需要错误填充) .
  • 传递错误的结构时,您不会很快注意到。使用您问题中的代码,如果指针算法错误,字段很快就会被丢弃。这是一件好事,因为它增加了更早发现错误的机会。
  • 它会导致一种态度 "my compiler will fix it for me"(有时不会),所有的转换都会导致一种 "I know better than the compiler" 的态度。后者会让你在理解错误信息之前自动插入转换,这会导致各种奇怪的问题。

Linux 内核将公共结构放在别处;可以但不一定要在最后。

亲:

  • 错误会提前显示
  • 你将不得不为每个结构做一些指针运算,所以你已经习惯了
  • 你不需要强制转换

缺点:

  • 不明显
  • 代码更复杂

它用于多重继承。 struct dev 不是您可以应用于 linux 内核中的结构的唯一接口,如果您有多个接口,只需将子 class 转换为基础 class 就不会了不工作。例如:

struct device {
     int a;
     int b;
     // etc...
};

struct asdf {
   int asdf_a;
};

struct usb_device {
    int usb_a;
    int usb_b;
    struct device dev;
    struct asdf asdf;
};