原文地址:http://wuzufei.cn/linux_list.html
通常情况下,我们使用C语言实现定义的链表节点形式如下:
struct Node { int data; struct Node* next; };常规使用一个结构体定义节点,然后在每个节点中保存前驱/后继节点的首地址,通过遍历每个节点中的next成员访问下一个节点;
而在linux 的内核实现中,链表节点定义方式如下:
struct list_head { struct list_head *next, *prev; } struct data_node { int val; struct list_head data_list; }linux 使用两个结构体,data_list保存链表节点信息,data_node则是用于保存数据,然后在data_node中包含data_list;不同data_node通过data_list节点的next和prev指针关联,遍历数据的过程时遍历链表节点,然后判断每个链表节点所在数据结构体的字段信息,进行相应的CRUD。
常规实现直接将各个数据结构体串联起来,即数据节点本身就是链表的一个node;
而linux实现是通过专门的链表节点实现数据节点间的串联,可以理解为把链表放进了数据里面,因为各个数据节点独立不相关。
在常规实现中,自然而然我们可以通过一个节点获取到下一个节点的首地址,然后遍历其成员的value,执行CRUD操作,但对于linux实现,从链表头节点开始遍历,我们只能找到各个数据节点链表节点的位置,但如何知道该数据节点的其他成员呢?
struct list_head header; struct data_node *node; /* 1 创建节点node并赋值,过程省略 */ /* 2 将node添加到链表 */ list_add_tail(&node->val, &header); /* 3 遍历链表 */ list_for_each_entry(node, &header, ptr_list) { printf("val: %s\n", node->val); } /* 4 删除节点 */ list_for_each_entry_safe(node, tmp, &bus, ptr_list) { if (node->val == 1) { list_del(&node->ptr_list); free(node); } }而list_for_each_entry和list_for_each_entry_safe最终是通过调用list_entry获取每个节点的首地址,进而访问节点的成员信息,更多的list使用看这里和这里, 因此以下具体对list_entry进行说明: list_entry展开为container_of,进一步展开为如下的两行代码,其中第一行是将传入的链表节点指针进行赋值,这个步骤的目的是为了在避免传入类型不符的指针而无法感知(类型不符在编译时会有warring),第二行是获取链表节点所在node的首地址,offset即获取链表节点距离node节点首地址的相对距离,然后用链表的地址减去相对距离得到首地址;原理具体如图所示:(这部分实在不理解,建议看这里)
member可以理解为链表节点成员所在的位置,然后size是链表节点距离node首地址的相对距离,ptr是指针的实际地址,用ptr减去size就可以得到结构体的首地址。
#define list_entry(ptr,type,member)\ container_of(ptr,type,member) #define offsetof(TYPE,MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #define container_of(ptr,type,member) \ ( { \ const typeof( ((type*)0)->member ) *__mptr=(ptr); \ (type*)( (char*)__mptr - offsetof(type,member) ); \ } )为什么数字0可以强转为指针,地址0不是不能使用吗? 将一个变量强转为其他类型,或许会好理解的多,为什么数字还能强转?实际上代码经过编译之后,每个变量名会对应一个虚拟内存地址,然后在运行阶段直接根据地址进行操作,不存在变量名这个说法;而在这里将数字0强转为数据节点的类型,可以理解为地址0被强转为一个数据节点的首地址,从而此时结构体中每个成员的地址等于该成员距离结构体首地址的距离,因此如果知道某个结构体中一个成员的具体位置,就可以直接知道那个结构体的首地址;地址0在这里并没有进行赋值,只是强转了一下用来获取链表节点具体的相对位置,如果赋值就coredump了;
一定要是0吗,可不可以是其他地址? 这个值是可以为其他值的,但我们要获取的是相对值,如果从其他值开始,返回的是绝对值还是要转换为相对位置,只是从0开始时相对距离和绝对距离是相等的,在x64上验证,最大值可以取到0x7fff,ffff。
linux这样实现有什么优点和缺点? 优点:链表和数据分离,任何结构体只要加上struct list_head成员就可以直接进行链表操作,所有操作都是通过宏定义实现,因此只要把list.h拷贝到本地就可直接使用; 缺点:没有什么明显的缺点,只是其他语言好像没法直接这样干。
参考资料Linux 内核链表 list.h 的使用C Linked List Data Structure Explained with an Example C ProgramLinux内核双链表语句list_entry(ptr, type, member)理解container of()函数简介Linked List | Set 1 (Introduction)