内核链表中container_of实现

这两天看书的时候看到了关于内核链表的内容,《linux内核设计与实现》一书只是大概的讲解,有些内容我就到网上去搜索。看到一片非常好的blog,就转载过来了。感觉内核中对于宏的使用异常精妙。

原文地址:http://www.groad.net/bbs/read.php?tid-1118.html

container_of() 宏的作用是通过结构体成员的指针找到对应结构体的指针,这个技巧在 linux 内核编程中十分常用。下面以实例来分析一下实现原理。


在 open() 操作里,第一个参数是,struct inode 结构体的指针。在 struct inode 结构体里,有一个 i_cdev 域,这个是一个指向 cdev 结构体的指针。在编写驱动程序时,一般情况下,不会单独的把 cdev 结构体拿出来用,而是将其封装在与驱动程序紧密相联的一个自定义的结构体里。借用 << linux device drivers 3>> 里的这么一种结构体的定义:

struct  scull_dev  {
             struct  scull_qset  * data;
             int  quantum;
             unsigned long  size;
             unsigend  int  access_key;
             struct  semaphore  sem;
             struct cdev cdev;
};


下面通过 container_of() 宏来获取 scull_dev 这个结构体的指针:
container_of ( inode->i_cdev, struct scull_dev, cdev);


container_of() 宏中,第 1 个参数就是 struct scull_dev 这种结构体中 cdev 成员的指针;第 2 个参数是 struct_scull 类型结构体;第 3 个参数就是 cdev 结构体。

在 <linux/kernel.h> 头文件中,可以看到 container_of() 宏的定义:
#define container_of(ptr, type, member) ({            \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

以 struct scull_dev 作为分解宏的参数展开并分析这个宏定义:

(type *)0 欺骗编译器,说 0 地址是一个 struct scull_dev 类型结构体的指针;

((type *)0)->member 指向的就是 c_dev 结构体;

typeof ( ((type *)0)->member) 是取得 member 类型的类型,这里也就是取得 cdev 结构体的类型,即 struct cdev ;

const typeof ( ((type *)0->member) *__mptr  定义了一个 struct cdev 类型的指针 __mptr ;

const typeof ( ((type *)0->member) *__mptr = (ptr) 是将 ptr 指针赋给 __mptr , 这个赋值的目的是要取得 cdev 这个成员定义在 scull_dev 中的位置,这在下面的第 2 条语句中会用到这个位置

第一条语句分析完,下面分析第 2 条语句:

在这条语句中,offsetof(type, member) 是取得 member 参数在 type 类型结构体中的偏移位置。offsetof() 也是一个宏,它的详细情况见http://www.groad.net/bbs/read.php?tid-1040.html

内容不多,我就贴在下面:

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》

在 <stddef.h> 中定义了个 offsetof(s,m)宏,这个宏用来取得结构体中元素的偏移量很方便,下面是此宏的具体定义:

#define offsetof(s, m) (size_t)&(((s *)0)->m)

ofssetof(s, m) 其中,s 是结构体名,m 是它的一个成员。s 和 m 同是宏  offsetof() 的形参,这个宏返回的是结构体 s 的成员 m 在结构体中的偏移地址。

(s *)0  :  这里的用法实际上是欺骗了编译器,使编译器认为 "0" 就是一个指向 s 结构体的指针(地址),换句话说 s 结构体就是位于 0x0 这个地址处。

(s *)0-> m : 自然就是指向这个结构体的 m 元素。

&((s *)0)->m :  表示 m 元素的地址。这里,如上面所说,因为编译器认为结构体 s 被认为是处于 0x0 地址处,所以 m 的地址自然的就是 m 在 s 中的偏移地址了。

最后将这个偏移值转化为 size_t 类型。

可能会感到迷惑,这样强制转换后的结构指针怎么可以用来访问结构体字段?呵呵,其实这个表达式根本没有也不打算访问m字段。ANSIC标准允许任何值为0的常量被强制转换成任何一种类型的指针,并且转换结果是一个NULL指针,因此((s*)0)的结果就是一个类型为s*的NULL指针。如果利用这个NULL指针来访问s的成员当然是非法的,但&(((s*)0)->m)的意图并非想存取s字段内容,而仅仅是计算当结构体实例的首址为((s*)0)时m字段的地址。聪明的编译器根本就不生成访问m的代码,而仅仅是根据s的内存布局和结构体实例首址在编译期计算这个(常量)地址,这样就完全避免了通过NULL指针访问内存的问题。又因为首址的值为0,所以这个地址的值就是字段相对于结构体基址的偏移。

size_t是针对系统定制的一种数据类型。它不是固定位数,在不同的系统里这个值都有可能不同( 它实际上是 unsigned int 类型 );而且在内存里,对于数据是高位对齐存储还是低位对齐存储各系统都不一样。所以,为了提高代码的可移植性,就有必要定议这样的数据类型。一般这种类型都会定义到它具体占几位内存等。当然,有些是编译器或系统已经给定义好的。具体要查看技手册。

下面是自定义 offsetof() 宏的测试代码:

#include <stdio.h>
struct  demo  {
      char  c;
      int   i;
      int   k;
      char  u;
      char  name [ 10 ];
};
int  main()
{
      size_t  pos;
     
      pos  = ( size_t) &((( struct  demo  *) 0) -> k);    
         
      printf( "%d \n " , pos);
      return  0;
}

运行及输出
beyes@linux-beyes:~/C/base> ./offsetof.exe 
8

为什么是 8 而不是 char c (1) + int i (4) = 5 呢?这是因为计算机为了方便存储数据进行了数据对齐,把 char 类型也再另外填充了 3 个字节变成了 4 个字节。
参考: http://www.groad.net/bbs/read.php?tid=1037

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》

在这里也就是,取得 cdev 成员在 scull_dev 中的偏移。

(char *)__mptr - offsetof(type, member) 是 __mptr 指针往上走 offsetof(type, member) 个偏移量,也就是到达了 scull_dev 这个结构体的头部,所以到此已经获得了 scull_dev 结构体的头地址了。

最后 (type *) ((char *)__mptr - offsetof(type, member)) 是把 scull_dev 结构体头部地址转换为 scull_dev 结构体类型,也就是说至此已经获得了 container_of() 宏中第 2 个参数的指针。

另外,这个宏实现,是纯粹的替换操作,不需要去设置返回值,返回值就是最后的操作语句,得到的内容。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值