内核链表

来源:互联网 发布:东方通达信软件下载 编辑:程序博客网 时间:2024/05/29 12:51

 

        在研究Linux内核前和驱动开发之前,需要先了解一下内核中常用的一些工具,接下来的主题就是内核链表。

        可能我们在数据结构里都听说过链表,或者学过链表。我们还知道链表这东西还挺麻烦,稍有不慎就导致整个程序崩溃了。

        Linux中大量用到了内核链表,我们就来看看这个链表有什么特殊之处,可以被广泛使用。

 

         首先,回顾一下我们在严蔚敏老太太书中呈现的链表,我们就直接看循环双链表吧。

         顾名思义:需要1.首尾相连(循环)

                                   2.指向下一个节点和上一个节点

                                   3.有一个头指针,方便操作

                                   4. 另外存放数据

        

 

       回顾完这个经典的例子,我们来看看内核中使用的链表。

       

 

          这里看起来似乎没有什么不同,可能细心的同学发现了,其实这个图画的也不够清晰。

          这里我们直接找到内核链表中的结点的定义,我们就能发现有什么不同了。

          include/linux/list.h

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

          我们发现数据域并不存放到结点里面,这就是两个图片的区别。可能你会问这有什么好处呢?遍历的时候速度快一些吗?

          当然,更重要的是这个链表和之前的比(通用性)是不能比的。

 

           情景介绍,当我再编程中碰到了使用两个链表,一个存放double类型的链表,一个存放char型的链表,我发现我不得复制黏贴代码一次,然后做一点修改才能用上。

           后来我想到

           1. 用c++的模板,然后修改一点点代码。

           2. 我把数据域的内容存为 void*类型。

           3. 既然用到了C++ STL提供给我们的就可以了,方便快捷。

 

           说了那么多标准C的做法只有2可行,由此也说明了模板在实际应用的重要性。 说了这么多,我们还是看看内核链表怎么解决这个问题的。

 

           好!没有数据域那么别的数据结构就是这样用的:

            struct  ex

           {

                    struct list;

                    int    xx;

                    int    xx2;

           }

          总而言之,我们任何的结构都可以嵌入一个链表节点把各个数据联系起来,这种链表结构避免了为每个数据项类型定义自己的链表的麻烦。

 

           接着,我们继续看链表的操作:

          

          1.初始化一个链表:

              我们来看看LIST_HEAD()这个宏:

              #define LIST_HEAD_INIT(name) { &(name), &(name) }

              #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

 

              比如:LIST_HEAD(mylist_head)     --->struct list_head  mylist_head = {.next =&mylist_head; ,prev=&mylist_head}

            

           2.插入

               对链表的插入操作有两种:在表头插入和在表尾插入。

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

              因为是循环双链表实际上头插和尾插差不多都是分别调用:

       __list_add(new, head, head->next);
       __list_add(new, head->prev, head);

 

          3.删除

             static inline void list_del(struct list_head *entry);

        

          4. 搬移

          Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

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

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

 

        5.合并

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

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

 

        6.遍历

         我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?

         我们可以通过 一个指向链节点的指针,整个存放链表节点的类型,和存放链表节点的数据结构中的参数名获取到整个结构的地址。

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

 

     container_of宏定义在[include/linux/kernel.h]中:

      这个宏当然不仅仅可以算出链表节点对应数据结构的地址,他当然用法在于 知道 成员的地址 便能得之整个结构的地址。

      这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

         #define container_of(ptr, type, member)  ({ \

              const typeof( ((type *)0)->member ) *__mptr = (ptr); \

             (type *)( (char *)__mptr - offsetof(type,member) );

         })

       这里最有趣的地方是((type *)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。

       在container_of宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;

       在offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

     offsetof宏定义在[include/linux/stddef.h]中:

        #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

 

        这里才是遍历

          函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。

          在[include/linux/list.h]中,list_for_each()宏是这么定义的:

           #define list_for_each(pos, head)

               for (pos = (head)->next, prefetch(pos->next); pos != (head);  pos = pos->next, prefetch(pos->next)) 

                它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head。

                大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。

 

  

          对此Linux给出了一个list_for_each_entry()宏,与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。  

          #define list_for_each_entry(pos, head, member)……

            某些应用需要反向遍历链表,Linux提供了

             list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,

            使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

 

            演示一下遍历的写法:

            还是那之前的struct  ex结构

         struct list_head *i;

         ……

         list_for_each(i, &e.list)

        {

          struct ex e =  list_entry(i, struct list_head, list);

          printf("xx %d xx2: %d/n",e->xx,e->xx2);

         }

         ……

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0 0