内核版本:2.6.34
OS:x86_64
一、list_head
list_head本质上是一个双向链表,被广泛应用于各类数据结构的管理和维护。而系统中各个进程的描述符task_struct也是通过list_head进行管理的。内核中关于list_head的定义都在include/linux/list.h中。
1. list_head的定义:
struct list_head {
struct list_head *next, *prev;
};
很简单,包含两个指针变量,*next指向后驱节点,*prev指向前驱节点。
2. list_head初始化API
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
方法一:直接通过LIST_HEAD()来创建一个名为name的链表并完成初始化(next和prev都指向自己)
LIST_HEAD(node);
// 通过打印发现三个地址是一样的
printf("&node = %#x\n", &node);
printf("node.prev = %#x\n", node.prev);
printf("node.next = %#x\n", node.next);
方法二:
- 先定义一个list_head;
- 再通过INIT_LIST_HEAD()完成初始化(同样还是next和prev都指向自己)
struct list_head list;
INIT_LIST_HEAD(&list);
// 通过打印发现三个地址是一样的
printf("list = %#x\n", &list);
printf("list.prev = %#x\n", list.prev);
printf("list.next = %#x\n", list.next);
3. 对list_head进行操作的API
1)添加节点:list_add、list_add_tail
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
// 在链表头head节点后面插入一个新的节点new
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
// 在链表尾head->prev节点后面插入一个新的节点new
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
2)删除指定节点:list_del
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void list_del(struct list_head *entry)
{
/* 将待删除节点的前驱节点prev和后驱节点next传入__list_del
__list_del会将prev节点和next节点连起来,即丢弃了待删除节点。 */
__list_del(entry->prev, entry->next);
/* 对被删除节点进行清除操作,两个宏代表防止被使用,这样就不会被访问到了。 */
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
3)其它API原理类似,不再赘述
二、普通的双向链表DNode
我们在学习C语言的双链表DNode的时候,接触到的双链表实际上和list_head是有差别的。差别就是一般DNode除了有前驱节点指针、后驱节点指针,还有数据域data。下面是一个常用的DNode定义。
struct DNode{
struct DNode *prev;
int data;
struct DNode *next;
};
其使用方法是通过遍历链表找到节点,然后访问其中的数据data。即数据域是嵌入在链表节点中的。
而list_head是一种侵入式链表,没有数据域,这样的好处是不用再关心数据域的类型、结构等等,变得更加通用。list_head是嵌入到结构体内部的,比如大名鼎鼎的进程描述符task_struct中就嵌入了多个list_head。
// include/linux/sched.h
struct task_struct {
volatile long state;
void *stack;
......
struct list_head rcu_node_entry;
struct list_head tasks;
struct list_head children;
struct list_head sibling;
......
}
那么怎么通过list_head,访问其所在结构体的其它数据域呢?例如:拿到了某个进程的tasks节点,怎么访问该进程的state值呢?这就需要通过结构体成员地址,找到结构体首地址,转成结构体指针,再去访问其它成员。内核中的API为:list_entry()。
三、list_entry
// include/linux/stddef.h
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
// include/linux/kernel.h
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
// include/linux/list.h
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
list_entry(ptr, type, member)参数说明:
- ptr:结构体中member成员的地址;
- type:结构体类型;
- member:结构体中名为member的成员;
通过下面测试例程来说明:
#include "stdio.h"
#include "stdlib.h"
#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) );})
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
struct Grade {
short Chinese;
short Math;
short English;
};
struct Student {
char name[20];
char sex;
short age;
struct Grade gra;
char addr[50];
};
int main(void)
{
struct Student stu = {
"jack",
'F',
20,
{99, 98, 97},
"North Garden Road"
};
printf("%#x\n", &stu);
struct Student *stu_ = list_entry(&stu.gra, struct Student, gra);
printf("%#x\n", stu_); // stu_ should be equal to &stu
printf("name = %s\n", stu_->name);
printf("sex = %c\n", stu_->sex);
printf("age = %d\n", stu_->age);
printf("gra = %d %d %d\n", stu_->gra.Chinese, stu_->gra.Math, stu_->gra.English);
printf("addr = %s\n", stu_->addr);
return 0;
}
已知stu.gra,通过list_entry得到了stu_。stu_实际上就是变量stu的首地址,然后就可以使用stu_来访问其它成员了。
执行结果如下:
0x5dd100f0
0x5dd100f0
name = jack
sex = F
age = 20
gra = 99 98 97
addr = North Garden Road
原理解析
list_entry直接拿来用就可以了,但是研究下它的实现原理,更有意思。
先来看offsetof():
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
将0地址,强转成 TYPE 类型的指针,然后指向 MEMBER 成员,再取其地址—— 这不就是 MEMBER 成员相对0地址的偏移量吗?!
再看:
const typeof( ((type *)0)->member ) *__mptr = (ptr);
将0地址强转成 type 类型的指针,然后指向其 member 成员,再调用 typeof() 获取其类型,也就是 member 成员的类型。再用其定义一个同类型的指针 __mptr 并指向ptr,其实就是将 ptr 地址暂存到了临时变量 __mptr 中。
这有个疑问:为什么要再定义一个临时指针存放 ptr 呢?直接用 ptr 不就好了?这样container_of()就可以更加简化了。有没有大神能解惑呢?
再看
(type *)( (char *)__mptr - offsetof(type,member) );
__mptr 即 ptr,即结构体成员member的地址,减去“member成员相对0地址的偏移量”,那不就是结构体的首地址嘛。
结合上面的测试例程,示意图如下: