klist
是list
的线程安全版本,它提供了整个链表的自旋锁,查找链表节点,对链表节点的插入和删除操作都要获得这个自旋锁。klist
的节点数据结构是klist_node
,klist_node
引入引用计数,只有当引用计数减到0时才允许该node
从链表中移除。当一个内核线程要移除一个node
,必须要等待到node
的引用计数释放,在此期间线程处于休眠状态。为了方便线程等待,klist
引入等待移除节点者结构体klist_waiter
,klist_waiter
组成klist_remove_waiters
(内核全局变量)链表,为保护klist_remove_waiters
线程安全,引入klist_remove_lock
(内核全局变量)自旋锁。为方便遍历klist
,引入了迭代器klist_iter
。
数据结构及接口声明
include/linux/klist.h:
klist_node
klist_node
有个dead
字段,联合在n_klist
指针中,n_klist
只能默认指向klist
。klist
的定义中,__attribute__ ((aligned (sizeof(void *))))
要求按void
指针类型大小进行内存字节对齐,这意味着klist
实例的地址低2位(x86_32)或者低3位(x86_64)总是0,这些为0的低位刚好可以作为其他用处,对该指针解引用前需与掉相应位。来看源码:
为什么要引入这个dead标识呢?
如果一个内核线程要让某个node
无效,不能简单的从klist
中把node
摘下来,只能减少node
的引用计数,但是由于其他内核线程也拥有该node
的引用计数,所以节点还是在klist
链中,遍历节点等操作时无法避开该node
。引入这个标识后,只要设置这个标识,尽管该node
还在klist
链上,但是迭代操作的时候通过这个标识避开dead
的节点。这样在该节点上不会有新的操作,通过链表遍历也无法获取到该节点,当其他内核线程不再引用该node
后,该node
自动从klist
链中移除。所以dead
的作用是禁止再使用该node
,但是已经被人家在用的还是可以继续使用。调用klist_del()
会标示该node
为dead
,并最终删除节点。
前面的四个函数都是内部静态函数,用于辅助API实现:knode_klist
是从节点找到链表头,knode_dead
是检查该节点是否已被请求删除,knode_set_klist
设置节点的链表头,knode_kill
将请求删除一个节点。
细心的话大家会发现这四个函数是对称的,而且都是操作节点的内部函数。
klist 的定义和初始化
常规定义:
便捷宏方式定义:
|
|
注意函数注释中对get/put参数(函数指针)的说明。
插入节点
klist_add_xxx
函数初始化node
,并将其插入链表,插入链表后,引用计数为1klist_add_tail
向后插入,klist_add_head
向前插入;kilst_add_after
在某个节点的后面插入,klist_add_before
在某个节点的前面插入。
三个内部函数,add_head
将节点加入链表头,add_tail
将节点加入链表尾,klist_node_init
是初始化节点。
klist_add_head
将节点初始化,并将其加入链表头,klist_add_tail
将节点初始化,并将其加入链表尾。
klist_add_after
将节点加到指定节点后面,klist_add_before
将节点加到指定节点前面。
删除节点
klist_del
调用klist_put
,减少引用计数,并设dead
标记,当引用计数减为0时,自动调用klist_release
,把节点从klist
中删除。
klist_remove
把当前线程加入等待移除链表,减少引用计数,如果有其他内核线程占用引用计数,把当前线程休眠。
klist
是怎么迫使remove
某个node
的线程休眠的,又是怎么唤醒的?为了方便进程管理,引入了klist_waiter
结构,如下:
来看使线程进入休眠的代码,klist_remove函数:
klist_del
函数调用klist_put
调用klist_dec_and_del
调用kref_put
,kref_put
当引用计数减到0时回调klist_release
函数,klist_release
会释放等待者。
进程的休眠与唤醒是klist_remove
和klist_release
共同作用的结果,我们来看klist_release
的源码:
klist_remove
函数把局部变量加入到全局链表中,但是由于klist_remove
会使线程休眠,它返回前总是由klist_release
把waiter
从klist_remove_waiters
移走,所以不会导致崩溃。其实klist_remove_waiters
的链表节点实际都是一些内核栈中的waiter
结构,这些线程都休眠在klist_remove
中。
遍历klist
klist
没有像list
一样定义一系列的list_for_each_xxx
宏。klist
提供专门的迭代器结构提klist_iter
,遍历前首先要初始化迭代器 ,klist_next()
函数把当前迭代器向后移动,并返回移动后的node
。klist_iter_init()
,klist_iter_init_node()
都是迭代器初始化函数,前者把迭代器当前位置设置为NULL
,klist_next()
自动从第一个node
开始,后者可以指定当前node
。迭代器指向node
时会增加node
的引用计数,当迭代器不用时必须调用klist_iter_exit
退出迭代器,释放当前node
的引用计数。
klist_node_attached
检查节点是否被包含在某链表中。
klist_iter_init_node
是从klist
中的某个节点开始遍历,而klist_iter_init
是从链表头开始遍历的。要注意,klist_iter_init
和klist_iter_init_node
的用法不同。klist_iter_init_node
可以在其后直接对当前节点进行访问,也可以调用klist_next
访问下一节点,而klist_iter_init
只能调用klist_next
访问下一节点。或许,klist_iter_init_node
的本意不是从当前节点开始,而是从当前节点的下一节点开始。
klist_iter_exit
遍历结束函数。在遍历完成时调不调无所谓,但如果想中途结束,就一定要调用klist_iter_exit
。
klist_next
是将循环进行到下一节点,实现中需要注意两点问题:
- 加锁,根据经验,单纯对某个节点操作不需要加锁,但对影响整个链表的操作需要加自旋锁。比如之前klist_iter_init_node中对节点增加引用计数,就不需要加锁,因为只有已经拥有节点引用计数的线程才会特别地从那个节点开始。而之后klist_next中则需要加锁,因为当前线程很可能没有引用计数,所以需要加锁,让情况固定下来。这既是保护链表,也是保护节点有效。符合kref引用计数的使用原则。
- 要注意,虽然在节点切换的过程中是加锁的,但切换完访问当前节点时是解锁的,中间可能有节点被删除,也可能有节点被请求删除,这就需要注意。首先要忽略链表中已被请求删除的节点,然后在减少前一个节点引用计数时,可能就把前一个节点删除了。这里之所以不调用klist_put,是因为本身已处于加锁状态,但仍要有它的实现。这里的实现和klist_put中类似,代码不介意在加锁状态下唤醒另一个线程,但却不希望在加锁状态下调用put函数,那可能会涉及释放另一个更大的结构。