网站首页 > 博客文章 正文
上一节较为详细的介绍了 linux 内核中链表的设计与实现,能够看出,内核实际上是将链表“塞入”数据结构的。事实上,为了方便的操作这些链表,linux内核实现了一系列方法,本节将了解此。
链表的初始化
正如上一节介绍的,list_head 本身没有记录额外的信息,它仅仅起到连接各个数据节点的作用,所以在 linux 内核中,链表常常都是嵌入数据结构使用的,由这些数据结构存储需要记录的数据。例如将 list_head 嵌入 struct fox:
struct fox{ int tail_len; int weight; }; // 嵌入 list_head,构成链表节点。 struct fox{ int tail_len; int weight; struct list_head list; };
链表特别适合记录动态创建的数据,请看下面的C语言代码:
struct fox* rf; rf = kmalloc(sizeof(struct fox), GFP_KERNEL); rf->tail_length = 40; rf->weight = 6; INIT_LIST_HEAD(&rf->list);
实际上,程序中的多数元素都是在程序运行时动态创建的,若希望使用链表,则最好将其初始化,linux 内核提供了 INIT_LIST_HEAD() 函数用于初始化链表,它的C语言代码如下:
static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; }
可以看出,linux 内核初始化链表相当简单,就是让 list 的 prev 和 next 指针都指向自己而已。INIT_LIST_HEAD() 虽然很简单,但是却很有用,它可以避免 list 的 prev 和 next 指向未知的区域。
指针指向未知区域,是非常危险的。(为什么危险,可以参考我之前的文章。)
我们也可以直接使用 LIST_HEAD() 宏定义并初始化一个链表,它的 C语言代码如下,请看:
#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name)
该宏定义了一个名为 name 的链表,并按照 INIT_LIST_HEAD() 函数方式初始化了该链表。
向链表加入一个节点
以上介绍的是 linux 内核创建并初始化一个新链表的方法,如何将其加入已有的链表呢?linux 内核定义了 list_add() 函数,它的C语言代码如下,请看:
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; } static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
该函数往链表 head 节点后插入了 new 节点。从 __list_add() 函数可以看出,这一操作无非就是调整了各个链表节点的指向关系而已。
上一节我们说过,linux 内核使用的链表都是双向环形链表,所有没有“首尾”的概念,每个节点都可以当作 head 节点。
例如,假设有如 strcut fox* 型的节点 f,若想将其加入 fox_list 链表,可以这么做:
list_add(&f->list, &fox_list)
list_add_tail() 函数和 list_add() 函数是相似的,它的 C语言代码如下:
static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
容易看出,与 list_add() 函数不同的是,list_add_tail() 函数是将 new 节点插入到 head 节点的前面。
从链表删除节点
既然有往链表加入节点的需求,也一定有从链表删除节点的需求,linux 内核是通过 list_del() 函数实现的,它的C语言代码如下:
static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; prev->next = next; } #define LIST_POISON1 ((void *) 0x00100100) #define LIST_POISON2 ((void *) 0x00200200) static inline void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }
__list_del() 函数是删除节点的核心部分,不过从C语言代码可以看出,它仅仅是将节点从链表移出而已,并没有释放该节点,也就是说,之后 linux 内核还能继续使用该节点中的数据。
list_del() 函数的后两行让被删除节点的 prev 和 next 指针指向指定值了,这两个值是非 NULL 值,但是也能引起页异常,避免调用者因为使用没有初始化的节点引发的错误。
遍历链表
上一节说过,链表适合记录需要遍历的数据,那么,linux 内核是怎样遍历链表的呢?答案是使用 list_for_each() 宏,它的C语言代码如下:
#define list_for_each(pos, head) \ for (pos = (head)->next; prefetch(pos->next), pos != (head); \ pos = pos->next)
容易看出,list_for_each() 宏其实就是一个 for(;;) 循环的头而已,它从 head->next 开始遍历,每次让 post 指向下一个节点,遍历到 pos == head 结束(一圈,即全部链表节点)。
不过,list_for_each() 宏操作的都是 list_head,我们更常需要遍历的其实是存储数据的结构体,当然可以如下使用:
list_for_each(p, &fox_list){ f = list_entry(p, struct fox, list); //... }
但是,更方便的方法是使用 list_for_each_entry() 宏,它帮助我们找到结构体指针了,请看C语言代码:
#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))
如上一节介绍的一致,双向链表既可以往前遍历,也可以往后遍历。linux 内核提供了反向遍历的宏——其实就是在正向宏后加上 reverse 而已:
list_for_each_entry_reverse(pos, head, member)
遍历同时删除
以上遍历宏都是建立在链表结构不会改变的基础上的,也就是说标准的遍历过程中是不能删除节点的,否则接下来的遍历就找不到 next 或者 prev 指针了。一个解决方法就是在删除节点时,使用临时变量先将该节点的信息存储下来,事实上,linux 内核也的确是这么设计的,请看:
#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_safe() 宏,我们就可以在遍历链表节点的同时删除它。
linux 内核操作链表非常关键的函数或者宏就介绍到这里。这里再延伸一点,使用上面介绍的方法遍历链表节点时,是不能有其他地方并发删除的,如果有,就应该做好同步。
欢迎在评论区一起讨论,质疑。文章都是手打原创(本文部分参考linux内核原理和设计),每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。
猜你喜欢
- 2024-09-17 带你阅读linux内核源码:下载源码、编译内核并运行一个最小系统
- 2024-09-17 每天五分钟—珍藏的8张Linux思维导图(附Linux自学教程分享)
- 2024-09-17 为什么要学习Linux内核,如何学习?
- 2024-09-17 这估计是在座的都想得到的,最详细的Linux思维导图和自学教程
- 2024-09-17 一张图看懂Linux内核(linux内核百度百科)
- 2024-09-17 Linux最齐全的8张入门思维导图+Linux入门学习视频教程分享
- 2024-09-17 linux学习4,全靠它,三分钟把系统内核完全跑起来,busybox介绍
- 2024-09-17 沉下来心来学五个月就能学会嵌入式的最佳路线
- 2024-09-17 「干货分享」嵌入式学习路线公开(书籍推荐+视频推荐+练手项目)
- 2024-09-17 linux学习笔记:linux学习流程图思维导图
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- powershellfor (55)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)