A diagram of a data structure earns its place only if the reader can point at the line it came from. So on this page the source sits in a sunken panel beside each figure — the code is the original, the figure is the reading of it, and the mapping is done by the eye instead of by memory.一张画数据结构的图,只有在读者能指着它对应的那行代码时才值得存在。所以这一页源码就放在每张图旁边的凹陷面板里 —— 代码是原文,图是对原文的解释,而这层对应关系用眼睛就完成了,不用靠记。
The fields you wrote are on the left, what the compiler laid out is on the right — and the padding is drawn as sunken gaps, because that is what it is: nothing. Two holes here cost 15 of 56 bytes. The dashed line is the cache line boundary, which is the reason this matters beyond tidiness.你写的字段在左边,编译器排出来的在右边 —— 而填充画成凹陷的空格,因为它就是这个:什么都没有。这里两个空洞吃掉了 56 字节里的 15 个。那条虚线是 cache line 边界,也是这件事不只是「整不整齐」的原因。
lanes up next to present: the two holes become one 6-byte hole, 72 bytes becomes 64, and the struct lands on exactly one cache line — name stops straddling.把 lanes 挪到 present 旁边:两个空洞并成一个 6 字节的,72 字节变 64,结构体正好落在一条 cache line 上 —— name 不再跨界。struct B b, struct B *b and struct list_head n are one character apart in the source and three different lifetimes in reality. Embedding is drawn as containment, a pointer as a hop out to somewhere else, and a list_head as neither — because the list holds the head, not your struct.struct B b、struct B *b、struct list_head n 在源码里差一个字符,在现实里是三条不同的生命周期。嵌入画成包含,指针画成跳到别处,而 list_head 两者都不是 —— 因为链表串的是那个 head,不是你的结构体。
&a->b is a valid pointer for exactly as long as A is.只有一次分配。 释放 A,B 跟着没了 —— 你永远不 free B。&a->b 这个指针的有效期,和 A 完全一样长。container_of(pos, struct A, n) to get back to A — and why the offset of n is what makes it work.链表串起来的是 list_head,不是 A。 所以才需要 container_of(pos, struct A, n) 才能拿回 A —— 而让它成立的,正是 n 在结构体里的偏移。Four lines of probe, drawn. The kernel hands you a struct device; you allocate your own struct and then park a pointer to it on the kernel’s side so that every later callback can get it back. The chain is short — what makes it worth a figure is that it is walked from memory every single time.probe 里的四行,画出来。内核给你一个 struct device;你分配自己的结构体,然后把指向它的指针寄存在内核那边,让之后每一次回调都能取回来。链很短 —— 值得画成图,是因为它每次都是凭记忆走的。
set_clientdata stores your struct on the kernel’s side, get_clientdata takes it back. Everything else in the chain the kernel already built for you.橙色那两行就是这一来一回:set_clientdata 把你的结构体存到内核那边,get_clientdata 再取回来。链上其余的部分,内核早就替你搭好了。Each row is one property and the field it becomes, with the function that does the conversion named on the row itself — because that name is what you actually go looking for. The last row is sunken on purpose: compatible never becomes a field at all.每一行是一个属性和它变成的那个字段,而且做这次转换的函数就写在这一行上 —— 因为你真正要找的就是那个函数名。最后一行故意是凹的:compatible 根本不会变成任何字段。
compatible is the one property that never becomes a field. It is consumed before your code runs, which is why a typo there produces silence rather than a wrong value.注意最后一行:compatible 是唯一一个永远不会变成字段的属性。它在你的代码跑起来之前就被消费掉了 —— 所以那里写错得到的是一片安静,不是一个错的值。__packed does to them5 · 位域,以及 __packed 对它做了什么Two things the source does not show. First, bitfields are cut out of a storage unit the compiler picks, filling from the least significant bit on a little-endian target — so mode is bits 2:0, not the top of the byte. Second, __packed does not just make the struct smaller: it moves every later field, and both of the ones after the bitfield land off their natural boundary — the u16 at offset 1, the u32 at offset 3. On arm64 that read is fixed up in a trap handler; on some 32-bit cores it is a fault. The figure puts both views under the source: the bit strip for the unit, the byte list for what moved.源码上看不出来的两件事。第一,位域是从编译器挑的一个存储单元里切出来的,小端目标上从最低位开始填 —— 所以 mode 是 bit 2:0,不是那个字节的高位。第二,__packed 不只是把结构体变小:它会把后面每个字段都往前挪,位域后面那两个字段都落在了非自然边界上 —— u16 在偏移 1,u32 在偏移 3。arm64 上这次读会掉进处理非对齐访问的陷阱里,有些 32 位核上直接是异常。这张图把两个视角都摆在源码下面:位带看单元怎么切,字节表看什么挪了位置。
__packedstruct tp_cfg · 加不加 __packedone byte saved, two fields left unaligned — the byte list is generated from pahole, not typed by hand省下一个字节,换来两个字段非对齐 —— 右边那张字节表是 pahole 生成的,不是手敲的__packed — total 7 B加了 __packed —— 共 7 B__packed the compiler inserts one pad byte at offset 1, rate lands at 2 and window at 4, and the struct is 8 bytes. You trade that one byte for an unaligned access on every read of both — and the compiler will not warn, because you asked for it. Every number here comes out of pahole; none of it is typed by hand.0 + 1 + 2 = 3,3 + 4 = 7。不加 __packed 时编译器会在偏移 1 塞一个填充字节,rate 落在 2、window 落在 4,结构体 8 字节。你用那一个字节换来的是此后每次读这两个都非对齐 —— 而且编译器不会警告,因为是你自己要的。这里每个数字都出自 pahole,没有一个是手敲的。An offset list cannot draw a union, because an offset list assumes one field comes after another. Here every member starts at byte 0 and they sit on top of each other. That is the whole point and it is also the whole danger: writing one member and reading another does not convert anything, it re-reads the same bytes under a different type. The bar that is longest sets the size; the tail nobody covers is still allocated.偏移表画不出联合体,因为偏移表的前提是「一个字段接着一个字段」。这里每个成员都从第 0 字节开始,互相压在一起。这既是它的全部用处,也是它的全部风险:写了一个成员再读另一个,不会做任何转换,只是换个类型把同一批字节再读一遍。最长的那根决定大小;没人盖到的尾巴照样占着内存。
point (4 bytes) and the driver reads stamp (8 bytes). Bytes 4–7 were never written, so the driver reads whatever was on the stack — and it reads it successfully, every time, with a different value. Copy the union in full or check which member the ioctl number promised; the type system will not do it for you.这张图讲的那个 bug:用户态填的是 point(4 字节),驱动读的是 stamp(8 字节)。第 4–7 字节从来没被写过,驱动读到的是栈上残留的东西 —— 而且每次都读成功,每次值还不一样。要么整个联合体拷全,要么按 ioctl 号确认该读哪个成员;类型系统不会替你挡。