您的位置:首页 > 其它

之二:内核中list_head的理解

2014-11-02 10:07 281 查看

内核中list_head的理解

1.需求分析

我们在《数据结构》课程上,学习过双向链表。现在假定有下列需求。

需求1:设计一个结构体,结构体本身支持双向链表,你的实现可能会是:

方案1

struct myStruct
{
int myValue;
struct myStruct *prev;
struct myStruct *next;
};
void OperateOnMyStruct(struct myStruct*);


没错,这可以很好的工作。这种链表,节点的“前驱”与“后继”是同一个类型,因此可以在节点的“前驱”与“后继”上执行结构体支持的操作,比如将前期和后继传给OperateOnMyStruct函数。

需求2:设计1000个不同的结构体,要求这些结构体支持双向链表。

呃,方案1的方法显得冗余,需要为每个结构体定义前驱和后继节点,多少重复代码呀,但还是忍了吧。

需求3:设计1000个不同的结构体,要求这些结构体支持双向链表,并且,结构体对象可以同时在多个队列中。比如结构体structA的对象objA,可以同时在链表A、B、C中,用图来描述,大概是这个样子:



此时,方案1的方法只怕是力不从心了。

2.内核的实现

抬头审视一下,方案1的缺陷在哪里?似乎没问题呀,用设计模式的思想来审视下呢?呃,违反了“开放封闭”原则。

该怎么优化这个方案呢?用面向对象的思维来思考下:呃,“链表”的需求,是所有结构体的共同需求,实现一个最基本的类,来处理链表的操作,然后其他的结构体继承自该类。

慢着,这样还是无法处理同一个对象在多个队列中的需求呀。设计模式不是告诉我们“多用组合,少用继承”吗?

对呀,我们可以实现一个处理链表的类,这个链表类再组合到其他的结构体中。相当于链表类“寄生”到其他的结构体中,其他的结构体就是“宿主”。链表类就像一个“粘钩”,一方面通过自身的prev和next指针,构成一个链表,这部分是粘钩的钩子,另一方,通过寄生在其他的结构体中,就像粘钩带粘性的那一端,将结构体粘住。这样作为宿主的结构体也在链表中了,示意图如下。有了“粘钩”,一个对象想粘在几个链表上都可以。



内核就是这么干的,“链表类”的名字叫list_head,定义如下:

<include/linux/types.h>
[code]185 struct list_head {
186        struct list_head  *next, *prev;
187 };


结构体里面只是定义了双向链表的指针,因为内核是用C写的,所以“链表类”中的操作只能定义在结构体之外了,具体定义在<include/linux/list.h>中。关于链表的操作,没必要做过多的解释。但是到目前为止,我们还有一问题没解决:在方案1中,前驱或者后继节点本身就是目标结构体类型的指针,因此可以在它们身上执行结构体支持的操作;但在新的方案中,链表的节点类型为list_head指针,如何通过该list_head指针找到目标结构体即宿主结构体的指针呢?

这个问题可以等效为:已知一个结构体的定义,以及结构体中某个成员的地址,如何确定该成员所在的结构体对象的地址?比如如下结构体定义:

struct myStruct {
[code]    int index;
char names[NAME_LEN];
int key;
};
struct myStruct structSample;


则在内存中的布局如下:



已知成员变量key所在的地址,如何确定整个结构体的基址?更进一步,已知int*指针pkey指向结构体成员key,如何得到结构体的指针?从内存布局中我们可以看到,如果能够知道key相对结构体的偏移,那么拿key的地址减去偏移就可以了。所以问题转换为如何求得偏移。

key的偏移是个固定值,用成员的地址减去结构体的地址即可得到,假定结构体所在的地址为baseAddr则:

key的偏移 = &((struct myStruct *)baseAddr)->key - baseAddr

偏移量跟结构体所在的具体地址没有关系,那么如果假设结构体所在的地址为0呢?则有:

key的偏移 = &((struct myStruct *)0)->key – 0 = &((struct myStruct *)0)-> key

注意这里的技巧,这里将0强转成structmyStruct类型的指针,然后再通过操作符”->”引用结构体的成员。有人可能要担心0地址了,没关系,0地址虽然是非法地址,但我们又不去读写它,没有问题的。这里的地址强转以及结构体成员引用在编译期间的行为。

偏移确定以后,结构体所在的地址就可以得到了:

baseAddr = key的地址 - &((struct myStruct *)0)-> key

知道地址后,我们得到结构体指针:

myStruct *pStru = (myStruct *) (pKey - &((struct myStruct *)0)-> key )

一切似乎很顺利,但我们忽略了一个问题。指针的加减操作跟指针的类型相关,比如,如果是指向int型的指针,指针每次加1,其实际的地址加4,而如果是指向char型的指针,指针每次加1,其实际地址加1。而我们这里的偏移是以字节为单位的,所以我们要保证这里的计算也是以字节为单位,很简单,将key强转成char *即可,于是:

myStruct *pStru = (myStruct *) ((char*)pKey - &((struct myStruct *)0)-> key )

可见,我们只要知道结构体类型以及成员名,就可以根据成员的指针推导出结构体的指针。

内核中实现此功能的宏为list_entry,其定义仍然在<include/linux/list.h>中

<include/linux/list.h >
/**
345  *list_entry - get the struct for this entry
346  *@ptr:        the &struct list_headpointer.
347  *@type:       the type of the struct thisis embedded in.
348  *@member:     the name of the list_structwithin the struct.
349  */
350 #define list_entry(ptr, type, member) \
351        container_of(ptr, type, member)

<include/linux/kernel.h >
825 /**
826  *container_of - cast a member of a structure out to the containing structure
827  *@ptr:        the pointer to the member.
828  *@type:       the type of the containerstruct this is embedded in.
829  *@member:     the name of the memberwithin the struct.
830  *
831  */
832 #define container_of(ptr, type, member) ({                      \
833        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
834        (type *)( (char *)__mptr - offsetof(type,member) );})

<include/linux/stddef.h >
15 #undef offsetof
16 #ifdef __compiler_offsetof
17 #define offsetof(TYPE,MEMBER)__compiler_offsetof(TYPE,MEMBER)
18 #else
19 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)
20 #endif
21 #endif


list_entry宏有三个参数:list_head的地址、寄主结构体的类型、寄主结构中list_head成员的名字。宏展开后如下:

list_entry(ptr, type, member)
/*
*({ })返回块中最后一个表达式的值
*/
({
/*
*typeof为gcc的保留字,这里定义一个指针变量__mptr,指针指向member所属类型,
*并将成员的地址赋给它。
*/
consttypeof( ((type *)0)->member ) *__mptr = (ptr);
/*
*根据成员变量的地址以及成员变量相对结构体基址的偏移得到结构体所在的地址,
*得到地址后,将地址强转成结构体类型的指针
*/
(type *)((char *)__mptr - ((size_t) &((type *)0)->member) );
})


内核中将list_head作为一个通用的“基础设施”,当需要用到链表的特性时,就用这个基础设施来“搭建”代码。类似的,红黑树也被实现为一个通用的基础设施,具体的实现在<lib/rbtree.c>中,lib目录下还有队列fifo以及位图bitmap的实现
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: