内核链表分析

借助上一篇文章container_of(ptr, type, member) 的实现,讨论分析链表在内核的实现。
我们用以下的结构体来描述一个学生
struct student {
    char *name;
    int age;
}
用链表来存储多个学生,则可以在结构体加上struct student *next 指向下一个学生
struct student {
    char *name;
    int age;
    struct student *next;
}
然后可以通过链表头遍历每个学生, 这种是最常见的链表结构: 数据域,指针域
通过container_of(ptr, type, member)我们知道如果已知ptr,而struct student 和 member 我们是已知的, 则可以访问student,那如果想要遍历每个学生就得每个学生都包含该类型ptr, 而且每个ptr应该相互链接,这样,我们可以把该ptr定义成跟包含他的结构体struct student类型无关的双向链表,而且只有指针域,无需数据域。
struct list_head {struct list_head *next, *prev;};
struct student {
    char *name;
    int age;
    struct list_head list;
}
struct list_head *head = &list;
这样可以通过双向循环链表list,  利用container_of(head, struct student, a.list) 返回struct student a 的指针,从而访问a的数据, 如果list是链接起来的,那就可以遍历访问每个学生了

双向循环链表操作
include/linux/list.h
// 结构
struct list_head {struct list_head *next, *prev;};
// 初始化链表宏,  指向自己,则链表为空
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
#define INIT_LIST_HEAD(ptr) do { \(ptr)->next = (ptr); (ptr)->prev = (ptr); \} while (0) // 运行时初始化列表
初始化一个student_list 链表
LIST_HEAD(student_list)
// 判断链表是否为空
static inline int list_empty(const struct list_head *head){return head->next == head;}
list 循环链表插入
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(&a.list, &student_list)
list_add_tail(&b.list, &student_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_del(struct list_head *entry);
list_del(&b.list); // 删除student_list b 中的链表节点
链表节点移动
static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);
list_move(&b.list, &student_list2) // 把b.list 从student_list 搬到student_list2 链表中
链表合并
static inline void list_splice(struct list_head *list, struct list_head *head); // list 合并 到head, 新链表head链表头仍指向head
static inline void list_splice_init(struct list_head *list, struct list_head *head); // 该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链
list_splice(&student_list, &student_list2) // student_list 合并到student_list2 链表头为原student_list2 中的链表头
链表遍历
list_entry(&a.next, struct student, a.list); // 获得struct student a 结构体的指针
#define list_entry(ptr, type, member) container_of(ptr, type, member)

#define container_of(ptr, type, member) ({\
    const typeof( ((type *)0)->member ) *__mptr = (ptr);\
    (type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define list_for_each(pos, head) \for (pos = (head)->next, prefetch(pos->next); pos != (head); \        pos = pos->next, prefetch(pos->next)) // 遍历双向循环链表中的每一个节点, 结合list_entry 可以遍历struct student, 即list_entry(ptr, type, member) 中type 结构体的数据域, 例如

struct list_head *i;
list_for_each(i, student_list) {
    student student *j = list_entry(i, struct student, list);
    printk("j->name = %s\n", j->name);
}

#define list_for_each_entry(pos, head, member) // 即合并list_for_each() 和 list_entry(), 大大 简化操作

struct student *i;
list_for_each_entry(i, student_list, list)
    printk("i->name = %s\n", i->name);

#define list_for_each_prev(pos, head) // 反向遍历列表节点
#define list_for_each_entry_reverse(pos, head, member) // 反向遍历列表,并且得到每个包含list的结构体指针

#define list_for_each_entry_continue(pos, head, member) // 从已知节点而不是链表头开始遍历
#define list_prepare_entry(pos, head, member) // 有时会出现如果pos有值,则从pos开始遍历,如果没有,则从链表头开始, list_prepare_entry(pos, head, member) 就是为此准备的, 可作为list_for_enach_entry_continue(pos, head, member) 中pos 的参
数

安全问题考虑
a) list_empty() 判断
基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。
b) 遍历时节点删除
前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。
当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

扩展阅读
1 hlist
精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构--单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。
因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的"*(node->pprev)"访问和修改前驱节点的next(或first)指针。

2. read-copy update
在Linux链表功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。

我们知道,系统中数据读取操作远多于写操作,而rwlock机制在smp环境下随着处理机增多性能会迅速下降(见参考资料4)。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及,有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表的使用和基本链表的使用方法基本相同。

参考资料

http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值