Linux内核链表的一些注解!

为了减少重复代码的数量,内核开发者建立了一套标准双向循环链表的实现。这套实现并没有实现任何的锁方案,所以如果有对链表的并发操作,需要自己实现一个锁方案。该链表文件实现在内核源码树下include/linux/list.h


一: 链表数据结构的定义

  
struct list_head {
  struct list_head *next, *prev;
};

  list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。另外该链表中有一个空的链表头(没有实际的数据,方便循环操作)。
    在实际代码的链表几乎总是由某种结构组成。为了在代码中使用linux链表,只需要在构成实际链表的结构里嵌入一个list_head。例如某驱动程序需要维护一个链表结构,可申明如下:
struct my_struct{
  struct list_head list;
  int value;

};


二:链表的操作
(1)初始化:链表在使用前需要先初始化,由于多数元素是动态创建,所以我们通常是在运行时初始化链表,方法如下:
struct my_struct new_struct;
new_struct.value = 10;
INIT_LIST_HEAD(&new_struct.
list);//注意初始化是初始化我们自定义链表结构my_struct中包含的linux提供的链表结构list_head)
如果在编译时静态创建链表,并且直接引用它,也可以采用如下方法:

struct my_struct new_struct = {
  .list = LIST_HEAD_INIT(new_struct.list),
  .value = 10
};

这里要注意下上面2种方法中使用的宏是不一样的,参数也不一样,前一个是以指针为参数的。


(2)添加,删除,合并
  通过查看list.h的实现,我们可以发现所有的操作函数都由内部函数和外部包装函数组成,内部和外部函数的名称一样,内部函数名多了2条下划线。比如__list_del(prev,next)和list_del(entry).可以看出内部函数是以prev和next作为参数的,而外部函数是通过找到prev和next指针来直接调用内部函数加以实现。所以如果我们在prev和next指针确认的情况下也可以直接调用内部函数来完成。下面我给出常用的一些外部函数的接口,相应内部函数可以查阅list.h文件。
.list_add(struct list_head* new,struct list_head* head)//向指定节点head后插入new节点
.list_add_tail(struct list_head* new,struct list_head* head)//向指定节点head前插入new节点
.list_del(struct list_head* entry)//从链表中删除entry节点,该操作不会释放entry所占用内存,只是将其从链表中移走,所以在该函数调用后,通常要销毁entry结构体
.list_del_init(struct list_head* entry)//和前一个函数一样,只是在移走entry后再次初始化entry
.list_move(struct list_head* list,struct list_head* head)//从一个链表中摘取list节点,然后将其加入到另一链表的head节点后。
.list_move_tail(struct list_head* list,struct list_head* head)//从一个链表中摘取list节点,然后将其加入到另一链表的head节点前。
.list_empty(struct list_head* head)//如果指定链表为空返回非0,否则0;这里空链表是指只有头节点1个节点
.list_splice(struct list_head* list,struct list_head* head)//将list链表插入到指定链表head后,这里会丢掉list节点,因为2个链表合并后只有一个表头节点,所以只保留head

.list_splice_init(struct list_head* list,struct list_head* head)//和上面一样,只是会重新初始化list节点。


(3)遍历

  遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从linux链表中访问到我们自己定义的数据项。


a) 由链表节点到数据项变量 
     我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名。比如:
struct list_head* ptr;
struct my_struct* my;
my = list_entry(ptr,struct my_struct,list);

这样我们就通过linux链表节点ptr的值得到包含linux链表的我们自己定义结构的指针my。下面我们对这个宏做些分析。

#define list_entry(ptr, type, member) container_of(ptr,type,member)
   //container_of宏定义在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ({ \
             const typeof( ((type *)0)->member ) *__mptr = (ptr); \
             (type *)( (char *)__mptr - offsetof(type,member) );})

    //offsetof宏定义在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
    // size_t最终定义为unsigned int(i386)。

对于宏container_of(ptr,type,member)可以分成下面几个步骤来看:
1. typeof((type*)0->member) 得到member的数据类型
2. const (typeof(type*)0->member)* __mptr = (ptr) 定义一个类型为typeof((type*)0->member)的指针,该指针值为(ptr)
3. offsetof(type,member)得到member在type中的偏移量(以字节为单位)
4. (char*)__mptr - offsetof(type,member)得到结构体类型为type的结构体的起始地址,这里因为offsetof(type,member)得到的是字节为单位的值,指针相减时要指针类型一致,所以__mptr要做一个类型转换为char*才能和后面相减。
5. (type*)((char*)__mptr - offsetof(type,member))再做一次类型转换得到类型为type的结构体的首指针,这就是从结构体某成员变量指针来求出该结构体的首指针。指针类型从结构体某成员变量类型转换为该结构体类型。

对与宏offsetof(type,member)可如下分析:
1. ( (TYPE *)0 ) 将零转型为TYPE类型指针; 
2. ((TYPE *)0)->MEMBER 访问结构中的数据成员; 
3. &( ( (TYPE *)0 )->MEMBER )取出数据成员的地址; 
4.(size_t)(&(((TYPE*)0)->MEMBER))结果转换类型.巧妙之处在于将0转换成(TYPE*
),结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址

对于typeof(),它 是 gcc 的扩展,和 sizeof() 类似,比如下面代码:
int i; 
typeof(i) x; 
x=100; 
printf("x:%d\n",x); 

谈到offsetof()宏,可以联想到一道题目:求结构中成员偏移。代码如下:

#includestdio.h> 
typedef struct test 

char i; 
int j; 
char k;
long m; 
}Test; 
int main() 

Test *p = 0; 
printf("%p\n", &(p->m)); 

//获取结构体test中成员变量m的偏移量,其中Test *p = 0 是关键的思想,和offsetof()宏类似
有了上面的解释,最后看看在遍历链表时用到的一些宏
.list_for_each(struct list_head* cursor,struct list_head* list)//该宏创建一个for循环,每当游标cursor指向链表的下一项时执行一次。
.list_for_each_prev(struct list_head* cursor,struct list_head* list)//向后遍历链表
.list_for_each_safe(struct list_head* cursor,struct list_head* next,struct list_head* list)//该宏在循环开始时,把链表中的下一项存储在next中,这样如果cursor所指向的节点被删除也不会造成遍历的失败,所以如果在遍历中可能会删除链表中的项的时候可以使用该版本的宏。
.list_for_each_entry(type* cursor,struct list_head* list,member)//该宏直接使用包含linux链表项的结构体,这样就不需要在list_for_each循环中为了访问自定义结构体而调用的list_entry宏了。
.list_for_each_entry_safe(type* cursor,type* next,struct list_head* list,member)//和list_for_each_safe一样,在cursor被删除时仍可顺利进程遍历过程。
还有其他一些宏完成更多功能,具体参考源代码,这里不详细介绍了。
最后使用一个例子说明遍历的使用方法。我们知道在linux中进程之间存在一个继承关系,系统中每一个进程必有一个父进程,每个进程也有若干子进程,拥有同一父进程的进程称为兄弟进程(sibling)。进程间的关系都存放在进程描述符task_struct中,每个进程的task_struct中包含一个指向父进程的parent指针,还包含children指针的子进程链表(子进程可能不止一个),所以对于当前进程,访问其所有子进程的代码如下:
struct task_struct* task;
struct list_head* list;
list_for_each(list,&current->children ){
             task = list_entry(list,struct task_struct,sibling);
}

//current是指向当前进程的task_struct的指针
当然如果使用list_for_each_entry()宏,则可如下访问
list_for_each_entry(task,&current->children,sibling){
}






本文来自ChinaUnix博客,如果查看原文请点: http://blog.chinaunix.net/u/21973/showart_363381.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值