无处不在的container_of

linux 内核中定义了一个非常精炼的双向循环链表及它的相关操作。如下所示:

struct list_head {struct list_head* next, * prev;
};

ubuntu 12.04 中这个结构定义在 /usr/src/linux-headers-3.2.0-24-generic/include/linux/types.h 中,各种操作定义在 list.h 中。可以通过 grep “struct list_head {” 来查找到,也可以使用 ctags 在 include 目录下生成 tags 文件,然后在 tags 里面查找到。

这个链表只有指针域,没有数据域,所以我们不能直接拿来使用。而是需要把这个结构体嵌在我们自己定义的结构体里,可以放在任意位置,开头,中间或结尾。比如:

struct book {int sn;char name[NAMESIZE];int price;struct list_head node;   //内核链表结构体放在最后
};

我们使用内核提供的链表操作函数或宏来快速地建立一个双向链表,如下所示:

int main()
{struct book* bp;int i;LIST_HEAD(head);  //宏,建立头结点for(i = 0 ; i < 3; i++){bp = (struct book*)malloc(sizeof(struct book));   /*if error*/bp->sn = i;snprintf(bp->name, NAMESIZE, "book%d", i);bp->price = rand()%60 + 20;/*insert*/list_add(&bp->node, &head);  //将该结点插入链表}/*TODO: travel*/return 0;
}

注意, LIST_HEAD(head) 不是函数,而是宏,所以不要对它传了一个没有定义的变量感到疑惑。这个宏的定义如下:

#define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)

它的作用是生成了一个变量名为 name 的头结点,并把指针域的值都初始化为指向自身。

list_add 函数实现把节点插入到以 head 为头结点的链表中。

上述一段简短的代码,快速地生成了一个可供自己使用的双向循环链表,其结构如下图所示:

从图中可以看到,每个节点中的 next 和 pre 指针指向的都是另一个结点中的 node 成员的地址,而不是整个节点的首地址,所以,问题就来了,我们要访问节点中的其它成员怎么办?那我们就必须通过 node 成员的地址,获取它所在的节点的首地址。方法很简单,用 node 成员的地址减去它相对首地址的偏移量即可,假设某个节点的 node 成员的地址是 ptr,偏移量暂时先用 offset(node, struct book) 来表示,则公式如下:

(struct book*) ( (char*)ptr - offset(node, struct book) )
~~~~~~~~~~~~~~~   ~~~~~~~~~~~   ~~~~~~~~~~~~~~~~~~~~~~~~~类型转换        转成指向 char 的指针  node 成员在结构体中的偏移量

那偏移量又该如何来计算呢,假想 struct book 这个结构体的首地址为 0,那么 node 节点的地址不就是偏移量吗?因此我们可以把地址 0 先转换成指向 struct book 的指针,再从这个指针取它的成员 node 的地址,就可以计算得偏移量,公式如下:

((size_t)    &((struct book*)0)->node)~~~~~~~~    ~ ~~~~~~~~~~~~~~~~~
转成无符号整数 取址

也许不少人和我一样,有一个疑惑,地址 0 转换成指针,不是 NULL 吗,用它去访问成员,不是非法的吗?我一开始也百思不得其解,后来明白了,取成员的地址,并不等于访问该成员。我们可以用下面一段程序来验证一下:

#include <stdio.h>struct st
{int a;      //0char b;     //4int c;      //8
};int main()
{printf("c addr  : %d\n", &((struct st*)0)->c);printf("c value : %d\n",  ((struct st*)0)->c);return 0;
}

在 ubuntu 12.04 32bit 上用 GCC 4.6.3 编译后运行的结果:

believe@ubuntu:~$ ./a.out
c addr  : 8
段错误 (核心已转储)

我是这么来理解的,地址 0 开始的这一段内存空间,就像是透明的充满机关的盒子,当里面放着结构体时,即使我们不打开这个盒子,从外面也可以知道里面的某个成员的位置(即地址),但你想打开盒子取出某个成员看看它的具体的值是多少时,却是万万不可的,会中箭而亡!

通过上面的步骤,我们就取到了节点的首地址,内核它也是这么做的,把上面表达式里的 struct book 换成 TYPE/type,把 node 换成 MEMBER/member,就是内核定义的样子:

// 定义在 include/linux/list.h 中
#define list_entry(ptr, type, member) \container_of(ptr, type, member)// 定义在 include/linux/kernel.h 中
#define container_of(ptr, type, member) ({          \(type*)( (char*)ptr - offsetof(type,member) );})// 定义在 include/linux/stddef.h 中
// 巧妙之处在于将地址0强制转换为type类型的指针,从而定位到member在结构体中偏移位置。编译器认为0是一个有效的地址,从而认为0是type指针的起始地址。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)

上面 container_of 为了便于理解,进行了简化,实际完整的定义是这样的:

/*** container_of - cast a member of a structure out to the containing structure* @ptr:    the pointer to the member.* @type:   the type of the container struct this is embedded in.* @member: the name of the member within the struct.**/
#define container_of(ptr, type, member) ({          \const typeof( ((type *)0)->member ) * __mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})

container_of 宏分为两部分,

第一部分:const typeof( ((type *)0)->member ) *__mptr = (ptr);

通过 typeof 定义一个member指针类型的指针变量 __mptr,(即 __mptr 是指向 member 类型的指针),并将 __mptr 赋值为 ptr。

第二部分: (type * )( (char * )__mptr - offsetof(type,member) ) ,通过 offsetof 宏计算出 member 在 type 中的偏移,然后用 member 的实际地址 __mptr 减去偏移,得到 type 的起始地址,即指向 type 类型的指针。

第一部分的目的是为了将统一转换为 member 类型指针。

它增加了一句,作用是通过 GCC 特有的类型运算符 typeof,取得 member 成员的类型,定义了一个此类型的指针 __mptr,并使它的值等于 ptr,后面就用 __mptr 替代 ptr 操作。这句话我想其实是多余的,可能是为了做最大的保护吧,可以通过下面这个例子来理解为什么要重新定义一个变量。

用 define 定义一个最简陋 MAX(a,b) 宏,像下面这样:

#define MAX(a,b) a > b ? a : b

这样的定义不堪一击,MAX(a+1, b) 的调用就会让它出错。
在 windows 下,最严谨的定义也不过如此了:

#define MAX(a,b) ((a)>(b)?(a):(b))

可是这样的定义,在面对这样的调用时,依然无能为力:MAX(++a, ++b),大家可以实验一下,其中的较大值会被加 2。

但在 linux 下,使用 typeof 运算符,可以解决这个问题:

#define MAX(a,b) ({typeof(a) A=a,B=b; A > B ? A : B;})

不过这种在 () 里包含 {} 的定义方法并不被标准 C 支持。

对于上面的这种定义,或许还是有人会有这样的疑问,假如调用 MAX(++a, ++b),a 还是会被加两次啊,typeof(++a) 时会加一次,A=++a 时又加一次。其实不会,因为 typeof 并不是一个运行时的函数或运算符,它和 sizeof 一样,是在编译的时候就确定了。下面这个例子可以验证:

int main()
{int a = 5;typeof(++a) b = 3;  //等同于 int b = 3;printf("a = %d, b = %d\n", a, b);return 0;
}---------------
运行结果:
a = 5, b = 3

如果我们把内核链表结构体放在我们自己的结构体的开头,其实就不需要这两个宏了,只需要进行类型转换即可。

另外,在 windows 内核中也有类似的宏,由于 windows 没有 typeof 运算符,所以就显得简单了一些,定义如下:

#define CONTAININT_RECORD(address, type, field) \((type*)((PCHAR)(address) - (PCHAR)(&((type*)0)->field)))

下面的程序将完整地展示如何使用 container_of 实现遍历。

int main()
{struct list_head *cur;struct book* bp;int i;LIST_HEAD(head);  //宏,建立头结点for(i = 0 ; i < 3; i++){bp = (struct book*)malloc(sizeof(struct book));   /*if error*/bp->sn = i;snprintf(bp->name, NAMESIZE, "book%d", i);bp->price = rand()%60 + 20;/*insert*/list_add(&bp->node, &head);  //将该结点插入链表}/*travel*/__list_for_each(cur, &head)//这也是一个宏,展开后是这样:for (cur = (&head)->next; cur != &head; cur = (&head)->next){bp = list_entry(cur, struct book, node);//list_entry 与 container_of 等价printf("sn = %d, name = %s, price = %d\n", bp->sn, bp->name, bp->price);}return 0;
}

无处不在的container_of相关推荐

  1. 让“云”无处不在-Citrix Xenserver之一 环境搭建

    让"云"无处不在-Citrix Xenserver 一.准备部署环境 马博峰 2011年8月 --------------------------------------- 一.X ...

  2. linux驱动开发要知道的那些知识(三)------container_of,定时器 及系统调用

    container_of宏 定义: /** * container_of - cast a member of a structure out to the containing structure ...

  3. jsp截取字符串前几位_7. Python3轻食丨丛林里的野蛮生长:无处不在的字符串(一)

    7. 字符串 7.1 定义与创建 字符串就是Python中的路人脸,它看起来非常普通也是python中最常见的数据类型.但也正是字符串无处不在的原因,让它具有极强的适应能力与包容性:哪儿都有它,它也能 ...

  4. 张亚勤:让人工智能像空气一样,无处不在

    来源:清华大学本文约3700字,建议阅读10+分钟 他是科学家中的企业家,企业家中的科学家. 1978年 一位12岁的少年 从六百万高考大军中脱颖而出 这个全国最小的大学生 目光稚嫩澄澈却坚定 即将在 ...

  5. 让AI计算无处不在,华为干了一件大事

    如何在更高的层次.更大的范围,让AI为人类带来更加美好的未来,如何让AI触手可及,这正是AI领导者.先行者华为规划和思考的问题. 智慧城市.智慧交通.智慧医疗.智慧建筑--AI技术正在缔造人类梦寐以求 ...

  6. 直观、形象、动态,一文了解无处不在的标准差

    选自 Fahd Alhazmi's Blog 作者:Fahd Alhazmi 机器之心编译 参与:魔王.杜伟 本文将对标准差这一概念提供直观的视觉解释. 本文作者为纽约市立大学在读博士生 Fahd A ...

  7. 深度丨当AI变得无处不在,人类社会将发生这五大变化!

    文章来源:腾讯科技 人工智能(AI)正以前所未有的速度急速崛起,并开始进入人们生活的各个方面.将来,当AI变得无处不在时,我们的社会将会变成什么样子?机器人会拥有自我意识吗?它们能享受与人类相同的权利 ...

  8. 毕马威发布《人工智能无处不在》研究报告:人工智能在五大行业的成就与挑战...

    来源:AIII研究院 毕马威近期发布了研究报告<人工智能无处不在>.该报告对751名在人工智能各行业应用领域具有一定了解的企业决策者进行调研并发布了"成绩单",总结分析 ...

  9. 无处不在的人工智能,IBM沃森的20个行业应用

    来源:资本实验室 聚焦前沿科技创新与传统产业升级 自2011年在美国综艺电视节目<危险边缘>中一战成名后,IBM的Watson就一直是最受关注的人工智能之一. 从菜谱分析到球队管理,从健康 ...

最新文章

  1. 杂志大片:摩登都市 爱之城堡
  2. 区块链技术指南笔记(一):区块链基本概念
  3. php 命令行方式运行时 几种传入参数的方式
  4. JS中与正则相关的方法
  5. 关于RESTful一些注意事项,接口开发规范
  6. 北京大学:“巍巍上庠 国运所系”北大为时代发展而歌(附历年高考各省投档线)...
  7. 高并发网络编程之epoll(个人遇到最好理解的一篇文章、易懂)
  8. python编程(python调用dll程序)
  9. Machine Learning课程中的常见单词的含义
  10. Vimtutor中文版
  11. 不再为无限级树结构烦恼,且看此篇
  12. 评委打分表模板_为什么你的学习/工作计划,总是执行不下去?(附计划表模板)...
  13. 1.海康威视-在浏览器中摄像头激活
  14. 基于大数据的主动科研管理模式与优化决策机制
  15. Caffeine一级缓存介绍和应用
  16. Lua中保留两位小数
  17. 选择的串口 _ 不存在或开发板没有连接_PC 和开发板之间传输文件
  18. Docker 无法启动 Failed to start LSB: Create lightweight, portable, self-sufficient containers.
  19. 我与程序员不得不说的二三事——一天一天
  20. 交换机的基本原理(特别是动态ARP、静态ARP、代理ARP)

热门文章

  1. centos7 mysql tar_CentOS7中mysql-5.7.21-el7-x86_64.tar.gz版MySQL的安装与配置
  2. 我的世界java版游戏崩溃_我的世界全攻略之-游戏崩溃的解决方法
  3. 定义一个dto对象_业务代码的救星——Java 对象转换框架 MapStruct 妙用
  4. leetcode511. 游戏玩法分析 I(SQL)
  5. 返回地址【数据结构】
  6. C++: 21---引用和指针
  7. 知道一点怎么设直线方程_【初中数学】反比例函数策略(二) ——构造方程法...
  8. centos网络隔一段时间就断_计算机网络总结
  9. 图像放大 问题 即 二维数组放大
  10. Android Studio 突然出现很多红色波浪线或红色感叹号解决方案