1. 问题提出
我们知道,如果有一个结构体定义如下:
- struct _st {
- int a;
- char b;
- } st ;
我们可以通过st访问到a或者b,方法就是st.a(或者如果有st的指针pst,那么就用pst->a)。但是,如果知道了结构体中元素的指针,是否可以获得当前结构体的指针呢?或者说,如果我只能访问到b,我可以访问到st和a么?
2. 这个问题的实际意义
首先,这样做有什么用呢?其实,自己早就知道linux内核中linus就是使用了container_of()的宏,就是利用的这个方法。最近,在项目中,碰到了一个问题,想了想暂时没有想到特别好的方法。而使用这个方法倒是可以比较好的解决问题。
大致上问题抽象出来就是:有一些BLOCK,需要用多个链表串起来。比如有B1,B2,B3,B4,B5共5个BLOCK,要用三个不同功能的链表串起来:
链表1: B1->B4->B5
链表2: B2->B5
链表3: B1->B2->B3->B4->B5
而项目希望使用glib中的链表库实现。而glib中的链表库(这里举一个单向链表的例子)是这样组织的(参见Glib文档):
- typedef struct {
- gpointer data;
- GSList *next;
- } GSList;
数据用data指针链接起来,实现单向链表。这里,如果有人有推荐的库也希望说出来,呵呵。这里我们如果用glib的库显然不容易实现我们想要的功能,因为我们不知道B2的next是要指向B5(如链表2中)还是B3(如链表3中)。我们显然需要多个next指针做到这一点。而如果我们这样使用:
- struct _block {
- int data;
- GSlist list1;
- GSlist list2;
- GSlist list3;
- };
因为每一个GSlist的next指针都是指向下一个block的list1(或者list2/3)元素的,我们也难以得到data字段的数据。这时候,就需要通过list1的指针找到data。而这也就是我们提出的问题。
3. 考虑
大致想一下可以知道,如果我们可以知道结构体在内存中存在的方式。比如:
- 1. 是否按照定义顺序放置在内存中?
- 2. 先定义的元素和后定义的元素,哪个地址在高段?
- 3. 是否在元素间有reserved区域用来align?
这样,我们就可以通过加减法计算出pa所指向的这个整形a所属于的结构体的地址。而以上的所有,编译器是肯定知道的,但是我们又怎么在代码中体现呢?
其实,为了用代码表示出来,我们根本不用知道上面的问题。看下面的方法一段便知道了。
4. container_of()的分析
既然之前知道在linux内核中,linus使用了container_of()这样tricky的宏,来通过一个结构体中元素的指针获得当前结构体的指针,这里,直接拿过来用其实就好了。
container_of()的实现方法很简单,也很巧妙。其核心就是两个宏定义:
- #ifndef offsetof
- #define offsetof(type, field) ((long) &((type *)0)->field)
- #endif /* offsetof */
-
- #ifndef container_of
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
- #endif
这里主要有三个宏:typeof(), offsetof(), container_of()。我谈谈我的一些理解:
- 1. typeof()可以得到一个变量的类型,这个是编译器要支持的。如我们可以这样写代码:{int a; typeof(a) b;}这段代码等价于:{int a; int b;}
- 2. offsetof()利用((type *)0)->field方式得到偏移地址,是假设有一个类型为type的结构在内存的0x0000000处,那么这个结构中field的地址的值就是field字段在结构中的偏移地址了!
- 3. container_of()首先定义了一个type.member同样类型的指针__mptr,并将ptr赋值给__mptr,这里其实是为了检验ptr的类型是否是type.member,增加了安全性(比如,当我随便传一个变量的地址ptr给这个宏后,加了校验的这个container_of()就会在那行报错,类型不匹配,如果是强制转换,编译或许是通过了,但是关键时刻一跑估计就是run time error了...);然后用__mptr的地址值减去member在type结构中的偏移地址,得到原始ptr所在结构的结构体地址。
- 4. 这里还要注意一个括号的使用方法:({...;...;...;}),我没有对这个的使用在编译器的手册中进行查找,但是大致的理解是,在{}中可以按照平时的习惯写程序,最后({})中可以看作一个值,它的大小是{}中最后一个语句的值。感觉有些类似于(...,...,...)的写法,但是只有()时不能有运算的语句。这里我写了一个简单的例子,输出是两个3:
- int main(void)
- {
- printf("%d.\n", (1,2,3));
- printf("%d.\n", ({int i=1,j;j=i+2;}));
- return 0;
- }
5. container_of()的使用例子
这里我举一个简单的使用container_of()的例子:
- /*
- * desc : a simple example to use container_of() macro
- * mail : [email protected]
- */
- #include <stdio.h>
-
- #ifndef offsetof
- #define offsetof(type, field) ((long) &((type *)0)->field)
- #endif /* offsetof */
-
- #ifndef container_of
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
- #endif
-
- typedef struct _A {
- int a;
- char b;
- long long c;
- double d;
- } SA ;
-
- SA sa = {
- .a = 10,
- .b = 'c',
- .c = 204,
- .d = 3.14,
- };
-
- int main(int argc,char *argv[])
- {
- double *pd = &sa.d;
-
- printf("SA.c = %lld\n", container_of(pd, SA, d)->c);
- return 0;
- }
这里,我定义了一个结构sa,并通过sa.d的指针得到sa.c的值。输出是:
- SA.c = 204
这样,我们自然也可以通过block.next得到block.data了。
参考资料:
- 1. ”对linux内核代码的一点疑惑:container_of的冗余?“, http://hi.baidu.com/joec3/blog/item/37b0c8900e397487a977a493.html,(其实在开始写这篇文章的时候,没有弄清楚为何要专门要用__mptr这个const指针,在这里找到的答案。同时,还有一篇和我这里分析container_of()很像的文章,都是从三个宏的角度。放到参考2里吧,呵呵)
- 2. “对linux内核中container_of宏的理解“, http://hi.baidu.com/tim_bi/blog/item/fdc3d81358e1f60b5aaf53c6.html