Re: [问题] 请益一段程式码 (offsetof/container_of)

楼主: LPH66 (-6.2598534e+18f)   2016-04-30 19:35:58
昨天晚上各种原因早早躺床了所以到现在才来回...
顺便改个标题给以后搜寻
※ 引述《j5128709 (j5128709)》之铭言:
: /*##### 程式码 #####*/
: do
: {
: dlink_t *entry;
: for ( (entry) = (&idle_1)->head.next;
: (entry) != &(&idle_1)->head;
: (entry) = (entry)->next;
: ) {
: u_idle_t *idle = ( (u_idle_t*) ((u8 *) (entry) -
: (u8 *) (&((u_idle_t *) 0)->link ))); //Q1
: idle->idle();
: } //end for
: }while(0);
: 想问说 Q1 这行的该如何解释? 这"0"是说位址嘛?
: 还是说有啥特别用意??
: 感谢各位前辈看完
原 PO 后来有丢水球给我说原始的 macro 是这一个:
#ifndef container_of
# define container_of(address, type, field) \
( (type *) ((u8 *) (address) - (u8 *) (&((type *) 0)->field )))
这个 macro 其实在 linux kernel 里满常见的, 它算是 offsetof 的逆向版
这里先讲一下什么是 offsetof:
offsetof 是标准中有定义的 macro (在 <stddef.h> 里)
offsetof(type, member) 之作用为:
给定一个结构 type, 及其成员名 member,
求此 member 在这 type 里的偏移字节量
(C++ 在这里有些规定, 不过这里先不管, 先讲 C 的部份)
所谓的偏移字节量就是在底层资料结构当中
这个成员是放在整个结构的什么地方, 相对于结构开头移了多少字节
一般来说, offsetof 在 C 的实作会是如此:
#define offsetof(type, member) (size_t)&(((type*)0)->member)
(这里跟这个 0 有关的东西等下会提)
可以看到这个定义跟原 PO 问的 macro 的后半段几乎一样
这里就要回头讲原 PO 问的 container_of 了
这个 macro 不是标准定义, 而是在 linux kernel 出现过
在那之中的定义是这样子的:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
先不看实作细节, 这 container_of 的作用是:
给定一个结构 type, 其成员名 member,
及一个已知指向某个 type 结构的 member 成员之指标 ptr,
求一指标指向此 type 结构
在 linux kernel 里的基本状况是某个 struct 里的成员其指标送去别的地方了
当别的地方送回来这个指标时, 我要求得这指标究竟是哪个 struct 的以进行后续操作
这时就会使用 container_of 来求得之
====
那么这里就来讲一下这两个 macro 的实作细节
先讲 offsetof, 这里重贴一下这个常见的实作:
#define offsetof(type, member) (size_t)&(((type*)0)->member)
它做的事其实是这样的:
(type*)0 一个型态为 type* 的空指标常数 (#1N8UkD6I)
&(( )->member) 其指向之 type 结构的 member 成员之指标
(size_t) 将其转为 size_t 做为结果
中间的第二步比较好懂, 就只是一个结构指标取出其成员的指标而已
这一步对编译器来说即是找出我们想求得的偏移值, 加上指标值之后做为结果
于是为了取出这个偏移值, 第一步取用了空指标常数
如果空指标常数也是指向位址 0 的话
那加上偏移值的位址就正好只剩下我们想要的偏移值, 所以转为 size_t 后即为所求
但是我们知道空指标常数并不一定指向位址 0
因此平常是不允许直接这样写的, 只在当环境是确定的时候才可以这么做
(在一些空指标常数可能不指向位址 0 的环境里也是有类似这样的定义存在:
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
这里为了补偿可能不指向位址 0 的空指标常数
在取得成员指标后又减去了空指标常数的值, 即是减去结构开头的位址
这样就可以取得想要的偏移值了)
因此这也是为什么 offsetof 会作为 C 标准里的一个 macro
就是因为由于各种原因它不可能有可携实作, 但在各自的环境里总是有办法写得出来
====
接下来是 container_of, 同样重贴一下 linux kernel 的实作:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
这里它使用了 gcc 里的 block expression
({ ... }) 这个东西回传 { } 里面最后一个 expression 的结果做为结果
这使我们可以在 { } 里宣告变量或使用 for/while/switch/等等
这里就是它宣告了一个变量 __mptr 其型态为
const typeof( ((type *)0)->member ) *
这个 typeof 也是 gcc 专有, 其效果有点类似 C++ 标准的 decltype
是用来求得其参数的型态
(注意不论是 typeof 或是 decltype 都不是 macro, 其结果是个型态)
这里用来求型态的式子是 ((type *)0)->member
从上面我们知道了这式子是对一个型态为 type * 的空指标常数求其成员
所以 typeof 的结果就是这个 member 的型态
有趣的是, 这里并不用担心刚才提到的空指标常数计算问题
这是因为 typeof 只关心这式子的型态而不关心其值
也就是说 typeof 并不会去实际计算那个式子, 而只会去看看它该产生什么型态而已
回到 __mptr, 于是这样这个 __mptr 的型态就很清楚了:
它是一个指向“member 的型态”的指标 (const 在此略去)
有了它之后, 第二行做了这些事:
(char *)__mptr 刚才那指标转成 char*
- offsetof(type,member) 减去偏移值
(type *)( ) 再转成 type *
我们知道 char 总是 1 byte, 所以转成 char * 再减去偏移值即是倒回去求开头
因此最后再转成 type * 即是所求了
这里有个细节要先提:
之所以在这里先宣告一个正确型态的变量来接
是因为我们不能控制 macro 传进来的参数的型态
而且有很大的可能这个指标是做为一个通用指标 (void *) 传去别的地方的
所以在转回来时得要先转回正确的型态才能倒回去求
但是把两行合并在一起的话会太丑, 所以就另外宣告一个变量型态来接
由于 { } 的 scope 的关系, 这个变量只在这里看得到
编译器的最佳化足以将这里的变量使用给消掉而不会实际占用一个变量空间
====
最后回到原 PO 问的定义:
#ifndef container_of
# define container_of(address, type, field) \
( (type *) ((u8 *) (address) - (u8 *) (&((type *) 0)->field )))
在经过上面的讲解之后, 这里很容易可以看出它把上面的实作做了一点化简合并起来
由于看起来这里的使用情境里 address 的型态永远是对的 (dlink_t *)
因此它略去了转回正确的 member 型态的操作
然后它把 offsetof 的实作也合并进来了, 可以看到后半段即是 offsetof
不过这里有一个大问题: offsetof 的结果的型态搞错了
第二个 (u8 *) 的转型应该要是个整数型态才是对的
例如 size_t, ptrdiff_t 或懒一点就 int
这样指标减整数才会得到指标
不然这里是指标减指标会得到整数, 再转回指标有一点操作上的危险性
这应该就是推文 longlongint 在讲的怪怪的地方了
====
到此为止都是在讲 C 的实作
C++ 由于可以对 operator & 进行重载
offsetof 的那个实作可能会呼叫到重载函式
因此 C++ 的 offsetof 不能使用这个方式实作, 而必须倚赖编译器提供 builtin
事实上 gcc/g++ 的 offsetof 即是使用 __builtin_offsetof 做为定义
刚才也提到 C++ 的 offsetof 对 type 上有些规定
标准规定它必须是所谓 Standard Layout 的型态
它比 POD 稍微松一点, 可以有一些有限度的继承
但基本上各个成员的位址依然是确定的, 所以才可以使用 offsetof 求得之
至于 container_of, 扣除当中 offsetof 的需求外倒是没什么其他要求就是了
====
最后关于我的水晶球 (!)
原 PO 贴的那一段程式有好几个迹象显示那是 macro 展开之后的结果:
其一, offsetof 一般不会展开来写
其二, 那个 for 循环的 entry 变量有一层括号, 这是 macro 定义的常见状况
(见置底十三诫之九)
其三, do...while(0) (见 #1DmPUtv4)
所以与其说是 macro 展开后, 这其实更像是 preprocess 过的程式码
算上 container_of 这里大概至少有三条 macro 套在一起...
作者: tuyutd0505 (Huang Jason)   2016-04-30 20:21:00
推水晶球大神好文
作者: chuegou (chuegou)   2016-04-30 21:08:00
最后那段水晶球(?)推理 精彩
作者: james732 (好人超)   2016-04-30 21:11:00
推水晶球
作者: Caesar08 (Caesar)   2016-04-30 21:22:00
作者: j5128709 (j5128709)   2016-04-30 23:09:00
推 感谢LPH大 专业的解说!!!
作者: oscar60111 (还得努力学习)   2016-05-01 00:35:00
推~~~ 请问这种高级水晶球哪儿买的到呢XD?
作者: mabinogi805 (焚离)   2016-05-01 04:49:00
高级水晶推理,慧眼穿云www
作者: name2name2 (yang~hi)   2016-05-01 11:22:00
作者: Hazukashiine (私は幸せです)   2016-05-01 11:34:00
推推
作者: prismwu   2016-05-01 17:51:00
我们该成立水晶球神教了
作者: wtchen (没有存在感的人)   2016-05-01 18:33:00
板工也要水晶球....
作者: Frozenmouse (*冰之鼠*)   2016-05-01 18:53:00
这水晶球我也想要一个www
作者: virve (std::vie)   2016-05-01 22:48:00
团购水晶球?XD
作者: VictorTom (鬼翼&娃娃鱼)   2016-05-02 03:30:00
推:)
作者: damody (天亮damody)   2016-05-02 15:19:00
作者: BlazarArc (Midnight Sun)   2016-05-04 01:58:00

Links booklink

Contact Us: admin [ a t ] ucptt.com