Linux内核klist链表分析

Posted cqlismy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核klist链表分析相关的知识,希望对你有一定的参考价值。

1、前言

在Linux内核源码中,除了简洁的list链表还有klist链表,它是list链表的线程安全版本,在结构中提供了整个链表的自旋锁,对链表节点查找、插入和删除等操作,都需要先获得这个自旋锁,klist链表节点数据结构klist_node引入了引用计数,只有当节点的的引用计数为0时,才允许该节点从klist链表中移除。

 

2、klist链表相关结构

内核源码中,klist相关的头文件在include/linux/klist.h,实现的文件在lib/klist.c中,接下来分析一下klist链表头和klist链表节点的定义:

首先是klist链表头的定义,如下:

struct klist 
    spinlock_t        k_lock;
    struct list_head    k_list;
    void            (*get)(struct klist_node *);
    void            (*put)(struct klist_node *);
 __attribute__ ((aligned (sizeof(void *))));

成员分析:

k_lock:链接节点操作需要的自旋锁;

k_list:嵌入的双向链表list;

get:函数指针,用于链表内的节点增加引用计数;

put:函数指针,用于链表内的节点减少引用计数。

需要注意的是,该结构体是sizeof(void *)字节对齐,假设是4字节对齐的话,说明klist链表的实例地址的最低位是0,并且在klist实例中,地址的最低位有其它用处。

 

接下来是klist链表中的节点结构定义,结构体名为struct klist_node,该定义如下:

struct klist_node 
    void            *n_klist;    /* never access directly */
    struct list_head    n_node;
    struct kref        n_ref;
;

成员分析:

n_klist:用于指向klist链表头;

n_node:嵌入的双向链表list;

n_ref:klist链表节点的引用计数器。

需要注意的是,n_klist指针是用来指向链表头的,它的最低位用来表示该节点是否已被请求删除,如果已经被请求删除的话,在klist链表中遍历是看不到该节点的。

 

这两个结构形成的一个简单链表结构如下所示:

技术图片 

 

3、klist链表实现

接下来对klist链表的相关操作进行简单分析:

#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)

/**
 * klist_init - Initialize a klist structure.
 * @k: The klist we‘re initializing.
 * @get: The get function for the embedding object (NULL if none)
 * @put: The put function for the embedding object (NULL if none)
 *
 * Initialises the klist structure.  If the klist_node structures are
 * going to be embedded in refcounted objects (necessary for safe
 * deletion) then the get/put arguments are used to initialise
 * functions that take and release references on the embedding
 * objects.
 */
void klist_init(struct klist *k, void (*get)(struct klist_node *),
        void (*put)(struct klist_node *))

    INIT_LIST_HEAD(&k->k_list);        //双向链表初始化
    spin_lock_init(&k->k_lock);        //自旋锁初始化
    k->get = get;
    k->put = put;

KLIST_INIT()宏用于对klist的链表头进行初始化,包括自旋锁以及嵌入的双向链表初始化,DEFINE_KLIST()宏则是用来定义一个klist,并调用KLIST_INIT()宏进行初始化,klist_init()则是使用函数的方式对klist进行初始化。

/*
 * Use the lowest bit of n_klist to mark deleted nodes and exclude
 * dead ones from iteration.
 */
#define KNODE_DEAD        1LU
#define KNODE_KLIST_MASK    ~KNODE_DEAD

static struct klist *knode_klist(struct klist_node *knode)

    return (struct klist *)
        ((unsigned long)knode->n_klist & KNODE_KLIST_MASK);//将指针的最低位清0,并返回指针


static bool knode_dead(struct klist_node *knode)

    return (unsigned long)knode->n_klist & KNODE_DEAD;//判断最低位


static void knode_set_klist(struct klist_node *knode, struct klist *klist)

    knode->n_klist = klist;
    /* no knode deserves to start its life dead */
    WARN_ON(knode_dead(knode));


static void knode_kill(struct klist_node *knode)

    /* and no knode should die twice ever either, see we‘re very humane */
    WARN_ON(knode_dead(knode));
    *(unsigned long *)&knode->n_klist |= KNODE_DEAD;//将指针的最低位置1

上面4个函数是内部的静态函数,用来辅助klist的API接口实现,knode_klist()函数用于从knode中寻找klist链表头,konde_dead()函数用于检查该节点是否已被请求删除,knode_set_klist()函数用于设置节点的klist链表头,knode_kill()则用于请求删除节点。

为什么要引入dead这个标识呢?当一个线程要让某个klist_node无效时,不能简单地从klist中删除,因为有可能有其它线程还在使用这个节点,因此只能减少klist_node的引用计数,由于其它线程也拥有这个klist_node的引用计数,所以节点还在klist中,遍历的时候将无法避开这个节点,但引入dead这个标识后,当这个标志位被设置后,遍历的时候可以进行判断标志位,然后有效地避开某些被申请删除的节点,当其它线程不引用该节点后,引用计数为0,该节点将会自动从klist链表中移除。

static void add_head(struct klist *k, struct klist_node *n)

    spin_lock(&k->k_lock);
    list_add(&n->n_node, &k->k_list);//在链表头后面插入节点
    spin_unlock(&k->k_lock);


static void add_tail(struct klist *k, struct klist_node *n)

    spin_lock(&k->k_lock);
    list_add_tail(&n->n_node, &k->k_list);//在链表头前面插入节点
    spin_unlock(&k->k_lock);


static void klist_node_init(struct klist *k, struct klist_node *n)

    INIT_LIST_HEAD(&n->n_node);//链表初始化
    kref_init(&n->n_ref);//引用计数初始化
    knode_set_klist(n, k);//klist_node设置头节点
    if (k->get)
        k->get(n);

接下来又是3个静态函数,add_head()函数用于在链表头后插入节点,add_tail()用于在链表尾部插入节点,需要注意的是,在对链表节点的操作之前需要获得自旋锁,klist_node_init()函数用于初始化klist_node节点,包括内部链表、引用计数器和链表头指针的初始化。

/**
 * klist_add_head - Initialize a klist_node and add it to front.
 * @n: node we‘re adding.
 * @k: klist it‘s going on.
 */
void klist_add_head(struct klist_node *n, struct klist *k)

    klist_node_init(k, n);
    add_head(k, n);


/**
 * klist_add_tail - Initialize a klist_node and add it to back.
 * @n: node we‘re adding.
 * @k: klist it‘s going on.
 */
void klist_add_tail(struct klist_node *n, struct klist *k)

    klist_node_init(k, n);
    add_tail(k, n);

上面两个函数中,klist_add_head()用于将节点进行初始化并添加到klist链表头,klist_add_tail()将节点初始化后并添加到klist尾部。

/**
 * klist_add_behind - Init a klist_node and add it after an existing node
 * @n: node we‘re adding.
 * @pos: node to put @n after
 */
void klist_add_behind(struct klist_node *n, struct klist_node *pos)

    struct klist *k = knode_klist(pos);

    klist_node_init(k, n);
    spin_lock(&k->k_lock);
    list_add(&n->n_node, &pos->n_node);//在某个节点后插入klist_node节点
    spin_unlock(&k->k_lock);


/**
 * klist_add_before - Init a klist_node and add it before an existing node
 * @n: node we‘re adding.
 * @pos: node to put @n after
 */
void klist_add_before(struct klist_node *n, struct klist_node *pos)

    struct klist *k = knode_klist(pos);

    klist_node_init(k, n);
    spin_lock(&k->k_lock);
    list_add_tail(&n->n_node, &pos->n_node);//在某个节点前插入klist_node节点
    spin_unlock(&k->k_lock);

上面两个函数中,klist_add_behind()用于将新节点初始化,然后将节点插入到pos节点后面,其实就是把klist链表的pos节点当成链表头,然后调用list_add()插入节点,klist_add_before()函数是相同的原理,用于将新节点插入到pos节点前面。

 

接下来,继续分析klist遍历相关的内容:

struct klist_waiter 
    struct list_head list;
    struct klist_node *node;
    struct task_struct *process;
    int woken;
;

static DEFINE_SPINLOCK(klist_remove_lock);//定义klist_remove_lock自旋锁
static LIST_HEAD(klist_remove_waiters);//定义klist_remove_waiters链表

static void klist_release(struct kref *kref)

    struct klist_waiter *waiter, *tmp;
    struct klist_node *n = container_of(kref, struct klist_node, n_ref);/*根据节点中kref指针获取节点首地址*/

    WARN_ON(!knode_dead(n));
    list_del(&n->n_node);//将嵌入的双向链表节点删除
    spin_lock(&klist_remove_lock);//上锁
    list_for_each_entry_safe(waiter, tmp, &klist_remove_waiters, list)     //链表遍历
        if (waiter->node != n)
            continue;

        list_del(&waiter->list);//将睡眠的进程移除
        waiter->woken = 1;    //唤醒标志位
        mb();
        wake_up_process(waiter->process);    //将进程唤醒
    
    spin_unlock(&klist_remove_lock);
    knode_set_klist(n, NULL);


static int klist_dec_and_del(struct klist_node *n)

    return kref_put(&n->n_ref, klist_release);//klist_node节点引用计数减1操作


static void klist_put(struct klist_node *n, bool kill)

    struct klist *k = knode_klist(n);//获取klist链表头
    void (*put)(struct klist_node *) = k->put;//获取引用计数减少函数指针

    spin_lock(&k->k_lock);//上锁
    if (kill)
        knode_kill(n);//请求删除节点
    if (!klist_dec_and_del(n))//判断节点的引用计数是否为0,是将调用klist_release移除
        put = NULL;
    spin_unlock(&k->k_lock);
    if (put)
        put(n);//节点的引用计数减1操作


/**
 * klist_del - Decrement the reference count of node and try to remove.
 * @n: node we‘re deleting.
 */
void klist_del(struct klist_node *n)

    klist_put(n, true);

在前面的klist_node链表节点中,可以知道链表节点引入了一个引用计数器,使用kref实现了节点的动态删除,当引用计数的值为0时,就会调用klist_release()函数将节点进行脱离,klist_dec_and_del()函数是kref_put()函数的进一步封装,用于减少引用计数操作,klist_del()用于释放引用计数,该函数调用了内部函数klist_put(),该函数会调用knode_kill()将链表节点设置为已请求删除,调用klist_dec_and_del()减少引用计数,并且有可能会调用put()函数。

在上面代码中,还引入了一个新的结构struct klist_waiter,用于链表删除时请求线程阻塞,当有线程申请删除某节点时,但是节点的引用计数还不为0,所以只能请求删除节点的线程阻塞,使用struct klist_waiter将线程阻塞在链表klist_remove_waiters上,因此,可以发现,klist_release()函数在调用时,还需要将阻塞的线程进行唤醒。

/**
 * klist_remove - Decrement the refcount of node and wait for it to go away.
 * @n: node we‘re removing.
 */
void klist_remove(struct klist_node *n)

    struct klist_waiter waiter;

    waiter.node = n;
    waiter.process = current;//当前进程
    waiter.woken = 0;
    spin_lock(&klist_remove_lock);
    list_add(&waiter.list, &klist_remove_waiters);//将节点插入klist_remove_waiters链表
    spin_unlock(&klist_remove_lock);

    klist_del(n);//减少引用计数并尝试移除klist_node节点

    for (;;) 
        set_current_state(TASK_UNINTERRUPTIBLE);//睡眠,中断不可唤醒
        if (waiter.woken)//判断唤醒标志是否置位
            break;
        schedule();//调度
    
    __set_current_state(TASK_RUNNING);//设置进程为运行状态

而klist_remove()函数不仅会减少引用计数操作,还会将一直阻塞到节点移除,该函数才是请求删除节点的线程需要调用的。

/**
 * klist_node_attached - Say whether a node is bound to a list or not.
 * @n: Node that we‘re testing.
 */
int klist_node_attached(struct klist_node *n)

    return (n->n_klist != NULL);

klist_node_attached()函数用来判断链表节点是否还在链表上,是通过判断节点中的n_klist指针来实现的。

struct klist_iter 
    struct klist        *i_klist;
    struct klist_node    *i_cur;
;

extern void klist_iter_init(struct klist *k, struct klist_iter *i);
extern void klist_iter_init_node(struct klist *k, struct klist_iter *i,
                 struct klist_node *n);
extern void klist_iter_exit(struct klist_iter *i);
extern struct klist_node *klist_prev(struct klist_iter *i);
extern struct klist_node *klist_next(struct klist_iter *i);

接下来是链表遍历的实现方法,klist的遍历需要引入迭代器struct klist_iter,因为需要考虑到遍历过程中节点删除的情况,还要考虑如何忽略哪些已被请求删除的节点,该结构定义如上代码所示。

/**
 * klist_iter_init_node - Initialize a klist_iter structure.
 * @k: klist we‘re iterating.
 * @i: klist_iter we‘re filling.
 * @n: node to start with.
 *
 * Similar to klist_iter_init(), but starts the action off with @n,
 * instead of with the list head.
 */
void klist_iter_init_node(struct klist *k, struct klist_iter *i,
              struct klist_node *n)

    i->i_klist = k;    //klist链表头赋值
    i->i_cur = NULL;    //klist_node指针赋值NULL
    if (n && kref_get_unless_zero(&n->n_ref))//引用计数加1(引用计数不为0),返回非零值
        i->i_cur = n;    //将klist_node的值赋值给迭代器


/**
 * klist_iter_init - Iniitalize a klist_iter structure.
 * @k: klist we‘re iterating.
 * @i: klist_iter structure we‘re filling.
 *
 * Similar to klist_iter_init_node(), but start with the list head.
 */
void klist_iter_init(struct klist *k, struct klist_iter *i)

    klist_iter_init_node(k, i, NULL);    //从klist链表头开始

在上面的代码中,klist_iter_init_node()函数用于初始化一个struct klist_iter迭代器,它是从某个链表节点开始遍历的,而不是链表头,而klist_iter_init()函数则是进一步封装,它是从链表头开始遍历。

/**
 * klist_iter_exit - Finish a list iteration.
 * @i: Iterator structure.
 *
 * Must be called when done iterating over list, as it decrements the
 * refcount of the current node. Necessary in case iteration exited before
 * the end of the list was reached, and always good form.
 */
void klist_iter_exit(struct klist_iter *i)

    if (i->i_cur) 
        klist_put(i->i_cur, false);    //将遍历的节点引用计数减1操作
        i->i_cur = NULL;
    

EXPORT_SYMBOL_GPL(klist_iter_exit);

static struct klist_node *to_klist_node(struct list_head *n)

    return container_of(n, struct klist_node, n_node);/*根据klist_node内的链表指针获取结构体首地址*/

上面代码中,klist_iter_exit()函数用于完成一个链表的迭代操作,完成链表的遍历后需要进行调用,to_klist_node()函数则是将klist_node结构体的首地址进行返回。

/**
 * klist_prev - Ante up prev node in list.
 * @i: Iterator structure.
 *
 * First grab list lock. Decrement the reference count of the previous
 * node, if there was one. Grab the prev node, increment its reference
 * count, drop the lock, and return that prev node.
 */
struct klist_node *klist_prev(struct klist_iter *i)

    void (*put)(struct klist_node *) = i->i_klist->put;    //获取klist的put函数指针
    struct klist_node *last = i->i_cur;    //当前klist_node节点赋值
    struct klist_node *prev;

    spin_lock(&i->i_klist->k_lock);    //上锁

    if (last)     //节点不为NULL,从某节点开始操作
        prev = to_klist_node(last->n_node.prev);/*根据前一个klist_node的list指针获取结构体首地址*/
        if (!klist_dec_and_del(last))    //引用计数减1操作
            put = NULL;
     else
        prev = to_klist_node(i->i_klist->k_list.prev);//从链表头开始遍历,获取前节点的地址

    i->i_cur = NULL;
    while (prev != to_klist_node(&i->i_klist->k_list)) 
        if (likely(!knode_dead(prev)))     //跳过dead节点
            kref_get(&prev->n_ref);    //引用计数加1
            i->i_cur = prev;
            break;
        
        prev = to_klist_node(prev->n_node.prev);//迭代前一个节点
    

    spin_unlock(&i->i_klist->k_lock);

    if (put && last)
        put(last);
    return i->i_cur;


/**
 * klist_next - Ante up next node in list.
 * @i: Iterator structure.
 *
 * First grab list lock. Decrement the reference count of the previous
 * node, if there was one. Grab the next node, increment its reference
 * count, drop the lock, and return that next node.
 */
struct klist_node *klist_next(struct klist_iter *i)

    void (*put)(struct klist_node *) = i->i_klist->put;
    struct klist_node *last = i->i_cur;
    struct klist_node *next;

    spin_lock(&i->i_klist->k_lock);

    if (last) 
        next = to_klist_node(last->n_node.next);
        if (!klist_dec_and_del(last))
            put = NULL;
     else
        next = to_klist_node(i->i_klist->k_list.next);

    i->i_cur = NULL;
    while (next != to_klist_node(&i->i_klist->k_list)) 
        if (likely(!knode_dead(next))) 
            kref_get(&next->n_ref);
            i->i_cur = next;
            break;
        
        next = to_klist_node(next->n_node.next);
    

    spin_unlock(&i->i_klist->k_lock);

    if (put && last)
        put(last);
    return i->i_cur;

在上面的代码中,klist_prev()函数用于迭代到链表的前一个节点,klist_next()用于迭代到链表的下一个节点,由函数注释可以知道,函数首先要拿到锁,然后减少前一个节点的引用计数,然后迭代到下一个节点,增加它的引用计数,并且解锁,将节点进行返回。需要注意的地方有两点,第一点是:对单个节点的操作不需要加锁,但是对影响整个链表的操作需要加锁处理,因为当前线程可能会没有引用计数,所以需要加锁,让情况固定,保护链表和节点,第二点是:要忽略链表中已被请求删除的节点,在减少前一个节点的引用计数时,可能会把前一个节点删除。

以上就是klist链表的节点添加、删除和遍历的实现操作方法,由于klist是的list的线程安全版本,因此,需要考虑的东西很多,实现也比较复杂,klist链表主要运用在Linux内核的设备驱动模型中,它是为了适应动态变化的设备和驱动而专门设计的链表。

 

4、小结

本文主要对Linux内核中klist链表的定义和实现做了简要分析,包括其链表节点的添加、删除和链表遍历等基本实现进行简单说明。

 

参考:

http://www.itboth.com/d/aEfmUv/linux

https://blog.csdn.net/qb_2008/article/details/6845854

https://www.linuxidc.com/Linux/2011-05/36116.htm

以上是关于Linux内核klist链表分析的主要内容,如果未能解决你的问题,请参考以下文章

深入分析Linux内核链表

Linux内核链表深度分析

链表的艺术——Linux内核链表分析

内核数据结构 内核链表分析

内核数据结构 内核链表分析

分析Linux内核创建一个新进程的过程