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


    转自: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这个宏的一些知识。我们在分析完这些知识以后在进行链表的遍历等操作。
  • 相关阅读:
    在CentOS7上部署OpenStack 步骤详解
    Linux运维工程师工作手册
    Nginx+Keepalived实现Web服务器负载均衡
    Django Nginx+uwsgi 安装配置
    Docker-搭建Docker Registry
    centos7系统默认防火墙Firewall使用方法
    Shell脚本编写及常见面试题(二)
    Shell脚本编写及常见面试题(一)
    Linux之解决每次git pull/git push都需输入密码设置
    基于thinkphp5的Excel上传
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/9597158.html
Copyright © 2020-2023  润新知