第6章内核数据结构之链表

介绍Linux内核常用的数据结构,内核开发者应该尽可能地使用这些数据结构。这些通用数据结构中最有用的几个:

链表、队列、映射、二叉树。

6.1 链表

链表是Linux内核中最简单、最普通的数据结构。链表是一种存放和操作可变数量元素(称为节点)的数据结构。链表和静态数组的不同之处在于:链表所包含的元素都是动态创建并插入链表的,在编译时不必知道具体需要创建多少个元素。也因为链表中每个元素的创建时间各不相同,所以在内存中无须占用连续内存区。正是因为元素不连续地存放,所以各元素需要通过某种方式被连接在一起。于是每个元素都包含一个指向下一个元素的指针,当有元素加入链表或从链表中删除元素时,调整指向下一个节点的指针就可以了。

1、单向链表和双向链表

可用一种最简单的数据结构来表示一个链表:

/*一个链表中的一个元素*/

struct list_element {

             void *data;//有效数据

             struct list_element *next;//指向下一个元素的指针

};

图6-1 描述一个链表结构体

在有些链表中,每个元素还包含一个指向前一个元素的指针,可以同时向前和向后相互连接,这种链表被称为双向链表。那种只能向后连接的链表被称作单向链表。

表示双向链表的一种数据结构如下:

/*一个链表中的一个元素*/

struct list_element {

             void *data;//有效数据

              struct list_element *prev;//指向前一个元素的指针

             struct list_element *next;//指向下一个元素的指针

};

图6-2描述一个双向链表

2、环形链表

通常情况下,因为链表中最后一个元素不再有下一个元素,所以将链表尾元素中的向后指针设置为NULL,表明它是链表中的最后一个元素。但在有些链表中,末尾元素并不指向特殊值,而是指向链表的首元素。这种链表因为首尾相连,所以被称为是环形链表。环形链表也存在双向链表和单向链表两种形式。在环形双向链表中,首节点的向前指针指向尾节点。

因为环形双向链表提供最大的灵活性,所以Linux内核的标准链表是环形双向链表。

3、沿链表移动

沿链表移动只能是线性移动。先访问某个元素,然后沿该元素的向后指针访问下一个元素,不断重复这个过程,就可以沿链表向后移动了。这是一种最简单的沿链表移动方法,也是最适合访问链表的方法。如果需要随机访问数据,一般不使用链表。使用链表存放数据的理想情况是,需要遍历所有数据或需要动态加入和删除数据时。

有时,首元素会用一个特殊指针表示——该指针称为头指针,利用头指针可方便、快速地找到链表的起始端。在非环形链表里,向后指针指向NULL的元素是尾元素,在环形链表里向后指针指向头元素的元素是尾元素。遍历一个链表需要线性地访问从第一个元素到最后一个元素之间的所有元素。对于双向链表,也可以反向遍历链表,可以从最后一个元素线性访问到第一个元素。还可以从链表中的指定元素开始向前和向后访问数个元素,并不一定要访问整个链表。

4、Linux内核中的实现

Linux内核方式与众不同,不是将数据结构塞入链表,而是将链表节点塞入数据结构。

1)链表数据结构

在2.1内核开发系列中,首次引入了官方内核链表实现。从此内核中的所有链表都使用官方的链表实现。链表代码在头文件<linux/list.h>中声明,其数据结构很简单:


/*
 * Simple doubly linked list implementation.
 *
 * Some of the internal functions ("__xxx") are useful when
 * manipulating whole lists rather than single entries, as
 * sometimes we already know the next/prev entries and we can
 * generate better code by using them directly rather than
 * using the generic single-entry routines.
 */

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

next指针指向下一个链表节点,prev指针指向前一个。什么是链表存储的具体内容呢?关键在于理解list_head是如何使用的。

上述结构体中,fox中的list.next指向下一个元素,list.prev指向前一个元素。现在链表已能使用,但是还不够方便。内核提供一组链表操作例程。例如,list_add()方法加入一个新节点到链表中。但是,这些方法都有一个统一的特点:只接受list_head结构作为参数。使用宏container_of()可以从链表指针找到父结构中包含的任何变量。这是因为在C中,一个给定结构中的变量偏移在编译时地址就被ABI固定下来。

/**
 * 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) );})

使用container_of()宏,定义一个简单的函数便可返回包含list_head的父类型结构体:
/**
 * 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)

依靠list_entry()方法,内核提供创建、操作以及其他链表管理的各种例程——所有这些方法都不要知道list_head所嵌入对象的数据结构。

2)定义一个链表

list_head本身并没有意义——它需要被嵌入到数据结构中才能生效:

链表需要在使用前初始化。因为多数元素都是动态创建的,最常见的方式是在运行时初始化链表(动态方式)。

如果一个结构在编译时静态创建,而需要在其中给出一个链表的直接引用,下面是最简方式:

strcut fox red_fox = {

        .tail_length =40,

        .weight = 6,

        .list = LIST_HEAD_INIT(red_fox .list),

};

3)链表头

上面展示如何把一个现有的数据结构改造成链表。简单修改上面代码,结构便可以被内核链表例程管理。但是在可以使用这些例程前,需要修改一个标准的索引指针指向整个链表,即链表的头指针。

内核链表实现中的特性是:fox节点无差别——每一个都包含一个list_head指针,可以从任何一个节点起遍历链表,直到看到所有节点。这种方式,有时需要一个特殊指针索引到整个链表。这个特殊的索引节点就是一个常规的list_head:

static LIST_HEAD(fox_list);

该函数定义并初始化一个名为fox_list例程,这些例程中的大多数都只接受一个或两个参数:头节点或者头节点上一个特殊链表节点。

5、操作链表

内核提供一组函数来操作链表,这些函数都要使用一个或多个list_head结构体指针作参数。因为函数都是用C以内联函数形式实现的,其原型在include/linux/list.h。所有这些函数的复杂度都为O(1)。这意味着,无论这些函数操作的链表大小如何,无论得到的参数如何?都在恒定时间内完成。

1)向链表中增加一个节点

给链表增加一个节点:

/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
static inline void list_add(struct list_head *new, struct list_head *head)
{
        __list_add(new, head, head->next);
}

该函数向指定链表的head节点后插入new节点。因为链表是循环的,而且通常没有首尾节点的概念,可以把任何一个节点当成head。如果把最后一个节点当作head,那么该函数可以用来实现栈。

假定创建一个新的struct fox,并把它加入fox_list,那么这样做:

list_add(&f->list, &fox_list);

把节点增加到链表尾

list_add_tail(struct list_head *new, struct list_head *head)

该函数向指定链表的head节点前插入new节点。因为链表是环形的,可以把任何一个节点当做head。如果把第一个元素当做head,那么该函数可用来实现一个队列。

2)从链表中删除一个节点

在链表中增加一个节点后,从中删除一个节点是另一个重要操作。从链表中删除一个节点,调用list_del():

void list_del(struct list_head *entry)

该函数从链表中删除entry元素。注意,该操作并不会释放entry或释放包含entry的数据结构体所占用的内存;该函数仅仅是将entry元素从链表中移走,所以该函数被调用后,通常还需要再撤销包含entry的数据结构体和其中的entry项。

例如,为了删除fox节点,回到前面增加节点的fox_list:

list_del(&f->list)

注意,该函数并没有接受fox_list作为输入参数。只接受一个特定的节点,并修改其前后节点的指针,这样给定的节点就从链表中删除。

/*
 * Delete a list entry by making the prev/next entries
 * point to each other.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
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)
{
        __list_del(entry->prev, entry->next);
}

从链表中删除一个节点并对其重新初始化:

list_del_init():


/**
 * list_del_init - deletes entry from list and reinitialize it.
 * @entry: the element to delete from the list.
 */
static inline void list_del_init(struct list_head *entry)
{
        __list_del_entry(entry);
        INIT_LIST_HEAD(entry);
}

3)移动和合并链表节点

把节点从一个链表移到另一个链表:

/**
 * list_move - delete from one list and add as another's head
 * @list: the entry to move
 * @head: the head that will precede our entry
 */
static inline void list_move(struct list_head *list, struct list_head *head)
{
        __list_del_entry(list);
        list_add(list, head);
}

该函数从一个链表中移除list项,然后将其加入到另一链表的head节点后面。

把节点从一个链表移到另一个链表的末尾:

/**
 * list_move_tail - delete from one list and add as another's tail
 * @list: the entry to move
 * @head: the head that will follow our entry
 */
static inline void list_move_tail(struct list_head *list,
                                  struct list_head *head)
{
        __list_del_entry(list);
        list_add_tail(list, head);
}

检查链表是否为空:

/**
 * list_empty - tests whether a list is empty
 * @head: the list to test.
 */
static inline int list_empty(const struct list_head *head)
{
        return head->next == head;
}

如果指定的链表尾空,该函数返回非0值;否则返回0。

把两个未连接的链表合并在一起:

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

        first->prev = prev;
        prev->next = first;

        last->next = next;
        next->prev = last;
}

/**
 * list_splice - join two lists, this is designed for stacks
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 */
static inline void list_splice(const struct list_head *list,
                                struct list_head *head)
{
        if (!list_empty(list))
                __list_splice(list, head, head->next);
}
 

该函数合并两个链表,将list指向的链表插入到指定链表的head元素的后面。

把两个为连接的链表合并在一起,并重新初始化原来的链表:


/**
 * list_splice_init - join two lists and reinitialise the emptied list.
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 *
 * The list at @list is reinitialised
 */
static inline void list_splice_init(struct list_head *list,
                                    struct list_head *head)
{
        if (!list_empty(list)) {
                __list_splice(list, head, head->next);
                INIT_LIST_HEAD(list);
        }
}

该函数和list_splice()一样,唯一的不同是由list指向的链表要被重新初始化。

6、遍历链表

已经知道如何在内核中声明、初始化和操作一个链表。但如果无法访问自己的数据,这些没有任何意义。链表仅仅是个能够包含重要数据的容器;必须利用链表移动并访问包含数据的结构体。内核提供了一组接口,可以用来遍历链表和引用链表中的数据结构体。

注意:和链表操作函数不同,遍历链表的复杂度为O(n),n是链表所包含的元素数目。

1)基本方法

遍历链表最简单的方法是使用list_for_each()宏,参见include/linux/list.h中:

/**
 * list_for_each        -       iterate over a list
 * @pos:        the &struct list_head to use as a loop cursor.
 * @head:       the head for your list.
 */
#define list_for_each(pos, head) \
        for (pos = (head)->next; pos != (head); pos = pos->next)

该宏使用两个list_head类型的参数,第一个参数用来指向当前项,这是一个必须要提供的临时变量,第二个参数是需要遍历的链表的以头节点形式存在的list_head。每次遍历中,第一个参数在链表中不断移动指向下一个元素,直到链表中的所有元素都被访问为止。用法如下:

struct list_head *p;

list_for_each(p, list){

/* p指向链表中的元素 */

}

其实一个指向链表结构的指针通常是无用的;所需要的是一个指向包含list_head的结构体的指针。可以使用list_entry()宏,来获得包含给定list_head的数据结构。
/**
 * 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)

struct list_head *p;

struct fox *f;

list_for_each(p, &for_list){

f = list_entry(p, struct fox, list);

}

2)可用的方法

多数内核代码采用list_for_each_entry()宏遍历链表。该宏内部也使用list_entry()宏,但简化了遍历过程:

/**
 * list_for_each_entry  -       iterate over list of given type
 * @pos:        the type * to use as a loop cursor.
 * @head:       the head for your list.
 * @member:     the name of the list_struct within the struct.
 */
#define list_for_each_entry(pos, head, member)                          \
        for (pos = list_first_entry(head, typeof(*pos), member);        \
             &pos->member != (head);                                    \
             pos = list_next_entry(pos, member))

这里pos是一个指向包含list_head节点对象的指针,可将它看做是list_entry宏的返回值。head是一个指向头节点的指针,即遍历开始位置。

struct fox *f;

list_for_each_entry(f, &fox_list, list){

 

}

3)反向遍历链表

宏list_for_each_entry_reverse()和list_for_each_entry()类似,不同点在于list_for_each_entry_reverse()是反向遍历链表的。不再是沿着next指针向前遍历,而是沿着prev指针向后遍历。

/**
 * list_for_each_entry_reverse - iterate backwards over list of given type.
 * @pos:        the type * to use as a loop cursor.
 * @head:       the head for your list.
 * @member:     the name of the list_struct within the struct.
 */
#define list_for_each_entry_reverse(pos, head, member)                  \
        for (pos = list_last_entry(head, typeof(*pos), member);         \
             &pos->member != (head);                                    \
             pos = list_prev_entry(pos, member))

很多原因会需要反向遍历链表。其中一个是性能原因——如果知道要寻找的节点最可能在搜索的起始点的前面,那么反向搜索更快。第二个原因是如果顺序很重要,比如,如果使用链表实现堆栈,那么需要从尾部先前遍历才能达到FIFO原则。

4)遍历的同时删除

标准的链表遍历方法在遍历链表的同时想要删除节点是不行的。因为标准的链表方法建立在操作不会改变链表项这一假设上,所以如果当前项在遍历循环中被删除,那么接下来的遍历就无法获得next(或prev)指针了。这其实是循环遍历处理中的一个常见范式,开发人员通过在潜在的删除操作之前存储next(或者prev)指针到一个临时变量中,以便能执行删除操作。Linux内核提供了例程处理这种情况:

/**
 * 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 cursor.
 * @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_first_entry(head, typeof(*pos), member),        \
                n = list_next_entry(pos, member);                       \
             &pos->member != (head);                                    \
             pos = n, n = list_next_entry(n, member))

list_for_each_entry_safe()启用next指针来将下一项存进表中,以使得能安全删除当前项。

如果需要在反向遍历链表的同时删除它,那么内核提供了list_for_each_entry_safe_reverse()宏。

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

注意:list_for_each_entry()的安全版本只能保护在循环体中从链表中删除数据。如果这时有可能从其他地方并发进行删除,或者有任何其他并发的链表操作,需要锁定链表。

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/81191782
今日推荐