话说Linux内核链表之“container_of“(二)

一、"container_of"简介

       Linux内核中,"container_of“宏的使用时无处不在的。它的功能是:通过传入的结构体成员的地址、结构体类型以及成员的名字,返回对应结构体变量的起始地址。对于内核"list"来说,”container_of"使得把对链表的操作与链表管理的对象分离开来。好吧,先看一下定义吧,注意内核版本是Linux4.14.10,后面我们的分析都是基于这个版本,定义具体如下:

//linux4.14.10
//include/kernel.h
//@ptr: 成员的地址
//@type: 结构体类型
//@member: 成员的名字
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&	\
			 !__same_type(*(ptr), void),			\
			 "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

#define offsetof(TYPE, MEMBER)	((size_t)&((TYPE *)0)->MEMBER)

       "container_of"的原理是什么呢?简单来说就是:(type *)addr = ptr - offset,其中ptr就是传入的结构体成员的地址,offset就是成员相对于结构体的偏移量。因为"ptr"是已知的,那么"offset"就是问题的关键了,那么offset具体是怎么计算得到了?这就得说一下通过"0"指针(这个叫法不知道是否恰当)计算结构体成员偏移量的原理了。

二、"0"指针使用

       假设结构体变量的起始地址为"0",那么具体成员的地址其实就是相对于结构体的偏移量了。(type *)0——把"0"强制转换成结构体类型的地址;((type *)0)->member——就是以0为起始地址的结构体成员变量;&((type *)0)->member——就是成员变量的地址。下面我们通过一个简单的实例演示一下,具体如下:

/***********************************************************
 *博客:https://blog.csdn.net/xie0812
 *作者:东子
 ***********************************************************/
#include 

struct stu {
    int age;
    int num;
    int score;
};

#define OFFSET(type, member) ((unsigned long)&((type *)0)->member)

int main(void)
{
    struct stu test;

    printf("test addr: %p\n", &test); //test结构体变量地址
    printf("test.age addr: %p\n", &test.age); //age成员地址
    printf("test.num addr: %p\n", &test.num); //num成员地址
    printf("test.score addr: %p\n", &test.score); //score成员地址

	//age成员相对于结构体的偏移量
    printf("age offset: %lu\n", OFFSET(struct stu, age)); 
	//num成员相对于结构体的偏移量
    printf("num offset: %lu\n", OFFSET(struct stu, num));
	//score成员相对于结构体的偏移量
    printf("score offset: %lu\n", OFFSET(struct stu, score));
    return 0;
}

执行结果如下:
话说Linux内核链表之“container_of“(二)_第1张图片
       从输出结果可以看到结构体类型的变量"test"的地址,以及成员"age"、“num”和“score”分别对应的地址。成员相对于结构体的偏移量其实就是两个地址相减,比如score相对于结构体的偏移量:0x7ffeaddd4008 - 0x7ffeaddd4000 = 8,正好跟后面打印的"score offset:8"是一致的。

三、再谈"container_of"

       通过上面的例程的演示,现在理解"container_of"这个宏应该就容易多了。下面分别解释一下每一句的含义:
       1、void *__mptr = (void *)(ptr);——这一句就是把传入的"ptr"赋值给新定义的"__mptr"变量,其中"__mptr"是"void *“类型的,这主要是为了在第三句中做地址的相减的运算。其实之前版本中的第一句是: const typeof( ((type *)0)->member ) *__mptr = (ptr); 它的主要功能是定义一个成员变量类型的指针,这样赋值时,如果"ptr"和成员变量类型的指针不一致,就会报warning,其中“typeof”是GNU C的扩展,通过变量来获得变量类型。现在把判断类型是否一样放在了第二句。
       2、BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && !__same_type(*(ptr), void), "pointer type mismatch in container_of()");——其中__same_type就是判断两个参数的类型是否一致;#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg),其实"BUILD_BUG_ON_MSG"就是编译时的断言(assert),当"cond"为trues时,编译就会中断,同时发送msg。如果在之前的内核版本上调用”container_of",当传入的"ptr"与实际的成员类型不一致时,就参数warning,现在就直接中断编译了,也就是说更严格了。是啊,在编译时就拦截出问题总是好的。
       3、((type *)(__mptr - offsetof(type, member)));——最后的这句相当于"container_of"的返回值,返回的就是对应结构体类型的指针。"__mptr - offsetof(type, member"就是成员变量的地址减去此成员相对于结构体的偏移量,结果就是结构体的起始地址。
       4、#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)——跟我们上面示例中的所说的是一样的。
       本来打算写一个简单的内核示例来演示一下"container_of"的用法,但又感觉太单一了,索性就放在下一篇的内核链表的示例中吧。

你可能感兴趣的:(Linux内核数据结构,内核,链表)