《电子技术应用》
您所在的位置:首页 > 嵌入式技术 > 业界动态 > C语言的那些小秘密之链表

C语言的那些小秘密之链表

2015-05-29

  大多数的读者在学习编程语言的时候都不喜欢那些枯燥的文字描述,包括我自己在开始学习编程的时候也是这样,对于代码的热情远远高于文字,所以我在我写东西的时候也不喜欢用枯燥的文字描述来向读者讲解,更喜欢用代码加上适当的文字描述的方式进行讲解,因为有些东西可能用枯燥的文字描述半天还不如实实在在的给读者呈现出一段简单的代码,让读者理解得更加的透彻些。但是并不是说文字描述就没用,文字描述也很重要,只是绝大部分读者都更加的希望直接达到最终的效果,都想跳过那些中间的步骤。接下来我们接着上一篇博客《C语言的那些小秘密之链表(三)》的内容继续讲解linux内核双向循环链表。[cpp] view plaincopystatic inline int list_empty(const struct list_head *head)

  {

  return head->next == head;

  }

  static inline int list_empty_careful(const struct list_head *head)

  {

  struct list_head *next = head->next;

  return (next == head) && (next == head->prev);

  }

 

  运行结果为:

  [html] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

  student num: 5 student name: Stu5

  student num: 4 student name: Stu4

  student num: 3 student name: Stu3

  student num: 2 student name: Stu2

  student num: 1 student name: Stu1

  使用list_empty()检测,链表非空

  使用list_empty_careful()检测,链表非空

  看看代码就知道如何使用了,接下来看看链表的合成。

  [html] view plaincopystatic inline void __list_splice(struct list_head *list,

  struct list_head *head)

  {

  struct list_head *first = list->next;

  struct list_head *last = list->prev;

  struct list_head *at = head->next;

  first->prev = head;

  head->next = first;

  last->next = at;

  at->prev = last;

  }

  

  在这种情况下会丢弃list所指向的头结点,因为两个链表有两个头结点,所以我们必须要去掉其中一个头结点。只要list非空链,head无任何限制,该函数就能实现链表的合并。

  [cpp] view plaincopystatic inline void list_splice_init(struct list_head *list,

  struct list_head *head)

  {

  if (!list_empty(list)) {

  __list_splice(list, head);

  INIT_LIST_HEAD(list);

  }

  }

  以上函数的功能是将一个链表list的有效信息合并到另外一个链表head后,重新初始化被去掉的空的链表头。这样的描述可能不是太好理解,接下来看看一段代码。

  [html] view plaincopy#include

  #include

  #include "list.h"

  typedef struct _stu

  {

  char name[20];

  int num;

  struct list_head list;

  }stu;

  int main()

  {

  stu *pstu,*pstu2;

  stu *tmp_stu;

  struct list_head stu_list,stu_list2;

  struct list_head *pos;

  int i = 0;

  INIT_LIST_HEAD(&stu_list);

  INIT_LIST_HEAD(&stu_list2);

  pstu = malloc(sizeof(stu)*3);

  pstu2 = malloc(sizeof(stu)*3);

  for(i=0;i<3;i++)

  {

  sprintf(pstu[i].name,"Stu%d",i+1);

  sprintf(pstu2[i].name,"Stu%d",i+4);

  pstu[i].num = i+1;

  pstu2[i].num = i+4;

  list_add( &(pstu[i].list), &stu_list);

  list_add( &(pstu2[i].list), &stu_list2);

  }

  printf("stu_list 链表\n");

  list_for_each(pos,&stu_list)

  {

  tmp_stu = list_entry(pos, stu, list);

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  }

  printf("stu_list2 链表\n");

  list_for_each(pos,&stu_list2)

  {

  tmp_stu = list_entry(pos, stu, list);

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  }

  printf("stu_list链表和stu_list2 链表合并以后\n");

  list_splice(&stu_list2,&stu_list);

  list_for_each(pos,&stu_list)

  {

  tmp_stu = list_entry(pos, stu, list);

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  }

  free(pstu);

  return 0;

  }

  运行结果为:

  [html] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

  stu_list 链表

  student num: 3 student name: Stu3

  student num: 2 student name: Stu2

  student num: 1 student name: Stu1

  stu_list2 链表

  student num: 6 student name: Stu6

  student num: 5 student name: Stu5

  student num: 4 student name: Stu4

  stu_list链表和stu_list2 链表合并以后

  student num: 6 student name: Stu6

  student num: 5 student name: Stu5

  student num: 4 student name: Stu4

  student num: 3 student name: Stu3

  student num: 2 student name: Stu2

  student num: 1 student name: Stu1

  有了直观的代码和运行结果,理解起来也更加的容易了。

  有了上面的这些操作,但是我们还一直没有讲到我们最终所关心的宿主结构,那么接下来我们一起来看看我们该如何取出宿主结构的指针呢?这也是我认为linux内核双向循环链表实现最为巧妙的地方了。

  [cpp] view plaincopy#define list_entry(ptr, type, member) \

  ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

  看看上面的代码,发现一个很熟悉的身影(unsigned long)(&((type *)0)->member)),这个我在前一篇博客《C语言的那些小秘密之字节对齐》中已经讲解过了,多以在此就不再做过多的讲解,如果有不明白的读者可以回过去看看讲解再回过来阅读。通过(unsigned long)(&((type *)0)->member))我们得出了成员变量member的偏移量,而ptr为指向member的指针,因为指针类型不同的原因,所以我们再次要先进行(char*)的转换之后再进行计算。所以我们用ptr减去member的偏移量就得到了宿主结构体的指针,这就是一个非常巧妙的地方,这也就使得linux内核双向循环链表能够区别于传统链表的关键所在。可能看到这儿的时候读者已经感觉非常的枯燥了,但是别放弃,坚持看完,因为虽然这样的讲解枯燥了点,但是非常有用。所以坚持坚持吧!

  [cpp] view plaincopy#define list_for_each(pos, head) \

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

  pos = pos->next)

  #define __list_for_each(pos, head) \

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

  #define list_for_each_prev(pos, head) \

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

  pos = pos->prev)

  遍历是双循环链表的基本操作,head为头节点,遍历过程中首先从(head)->next开始,当pos==head时退出,故head节点并没有访问,这和链表的结构设计有关,通常头节点都不含有其它有效信息,因此可以把头节点作为双向链表遍历一遍的检测标志来使用。在list_for_each宏中读者可能发现一个比较陌生的面孔,我们在此就不将prefetch展开了讲解了,有兴趣的读者可以自己查看下它的实现,其功能是预取内存的内容,也就是程序告诉CPU哪些内容可能马上用到,CPU预先其取出内存操作数,然后将其送入高速缓存,用于优化,是的执行速度更快。接下来的__list_for_each()宏和list_for_each_prev()宏就不在此做一一的讲解了,和list_for_each()宏类似。 就是遍历的方向有所改变或者不使用预取。

  通过上面的讲解和前面那么多的代码,相信读者这个时候对于list_for_each()已经不再感到陌生了。上面的但三个遍历链表的宏都类似,继续往下看。

  [cpp] view plaincopy#define list_for_each_safe(pos, n, head) \

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

  pos = n, n = pos->next)

  以上list_for_each_safe()宏也同样是用于遍历的,不同的是里边多出了一个n暂存pos下一个节点的地址,避免了因为pos节点被释放而造成的断链,这也就体现出了safe。也就是说你可以遍历完当前节点后将其删除,同时可以接着访问下一个节点,遍历完毕后就只剩下一个头节点。当然这有一个最为典型的应用,那就是我们在多进程编程时候,多个进程等待在同一个等待队列上,若事件发生时唤醒所有进程,则可以唤醒后将其依次从等待队列中删除。

  [html] view plaincopy#include

  #include

  #include "list.h"

  typedef struct _stu

  {

  char name[20];

  int num;

  struct list_head list;

  }stu;

  int main()

  {

  stu *pstu;

  stu *tmp_stu;

  struct list_head stu_list;

  struct list_head *pos,*n;

  int i = 0;

  INIT_LIST_HEAD(&stu_list);

  pstu = malloc(sizeof(stu)*3);

  for(i=0;i<3;i++)

  {

  sprintf(pstu[i].name,"Stu%d",i+1);

  pstu[i].num = i+1;

  list_add( &(pstu[i].list), &stu_list);

  }

  printf("通过list_for_each_safe()遍历使用list_del(pos)删除结点前\n");

  list_for_each_safe(pos, n, &stu_list)

  {

  tmp_stu = list_entry(pos, stu, list);

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  list_del(pos);

  }

  printf("通过list_for_each()遍历使用list_del(pos)删除结点后\n");

  list_for_each(pos,&stu_list)

  {

  tmp_stu = list_entry(pos, stu, list);

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  }

  free(pstu);

  return 0;

  }

  运行结果为:

  [html] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

  通过list_for_each_safe()遍历使用list_del(pos)删除结点前

  student num: 3 student name: Stu3

  student num: 2 student name: Stu2

  student num: 1 student name: Stu1

  通过list_for_each()遍历使用list_del(pos)删除结点后

  读者可以结合运行结果再去阅读上面的文字描述部分。

  如果只提供对list_head结构的遍历操作是远远不够的,我们希望实现的是对宿主结构的遍历,即在遍历时直接获得当前链表节点所在的宿主结构项,而不是每次要同时调用list_for_each()和list_entry()。为此Linux特地提供了list_for_each_entry()宏来实现

  [cpp] view plaincopy#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_head结构,第三个参数为list_head结构在宿主结构中的成员名。有时候做过多的讲解反而没有看看代码的效果好,我们还是用段代码来说明下吧。

  [html] view plaincopy#include

  #include

  #include "list.h"

  typedef struct _stu

  {

  char name[20];

  int num;

  struct list_head list;

  }stu;

  int main()

  {

  stu *pstu;

  stu *tmp_stu;

  struct list_head stu_list;

  struct list_head *pos,*n;

  int i = 0;

  INIT_LIST_HEAD(&stu_list);

  pstu = malloc(sizeof(stu)*3);

  for(i=0;i<3;i++)

  {

  sprintf(pstu[i].name,"Stu%d",i+1);

  pstu[i].num = i+1;

  list_add( &(pstu[i].list), &stu_list);

  }

  list_for_each_entry(tmp_stu, &stu_list, list)

  printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

  free(pstu);

  return 0;

  }

  运行结果为:

  [html] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

  student num: 3 student name: Stu3

  student num: 2 student name: Stu2

  student num: 1 student name: Stu1

  如果读者一开始对于文字描述感到陌生的话,那么就再次结合代码去阅读。

  接下来再来看看最后几个。

  [html] view plaincopy#define list_for_each_entry_reverse(pos, head, member) \

  for (pos = list_entry((head)->prev, typeof(*pos), member); \

  prefetch(pos->member.prev), &pos->member != (head); \

  pos = list_entry(pos->member.prev, typeof(*pos), member))

  #define list_prepare_entry(pos, head, member) \

  ((pos) ? : list_entry(head, typeof(*pos), member))

  #define list_for_each_entry_continue(pos, head, member) \

  for (pos = list_entry(pos->member.next, typeof(*pos), member); \

  prefetch(pos->member.next), &pos->member != (head); \

  pos = list_entry(pos->member.next, typeof(*pos), member))

  #define list_for_each_entry_safe(pos, n, head, member) \

  for (pos = list_entry((head)->next, typeof(*pos), member), \

  n = list_entry(pos->member.next, typeof(*pos), member); \

  &pos->member != (head); \

  pos = n, n = list_entry(n->member.next, typeof(*n), member))

  以上几个与list_for_each_entry类似,只是其中略有差别,list_prepare_entry()中含有prefetch(),它的作用在上面已经讲解,有什么疑惑可以返回去阅读下,在此不再做过多的讲解;list_for_each_entry_continue()和list_for_each_entry()的区别主要是list_for_each_entry_continue()可以不从链表头开始遍历,而是从已知的某个pos结点的下一个结点开始遍历。在某些时候如果不是从头结点开始遍历,那么为了保证pos的初始值有效,必须使用list_prepare_entry()。其含义就是如果pos非空,那么pos的值就为其本身,如果pos为空,那么就从链表头强制扩展一个虚pos指针,读者自己分析list_prepare_entry()的实现就明白了。list_for_each_entry_safe()要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的宿主结构体的地址,避免因pos节点被释放而造成的断链。

  到此我们linux内核双向循环链表的旅程就结束了,下一篇我们将开始一个新的旅程。由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下你宝贵的意见。


本站内容除特别声明的原创文章之外,转载内容只为传递更多信息,并不代表本网站赞同其观点。转载的所有的文章、图片、音/视频文件等资料的版权归版权所有权人所有。本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如涉及作品内容、版权和其它问题,请及时通过电子邮件或电话通知我们,以便迅速采取适当措施,避免给双方造成不必要的经济损失。联系电话:010-82306118;邮箱:aet@chinaaet.com。