Linux内核入门(三)—— C语言基本功

    linux是一个复杂的软件系统,其内核源代码往往会用到一些在应用程序设计中部常见语言成分和编程技巧,这些都是分析内核源代码的基本功,希望大家能重点掌握。

    首先,gcc编译器从语言里吸收了“inline”和“const”。inline函数的大量使用,有利于提高运行效率,由此相当一部分代码从.c文件移入了.h文件。

    gcc增加了一种新的基本数据类型“long long int”用于支持64位CPU结构。

    许多C语言都支持一些“属性描述符”,如“aligned”,“packed”等。相当于一些新的保留字。但是,在原来的C语言这些词并非保留字,而是一 些普通的变量,这样就会产生一些冲突。例如:inline在老的代码中已经是作为变量在使用了,为了解决这个问题,gcc将作为保留字的“inline” 设计成__inline__,这样就不会冲突了。

    gcc还支持一个保留字“attribute”,用来作属性描述。如:attribute__ <<packed>>,这样packed就成为属性描述符(一种特殊保留字)而非变量了。

    gcc中有大量对宏操作的使用,大家肯定会对内核代码中的一些宏操作的定义方式感到不解,如:

#define DUMP_WRITE(addr, nr) do{ memcpy(bufp,addr,nr);
                                 bufp+=nr;}while(0)

这是必须的,是为了防止在IF-ELSE语句中使用该宏定义时发生错误。

    linux内核代码中大量使用链表,但其使用方法与我们在《数据结构》中学到的不大一样,大家可以回顾一下我们在课堂上学习链表的时候,通常除了对其数据 结构进行了定义,还定义了若干对该结构的操作。但对于大量使用链表的linux内核来说,如果定义了一个结构就要定义其相关的操作的话,显然代码量不小。 为了提高效率,内核采用了一套通用的,一般的,可以用到各种不同数据结构的队列操作。在include/linux/ list.h中,有如下申明:

struct list_head {
    struct list_head *next, *prev;
};

于是,其余数据结构如inode等将其引用而作为一个成员,就可以完成下面将要讲到的若干操作而不去单独定义了。引用该数据结构的结构我们称为宿主结构

    1.定义和初始化

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) /
    struct list_head name = LIST_HEAD_INIT(name)

    需要注意的是,Linux 的每个双循环链表都有一个链表头,链表头也是一个节点, 只不过它不嵌入到宿主数据结构中,即不能利用链表头定位到对应的宿主结构,但可以由之获得虚拟的宿主结构指针。

    LIST_HEAD()宏可以同时完成定义链表头,并初始化这个双循环链表为空。

    静态定义一个list_head 类型变量,该变量一定为头节点。 name为struct list_head{}类型的一个变量, &(name)为该结构体变量的地址。用name结构体变量的始地址将该结构体变量进行初始化。

#define INIT_LIST_HEAD(ptr) do { /
    (ptr)->next = (ptr); (ptr)->prev = (ptr); /
} while (0)

    动态初始化一个已经存在的list_head对象,ptr为一个结构体的指针, 这样可以初始化堆栈以及全局区定义的list_head对象。 ptr使用时候,当用括号,(ptr),避免ptr为表达式时宏扩展带来的异常问题。 此宏很少用于动态初始化内嵌的list对象,主要是链表合并或者删除后重新初始化头部。 若是在堆中申请了这个链表头,调用INIT_LIST_HEAD()宏初始化链表节点, 将next和prev指针都指向其自身,我们就构造了一个空的双循环链表。

    2.6内核中内联函数版本如下:
        static inline void INIT_LIST_HEAD(struct list_head *list)
        {
            list->next = list;
            list->prev = list;
        }
    此时的参数有明确的类型信息struct list_head,同时可以看出其为指针,list无须象宏中那样(), 即使参数为表达式,其也是求值后再作为参数传入的。内联函数有严格的参数类型检查, 同时不会出现宏函数扩展带来的异常问题,但是运行效率和空间效率与宏函数一致。

    2.通过对列头head链入一个新队列new

     所有链表(包括添加、删除、移动和拼接等)操作都是针对数据结构list_head进行的。 提供给用户的的添加链表的操作有两种:表头添加和表尾添加。注意到, Linux双循环链表中有一个链表头,表头添加是指添加到链表头之后,而表尾添加则是添加到链表头的prev所指链表节点之后。

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;
}

普通的在两个非空结点中插入一个结点,注意new、prev、next都不能是空值。 Prev可以等于next,此时在只含头节点的链表中插入新节点。 参数new指向将要链入队列的宿主数据结构(如inode、page等)内部指定的list_head数据结构。

static __inline__ void list_add(struct list_head *new,
                                struct list_head *head)
{
    __list_add(new, head, head->next);
}

    在head和head->next两指针所指向的结点之间插入new所指向的结点。 即:在head指针后面插入new所指向的结点。Head并非一定为头结点。 当现有链表只含有一个头节点时,上述__list_add(new, head, head->next)仍然成立。

static inline void list_add_tail(struct list_head *new,
                                 struct list_head *head)
{
    __list_add(new, head->prev, head);
}

    在结点指针head所指向结点的前面插入new所指向的结点。当head指向头节点时,也相当于在尾结点后面增加一个new所指向的结点。 。

    注意:head->prev不能为空,即若head为头结点,其head->prev当指向一个数值,一般为指向尾结点,构成循环链表。

    上述三个函数实现了添加一个节点的任务,其中__list_add()为底层函数,“__”通常表示该函数是底层函 数,供其他模块调用,此处实现了较好的代码复用,list_add和list_add_tail虽然原型一样,但调用底层函数__list_add时传递 了不同的参数,从而实现了在head指向节点之前或之后添加新的对象

    3.脱链的操作

    如果要从链表中删除某个链表节点,则可以调用list_del或list_del_init。 需要注意的是,上述操作均仅仅是把节点从双循环链表中拿掉, 用户需要自己负责释放该节点对应的数据结构所占用的空间,而这个空间本来就是用户分配的。

static __inline__ void __list_del(struct list_head * prev,
                                  struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}

     在prev和next指针所指向的结点之间,两者互相所指。在后面会看到: prev是待删除的结点的前面一个结点,next是待删除的结点的后面一个结点。

static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}

    表 示删除entry所指的结点,同时将entry所指向的结点指针域封死。 对LIST_POISON1,LIST_POISON2的解释说明: Linux 内核中解释:These are non-NULL pointers that will result in page faults under normal circumstances, used to verify that nobody uses non-initialized list entries.
    #define LIST_POISON1 ((void *) 0x00100100)
    #define LIST_POISON2 ((void *) 0x00200200)
常规思想是:entry->next = NULL; entry->prev = NULL; 保证不可通过该节点进行访问。

static inline void list_del_init(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    INIT_LIST_HEAD(entry);
}

    删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除节点为作为链表头构建一个新的空双循环链表。

    4.移动节点操作

    Linux还提供了两个移动操作:list_move和list_move_tail。

static inline void list_move(struct list_head *list,                              struct list_head *head)
{
    __list_del(list->prev, list->next);
    list_add(list, head);
}

    将list结点前后两个结点互相指向彼此,删除list指针所指向的结点, 再将此结点插入head,和
head->next两个指针所指向的结点之间。 即:将list所指向的结点移动到head所指向的结点的后面。

static inline void list_move_tail(struct list_head *list,                                   struct list_head *head)
{
    __list_del(list->prev, list->next);
    list_add_tail(list, head);
}

    删除了list所指向的结点, 将其插入到head所指向的结点的前面,如果head->prev指向链表的尾结点的话,就是将list所指向的结点插入到链表的结尾。

    5.链表判空

    Linux中由list-head构成的双向循环链表中,通常有一个头节点,其不含有有效信息, 初始化时prev和next都指向自身。判空操作是判断除了头节点外是否有其他节点。

static inline int list_empty(const struct list_head *head)
{
    return head->next == head;
}

    测试链表是否为空,如果是只有一个结点,head,head->next,head->prev都指向同一个结点, 则这里会返回1,表示空;但这个空不是没有任何结点,而是只有一个头结点, 因为头节点只是纯粹的list节点,没有有效信息,故认为为空。

static inline int list_empty_careful(const struct list_head *head)
{
    struct list_head *next = head->next;
    return (next == head) && (next == head->prev);
}

    1.只有一个头结点head,这时head指向这个头结点,head->next,head->prev指向head, 即:head==head->next==head->prev,这时候list_empty_careful()函数返回1。

    2.有两个结点,head指向头结点,head->next,head->prev均指向后面那个结点, 即:head->next==head->prev,而head!=head->next,head!=head->prev. 所以函数将返回0。

    3.有三个及三个以上的结点,这是一般的情况,自己容易分析了。

    注意:这里empty list是指只有一个空的头结点,而不是毫无任何结点。并且该头结点必须其
head->next==head->prev==head。

    6.链表合并

    Linux还支持两个链表的拼接,提供给用户的具体函数是list_splice和list_splice_init:

static inline void __list_splice(struct list_head *list,
                                 struct list_head *head)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;
    struct list_head *at = head->next;
    
    first->prev = head;
    head->next = first;
    
    last->next = at;
    at->prev = last;
}

    将一个非空链表(list)插入到另外一个链表的head位置之后中。不作链表是否为空的检查,由调用者默认保证。 因为每个链表只有一个头节点,将空链表插入到另外一个链表中是没有意义的。但被插入的链表可以是空的。

static inline void list_splice(struct list_head *list,
                               struct list_head *head)
{
    if (!list_empty(list))
        __list_splice(list, head);
}

    这种情况会丢弃list所指向的头结点,这是特意设计的,因为两个链表有两个头结点,要去掉一个头结点。只要list非空链,head无任何限制,该程序都可以实现链表合并。

static inline void list_splice_init(struct list_head *list,
                                    struct list_head *head)
{
    if (!list_empty(list)){
        __list_splice(list, head);
        INIT_LIST_HEAD(list);
    }
}

    将一个链表的有效信息合并到另外一个链表后,重新初始化空的链表头。

    7.返回宿主结构操作(重点)

    如果需要有某种数据结构的队列,就在这种数据结构定义内部放上一个list_head数据结构。 例如,建立数据结构foo链表的方式是,在foo的定义中,嵌入了一个list_head成员list。 这里foo就是所指的"宿主":

    typedef struct foo {
        …
        struct list_head list;
        …
    };

    但是,如何通过list_head成员访问到宿主结构项呢?毕竟list_head不过是个连接件,而我们需要的是一个"特定"的数据结构链表。先介绍几个基本宏:offsetof、typeof、containerof。

    -------/linux/stddef.h----------------
    #define __compiler_offsetof(a,b) __builtin_offsetof(a,b)
而__builtin_offsetof()宏就是在编译器中已经设计好了的函数,直接调用即可。
    -------------------------------
    #undef offsetof //取消先前的任何定义,可以保证下面的定义生效
    #ifdef __compiler_offsetof
    #define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
    #else
    #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
    #endif

解释:一共4步
1. ( (TYPE *)0 ) 0地址强制 "转换" 为 TYPE结构的指针;
2. ((TYPE *)0)->MEMBER 访问结构中的数据成员;
3. &( ( (TYPE *)0 )->MEMBER)取出数据成员的地址;
4. (size_t)(&(((TYPE*)0)->MEMBER))结果转换类型.巧妙之处在于将0转换成(TYPE*),结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址;

举例说明:
#include<stdio.h>
typedef struct _test
{
    char i;
    int j;
    char k;
}Test;
int main()
{
    Test *p = 0;
    printf("%p/n", &(p->k));
}
这里使用的是一个利用编译器技术的小技巧(编译器自动算出成员的偏移量),即先求得结构成员变量在结构体中的相对于结构体的首地址的偏移地址,然后根据结 构体的首地址为0,从而得出该偏移地址就是该结构体变量在该结构体中的偏移,即:该结构体成员变量距离结构体首的距离。在offsetof()中,这个 member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。对于给定一个结 构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

---------------------typeof()--------------------
unsigned int i;
typeof(i) x;
x=100;
printf("x:%d/n",x);
typeof() 是 gcc 的扩展,和 sizeof() 类似。
------------------------
在 container_of 宏中,它用来给 typeof() 提供参数,以获得 member 成员的数据类型;

---------------container_of()--------------------
container_of() 来自/linux/kernel.h
内核中的注释: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) );})
自己分析:

    1.(type *)0->member为设计一个type类型的结构体,起始地址为0,编译器将结构体的起始的地址加上此结构体成员变量的偏移得到此结构体成员变 量的地址,由于结构体起始地址为0,所以此结构体成员变量的偏移地址就等于其成员变量在结构体内距离结构体开始部分的偏移量。即:&(type *)0->member就是取出其成员变量的偏移地址。而其等于其在结构体内的偏移量:即为:(size_t)(& ((type *)0)->member)经过size_t的强制类型转换后,其数值为结构体内的偏移量。该偏移量这里由offsetof()求出。

    2.typeof( ( (type *)0)->member )为取出member成员的变量类型。用其定义__mptr指针;ptr为指向该成员变量的指针。__mptr为member数据类型的常量指针,其指向ptr所指向的变量处。

    3.(char *)__mptr转换为字节型指针。(char *)__mptr - offsetof(type,member) )用来求出结构体起始地址(为char *型指针),然后(type *)( (char *)__mptr - offsetof(type,member) )在(type *)作用下进行将字节型的结构体起始指针转换为type *型的结构体起始指针。

    这就是从结构体某成员变量指针来求出该结构体的首指针。指针类型从结构体某成员变量类型转换为该结构体类型。

    介绍了上面的几种基本宏后,对list_entry的理解就容易了。 list_entry()宏,获取当前list_head链表节点所在的宿主结构项。 第一个参数为当前list_head节点的指针,即指向宿主结构项的list_head成员。 第二个参数是宿主数据结构的定义类型。 第三个参数为宿主结构类型定义中list_head成员名。

#define list_entry(ptr, type, member) /
container_of(ptr, type, member)

扩展替换即为:

#define list_entry(ptr, type, member) /
        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

例:page = list_entry(curr, struct page, list)
则:page = ((struct page *)((char *)(curr)-(unsigned long)(&((struct page *)0)->list)))
    curr是page结构内部成分list的地址,而我们要找的是list所处page结构本身的地址,所以要从curr减去一个位移量,即成份list在 page内部的位移量:&((struct page *)0)->list表示当结构page正好在地址0上时list的(绝对)地址。

获取宿主对象指针的原理如上图所示。我们考虑list_head类型成员member相对于宿主结构(类型为type)起始地址的偏移量。对于所有 该类型的宿主对象,这个偏移量是固定的。并且可以在假设宿主对象地址值为0,通过返回member成员的地址获得,即等于(unsigned long)(&((type *)0)->member)。这样,将当前宿主对象的"连接件"地址(ptr)减去这个偏移量,得到宿主对象地址,再将它转换为宿主数据结构类型的 指针。

    需要重申的是,链表头没有被嵌入到宿主对象中,因此对链表头执行宿主对象指针获取操作是没有意义的。

    8.遍历

    (一)List-head链表遍历

    遍 历是双循环链表的基本操作,为此Linux定义了一些宏。 list_for_each对遍历链表中的所有list_head节点,不涉及到对宿主结构的处理。list_for_each实际是一个 for 循环,利用传入的指向list_head结构的指针作为循环变量,从链表头开始(并跳过链表头),逐项向后移动指针,直至又回到链表头。 :

#define list_for_each(pos, head) /
    for (pos = (head)->next; prefetch(pos->next), pos != (head); /
        pos = pos->next)

    head为头节点,遍历过程中首先从(head)->next开始,当pos==head时退出,故head节点并没有访问,这和list 结构设计有关,通常头节点就是纯粹的list结构,不含有其他有效信息,或者头节点含有其他信息,如内核PCB链表中的头节点为idle任务,但其不参予 比较优先级,因此此时头节点只是作为双向链表遍历一遍的检测标志。

    为提高遍历速度,还使用了预取:

-----asm-x86_64/processor.h---prefetch()---------
static inline void prefetch(void *x)
{
    asm volatile("prefetcht0 %0" :: "m" (*(unsigned long *)x));
}

将x指针作强制类型转换为unsigned long *型,然后取出该内存操作数,送入高速缓存。

----------------__list_for_each()-----------------
#define __list_for_each(pos, head) /
    for (pos = (head)->next; pos != (head); pos = pos->next)

list_for_each()有prefetch()用于复杂的表的遍历,而__list_for_each()无prefetch()用于简单的表的遍历,此时表项比较少,无需缓存。

#define list_for_each_prev(pos, head) /
    for (pos = (head)->prev; prefetch(pos->prev), pos != (head); /         pos = pos->prev)

反向遍历节点

    如果在遍历过程中,包含有删除或移动当前链接节点的操作,由于这些操作会修改遍历指针,这样会导致遍历的中断。这种情况下,必须使用list_for_each_safe宏,在操作之前将遍历指针缓存下来:

/*
 * list_for_each_safe - iterate over a list safe against removal of list entry
 */
#define list_for_each_safe(pos, n, head) /
    for (pos = (head)->next, n = pos->next; pos != (head); /
        pos = n, n = pos->next)

在for循环中n暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。也就是说你可以遍历完当前节点后将其删除,同时可以接着访问下一个节 点,遍历完毕后就只剩下一个头节点。这就叫safe。十分精彩。典型用途是多个进程等待在同一个等待队列上,若事件发生时唤醒所有进程,则可以唤醒后将其 依次从等待队列中删除。

    (二)遍历宿主对象

    如果只提供对list_head结构的遍历操作是远远不够的,我们希望实现的是对宿主结构的遍历,即在遍历时直接获得当前链表节点所在的宿主结构 项,而不是每次要同时调用list_for_each和list_entry。对此,Linux提供了list_for_each_entry()宏,第 一个参数为传入的遍历指针,指向宿主数据结构,第二个参数为链表头,为list_head结构,第三个参数为list_head结构在宿主结构中的成员 名。

#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))

    我们举个例子,这是用于嵌套的结构体中的宏:
    struct example_struct
    {
        struct list_head list;
        int priority;
        ... //其他结构体成员
    };
    struct example_struct *node = list_entry(ptr,struct example_struct,list);

    对比list_entry(ptr,type,member)可知有以下结果: 其中list相当于member成员,struct example_struct相当于type成员,ptr相当于ptr成员。而list{}成员嵌套于example_struct{}里面。ptr指向 example_struct{}中的list成员变量的。在list_entry()作用下,将ptr指针回转指向struct example_struct{}结构体的开始处。

    pos当指向外层结构体,比如指向struct example_struct{}的结点,最开始时候,head指向链表结构体struct list_head{}的头结点,头节点不含有有效信息,(head)->next则指向第一个外层结点的内嵌的链表结点struct list_head{} list,由此得出的pos当指向第一个有效结点。member即是指出该 list为其内嵌的结点。

    思路:用pos指向外层结构体的结点,用head指向内层嵌入的结构体的结点。用 (head)->next,pos->member.next(即:ptr->list.next)来在内嵌的结构体结点链表中遍历。 每遍历一个结点,就用list_entry()将内嵌的pos->member.next指针回转为指向该结点外层结构体起始处的指针,并将指针进 行指针类型转换为外层结构体型pos。&pos->member! = (head)用pos外层指针引用member即:list成员,与内层嵌入的链表之头结点比较来为循环结束条件。

    当遍历到头节点时,此时并没有pos这样一个type类型数据指针,而是以member域强制扩展了一个type类型的pos指针,此时其member域的地址就是head指针所指向的头节点,遍历结束,头节点的信息没有被访问。

#define list_for_each_entry_reverse(pos, head, member) /
    for (pos = list_entry((head)->prev, typeof(*pos), m+ember); /
        prefetch(pos->member.prev), &pos->member != (head); /
        pos = list_entry(pos->member.prev, typeof(*pos), member))

    遍历倒置,分析与前类似。

    如果遍历不是从链表头开始,而是从已知的某个pos结点开始,则可以使用 list_for_each_entry_continue(pos,head,member)。但为了确保pos的初始值有效,Linux专门提供了一 个list_prepare_entry(pos,head,member)宏,如果pos有值,则其不变;如果没有,则从链表头强制扩展一个虚pos指 针。将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

    内核中的list_prepare_entry()的代码:

#define list_prepare_entry(pos, head, member) /
    ((pos) ? : list_entry(head, typeof(*pos), member))

分析: :前面是个空值,即:若pos不为空,则pos为其自身。等效于: (pos)? (pos): list_entry(head,typeof(*pos),member) 注意内核格式::前后都加了空格。

#define list_for_each_entry_continue(pos, head, member) /
    for (pos = list_entry(pos->member.next, typeof(*pos), member); /
        prefetch(pos->member.next), &pos->member != (head); /
        pos = list_entry(pos->member.next, typeof(*pos), member))

    此时不是从头节点开始遍历的,但仍然是以头节点为结束点的,即没有遍历完整个链表。 要注意并不是从pos开始的,而是从其下一个节点开始的,因为第一个有效pos是从pos->member.next扩展得到的。

/**
 * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
 * @pos: the type * to use as a loop counter.
 * @n: another type * to use as temporary storage
 * @head: the head for your list.
 * @member: the name of the list_struct within the struct.
 */
#define list_for_each_entry_safe(pos, n, head, member) /
    for (pos = list_entry((head)->next, typeof(*pos), member), /
        n = list_entry(pos->member.next, typeof(*pos), member); /
        &pos->member != (head); /
        pos = n, n = list_entry(n->member.next, typeof(*n), member))

它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

#define list_for_each_entry_safe_continue(pos, n, head, member) /
    for (pos = list_entry(pos->member.next, typeof(*pos), member), /
        n = list_entry(pos->member.next, typeof(*pos), member); /
        &pos->member != (head); /
        pos = n, n = list_entry(n->member.next, typeof(*n), member))

    9.hlist哈希链表

    在include/Linux/list.h中有list链表与hlist哈希链表结构的定义,下面都列出它们的定义,可以对比一下:

struct list_head {
    struct list_head *next, *prev;
};
struct hlist_head {
    struct hlist_node *first;
};
struct hlist_node {
    struct hlist_node *next, **pprev;
};

    双头(next,prev)的双链表对于Hash表来说“过于浪费”,因而另行设计了一套Hash表专用的hlist数据结构——单指针表头双循 环链表,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的Hash表中存储的表头就能减少一半的空间消耗。

    pprev因为hlist不是一个完整的循环链表而不得不使用。在list中,表头和节点是同一个数据结构,直接用prev没问题;在hlist 中,表头没有prev,也没有next,只有一个first。为了能统一地修改表头的first指针,即表头的first指针必须修改指向新插入的节 点,hlist就设计了pprev。hlist节点的pprev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则 是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的“*(node->pprev)”访问和修改前节点的next(或first)指针。

    下面是hlist中常用的几个宏:

#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
#define INIT_HLIST_NODE(ptr) ((ptr)->next = NULL, (ptr)->pprev = NULL)

    下面只列出hlist_add_before操作函数,其他hlist链表操作函数操作方法类似。这个函数中的参数next不能为空。它在next前面加入了n节点。函数的实现与list中对应函数类似。

static inline void hlist_add_before(struct hlist_node *n,
                                    struct hlist_node *next)
{
    n->pprev = next->pprev;
    n->next = next;
    next->pprev = &n->next;
    *(n->pprev) = n;
}

    10.RCU操作保护的链表

    RCU(Read-Copy Update)通过延迟写操作来提高同步性能,具体请参见第13章。这里只分析具有RCU的链表。

    RCU常用来保护读操作占多数的链表与数组。具有RCU的链表的操作函数与普通链表操作函数的区别是在函数名后加上了_rcu,如list_for_each_rcu函数。

    函数list_for_each_rcu的功能是遍历一个rcu保护的链表。其中,参数pos表示用来做链表位置计数的&struct list_head结构,参数head表示链表头。只要遍历被rcu_read_lock()保护,使用诸如list_add_rcu()的函数对链表同 时访问是安全的。

    函数List_for_each_rcu列出如下:

#define list_for_each_rcu(pos, head) /
    for (pos = (head)->next, prefetch(pos->next); pos != (head); /
        pos = rcu_dereference(pos->next), prefetch(pos->next))

    函数rcu_dereference在RCU读临界部分中取出一个RCU保护的指针。在需要内存屏障的体系中进行内存屏障(目前只有Alpha体系需要)。

    11.Linux双循环链表综合实例

    本文例子来自 http://isis.poly.edu/kulesh/stuff/src/klist/ ,只是对其中注释部分作了翻译。

#include <stdio.h>
#include <stdlib.h>
#include "list.h"
struct kool_list{
    int to;
    struct list_head list;
    int from;
};
int main(int argc, char **argv){
    struct kool_list *tmp;
    struct list_head *pos, *q;
    unsigned int i;
    struct kool_list mylist;
    INIT_LIST_HEAD(&mylist.list);
    /* 您也可以使用宏LIST_HEAD(mylist)来声明并初始化这个链表 */
    /*向链表中添加元素*/
    for(i=5; i!=0; --i){
        tmp= (struct kool_list *)malloc(sizeof(struct kool_list));
        /*INIT_LIST_HEAD(&tmp->list); 调用这个函数将初始化一个动态分配的list_head。也可以不调用它,因为在后面调用的add_list()中将设置next和prev域。*/
        printf("enter to and from:");
        scanf("%d %d", &tmp->to, &tmp->from);
        /*将tmp添加到mylist链表中*/
        list_add(&(tmp->list), &(mylist.list));
        /*也可以使用list_add_tail()将新元素添加到链表的尾部。*/
    }
    printf("/n");
    /*现在我们得到了数据结构struct kool_list的一个循环链表,我们将遍历这个链表,并打印其中的元素。*/
    /*list_for_each()定义了一个for循环宏,第一个参数用作for循环的计数器,换句话说,在整个循环过程中它指向了当前项的list_head。第二个参数是指向链表的指针,在宏中保持不变。*/
    printf("traversing the list using list_for_each()/n");
    list_for_each(pos, &mylist.list){
        /*此刻:pos->next指向了下一项的list变量,而pos->prev指向上一项的list变量。而每项都是 struct kool_list类型。但是,我们需要访问的是这些项,而不是项中的list变量。因此需要调用list_entry()宏。*/
        tmp= list_entry(pos, struct kool_list, list);
        /*给定指向struct list_head的指针,它所属的宿主数据结构的类型,以及它在宿主数据结构中的名称,list_entry返回指向宿主数据结构的指针。例如,在上面 一行, list_entry()返回指向pos所属struct kool_list项的指针。*/
        printf("to= %d from= %d/n", tmp->to, tmp->from);
    }
    printf("/n");
    /* 因为这是一个循环链表,我们也可以向前遍历。只需要将list_for_each替换为list_for_each_prev。我们也可以使用list_for_each_entry()遍历链表,在给定类型的项间进行循环。例如:*/
    printf("traversing the list using list_for_each_entry()/n");
    list_for_each_entry(tmp, &mylist.list, list)
    printf("to= %d from= %d/n", tmp->to, tmp->from);
    printf("/n");
    /*下面将释放这些项。因为我们调用list_del()从链表中删除各项,因此需要使用list_for_each()宏的"安全"版本,即 list_for_each_safe()。务必注意,如果在循环中有删除项(或把项从一个链表移动到另一个链表)的操作,必须使用这个宏。*/
    printf("deleting the list using list_for_each_safe()/n");
    list_for_each_safe(pos, q, &mylist.list){
        tmp= list_entry(pos, struct kool_list, list);
        printf("freeing item to= %d from= %d/n", tmp->to, tmp->from);
        list_del(pos);
        free(tmp);
    }
    return 0;
}

    注意:上述代码在使用gcc编译时需要加上__KERNEL__定义。

 

阅读更多
换一批

没有更多推荐了,返回首页