linux 内核 链表 list_head 使用方法
做内核驱动开发经常会使用linux内核最经典的双向链表 list_head, 以及它的拓展接口(或者宏定义): list_add , list_add_tail, list_del , list_entry ,list_for_each , list_for_each_entry ......
每次看到这些接口,感觉都很像,今天专门研究了一下内核,对它们做一些总结,希望为后续开发提供方便。
首先找到list_head 结构体定义,kernel/inclue/linux/types.h 如下:
struct list_head {struct list_head *next, *prev;
};
然后就开始围绕这个结构开始构建链表,然后插入、删除节点 ,遍历整个链表等等,其实内核已经提供好了现成的接口,接下来就让我们进入 kernel/include/linux/list.h中:
一. 创建链表
内核提供了下面的这些接口来初始化链表:
#define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)static inline void INIT_LIST_HEAD(struct list_head *list)
{WRITE_ONCE(list->next, list);list->prev = list;
}
如: 可以通过 LIST_HEAD(mylist) 进行初始化一个链表,mylist的prev 和 next 指针都是指向自己。
struct list_head mylist = {&mylist, &mylist};
但是如果只是利用mylist这样的结构体实现链表就没有什么实际意义了,因为正常的链表都是为了遍历结构体中的其它有意义的字段而创建的,而我们mylist中只有 prev和next指针,却没有实际有意义的字段数据,所以毫无意义。
综上,我们可以创建一个宿主结构,然后在此结构中再嵌套mylist字段,宿主结构又有其它的字段(进程描述符 task_struct,页面管理的page结构,等就是采用这种方法创建链表的)。为简便理解,定义如下:
struct my_task_list {int val ;struct list_head mylist;
}
创建第一个节点
struct my_task_list first_task =
{ .val = 1,.mylist = LIST_HEAD_INIT(first_task.mylist)
};
这样mylist 就prev 和 next指针分别指向mylist自己了,如下图:
二. 添加节点
内核已经提供了添加节点的接口了
1. list_add
如下所示。 根据注释可知,是在链表头head后方插入一个新节点new。
并且还说了一句:这个接口利用实现堆栈 (why? 稍后再做分析)
/*** list_add - add a new entry* @new: new entry to be added* @head: list head to add it after** Insert a new entry after the specified head.* This is good for implementing stacks.*/
static inline void list_add(struct list_head *new, struct list_head *head)
{__list_add(new, head, head->next);
}
list_add再调用__list_add接口
/** Insert a new entry between two known consecutive entries.** This is only for internal list manipulation where we know* the prev/next entries already!*/
static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next)
{if (!__list_add_valid(new, prev, next))return;next->prev = new;new->next = next;new->prev = prev;WRITE_ONCE(prev->next, new);
}
其实就是在head 链表头后和链表头后第一个节点之间插入一个新节点。然后这个新的节点就变成了链表头后的第一个节点了。
依然用上面的my_task_list结构体举例子
首先我们创建一个链表头 header_task
LIST_HEAD(header_task);
然后再创建实际的第一个节点
struct my_task_list my_first_task =
{ .val = 1,.mylist = LIST_HEAD_INIT(my_first_task.mylist)
};
接着把这个节点插入到header_task之后
list_add(&my_first_task.mylist, &header_task);
然后在创建第二个节点,同样把它插入到header_task之后
struct my_task_list my_second_task =
{ .val = 2,.mylist = LIST_HEAD_INIT(my_second_task.mylist)
};
其实还可以用另外一个接口 INIT_LIST_HEAD 进行初始化(参数为指针变量), 如下:
struct my_task_list my_second_task;
my_second_task.val = 2;
INIT_LIST_HEAD(&my_second_task.mylist);
list_add(&my_second_task.mylist, &header_task)
以此类推,每次插入一个新节点,都是紧靠着header节点,而之前插入的节点依次排序靠后,那最后一个节点则是第一次插入header后的那个节点。最终可得出:先来的节点靠后,而后来的节点靠前,“先进后出,后进先出”。所以此种结构类似于 stack“堆栈”, 而 header_task就类似于内核stack中的栈顶指针esp, 它都是紧靠着最后push到栈的元素。
2. list_add_tail 接口
上面所讲的list_add接口是从链表头header后添加的节点。 同样,内核也提供了从链表尾处向前添加节点的接口list_add_tail. 让我们来看一下它的具体实现。
/*** list_add_tail - add a new entry* @new: new entry to be added* @head: list head to add it before** Insert a new entry before the specified head.* This is useful for implementing queues.*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{__list_add(new, head->prev, head);
}
从注释可得出:(1)在一个特定的链表头前面插入一个节点
(2)这个方法很适用于队列的实现 (why?)
进一步把__list_add ()展开如下:
/** Insert a new entry between two known consecutive entries.** This is only for internal list manipulation where we know* the prev/next entries already!*/
static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next)
{if (!__list_add_valid(new, prev, next))return;next->prev = new;new->next = next;new->prev = prev;WRITE_ONCE(prev->next, new);
}
所以,很清楚明了, list_add_tail就相当于在链表头前方依次插入新的节点(也可理解为在链表尾部开始插入节点,此时,header节点既是为节点,保持不变)
利用上面分析list_add接口的方法可画出数据结构图形如下。
(1)创建一个 链表头(实际上应该是表尾), 同样可调用 LIST_HEAD(header_task);
(2)插入第一个节点 my_first_task.mylist , 调用 list_add_tail(& my_first_task.mylist, & header_task);
(3) 插入第二个节点my_second_task.mylist,调用list_add_tail(& my_second_task.mylist, &header_task);
依此类推,每次插入的新节点都是紧挨着 header_task表尾,而插入的第一个节点my_first_task排在了第一位,my_second_task排在了第二位,可得出:先插入的节点排在前面,后插入的节点排在后面,“先进先出,后进后出”,这不正是队列的特点吗(First in First out)!
三. 删除节点
内核同样在list.h文件中提供了删除节点的接口 list_del(), 让我们看一下它的实现流程
static inline void list_del(struct list_head *entry)
{__list_del_entry(entry);entry->next = LIST_POISON1;entry->prev = LIST_POISON2;
}
/** Delete a list entry by making the prev/next entries* point to each other.** This is only for internal list manipulation where we know* the prev/next entries already!*/
static inline void __list_del(struct list_head * prev, struct list_head * next)
{next->prev = prev;WRITE_ONCE(prev->next, next);
}/*** list_del - deletes entry from list.* @entry: the element to delete from the list.* Note: list_empty() on entry does not return true after this, the entry is* in an undefined state.*/
static inline void __list_del_entry(struct list_head *entry)
{if (!__list_del_entry_valid(entry))return;__list_del(entry->prev, entry->next);
}
利用list_del(struct list_head *entry) 接口就可以删除链表中的任意节点了,但需注意,前提条件是这个节点是已知的,既在链表中真实存在,切prev,next指针都不为NULL。
四. 链表遍历
内核是同过下面这个宏定义来完成对list_head链表进行遍历的,如下 :
/*** list_for_each - iterate over a list* @pos: the &struct list_head to use as a loop cursor.* @head: the head for your list.*/
#define list_for_each(pos, head) \for (pos = (head)->next; pos != (head); pos = pos->next)
上面这种方式是从前向后遍历的,同样也可以使用下面的宏反向遍历
/*** list_for_each_prev - iterate over a list backwards* @pos: the &struct list_head to use as a loop cursor.* @head: the head for your list.*/
#define list_for_each_prev(pos, head) \for (pos = (head)->prev; pos != (head); pos = pos->prev)
而且,list.h 中也提供了list_replace( 节点替换) list_move(节点移位) ,翻转,查找等接口,这里就不在一一分析了。
五. 宿主结构
1.找出宿主结构 list_entry(ptr, type, member)
上面的所有操作都是基于list_head这个链表进行的,涉及的结构体也都是:
struct list_head {struct list_head *next, *prev;
};
其实,正如文章一开始所说,我们真正更关心的是包含list_head这个结构体字段的宿主结构体,因为只有定位到了宿主结构体的起始地址,我们才能对对宿主结构体中的其它有意义的字段进行操作。
struct my_task_list {int val ;struct list_head mylist;
}
那我们如何根据mylist这个字段的地址而找到宿主结构my_task_list的位置呢???
做linux驱动开发的同学是不是想到了LDD3这本书中经常使用的一个非常经典的宏定义呢!那就是:
container_of(ptr, type, member)
没错就是它,在LDD3这本书中的第三章字符设备驱动,以及第十四章驱动设备模型中多次提到,所以我觉得这个宏应该是内核最经典的宏之一。 那list.h中使用什么接口实现的这个转换功能呢?
/*** list_entry - get the struct for this entry* @ptr: the &struct list_head pointer.* @type: the type of the struct this is embedded in.* @member: the name of the list_head within the struct.*/
#define list_entry(ptr, type, member) \container_of(ptr, type, member)
list.h中提供了list_entry宏来实现对应地址的转换,但最终还是调用了container_of宏,所以container_of宏的伟大之处不言而喻。 那接下来让我们揭开她的面纱:
此宏在内核代码 kernel/include/linux/kernel.h中定义(此处kernel版本为3.10;新版本4.13之后此宏定义改变,但实现思想保持一致)
/** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
而offsetof定义在 kernel/include/linux/stddef.h ,如下:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
看下container_of宏的注释:
(1)根据结构体重的一个成员变量地址导出包含这个成员变量mem的struct地址。
(2)参数解释:
ptr : 成员变量mem的地址
type: 包含成员变量mem的宿主结构体的类型
member: 在宿主结构中的mem成员变量的名称
如果用我们之前定义的结构体struct my_task_list举例
struct my_task_list {int val ;struct list_head mylist;
}
struct my_task_list first_task =
{ .val = 1,.mylist = LIST_HEAD_INIT(first_task.mylist)
};
ptr : &first_task.mylist
type : struct my_task_list
member : mylist
而container_of宏的功能就是根据 first_task.mylist字段的地址得出first_task结构的其实地址。
把上面offsetof的宏定义代入container_of宏中,可得到下面定义:
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - ((size_t) &((type *)0)->member) );})
再把宏中对应的参数替换成实参:
const typeof( ((struct my_task_list *)0)->mylist ) *__mptr = (&first_task.mylist); \
(struct my_task_list *)( (char *)__mptr - ((size_t) &((struct my_task_list *)0)->mylist) );})
typeof 是 GNU对C新增的一个扩展关键字,用于获取一个对象的类型 ,比如这里((struct my_task_list *)0)->mylist 是把0地址强制转换成struct my_task_list 指针类型,然后取出mylist元素。 然后再对mylist元素做typeof操作,其实就是获取 my_task_list结构中mylist字段的数据类型struct list_head,所以这行语句最后转化为:
const struct list_head *__mptr = (&first_task.mylist);
第二条语句中在用 __mptr这个指针 减去 mylist字段在 my_task_list中的偏移(把0地址强制转换成struct my_task_list指针类型,然后取出mylist的地址,此时mylist的地址也是相对于0地址的偏移,所以就是mylist字段相对于宿主结构类型struct my_task_list的偏移) 正好就是宿主结构的起始地址。C语言的灵活性得到了很好的展示!!!
2. 宿主结构的遍历
我们可以根据结构体中成员变量的地址找到宿主结构的地址, 并且我们可以对成员变量所建立的链表进行遍历,那我们是不是也可以通过某种方法对宿主结构进行遍历呢?
答案肯定是可以的,内核在list.h中提供了下面的宏:
/*** list_for_each_entry - iterate over list of given type* @pos: the type * to use as a loop cursor.* @head: the head for your list.* @member: the name of the list_head within the struct.*/
#define list_for_each_entry(pos, head, member) \for (pos = list_first_entry(head, typeof(*pos), member); \&pos->member != (head); \pos = list_next_entry(pos, member))
其中,list_first_entry 和 list_next_entry宏都定义在list.h中,分别代表:获取第一个真正的宿主结构的地址; 获取下一个宿主结构的地址。它们的实现都是利用list_entry宏。
/*** list_first_entry - get the first element from a list* @ptr: the list head to take the element from.* @type: the type of the struct this is embedded in.* @member: the name of the list_head within the struct.** Note, that list is expected to be not empty.*/
#define list_first_entry(ptr, type, member) \list_entry((ptr)->next, type, member)/*** list_next_entry - get the next element in list* @pos: the type * to cursor* @member: the name of the list_head within the struct.*/
#define list_next_entry(pos, member) \list_entry((pos)->member.next, typeof(*(pos)), member)
最终实现了宿主结构的遍历
#define list_for_each_entry(pos, head, member) \for (pos = list_first_entry(head, typeof(*pos), member); \&pos->member != (head); \pos = list_next_entry(pos, member))
首先pos定位到第一个宿主结构地址,然后循环获取下一个宿主结构地址,如果查到宿主结构中的member成员变量(宿主结构中struct list_head定义的字段)地址为head,则退出,从而实现了宿主结构的遍历。如果要循环对宿主结构中的其它成员变量进行操作,这个遍历操作就显得特别有意义了。
我们用上面的 my_task_list结构举个例子:
struct my_task_list *pos_ptr = NULL ;
list_for_each_entry (pos_ptr, & header_task, mylist )
{ printk ("val = %d\n" , pos_ptr->val);
}
参考文档:https://kernelnewbies.org/FAQ/LinkedLists
《Understanding linux kernel》
《Linux device drivers》
linux 内核 链表 list_head 使用方法相关推荐
- linux内核链表以及list_entry--linux内核数据结构(一)
传统的链表实现 之前我们前面提到的链表都是在我们原数据结构的基础上增加指针域next(或者prev),从而使各个节点能否链接在一起, 比如如下的结构信息 typedef struct fox { un ...
- linux内核链表分析
一.常用的链表和内核链表的区别 1.1 常规链表结构 通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系.按照指针域的组织以及各个节 ...
- Linux内核链表交换节点,[笔记]Linux内核链表:结点的插入、删除以及链表的遍历...
Linux内核链表:结点的插入.删除以及链表的遍历 1. Linux内核链表的核心思想是:在用户自定义的结构A中声明list_head类型的成员p,这样每个结构类型为A的变量a中,都拥有同样的成员p, ...
- 深入分析 Linux 内核链表--转
引用地址:http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html 一. 链表数据结构简介 链表是一种常用的组织有序数据 ...
- Linux 内核链表 【转】
原文:http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html 一. 链表数据结构简介 链表是一种常用的组织有序数据的数 ...
- Linux驱动编程 step-by-step (十) Linux 内核链表
终于可以清闲下来打理一下我的blog了,台资企业真的事情很多很烦-- 前几篇文章对字符设备有个简单介绍,并以简单的一个字符设备驱动作结尾,其实linux上大部分驱动程序都是字符设备程序,Linux源码 ...
- 深入理解Linux内核链表
之前写过的链表文章,再结合这篇,我觉得是一道硬菜. Linux内核链表 C语言,链表 大家五一节日快乐,我知道劳动节大家都辛苦了,吃点硬菜好顶住饿肚子~ #一. 链表数据结构简介 链表是一种常用的组织 ...
- Linux 内核 链表 的简单模拟(1)
第零章:扯扯淡 出一个有意思的题目:用一个宏定义FIND求一个结构体struct里某个变量相对struc的编移量,如 structstudent {int a; //FIND(struct stude ...
- 【C++】链表反转逆序|建立、删除、修改、插入|linux内核链表与普通链表
目录 C++实现链表逆序 链表的建立.删除.修改.插入 linux内核链表与普通链表 C++实现链表逆序 实现链表逆序,首先要有一个链表,下面是链表的结构体: typedef struct listn ...
最新文章
- 华为FusionCloud 云计算解决方案及相关资料下载
- 利用js实现table增加一行
- openssl之EVP系列之1---算法封装
- java中如何创建子包,父包类可以从java中的子包类访问?
- cs架构用什么语言开发_用Rust语言开发微信小程序
- CentOs6.x yum源停止维护,安装yum源
- android 创建模拟器打不开,解决Android模拟器打不开的问题!...
- HDU 2896病毒侵袭
- 大腾讯的第一个开源项目「Tinker」
- python 类self作用_self在Python中有什么用途?
- c语言写识别电压的程序,PIC单片机C语言编程实例——交流电压测量
- 面经个人向(算法岗)
- 计量芯片HLW8032在充电桩设备中的典型应用
- vue 倒计时 插件_vue倒计时组件
- 大学计算机基础第五版习题和课后题答案
- QQ浏览器的历史记录在那 QQ浏览器查看浏览历史的方法
- WSJ Merkel Top On Forbes' Most Powerful Women List For 4th Year
- Netflix时代之后Spring Cloud微服务的未来
- POJ 1616 Harmonic Periods 笔记
- MSP430 BSL编程原理