本文简单介绍一下Linux内核中的双向链表。
1. 第一个宏 offsetof
首先,看一个宏 offsetof. 这个宏定义在<kernel.h>中。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
这个宏解决什么问题呢?看下面三个结构体的定义。
struct list_head {
struct list_head *next, *prev;
};
struct file {
struct list_head f_list;
// still other members
};
struct super_block {
// firstly, here are some members, e.g.
int data;
struct list_head s_files;
// also, there are other members here.
};
可以看到,在结构体file和结构体super_block中,都包含有list_head结构体的实例。那么,如何知道相应的list_head结构体的实例在file和super_block中的相对位移分别是多少呢?对了,这个时候,就可以用到offsetof这个宏了。代码如下:
int a = offsetof(file, f_list);
int b = offsetof(super_block, s_files);
printf("a = %d\n", a); // 0
printf("b = %d\n", b); // 8
这个宏是怎么做到的呢?首先,它将0强制转换为结构体TYPE的指针类型,然后取结构体TYPE的成员变量MEMBER的地址。这也行得通,有意思。
下面是一个验证的小程序:
#include <stdio.h>
#define myoffsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
struct list_head {
struct list_head *next, *prev;
};
struct file {
struct list_head f_list;
// still other members
};
struct super_block {
// firstly, here are some members, e.g.
int data;
struct list_head s_files;
// also, there are other members here.
};
int main()
{
int a = myoffsetof(struct file, f_list);
int b = myoffsetof(struct super_block, s_files);
printf("a = %d\n", a); // 0
printf("b = %d\n", b); // 8
printf("size of list_head = %d", sizeof(struct list_head)); // 16
return 0;
}
2. 链表的数据结构与容器机制
链表节点的数据结构就是上面的struct list_head (定义在<list.h>中),不再赘述。在实际使用时,它是被内嵌入其他的数据结构中的。这是利用了容器机制(此容器非彼容器docker)。
所谓容器机制,看下面的代码:
struct A {
...
struct B {
...
} element;
} container;
A包含了B,故A称之为B的一个容器。
而在Linux内核的实现中,A就是一些实用的数据结构,如上述的super_block和file;而B就是链表节点list_head.
3. 第二个宏 container_of
先看定义:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); })
先解释一下3个参数:
- ptr: 指向成员数据的指针。在双向链表的例子里,就是链表节点的实例的地址。
- type: 容器结构体的类型,即上面的A或file或super_block.
- member: 成员在容器内的名称,即上述的f_list或s_files.
这个宏是用来干什么的呢?
举个栗子:已知一个结构体struct file的实例以及它的内部成员f_list实例的内存地址,求这个struct file实例的内存地址。
下面给出一段示例程序:
#include <stdio.h>
#define myoffsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - myoffsetof(type, member) ); })
#define my_container_of(ptr, type, member) \
( (type *)( (char *)(ptr) - myoffsetof(type, member) ) )
struct list_head {
struct list_head *next, *prev;
};
struct file {
struct list_head f_list;
// still other members
};
int main()
{
struct file abc;
struct file *p1 = &abc;
printf("p1 = %0x\n", p1); // ffffcbe0
struct file *p2 = container_of(&(abc.f_list), struct file, f_list);
printf("p2 = %0x\n", p2); // ffffcbe0
struct file *p3 = my_container_of(&(abc.f_list), struct file, f_list);
printf("p3 = %0x\n", p3); // ffffcbe0
return 0;
}
其实,笔者还没有完全理解container_of宏的实现中为何要有第一行,所以笔者自己实现了一个my_container_of宏,貌似也能工作。
4. 第3个宏 list_entry
这个宏 list_entry 其实和 container_of 完全一样。唯一的区别是,list_entry宏定义在<list.h>中,而container_of宏定义在 <kernel.h> 中。故笔者猜测它们的区别其实是使用场景不同,尽管实现完全一样。比如,对于双向链表的应用而言,我们就用list_entry,而不用container_of,这从代码可读性的角度来说,要好一些。
#define list_entry(ptr, type, member) \
container_of (ptr, type, member)
简单来讲,list_entry(ptr, type, member)就相当于是 (type *)((char *)(ptr) - offsetof(type, member)), 即,通过成员的地址,找到容器对象的地址。而用在双向链表这个例子里,就是:通过链表节点的地址,找到容器对象的地址。
5. 第4个宏 list_for_each_entry
先来看定义:
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
prefetch(pos->member.next), &pos->member != (head); \
pos = list_entry(pos->member.next, typeof(*pos), member))
解释一下这里的3个参数(其实即使不解释,从上节对list_entry的解释也能猜出来):
- pos: 容器对象的指针
- head: 链表的表头
- member: 容器结构体内部的struct list_head对象的名称
由上节对list_entry的理解,这里很容易看出:
1> for循环第一个分句表示:从包含链表第2个节点的容器对象的地址(即指针)开始遍历;
这里比较奇怪的是使用的是head->next这个链表节点地址,而不是直接用head;
2> for循环的第二个分句表明退出for循环的条件,即,当再次循环到head时就退出;
这样看起来,这个双向链表的头节点似乎并没有被遍历过;
另外,prefetch语句只是表示将数据优先从内存传输到CPU的高速缓存,不影响逻辑。
3> for循环的第三个分句,表示将当前容器对象的链表节点的下一个节点所对应的容器对象的地址赋给pos,即走到下一个容器对象。
下面给出一个笔者的示例程序:
#include <stdio.h>
#define myoffsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - myoffsetof(type, member) ); })
#define list_entry(ptr, type, member) container_of(ptr, type, member)
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
&pos->member != (head); \
pos = list_entry(pos->member.next, typeof(*pos), member))
struct list_head {
struct list_head *next, *prev;
};
struct file {
struct list_head f_list;
int inode_count;
};
int main()
{
struct file f1, f2, f3;
f1.inode_count = 1;
f2.inode_count = 2;
f3.inode_count = 3;
f1.f_list.next = &(f2.f_list);
f1.f_list.prev = &(f3.f_list);
f2.f_list.next = &(f3.f_list);
f2.f_list.prev = &(f1.f_list);
f3.f_list.next = &(f1.f_list);
f3.f_list.prev = &(f2.f_list);
struct file *pos = &f1;
list_for_each_entry(pos, &f1.f_list, f_list) {
printf("inode_count = %d\n", pos->inode_count);
}
// only print 2 and 3
return 0;
}
从该示例程序可以看出,确实,双向链表头节点所在的容器对象并没有被遍历到。
6. Linux内核中的实际使用
从《深入Linux内核架构》这本书的附录C的C.2.7节可以看到关于双向链表在内核中的一个实际用例。
原文是这么描述的: “遍历一个与超级块(struct super_block)相关联的所有文件(通过struct file表示),这些file实例包含在一个双链表中,表头位于超级块实例中,如下所示:”
struct super_block *sb = get_some_sb();
struct file *f;
list_for_each_entry(f, &sb->s_files, f_list) {
// handling code
}
注意!这里list_for_each_entry的第2个参数并不是struct file中的链表对象的地址,而是super_block中的链表对象的地址。这只能说明,这2个结构体内包含的是同一个链表。而如原文所述,表头是存放在super_block中的,而list_for_entry的遍历是从表头的下一个节点开始的,所以,这似乎就只能说明,super_block中存放的仅仅是一个空的表头,而struct file中存放的才是有内容的第二个链表节点。这一部分是笔者个人的猜测,有待日后的验证。
(完)