linux的数据结构---kernel链表

20 篇文章 0 订阅

链表是一种常见的组织有序数据的数据结构,相对于数组,具有更好的动态性,可以高效的在链表的任意位置实时的插入或者删去。在linux的源代码中,大量的使用了链表。通常链表数据结构至少有两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。
在数据结构中定义一个指向任意类型为:

struct list
{
    void *data;
    struct list *next;
};

而在内核中,定义了一种通用的双向循环链表,链表的定义为:

struct list_head {
    struct list_head *next, *prev;
};

有prev和next两个指针,分别指向链表中的前一节点和后一节点。在初始化的时候,都指向自己本身。

static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

向链表中添加节点:

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);
}

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的区别不大,linux实现两个接口,在表头插入是插入在head后,而在表尾插入是在head->prev之后,只是表明插入到该节点之后或者之前。链表中的节点删去

static inline void __list_del(struct list_head *prev, struct list_head *next)
{
    next->prev = prev;
    prev->next = next;
}

static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->next = (void *)0xDEADBEEF;
    entry->prev = (void *)0xBEEFDEAD;
}

被删去entry删去后分别指向两个值,这个设置是为了保持不在链表中的节点项以后不会被访问。

static inline void list_del_init(struct list_head *entry)
{
    __list_del_entry(entry);
    INIT_LIST_HEAD(entry);
}

该函数将节点从链表中解下来后,调用INIT_LIST_HEAD将节点设置为空链表。

static inline void list_replace(struct list_head *old,
                struct list_head *new)
{
    new->next = old->next;
    new->next->prev = new;
    new->prev = old->prev;
    new->prev->next = new;
}

static inline void list_replace_init(struct list_head *old,
                    struct list_head *new)
{
    list_replace(old, new);
    INIT_LIST_HEAD(old);
}

list_replace是将链表中的一个老节点换乘一个新节点,从实现上来看,即使old所在链表只有一个old节点,new也可以成功替换。

static inline void list_move(struct list_head *list, struct list_head *head)
{
    __list_del_entry(list);
    list_add(list, head);
}

static inline void list_move_tail(struct list_head *list,
                 struct list_head *head)
{
    __list_del_entry(list);
    list_add_tail(list, head);
}

list_move的作用是把list节点从原链表中去除,并加入到新的链表中;而list_move_tail只是加入新链表。前者是加到链表的头部,而后者是加入到链表的尾部。

static inline int list_is_last(const struct list_head *list,
                const struct list_head *head)
{
    return list->next == head;
}

上面函数主要是判断list是否处于head链表的尾部。

static inline int list_empty(const struct list_head *head)
{
    return head->next == head;
}

static inline int list_empty_careful(const struct list_head *head)
{
    struct list_head *next = head->next;
    return (next == head) && (next == head->prev);
}

list_empty判断head链表是否为空,而list_empty_careful同样也是判断链表是否为空,只是多了一个检测条件。

static inline int list_is_singular(const struct list_head *head)
{
    return !list_empty(head) && (head->next == head->prev);
}

list_is_singular判断head是否只有一个节点,在检测链表头head外是否只有一个节点。list的操作包括链表的节点添加和删去,节点从一个链表转移到另外一个链表,替换和合并,由于其数据结构中只是定义了两个指针域,没有定义数据域。那么怎么去获得数据域呢?linux中有一种新的方法来获取,通过获取一个结构体中一个成员在这个结构体中的偏移。

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

来个例子,例如:

typedef struct
{
    int i;
    int j;
}ember;

这个ember结构体占用了8个字节,假设声明一个变量ember a;想知道a的地址该如果办呢?只要知道j在a中的偏移量,然后把j的地址减去这个偏移量就是a的地址。list_entry主要用于从list节点查找其内嵌的结构。

#define list_first_entry(ptr, type, member) \
    list_entry((ptr)->next, type, member)

list_first_entry是将ptr看成一个链表头,取出其中第一个节点对应的结构地址。下面来看看在总线设备驱动模型中大量被使用的list_for_each,循环遍历链表中的每个节点,从链表的头部的第一个节点,一直到链表的尾部。

#define list_for_each(pos, head) \
    for (pos = (head)->next; prefetch(pos->next), pos != (head); \
        pos = pos->next)

#define __list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)
#define list_for_each_prev(pos, head) \
    for (pos = (head)->prev; prefetch(pos->prev), pos != (head); \
        pos = pos->prev)

list_for_each_prev从链表的尾部逆向遍历到链表头。

#define list_for_each_entry(pos, head, member)                \
    for (pos = list_entry((head)->next, typeof(*pos), member);    \
     &pos->member != (head);     \
     pos = list_entry(pos->member.next, typeof(*pos), member))

上面这个函数不是遍历链表的节点,而是遍历链表节点锁嵌套的结构。其等价为list_for_each加list_entry。在list_head,有以上一些通用的链表的形式,但是还有一种klist和hlist也占据了很大的一部分,hlist也就是哈希表。哈希表也是一个哈希数组,为了解决映射冲突的问题,其实哈希组的每一个项做成一个链表,哈希组的项很多,list_head的话每个链表都需要两个指针空间,就发明了hlist,一是它的链表只需要一个指针,二是它的每一项都可以找到前一个节点。

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 (sizeof(void *))));

#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()用于对结构的引用。这样在节点的初始化调用get(),在节点删去时调用put()。
在看看struct klist_node,除了链表需要的node,一个引用计数,还有一个n_klist指针。初始化klist

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;
}

这里写图片描述
K_lock:是一把锁,用来锁表的。
k_list:双向链表,用来联系各节点及链表头。
get、put:两个函数指针,是用来操作链表中的节点接口
n_klist是一个空指针,随便用来指啥,但在我们的klist原语中是用来指向链表头的。另外其最低位用来做标志位。
n_node:双向链表,用来联系各节点及链表头。
n_ref:引用计数

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);  
}  

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;  
}  

这四个函数都是内部静态函数,帮助API实现的。knode_klist()是从节点找到链表头。knode_dead()是检查该节点是否已被请求删除。
knode_set_klist设置节点的链表头。knode_kill将该节点请求删除。细心的话会发现这四个函数是对称的,而且都是操作节点的内部函数。

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。

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);  
    if (k->get)  
        k->get(n);  
}  

又是三个内部函数,add_head()将节点加入链表头,add_tail()将节点加入链表尾,klist_node_init()是初始化节点。注意在节点的引用计数初始化时,因为引用计数变为1,所以也要调用相应的get()函数。

void klist_add_head(struct klist_node *n, struct klist *k)  
{  
    klist_node_init(k, n);  
    add_head(k, n);  
}  

void klist_add_tail(struct klist_node *n, struct klist *k)  
{  
    klist_node_init(k, n);  
    add_tail(k, n);  
}  

klist_add_head()将节点初始化,并加入链表头。

klist_add_tail()将节点初始化,并加入链表尾。

它们正是用上面的三个内部函数实现的,可见linux内核中对函数复用有很强的执念,其实这里add_tail和add_head是不用的,纵观整个文件,也只有klist_add_head()和klist_add_tail()对它们进行了调用。

void klist_add_after(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);  
    spin_unlock(&k->k_lock);  
}  

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);  
    spin_unlock(&k->k_lock);  
}  

klist_add_after()将节点加到指定节点后面。

klist_add_before()将节点加到指定节点前面。

这两个函数都是对外提供的API。在list_head中都没有看到有这种API,所以说需求决定了接口。虽说只有一步之遥,klist也不愿让外界介入它的内部实现。

之前出现的API都太常见了,既没有使用引用计数,又没有跳过请求删除的节点。所以klist的亮点在下面,klist链表的遍历。

truct 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_next(struct klist_iter *i);  

以上就是链表遍历需要的辅助结构struct klist_iter,和遍历用到的四个函数。

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

static DEFINE_SPINLOCK(klist_remove_lock);  
static LIST_HEAD(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);  

    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;  

        waiter->woken = 1;  
        mb();  
        wake_up_process(waiter->process);  
        list_del(&waiter->list);  
    }  
    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);  
}  

static void klist_put(struct klist_node *n, bool kill)  
{  
    struct klist *k = knode_klist(n);  
    void (*put)(struct klist_node *) = k->put;  

    spin_lock(&k->k_lock);  
    if (kill)  
        knode_kill(n);  
    if (!klist_dec_and_del(n))  
        put = NULL;  
    spin_unlock(&k->k_lock);  
    if (put)  
        put(n);  
}  

/** 
 * 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实现必须的。因为使用kref动态删除,自然需要一个计数降为零时调用的函数klist_release。

klist_dec_and_del()就是对kref_put()的包装,起到减少节点引用计数的功能。

至于为什么会出现一个新的结构struct klist_waiter,也很简单。之前说有线程申请删除某节点,但节点的引用计数仍在,所以只能把请求删除的线程阻塞,就是用struct klist_waiter阻塞在klist_remove_waiters上。所以在klist_release()调用时还要将阻塞的线程唤醒。knode_kill()将节点设为已请求删除。而且还会调用put()函数。

释放引用计数是调用klist_del(),它通过内部函数klist_put()完成所需操作:用knode_kill()设置节点为已请求删除,用klist_dec_and_del()释放引用,调用可能的put()函数。

/** 
 * 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);  
    spin_unlock(&klist_remove_lock);  

    klist_del(n);  

    for (;;) {  
        set_current_state(TASK_UNINTERRUPTIBLE);  
        if (waiter.woken)  
            break;  
        schedule();  
    }  
    __set_current_state(TASK_RUNNING);  
}  

klist_remove()不但会调用klist_del()减少引用计数,还会一直阻塞到节点被删除。这个函数才是请求删除节点的线程应该调用的。

int klist_node_attached(struct klist_node *n)  
{  
    return (n->n_klist != NULL);  
}  

klist_node_attached()检查节点是否被包含在某链表中。

以上是klist的链表初始化,节点加入,节点删除函数。下面是klist链表遍历函数。

klist的遍历有些复杂,因为它考虑到了在遍历过程中节点删除的情况,而且还要忽略那些已被删除的节点。宏实现已经无法满足要求,迫不得已,只能用函数实现,并用struct klist_iter记录中间状态。

void klist_iter_init_node(struct klist *k, struct klist_iter *i,  
              struct klist_node *n)  
{  
    i->i_klist = k;  
    i->i_cur = n;  
    if (n)  
        kref_get(&n->n_ref);  
}  

void klist_iter_init(struct klist *k, struct klist_iter *i)  
{  
    klist_iter_init_node(k, i, NULL);  
}  

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()的本意不是从当前节点开始,而是从当前节点的下一节点开始。

static struct klist_node *to_klist_node(struct list_head *n)  
{  
    return container_of(n, struct klist_node, n_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_next()是将循环进行到下一节点。
实现中需要注意两点问题:
1、加锁,根据经验,单纯对某个节点操作不需要加锁,但对影响整个链表的操作需要加自旋锁。比如之前klist_iter_init_node()中对节点增加引用计数,就不需要加锁,因为只有已经拥有节点引用计数的线程才会特别地从那个节点开始。而之后klist_next()中则需要加锁,因为当前线程很可能没有引用计数,所以需要加锁,让情况固定下来。这既是保护链表,也是保护节点有效。符合kref引用计数的使用原则。
2、要注意,虽然在节点切换的过程中是加锁的,但切换完访问当前节点时是解锁的,中间可能有节点被删除(这个通过spin_lock就可以搞定),也可能有节点被请求删除,这就需要注意。首先要忽略链表中已被请求删除的节点,然后在减少前一个节点引用计数时,可能就把前一个节点删除了。这里之所以不调用klist_put(),是因为本身已处于加锁状态,但仍要有它的实现。这里的实现和klist_put()中类似,代码不介意在加锁状态下唤醒另一个线程,但却不希望在加锁状态下调用put()函数,那可能会涉及释放另一个更大的结构。

void klist_iter_exit(struct klist_iter *i)  
{  
    if (i->i_cur) {  
        klist_put(i->i_cur, false);  
        i->i_cur = NULL;  
    }  
}  

klist_iter_exit(),遍历结束函数。在遍历完成时调不调无所谓,但如果想中途结束,就一定要调用klist_iter_exit()。

klist主要用于设备驱动模型中,为了适应那些动态变化的设备和驱动,而专门设计的链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值