Linux内核中双向列表的实现

0. 说明

作者:Gao Peng<gaopenghigh@gmail.com>
本文章由Gao Peng编写,转载请注明出处。



1. list_head数据结构

为了理解内核中双向链表的实现,我们先来了解一下list_head数据结构:
/* <linux/types.h> */
struct list_head {
    struct list_head *next, *prev;
};

一个双向链表的元素包含一个类型为list_head的变量,list_head结构里面,next和prev分别表示通用双向列表向前和向后的指针元素。需要注意的是,入下图所示,next和prev所指向的是另一个list_head结构的地址,而非这个链表元素的地址。(要得到这个链表元素的地址,可以用list_entry()宏来得到,具体见下面)

对包含了list_head的双向列表,有以下的处理函数和宏:
 LIST_HEAD(name)               初始化一个名字叫做name的链表头
 list_add(n, p)                把n指向的元素插入p指向的元素之后
 list_add_tail(n, p)           把n指向的元素插入p指向的元素之前
 list_del(p)                   删除p所指向的元素
 list_empty(p)                 检查p指向的链表是否为空
 list_entry(p, t, m)           返回类型为t的数据结构的地址,其中类型t中含有list_head字段,
                               且list_head字段的地址为p,名字为m。也就是在链表中找到一个元素,
                               这个元素的类型为t,这个元素里面包含了类型为list_head的名称为m的属性。
 list_for_each(p, h)           对表头地址h指定的链表进行扫描,在每次循环时,通过p返回指向链表元素的list_head结构的指针。
 list_for_each_entry(p, h, m)  与list_for_each类似,只是返回的是链表元素的指针,也就是返回包含了
                               地址为p且名称为m的list_head的元素的指针,而不是list_head结构本身的地址。




2. list_entry宏的实现

作为一个例子,我们来看看list_entry宏是怎么实现的。下面的叙述中SRC表示Linux内核源码所在的目录。
在SRC/include/linux/list.h中有如下宏:
/**
 * list_entry - get the struct for this entry
 * @ptr:    the &struct list_head pointer.
 * @type:   the type of the struct this is embedded in.
 * @member: the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

这个container_of宏则在SRC/drivers/staging/rtl8192e/rtllib.h中定义:
#ifndef container_of
/**
 * container_of - cast a member of a structure out to the containing structure
 *
 * @ptr:    the pointer to the member.
 * @type:       the type of the container struct this is embedded in.
 * @member:     the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({            \
    const typeof(((type *)0)->member)*__mptr = (ptr);    \
    (type *)((char *)__mptr - offsetof(type, member)); })
#endif

看起来很有意思,我们仔细分析一下上面的这一段宏。

((type *)0)
把0强制转换为type类型的指针,它指向一个空的type类型

((type *)0)->member)*__mptr = (ptr)
从上面得到的空的type类型得到类型中名称为member的一项,把ptr强制转换为member的类型的指针,赋值给变量__mptr

offsetof(type, member)
这个宏得到的是在类型type的内存结构中,名称为member的一个属性相对预type的基地址的偏移量。这个宏的具体实现下面讲。

(type *)((char *)__mptr - offsetof(type, member))
把之前赋值好的__mptr强制转换为char指针类型,这样可以和offsetof得到的偏移值做减法,由member的地址减去member相对于type的偏移值,得到的就是type的地址了。最后再把这个地址转换为type类型的指针。这样我们就得到了"container"的指针。



3. offsetof宏的实现

offsetof的宏定义在SRC/include/linux/stddef.h中:
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif

还是同样的方法,把0强制转换为TYPE类型的指针,找到这个指针代表的TYPE元素的MEMBER属性,得到它的地址,由于是由0强制转换而来,所以这个地址就是MEMBER属性相对与TYPE结构在内存中的偏移地址。再把这个偏移地址转换为size_t类型。所谓size_t类型,也就是用sizeof函数返回值的类型,也就是用char来衡量的一个数据结构的大小的值。

事实上这里还涉及到字节对齐的问题。比如说
struct S1
{
    char c;
    int i;
};
S1 s1;

我们可能会想,char占1字节,int占4字节,那么sizeof(s1)等于5字节么?不是的,事实上sizeof(s1)等于8字节。这就是所谓的“字节对齐”,之所以要字节对齐是因为这样有助于加快计算机的取数速度。编译器默认会让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于等被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节。 字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
a. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
b. 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
c. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

上面说到的基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。比如有:
struct S3
{
    char c1;
    S1 s;
    char c2
};

S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。


参考资料:
《深入理解Linux内核》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值