前面我们说到过list_head,这是linux中通用的链表形式,双向循环链表,功能强大,实现简单优雅。可如果您认为list_head就是链表的极致,应该在linux链表界一统天下,那可就错了。据我所知,linux内核代码中至少还有两种链表能占有一席之地。一种就是hlist,一种就是本节要介绍的klist。虽然三者不同,但hlist和klist都可以看成是从list_head中发展出来的,用于特殊的链表使用情景。hlist是用于哈希表中。众所周知,哈希表主要就是一个哈希数组,为了解决映射冲突的问题,常常把哈希数组的每一项做成一个链表,这样有多少重复的都可以链进去。但哈希数组的项很多,list_head的话每个链表头都需要两个指针的空间,在稀疏的哈希表中实在是一种浪费,于是就发明了hlist。hlist有两大特点,一是它的链表头只需要一个指针,二是它的每一项都可以找到自己的前一节点,也就是说它不再循环,但仍是双向。令人不解的是,hlist的实现太绕了,比如它明明可以直接指向前一节点,却偏偏指向指针地址,还是前一节点中指向后一节点的指针地址。即使这种设计在实现时占便宜,但它理解上带来的不便已经远远超过实现上带来的小小便利。
同hlist一样,klist也是为了适应某类特殊情形的要求。考虑一个被简化的情形,假设一些设备被链接在设备链表中,一个线程命令卸载某设备,即将其从设备链表中删除,但这时该设备正在使用中,这时就出现了冲突。当前可以设置临界区并加锁,但因为使用一个设备而锁住整个设备链表显然是不对的;又或者可以从设备本身做文章,让线程阻塞,这当然也可以。但我们上节了解了kref,就该知道linux对待这种情况的风格,给它一个引用计数kref,等计数为零就删除。klist就是这么干的,它把kref直接保存在了链表节点上。之前说到有线程要求删除设备,之前的使用仍存在,所以不能实际删除,但不应该有新的应用访问到该设备。klist就提供了一种让节点在链表上隐身的方法。下面还是来看实际代码吧。
klist的头文件是include/linux/klist.h,实现在lib/klist.c。
struct klist_node;
struct klist {
spinlock_t k_lock;
struct list_head k_list;
void (*get)(struct klist_node *);
void (*put)(struct klist_node *);
} __attribute__ ((aligned (4)));
#define KLIST_INIT(_name, _get, _put) \
{ .k_lock = __SPIN_LOCK_UNLOCKED(_name.k_lock), \
.k_list = LIST_HEAD_INIT(_name.k_list), \
.get = _get, \
.put = _put, }
#define DEFINE_KLIST(_name, _get, _put) \
struct klist _name = KLIST_INIT(_name, _get, _put)
extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
void (*put)(struct klist_node *));
struct klist_node {
void *n_klist; /* never access directly */
struct list_head n_node;
struct kref n_ref;
};
可以看到,klist的链表头是struct klist结构,链表节点是struct klist_node结构。先看struct klist,除了包含链表需要的k_list,还有用于加锁的k_lock。剩余的get()和put()函数是用于struct klist_node嵌入在更大的结构中,这样在节点初始时调用get(),在节点删除时调用put(),以表示链表中存在对结构的引用。再看struct klist_node,除了链表需要的n_node,还有一个引用计数n_ref。还有一个比较特殊的指针n_klist,n_klist是指向链表头struct klist的,但它的第0位用来表示是否该节点已被请求删除,如果已被请求删除则在链表循环时是看不到这一节点的,循环函数将其略过。现在你明白为什么非要在struct klist的定义后加上__attribute__((aligned(4)))。不过说实话这样在x86下仍然不太保险,但linux选择了相信gcc,毕竟是多年的战友和兄弟了,相互知根知底。
看过这两个结构,想必大