在 Linux 内核代码中,我发现了以下我无法理解的内容。
struct bts_action {
u16 type;
u16 size;
u8 data[0];
} __attribute__ ((packed));
代码在这里: http: //lxr.free-electrons.com/source/include/linux/ti_wilink_st.h
零元素数据数组的需求和目的是什么?
在 Linux 内核代码中,我发现了以下我无法理解的内容。
struct bts_action {
u16 type;
u16 size;
u8 data[0];
} __attribute__ ((packed));
代码在这里: http: //lxr.free-electrons.com/source/include/linux/ti_wilink_st.h
零元素数据数组的需求和目的是什么?
这是一种具有可变数据大小的方法,而无需调用malloc
(kmalloc
在这种情况下)两次。你会像这样使用它:
struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);
这曾经不是标准的,被认为是一种 hack(正如 Aniket 所说),但它在 C99 中被标准化。现在它的标准格式是:
struct bts_action {
u16 type;
u16 size;
u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */
请注意,您没有提及该data
字段的任何大小。另请注意,此特殊变量只能出现在结构的末尾。
在 C99 中,这件事在 6.7.2.1.16 (强调我的)中进行了解释:
作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为灵活数组成员. 在大多数情况下,灵活数组成员被忽略。特别是,结构的大小就像省略了柔性数组成员一样,只是它可能具有比省略所暗示的更多的尾随填充。然而,当一个 . (或->)运算符的左操作数是(指向)具有灵活数组成员的结构,右操作数命名该成员,它的行为就像该成员被替换为最长的数组(具有相同的元素类型) 不会使结构大于被访问的对象;数组的偏移量应保持灵活数组成员的偏移量,即使这与替换数组的偏移量不同。如果这个数组没有元素,
或者换句话说,如果你有:
struct something
{
/* other variables */
char data[];
}
struct something *var = malloc(sizeof(*var) + extra);
您可以使用var->data
中的索引进行访问[0, extra)
。请注意,这sizeof(struct something)
只会给出其他变量data
的大小,即大小为 0。
值得注意的是,标准实际上是如何给出malloc
这样一个构造的示例(6.7.2.1.17):
struct s { int n; double d[]; };
int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));
标准在同一位置的另一个有趣的注释是(强调我的):
假设对 malloc 的调用成功,p 指向的对象的行为,在大多数情况下,就好像 p 已被声明为:
struct { int n; double d[m]; } *p;
(在某些情况下,这种等价性会被破坏;特别是成员 d 的偏移量可能不一样)。
实际上,对于GCC ( C90 ),这实际上是一个 hack 。
它也被称为struct hack。
所以下一次,我会说:
struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);
这相当于说:
struct bts_action{
u16 type;
u16 size;
u8 data[100];
};
我可以创建任意数量的此类结构对象。
这个想法是允许在结构的末尾有一个可变大小的数组。据推测,bts_action
是一些具有固定大小标头(type
和size
字段)和可变大小data
成员的数据包。通过将其声明为长度为 0 的数组,它可以像任何其他数组一样被索引。然后,您将分配一个bts_action
1024 字节data
大小的结构,如下所示:
size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);
该代码是无效的 C(见此)。由于显而易见的原因,Linux 内核根本不关心可移植性,因此它使用了大量非标准代码。
他们正在做的是一个数组大小为 0 的 GCC 非标准扩展。一个符合标准的程序会写u8 data[];
出来,这意味着同样的事情。Linux 内核的作者显然喜欢让事情变得不必要的复杂和非标准,如果这样做的选项会显露出来的话。
在较早的 C 标准中,以空数组结束结构被称为“结构黑客”。其他人已经在其他答案中解释了它的目的。在 C90 标准中,struct hack 是未定义的行为,可能导致崩溃,主要是因为 C 编译器可以在结构末尾自由添加任意数量的填充字节。这样的填充字节可能会与您尝试在结构末尾“破解”的数据发生冲突。
GCC 很早就做了一个非标准的扩展,将其从未定义行为更改为明确定义的行为。C99 标准随后采用了这个概念,因此任何现代 C 程序都可以毫无风险地使用这个特性。在 C99/C11 中称为灵活数组成员。
零长度数组的另一种用法是作为结构内的命名标签,以帮助编译时结构偏移检查。
假设您有一些大型结构定义(跨越多个高速缓存行),您希望确保它们在开始和穿过边界的中间都与高速缓存行边界对齐。
struct example_large_s
{
u32 first; // align to CL
u32 data;
....
u64 *second; // align to second CL after the first one
....
};
在代码中,您可以使用 GCC 扩展来声明它们,例如:
__attribute__((aligned(CACHE_LINE_BYTES)))
但是您仍然希望确保在运行时强制执行此操作。
ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);
这适用于单个结构,但很难涵盖许多结构,每个结构都有不同的成员名称要对齐。您很可能会得到如下代码,您必须在其中找到每个结构的第一个成员的名称:
assert (offsetof (one_struct, <name_of_first_member>) == 0);
assert (offsetof (one_struct, <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);
您可以在结构中声明一个长度为零的数组,作为具有一致名称但不占用任何空间的命名标签,而不是采用这种方式。
#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
CACHE_LINE_ALIGN_MARK (cacheline0);
u32 first; // align to CL
u32 data;
....
CACHE_LINE_ALIGN_MARK (cacheline1);
u64 *second; // align to second CL after the first one
....
};
然后运行时断言代码将更容易维护:
assert (offsetof (one_struct, cacheline0) == 0);
assert (offsetof (one_struct, cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);