linux内核数据结构

   内容来源于网络!
struct list_head { 

        struct list_head *next, *prev; 

}; 

这里的list_head没有数据域,在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个 struct list_head list 成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。如下图,这里假设有四个链表节点。

nf_sockopts链表示意图

 linux内核数据结构_第1张图片

链表的操作接口:

1. 声明和初始化
实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

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

当我们用LIST_HEAD(list)声明一个名为list的链表头时,它的next、prev指针都初始化为指向自己,这样我们就有了一个空链表,因此Linux用头指针的next是否指向自己来判断链表是否为空:

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

除了用LIST_HEAD()宏在声明的时候静态初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时动态初始化链表:

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

2. 插入/删除/合并
a) 插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用__list_add(new, head, head->next)和__list_add(new, head->prev, head)来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。
b) 删除

static inline void list_del(struct list_head *entry);

被剔除下来的节点,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

c)合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):
图4 链表合并list_splice(&list1,&list2)
 

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);

 

Linux内核源码中经常要对链表进行操作,其中一个很重要的宏是list_for_each_entry,它的意思大体如下:
假设只有两个结点,则第一个member代表head,list_for_each_entry的作用就是通过循环遍历每一个pos中的member子项,从而找到每一个pos项。


图1:
struct pos:                                          struct  pos:
___________                                     ____________
|                       |                                     |                          |
|                       |                                     |                          |
|    ...........        |                                     |   ................       |
|                       |                                     |                           |
|                       |                                     |                           |
|   member:   |                 |------------------> member    |
|   {                  |                 |                    |  {                       |
|        *prev;    |                 |                    |       *prev;        |
|        *next;---|------------|                    |        *next;---------------|
|    }                 |                                      |  }                       |           |
|—^———— |                                   |____________|          |
      |                                                                                               |
      |                                                                                               |
      |_________________________________________|

宏list_for_each_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_entry((head)->next, typeof(*pos), member);      \
            prefetch(pos->member.next), &pos->member != (head);        \
            pos = list_entry(pos->member.next, typeof(*pos), member))

list_entry((head)->next, typeof(*pos), member)返回(head)->next物理指针所处位置向前减去offsetof()个字节数据之后, 其父变量pos的物理地址,父变量的类型在编译时由typeof(*pos)自动返回.prefetch代表指令预取,GCC内部定义。
所以list_for_each_entry遍历head 下面挂接的类型为typeof(*pos)的childs结构体们,当然每个child结构体包含struct list_head node之类相似的双向链表list_head类型项,就这样通过循环pos将依次指向双向链表上的各个child.(member就是child类型中被定义的变量名)
下面看list_entry宏的定义,它实际上就是container_of宏:

/*

* 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宏的含义就是通过传递进来的ptr指针(类型与member相同)指向的member成员的地址,减去一个偏移量,返回指向type的指针。定义如下:

#define container_of(ptr, type, member) ({            \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

 这里难以理解的可能是为什么出现个(type*)0,它是把type类型结构体的首地址虚拟成0,便于计算成员地址和偏移。其中offsetof宏的含义就是求得成员member与type类型结构体首地址偏移量,定义如下:

#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER)   __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER)   ((size_t) &((TYPE *)0)->MEMBER)
#endif

 大家想一想,如果struct list_head member成员是结构体里面的第一个成员,则它的地址和结构体首地址是一样的,就可以直接用list_for_each宏,而不需要用list_for_each_entry(其中会有求偏移量的步骤),但这是不规范的,因为不能保证member是结构体第一个成员。

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

注意这里的pos是struct list_head 类型的,而不像list_for_each_entry中,pos是外包结构体指针类型。

反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,操作类似。

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此,Linux专门提供了一个list_prepare_entry(pos,head,member)宏(找到返回相应节点,找不到返回头节点?),将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

安全性考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护

b) 遍历时节点删除

前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

你可能感兴趣的:(linux内核数据结构)