无处不在的container_of

无处不在的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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/448055.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员学习能力提升三要素

摘要&#xff1a;IT技术的发展日新月异&#xff0c;新技术层出不穷&#xff0c;具有良好的学习能力&#xff0c;能及时获取新知识、随时补充和丰富自己&#xff0c;已成为程序员职业发展的核心竞争力。本文中&#xff0c;作者结合多年的学习经验总结出了提高程序员学习能力的三…

AMD Mantle再添新作,引发下代GPU架构猜想

摘要&#xff1a;今年秋天即将发布的《希德梅尔文明&#xff1a;太空》将全面支持AMD Mantle API&#xff0c;如此强大的功能背后离不开强大的CPU、GPU支持。上周AMD爆出了下一代海盗岛R9 300系列&#xff0c;据网友猜测海盗岛家族可能用上速度更快的HBM堆栈式内存。 小伙伴们…

VUE : 双重 for 循环写法、table 解析任意 list 、万能表格组件、解析一维数组、动态生成 table 所有数据

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1.需求&#xff1a; 我想要一个 table 组件能在实际调用时动态生成所有的 tr 、td 。 后端返回的只是一个 list &#xff0c; 前端页…

超详细 图解 : IntelliJ IDEA 逆向生成 JAVA 实体类

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1.配置数据库,&#xff0c;这里连接的是mysql。 2.填写 连接数据库的信息&#xff0c;填写完成后可以点击Test Connection,测试一下是否…

leetcode练习——数组篇(1)(std::ios::sync_with_stdio(false);std::cin.tie(nullptr);)

题号1. 两数之和&#xff1a; 给定一个整数数组 nums 和一个目标值 target&#xff0c;请你在该数组中找出和为目标值的那 两个 整数&#xff0c;并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;你不能重复利用这个数组中同样的元素。 示例: …

intellij idea 中去除 @Autowired 注入对象带来的红色下划线报错提示

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 PS&#xff1a; 有 2 种方法&#xff0c;第 2 种方法更简单&#xff0c;在此谢谢好心友人的评论。 方法1&#xff1a; idea中通过Autow…

Coolite动态加载CheckboxGroup,无法在后台中获取

Coolite在后台动态加载CheckboxGroup&#xff0c;页面显示都正常&#xff0c;但是在后台去获取选中的checkbox时&#xff0c;使用下方法&#xff1a; ///<summary>///获取所选权限 ///</summary>///<returns></returns>privatestringGetPermiss…

图解 IDEA 中 springboot 项目 MyBatis Generator 逆向生成实体类及 mapper 配置文件

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、准备工作&#xff1a; 1. 新建一个 配置文件&#xff1a;generatorConfig.xml 。 <?xml version"1.0" encoding&qu…

关于IIS 7.5 限制连接数与流量限制模块

网页中的视频是用户喜闻乐见的常见形式之一&#xff0c;并在主要的站点中中以某种形式&#xff08;产品视频、教程视频、理财场景、user generated content、消费报告等&#xff09;在更广泛的应用。 其中的一个挑战是把视频加入到站点&#xff0c;虽然这并不花费很多代价。高质…

汽车标志大全 买车必知

简要介绍&#xff1a;为您提供汽车标志、世界汽车标志大全、各种汽车标志、国产汽车标志大全、汽车标志图片、汽车标志及名称、名车标志大全、世界名车排行榜、世界十大名车、世界名车图片等有关汽车标志、汽车图片、汽车名字及汽车品牌方面的知识。 欧美汽车标志图片大全_欧美…

解决: Caused by: java.lang.IllegalStateException: Cannot load driver class: com.mysql.jdbc.Driver

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 报错&#xff1a; Caused by: java.lang.IllegalStateException: Cannot load driver class: com.mysql.jdbc.Driver 2.但是&…

解决:Field xxMapper in xx.service.impl.xxServiceImpl required a bean of type ‘xx.mapper.xxMapper‘

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 启动 springboot 项目报错&#xff1a; Field userMapper in gentle.service.impl.UserServiceImpl required a bean of type gent…

Linux 查看 MySQL 版本的四种方法

1 在终端下执行 mysql -V 2 在help中查找 mysql --help |grep Distrib 3 在mysql 里查看 select version() 4 在mysql 里查看 status 转自&#xff1a;https://blog.csdn.net/chengyuc/article/details/77094775

html 基本布局介绍

1、div默认是纵向排列的&#xff0c;例子如下&#xff1a; <div id"wrap"><div id"div1">div1</div><div id"div2">div2</div><div id"div3">div3</div> </div> 2、如果要div横向排列…

@JsonSerialize 使用:注解方式 实现条件判断属性值、条件修改属性值

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 数据库中是 1、0 记录性别。 预期效果为&#xff1a;当查到属性值为 1 时&#xff0c;就给序列化后的 json 中性别字段赋值为 “男”…

【Android开发】NDK开发(1)-Hello World!

上半年&#xff0c;公司的一个项目&#xff0c;本身我是不喜欢Android开发的&#xff0c;但是学习到的东西总需要整理一下。 从iOS转到Android&#xff0c;真心有些不习惯。就IDE来说&#xff0c;eclipse比Xcode差的不是一点半点。Android模拟器竟然还要开机&#xff01;我勒个…

jQuery 实现表格与 ckeckbox 的全选、单选功能

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 功能点 1. 用户点击头的checkbox时&#xff0c;所有表格数据行的checkbox全选或反选。 当数据行某一行没有选中时&#xff0c;头check…

VUE:checkbox 单选框(单选、多选)、一选全选 / 全选一选、表格单选列实现

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 效果&#xff1a;如红框中部分。 一选全选&#xff1a;表头上的单选框选中则下面每行都选中。 全选一选&#xff1a;表中数据每行都…

我爱学习第一天(委托)

---恢复内容开始--- 1.delegate 一般用法 delegate void WriteValue(string vale);//申明一个委托,参数是string类型,无返回值static void Main(string[] args){WriteValue write new WriteValue(WriteLine);//实例化委托write("喝水");//调用Console.ReadKey();}pu…

VUE:父子组件间传参、子组件传值给父组件、父组件传值给子组件

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Vue是一个轻量级的渐进式框架&#xff0c;对于它的一些特性和优点在此就不做赘述&#xff0c;本篇文章主要来探讨一下Vue子父组件通信的…