内核中常用的双向链表数据结构(list_head)详解

1.什么是list_head结构

内核定义了list_head结构来实现链表结构。字段next和prev分别表示通用双向链表向前和向后的指针元素。list_head字段的指针中存放的是另一个list_head字段的地址,而不是含有list_head结构的整个数据结构地址

文件在include/linux/list.h

struct list_head {

  struct list_head *next, *prev;

};


2.使用方法

大家看到list_head数据结构的定义可能会感觉很奇怪,怎么没有数据成员?参考上图,其实我们是把list_head插入到其他结构题中,充当链表中的链接部分。

内核为我们提供了操作这种数据结构的原语接口(即不能被中断的操作)


3.简要说明实现原理

struct file_node{

  char c;

  struct list_head node;

};

比如:list_entry(ptr,type,member)宏的功能就是,由结构体成员地址求结构体地址。其中ptr 是所求结构体中list_head成员指针,type是所求结构体类型,member是结构体list_head成员名。通过下图来总结一下:

此时list_head就作为它的父结构中的一个成员了,当我们知道list_head的地址(指针)时,我们可以通过list.c提供的宏 list_entry 来获得它的父结构的地址。下面我们来看看list_entry的实现:

#define list_entry(ptr,type,member)\

  container_of(ptr,type,member)

 

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

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

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

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

 

这里涉及到三个宏,还是有点复杂的,我们一个一个来看:

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

我们知道 0 地址内容是不能访问的,但 0地址的地址我们还是可以访问的, 这里用到一个取址运算符

(TYPE *)0 它表示将 0地址强制转换为TYPE类型,((TYPE *)0)-> MEMBER 也就是从0址址找到TYPE 的成员MEMBER 。

我们结合上面的结构来看

struct file_node{

  char c;

  struct list_head node;

};

将实参代入 offset( struct file_node, node );最终将变成这样:

( (size_t) & ((struct file_node*)0)-> node );这样看的还是不很清楚,我们再变变:

struct file_node *p = NULL;

& p->node;

这样应该比较清楚了,即求 p 的成员 node的地址,只不过p 为0地址,从0地址开始算成员node的地址,也就是 成员 node 在结构体 struct file_node中的偏移量。offset宏就是算MEMBER在TYPE中的偏移量的。

我们再看第二个宏

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

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

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

这个宏是由两个语句组成,最后container_of返回的结果就是第二个表达式的值。这里__mptr为中间变量,这就是list_head指针类型,它被初始化为ptr的值,而ptr就是当前所求的结构体中list_head节点的地址。为什么要用中间变量,这是考虑到安全性因素,如果传进来一个ptr++,所有ptr++放在一个表达式中会有副作用,像 (p++)+(p++)之类。

(char*)__mptr 之所以要强制类型转化为char是因为地址是以字节为单位的,而char的长度就是一个字节。

container_of的值是两个地址相减,

刚说了__mptr是结构体中list_head节点的地址,offset宏求的是list_head节点MEMBER在结构体TYPE中的偏移量,那么__mptr减去它所在结构体中的偏移量,就是结构体的地址。

所以list_entry(ptr,type,member)宏的功能就是,由结构体成员地址求结构体地址。其中ptr 是所求结构体中list_head成员指针,type是所求结构体类型,member是结构体list_head成员名。通过下图来总结一下:

 

 

继续列举一些双链表的常用操作:

双向链表的遍历——list_for_each

//注:这里prefetch 是gcc的一个优化选项,也可以不要

#define list_for_each(pos, head) \

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

                 pos = pos->next)

 

生成双向链表的头结点——LIST_HEAD()

LIST_HEAD() -- 生成一个名为name的双向链表头节点

#define LIST_HEAD(name) \

struct list_head name = LIST_HEAD_INIT(name)

static inline void INIT_LIST_HEAD(struct list_head *list)

{

  list->next = list;

  list->prev = list;

}

双向链表的插入操作 -- list_add()

将new所代表的结构体插入head所管理的双向链表的头节点head之后: (即插入表头)

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

{

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

}

static inline void __list_add( struct list_head *new, struct list_head *prev, struct list_head *next)

{

  next->prev = new;

  new->next = next;

  new->prev = prev;

  prev->next = new;

}

从list中删除结点——list_del()

static inline void list_del(struct list_head *entry)

{

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

  entry->next = LIST_POISON1;

  entry->prev = LIST_POISON2;

}

static inline void __list_del(struct list_head * prev, struct list_head * next)

{

  next->prev = prev;

  prev->next = next;

}

 

判断链表是否为空(如果双向链表head为空则返回真,否则为假)——list_empty()

static inline int list_empty(const struct list_head *head)

{

  return head->next == head;

}


4.举例说明(以下内容来自http://www.cnblogs.com/zhuyp1015/archive/2012/06/02/2532240.html)

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include "list.h"

  4. struct int_node
  5. {
  6.     int val;
  7.     struct list_head list;
  8. };

  9. int main()
  10. {
  11.     struct list_head head,*plist;
  12.     struct int_node a,b,c;
  13.     
  14.     a.val = 1;
  15.     b.val = 2;
  16.     c.val = 3;
  17.     
  18.     INIT_LIST_HEAD(&head);            //初始化链表头
  19.     list_add_tail(&a.list,&head);     //添加节点
  20.     list_add_tail(&b.list,&head);
  21.     list_add_tail(&c.list,&head);

  22.     list_for_each(plist,&head)//遍历链表,打印结果
  23.     {
  24.         struct int_node *node = list_entry(plist,struct int_node,list);//然后取得数据项,因此一般来说和list_for_each配合使用
  25.         printf("val = %d\n",node->val);
  26.     }//print 1 2 3

  27.     printf("*******************************************\n");
  28.     list_del_init(&b.list);            //删除节点b
  29.     list_for_each(plist,&head)         //重新遍历链表,打印结果
  30.     {
  31.         struct int_node *node = list_entry(plist,struct int_node,list);
  32.         printf("val = %d\n",node->val);
  33.     }//print 1 3

  34.     printf("*******************************************\n");
  35.     struct int_node d,e;
  36.     struct list_head head1;
  37.     d.val = 4;
  38.     e.val = 5;
  39.     INIT_LIST_HEAD(&head1);            //重新建立链表,表头为head1
  40.     list_add_tail(&d.list,&head1);
  41.     list_add_tail(&e.list,&head1);

  42.     list_splice(&head1,&head);        //把两个链表进行连接
  43.     list_for_each(plist,&head)
  44.     {
  45.         struct int_node *node = list_entry(plist,struct int_node,list);
  46.         printf("val = %d\n",node->val);
  47.     }//print 4 5 1 3

  48.     printf("*******************************************\n");
  49.     if(!list_empty(&head))          //判断链表是否为空
  50.     {
  51.         printf("the list is not empty!\n");
  52.         
  53.     }

  54.     return 0;
  55. }

输出结果如下:
val = 1
val = 2
val = 3
*******************************************
val = 1
val = 3
*******************************************
val = 4
val = 5
val = 1
val = 3
*******************************************
the list is not empty!




  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五癫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值