Linux内核(10) - 内核中的链表

Posted AlanTu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核(10) - 内核中的链表相关的知识,希望对你有一定的参考价值。

早上上班坐地铁要排队,到了公司楼下等电梯要排队,中午吃饭要排队,下班了追求一个女孩子也要排队,甚至在网上下载个什么门的短片也要排队,每次看见人群排成一条长龙时,才真正意识到自己是龙的传人。那么下面咱们就说说队列(链表)。

 

使用链表的目的很明确,因为有很多事情要做,于是就把它放进链表里,一件事一件事的处理。比如在USB子系统里,U盘不停的提交urb请求,USB键盘也提交,USB鼠标也提交,那USB主机控制器咋应付得过来呢?很简单,建一个链表,然后你每次提交就是往里边插入,然后USB主机控制器再统一去调度,一个一个来执行。这里有力得证明了,谭浩强大哥的C程序设计是我们学习Linux的有力武器,书中对链表的介绍无疑是英明的,谭大哥,您不是一个人在战斗!

 

内核中链表的实现位于include/linux/list.h文件,链表数据结构的定义也很简单。

 

21 struct list_head {

22   struct list_head *next, *prev;

23 };

 

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核中的链表实际上都是双链表(通常都是双循环链表)。

通常,我们在数据结构课堂上所了解的链表定义方式是这样的(以单链表为例):

 

struct list_node {

    struct list_node *next;

    ElemType data;

};

 

通过这种方式使用链表,对每一种数据类型,都要定义它们各自的链表结构。而内核中的链表却与此不同,它并没有数据域,不是在链表结构中包含数据,而是在描述数据类型的结构中包含链表。

 

比如在hub驱动中使用struct usb_hub来描述hub设备,hub需要处理一系列的事件,比如当探测到一个设备连进来时,就会执行一些代码去初始化该设备,所以hub就创建了一个链表来处理各种事件,这个链表的结构如下图。

技术分享图片

 

(1)声明与初始化。

 

链表的声明可以使用两种方式,一种为使用LIST_HEAD宏在编译时静态初始化,一种为使用INIT_LIST_HEAD()在运行时进行初始化。

 

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

26

27 #define LIST_HEAD(name) /

28   struct list_head name = LIST_HEAD_INIT(name)

29

30 static inline void INIT_LIST_HEAD(struct list_head *list)

31 {

32   list->next = list;

33   list->prev = list;

34 }

 

无论采用哪种方式,新生成的链表头的两个指针next、prev都初始化为指向自己。

 

(2)判断链表是否为空。

 

298 static inline int list_empty(const struct list_head *head)

299 {

300   return head->next == head;

301 }

 

(3)插入。

 

有了链表,自然就要往里面加东西、减东西。就像我们每个人每天都在不停的走进去,又走出来,似是梦境又不是梦境。一切都是不经意的。走进去是一年四季,走出来是春夏秋冬。list_add()和list_add_tail()这两个函数就是往队列里加东西。

 

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

68 {

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

70 }

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

85 {

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

87 }

 

其中,list_add()将数据插入在head之后,list_add_tail()将数据插入在head->prev之后。其实对于循环链表来说,表头的next、prev分别指向链表中的第一个和最后一个节点,所以,list_add()和list_add_tail()的区别并不大。

 

(4)删除。

搞懂谭浩强那本书之后看这些链表的代码那就是小菜一碟。再来看下一个 list_del_init(),里的元素不能只加不减,没用了的元素就该删除掉,把空间腾出来给别人。郭敬明说过,我生命里的温暖就那么多,我全部给了你,但是你离开了我,你叫我以后怎么再对别人笑……

链表里的元素不能只加不减,没用了的元素就应该删除掉。

 

254 static inline void list_del_init(struct list_head *entry)

255 {

256     __list_del(entry->prev, entry->next);

257  INIT_LIST_HEAD(entry);

258 }

 

list_del_init()从链表里删除一个元素,并且将其初始化。

 

(5)遍历。

 

内核中的链表仅仅保存了list_head结构的地址,我们如何通过它或取一个链表节点真正的数据项?这就要提到有关链表的所有操作里面,最为重要超级经典的list_entry宏了,我们可以通过它很容易地获得一个链表节点的数据。

 

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

426  container_of(ptr, type, member)

 

我相信,list_entry()这个宏在Linux内核代码中的地位,就相当于广告词中的任静付笛生的洗洗更健康,相当于大美女关之琳的一分钟轻松做女人,这都是耳熟能详妇孺皆知的,是经典中的经典。如果你说你不知道list_entry(),那你千万别跟人说你懂Linux内核,就好比你不知道陈文登不知道任汝芬你就根本不好意思跟人说你考过研,要知道每个考研人都是左手一本陈文登右手一本任汝芬。

 

可惜,关于list_entry,这个谭浩强老师的书里就没有了,当然你不能指责谭浩强的书不行,再好的书也不可能包罗万象。

 

关于list_entry(),让我们结合实例来看,还是hub驱动的那个例子,当我们真的要处理hub的事件的时候,我们当然需要知道具体是哪个hub触发了这起事件。而list_entry的作用就是,从struct list_head event_list得到它所对应的struct usb_hub结构体变量。比如以下四行代码:

 

    struct list_head *tmp;

    struct usb_hub *hub;

    tmp = hub_event_list.next;

    hub = list_entry(tmp, struct usb_hub, event_list);

 

从全局链表hub_event_list中取出一个来,叫做tmp,然后通过tmp,获得它所对应的struct usb_hub。

以上是关于Linux内核(10) - 内核中的链表的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核 内存管理RCU 机制 ① ( RCU 机制简介 | RCU 机制的优势与弊端 | RCU 机制的链表应用场景 )

Linux 内核 内存管理RCU 机制 ① ( RCU 机制简介 | RCU 机制的优势与弊端 | RCU 机制的链表应用场景 )

循环遍历内核空间中的链表

内核数据结构 内核链表分析

内核数据结构 内核链表分析

第四季-专题7-Linux内核链表