转自:http://blog.chinaunix.net/uid-30254565-id-5637596.html

  1. linux内核中链表代码分析---list.h头文件分析(一)
  2. 16年2月27日17:13:14
  3. 在学习数据结构时,有一个重要的知识点就是链表。对于链表的一些基本操作,它的最好学习资料就是内核中的list.h头文件,内核中大量的使用链表,都是基于此文件的,下面来仔细分析它:
  4. (一) 结构体的定义
  5. 首先需要明确的一点是,在数据结构书中,大部分的链表定义是这样的(双向链表):
  6. typedef struct DulNode {
  7. ElemType data;
  8. DulNode *prior, *next;
  9. }DuLNode, *DuLinkList;
  10. 在链表里面包含data数据域和链表的指针域,由于这个ElemType的不同,对于每一种数据类型,都需要定义各自的链表结构。那怎么行,太麻烦了~
  11. 在linux内核中,对于需要通过链表组织起来的数据通常都是在相应的结构体里面包含一个struct list_head的成员,这个成员里面只包含链表的指针域,这样就将链表指针的操作抽象出来。
  12. struct list_head {
  13. struct list_head *next, *prev;
  14. };
  15. #define LIST_HEAD_INIT(name) { &(name), &(name) }
  16. #define LIST_HEAD(name) \
  17. struct list_head name = LIST_HEAD_INIT(name)
  18. 申请一个变量LIST_HEAD(temp)等价于 struct list_head temp = {&(temp), &(temp)};
  19. 附带知识:
  20. 1、对成员赋值
  21. 例如结构体struct st1 {
  22. int a;
  23. int b;
  24. int c;
  25. }
  26. 1.1 用{}形式
  27. struct st1 st1 = {1,2,3);
  28. 1.2 linux kernel风格.
  29. struct st1 st1 = {
  30. .a = 1;
  31. .b = 2;
  32. };
  33. //注此风格(即在成员变量之前加点“.”),可以不按成员变量的顺序进行赋值。如可以为
  34. struct st1 st1 = {
  35. .c = 3;
  36. .a = 1;
  37. .b = 2;
  38. };
  39. 2、对整体赋值.
  40. struct st1 a, b;
  41. b = a;
  42. 3、结构体作为函数返回值对另一个结构体赋值.
  43. struct st1 func1();
  44. struct st1 a = func1();
  45. (二)结构体的初始化
  46. static inline void INIT_LIST_HEAD(struct list_head *list)
  47. {
  48. list->next = list;
  49. list->prev = list;
  50. }
  51. 初始化结构体list,使它的next和prev指针都指向它自己,这是一个链表的初始状态,判断一个链表是否为空的方法就是判断它的next是否指向它本身(后面讲解)。
  52. (三) 增加结点
  53. 增加结点可以分为在头插法和尾插法,如下所示:
  54. 头插法:
  55. static inline void list_add(struct list_head *new, struct list_head *head)
  56. {
  57. __list_add(new, head, head->next);
  58. }
  59. 尾插法:
  60. static inline void list_add_tail(struct list_head *new, struct list_head *head)
  61. {
  62. __list_add(new, head->prev, head);
  63. }
  64. 他们都调用的是__list_add这个函数,可以看出来linux中对于相同的代码做了很好的封装。下面来看这个核心的__list_add函数:
  65. static inline void __list_add(struct list_head *new,
  66. struct list_head *prev,
  67. struct list_head *next)
  68. {
  69. next->prev = new;
  70. new->next = next;
  71. new->prev = prev;
  72. prev->next = new;
  73. }
  74. 如下图所示,对于一个名称为new的结点想要插入到名称为prev和next的两个结点之间的话,它的指针应该这样设置:
  75. 对于上述两种插入方法头插法和尾插法,他们的内部实现相同,只是插入的结点位置不同。内核中的链表是双向循环链表,所以头插法是将名字为new的结点插入到head结点和head->next结点之间。同样的,尾插法就是将名字为new的结点插入到head->prev结点(双向循环链表中的最后一个结点)和head结点之间。
  76. (四) 删除结点
  77. 删除结点的核心操作是__list_del函数,如下所示:
  78. static inline void __list_del(struct list_head * prev, struct list_head * next)
  79. {
  80. next->prev = prev;
  81. prev->next = next;
  82. }
  83. 这个函数是表示将entry结点的前一个和后一个结点建立联系的步骤。在这又是体现linux封装思想的一个地方,如果你想要删除名字为entry的结点,你只需要将entry的前一个和后一个结点作为参数传给这个函数即可。下面几个函数就是这样做的:
  84. static inline void __list_del_entry(struct list_head *entry)
  85. {
  86. __list_del(entry->prev, entry->next);
  87. }
  88. 这个__list_del_entry函数就是如上面咱们分析那样调用__list_del函数的。这个函数是平时所常用的。
  89. static inline void list_del(struct list_head *entry)
  90. {
  91. __list_del(entry->prev, entry->next);
  92. entry->next = LIST_POISON1;
  93. entry->prev = LIST_POISON2;
  94. }
  95. 至于这个list_del函数,它把entry的prev、next指针分别设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在 链表中的节点项不可访问(对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障)。
  96. 下面这个list_del_init函数,它除了将entry从链表中删除以外,还将entry初始化为一个空的链表。
  97. static inline void list_del_init(struct list_head *entry)
  98. {
  99. __list_del_entry(entry);
  100. INIT_LIST_HEAD(entry);
  101. }
  102. (五) 替换结点
  103. static inline void list_replace(struct list_head *old,
  104. struct list_head *new)
  105. {
  106. new->next = old->next;
  107. new->next->prev = new;
  108. new->prev = old->prev;
  109. new->prev->next = new;
  110. }
  111. 这个函数就是将old结点替换成new结点,函数代码很好理解,就不画图来表示了。
  112. static inline void list_replace_init(struct list_head *old,
  113. struct list_head *new)
  114. {
  115. list_replace(old, new);
  116. INIT_LIST_HEAD(old);
  117. }
  118. 这个list_replace_init函数,除了将old就诶点替换成new结点外,同时将old结点初始化为空链表。
  119. (六) 搬移结点
  120. static inline void list_move(struct list_head *list, struct list_head *head)
  121. {
  122. __list_del_entry(list);
  123. list_add(list, head);
  124. }
  125. static inline void list_move_tail(struct list_head *list,
  126. struct list_head *head)
  127. {
  128. __list_del_entry(list);
  129. list_add_tail(list, head);
  130. }
  131. 表示将list这个结点从它所在的链表中删除,然后将它重新插入到一个新的链表中。前一种方法是采用头插法的方式,后一种方法是采用尾插法的方式。
  132. (七)判断list结点是不是链表head的最后一项:
  133. static inline int list_is_last(const struct list_head *list,
  134. const struct list_head *head)
  135. {
  136. return list->next == head;
  137. }
  138. (八) 判断head这个链表是否为空,在上面我们提到,在初始化的时候,将一个链表头的prev和next指向它本身,在这,我们就是通过判断这个来判断链表是否为空。
  139. static inline int list_empty(const struct list_head *head)
  140. {
  141. return head->next == head;
  142. }
  143. static inline int list_empty_careful(const struct list_head *head)
  144. {
  145. struct list_head *next = head->next;
  146. return (next == head) && (next == head->prev);
  147. }
  148. list_empty()函数和list_empty_careful()函数都是用来检测链表是否为空的。但是稍有区别的就是第一个链 表使用的检测方法是判断表头的结点的下一个结点是否为其本身,如果是则返回为1,否则返回0。第二个 函数使用的检测方法是判断表头的前一个结点和后一个结点是否为其本身,如果同时满足则返回1,否则
  149. 返回0。
  150. 这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认, 这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说, 还是需要加锁保护。
  151. 下面这个函数用来判断是否一个链表只有一个成员(不算head头结点):
  152. static inline int list_is_singular(const struct list_head *head)
  153. {
  154. return !list_empty(head) && (head->next == head->prev);
  155. }
  156. 正因为内核中的链表是双向循环链表,所以才能用有上述的比较方式,如果只有一个结点的话,head->next == head->prev。
  157. (九) 左旋链表
  158. static inline void list_rotate_left(struct list_head *head)
  159. {
  160. struct list_head *first;
  161. if (!list_empty(head)) {
  162. first = head->next;
  163. list_move_tail(first, head);
  164. }
  165. }
  166. 之前一直看不懂这个函数为什么叫左旋链表函数,后来画了一个示意图后明白了,下面这个示意图只是示意所用,它代表内核中的双向循环链表,注意它的两个结点head头结点和first结点。分析上面这个list_rotate_left函数,可以看出来,first结点是head头结点以后的第一个结点。
  167. 之后调用list_move_tail函数,先将这个first结点删除,然后再移动到head链表的尾部。如下图所示:
  168. 经过这个操作,就好像这个循环链表向左旋转一样。
  169. (十)分割链表
  170. static inline void __list_cut_position(struct list_head *list,
  171. struct list_head *head, struct list_head *entry)
  172. {
  173. struct list_head *new_first = entry->next;
  174. list->next = head->next;
  175. list->next->prev = list;
  176. list->prev = entry;
  177. entry->next = list;
  178. head->next = new_first;
  179. new_first->prev = head;
  180. }
  181. 在执行这个操作之前,链表是这样的:它是以head为头部,entry是这个链表其中一项。list是一个空的头部,它是将剪切下来的结点加进来的链表。head是被剪切的链表。
  182. 经过__list_cut_position这个函数,他们发生了变化,变化以后是这样的:
  183. 不理解的地方自己手动画画图就清楚了,下面来看 list_cut_position这个函数,如下所示:
  184. static inline void list_cut_position(struct list_head *list,
  185. struct list_head *head, struct list_head *entry)
  186. {
  187. if (list_empty(head))
  188. return;
  189. if (list_is_singular(head) &&
  190. (head->next != entry && head != entry))
  191. return;
  192. if (entry == head)
  193. INIT_LIST_HEAD(list);
  194. else
  195. __list_cut_position(list, head, entry);
  196. }
  197. 它进行了一些判断语句,如果被剪切的head链表为空的话,就直接返回;如果被剪切的head链表只有一个结点,并且entry不是head或者head->next任意一个的话,就代表出错了,直接返回;如果entry正好等于head的话,就不用剪切了,直接对list进行初始化就行。
  198. (十一)合并链表
  199. 它的核心函数就是下面这个__list_splice函数:
  200. static inline void __list_splice(const struct list_head *list,
  201. struct list_head *prev,
  202. struct list_head *next)
  203. {
  204. struct list_head *first = list->next;
  205. struct list_head *last = list->prev;
  206. first->prev = prev;
  207. prev->next = first;
  208. last->next = next;
  209. next->prev = last;
  210. }
  211. 可以理解为将list这个链表插入到prev和next这两个结点之间。
  212. static inline void list_splice(const struct list_head *list,
  213. struct list_head *head)
  214. {
  215. if (!list_empty(list))
  216. __list_splice(list, head, head->next);
  217. }
  218. 如果理解了上面那个__list_splice函数的话,这个list_splice函数应该就好理解了,它就是把list这个链表插入到head结点和head->next结点之间,类似于头插法。
  219. static inline void list_splice_tail(struct list_head *list,
  220. struct list_head *head)
  221. {
  222. if (!list_empty(list))
  223. __list_splice(list, head->prev, head);
  224. }
  225. 这个list_splice_tail函数就是把list链表插入到head->prev结点和head结点之间,类似与尾插法。
  226. 但是上面两个函数都有一个缺点,就是这个list结点的prev和next指针都还指向原来的位置,它没有改变,但是这两个链表已经进行了合并,这样就会发生混乱,于是就产生了下面两个函数,他们在合并两个链表的同时,将list链表初始化了。
  227. static inline void list_splice_init(struct list_head *list,
  228. struct list_head *head)
  229. {
  230. if (!list_empty(list)) {
  231. __list_splice(list, head, head->next);
  232. INIT_LIST_HEAD(list);
  233. }
  234. }
  235. static inline void list_splice_tail_init(struct list_head *list,
  236. struct list_head *head)
  237. {
  238. if (!list_empty(list)) {
  239. __list_splice(list, head->prev, head);
  240. INIT_LIST_HEAD(list);
  241. }
  242. }
  243. 到这位置,链表的一些基本操作就算分析完了,还剩下链表的遍历等操作,他们需要用到linux内核中container_of这个宏的一些知识。我们在分析完这些知识以后在进行链表的遍历等操作。

linux内核中链表代码分析---list.h头文件分析(一)【转】的更多相关文章

  1. linux内核中链表代码分析---list.h头文件分析(二)【转】

    转自:http://blog.chinaunix.net/uid-30254565-id-5637598.html linux内核中链表代码分析---list.h头文件分析(二) 16年2月28日16 ...

  2. Linux内核中链表的实现与应用【转】

    转自:http://blog.chinaunix.net/uid-27037833-id-3237153.html 链表(循环双向链表)是Linux内核中最简单.最常用的一种数据结构.         ...

  3. Linux内核中链表实现

    关于双链表实现,一般教科书上定义一个双向链表节点的方法如下: struct list_node{ stuct list_node *pre; stuct list_node *next; ElemTy ...

  4. Linux内核中链表的学习

    一.自己学习链表 数组的缺点:(1)数据类型一致:(2)数组的长度事先定好,不能灵活更改. 从而引入了链表来解决数组的这些缺点:(1)结构体解决多数据类型(2)链表的组合使得链表的长度可以灵活设置. ...

  5. Linux内核中Makefile、Kconfig和.config的关系(转)

    我们在编译Linux内核时,往往在Linux内核的顶层目录会执行一些命令,这里我以RK3288举例,比如:make firefly-rk3288-linux_defconfig.make menuco ...

  6. Linux内核中的GPIO系统之(3):pin controller driver代码分析

    一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道,pin control subsystem也不例外,被它驱动的硬件叫做pin controller(一般ARM soc的datash ...

  7. Linux内核中的GPIO系统之(3):pin controller driver代码分析--devm_kzalloc使用【转】

    转自:http://www.wowotech.net/linux_kenrel/pin-controller-driver.html 一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道 ...

  8. CVE-2019-11477:Linux 内核中TCP协议栈整数溢出漏洞详细分析 代码卫士 今天

    CVE-2019-11477:Linux 内核中TCP协议栈整数溢出漏洞详细分析 代码卫士 今天

  9. Linux内核中SPI总线驱动分析

    本文主要有两个大的模块:一个是SPI总线驱动的分析 (研究了具体实现的过程): 另一个是SPI总线驱动的编写(不用研究具体的实现过程). 1 SPI概述 SPI是英语Serial Peripheral ...

随机推荐

  1. STM32 堆栈使用解析

    安富莱_STM32-V5开发板_μCOS-III教程.pdf 第4章

  2. 洛谷 P4127 [AHOI2009]同类分布 解题报告

    P4127 [AHOI2009]同类分布 题目描述 给出两个数\(a,b\),求出\([a,b]\)中各位数字之和能整除原数的数的个数. 说明 对于所有的数据,\(1 ≤ a ≤ b ≤ 10^{18 ...

  3. cf379F New Year Tree (树的直径+倍增lca)

    可以证明,如果合并两棵树,新的直径的端点一定是原来两树中直径的端点 可以把新加两个点的操作看成是把两个只有一个点的树合并到原来的树上,然后用其中的一个点去和原来树上的直径两端点更新直径就可以了 #in ...

  4. A1012. The Best Rank

    To evaluate the performance of our first year CS majored students, we consider their grades of three ...

  5. (转)sublime text3简体中文版汉化教程

    preferens——package controller——输入 install package——等待安装完成后输入 localizationChinese 请使用主菜单的 帮助/Language ...

  6. 查询字符串(性能对比): Array Vs HashMap

    ip字符串长度: 15 ip count: 25 time - array:16ms, 查询次数:25000time - map:15ms, 查询次数:25000 ip count: 42 time ...

  7. 右值引用&&

    以下内容参考https://blog.csdn.net/china_jeffery/article/details/78520237 右值引用若不作为函数参数使用,基本等于滥用 右值引用 (Rvalu ...

  8. typescript函数(笔记非干货)

    函数类型 Function Type 为函数定义类型 Define types for functions 我们可以给每个参数添加类型之后再为函数本身添加返回值类型. TypeScript能够根据返回 ...

  9. javascript高级程序设计第一章有感

    第一章JavaScript简介 Javascript的诞生最早是为了处理表单数据验证的问题,以前主要是使用perl这个强大的服务端脚本语言处理的.在未诞生javascript之前, 人们每次提交表单就 ...

  10. 第二节,TensorFlow 使用前馈神经网络实现手写数字识别

    一 感知器 感知器学习笔记:https://blog.csdn.net/liyuanbhu/article/details/51622695 感知器(Perceptron)是二分类的线性分类模型,其输 ...