Linux内核源码学习之链表

       链表是Linux内核中最简单、最普通的数据结构,运用及其广泛。链表是一种存放和操作可变数量元素(常称为节点)的数据结构。链表和静态数组的不同之处在于,它所包含的元素都是动态创建并插入链表的。在编译时不必知道具体需要创建多少个元素,另外由于每个元素的创建时间都不相同,所以它们在内存中无需占用连续内存区。由于元素是不连续存放,所以各元素需要通过某种方式被连接在一起,于是每个元素都包含一个指向下一个元素的指针。当有元素加入链表或从链表中删除元素时,只需要简单地调整指向下一个节点的指针就可以了。

       链表的基本概念和原理可以参考数据结构相关教材。至于链表怎么用,可以去看看UCOSII源码,里面有大量的链表使用,而且比较容易看懂。 本文将分成3部分,第一部分简单描述下Linux链表特点,第二部分详细解释Linux链表各个功能,第三部分给出Linux链表使用例程。

 一、Linux链表特点

       2.6版本内核的链表的实现在list.h文件中。内核链表设计的功能很强大、很灵活、移植性很好,设计简洁(只用一个.h文件搞定!)。Linux链表有以下几个特点:

 1. Linux链表是环形双向链表

 

2.Linux链表的实现方式是将链表节点塞入数据结构

       这条可能不太好理解,这里引用“Linux内核设计与实现(第三版)” 里的解释。普遍的链表实现方式是在数据结构中添加一个指向数据结构的next或previous指针。比如,假定我们有一个fox数据结构来描述犬科动物中的一员。

struct fox {
    unsigned long tail_length; /* 尾巴长度,以厘米为单位 */
    unsigned long weight;      /* 重量,以千克为单位 */
    bool     is_fantastic;     /* 这只狐狸奇妙吗? */
};

存储这个结构到链表里的通常做法是在数据结构中嵌入一个链表指针,比如:

struct fox {
    unsigned long tail_length;    /* 尾巴长度,以厘米为单位 */
    unsigned long weight;         /* 重量,以千克为单位 */
    bool          is_fantastic;   /* 这只狐狸奇妙吗? */
    struct fox    *next;          /* 指向下一个狐狸 */
    struct fox    *prev;          /* 指向上一个狐狸 */

};

Linux内核链表实现方式与众不同,它不是将数据结构塞入链表,而是将链表节点塞入数据结构!

/* Linux链表节点数据结构 */
struct list_head {
	struct list_head *next, *prev;
};

/* 将链表节点塞入fox结构 */
struct fox {
    unsigned long    tail_length;  /* 尾巴长度,以厘米为单位 */
    unsigned long    weight;       /* 重量,以千克为单位 */
    bool             is_fantastic; /* 这只狐狸奇妙吗? */
    struct list_head list;         /* 所有fox结构体形成链表 */
};

二、Linux链表功能详解

       list.h中实现了两种双向循环链表,第一种是运用于常规的,第二种是运用于hash表的。这里只介绍第一种。第二种运用于hash表的分析可以参考https://blog.csdn.net/hs794502825/article/details/24597773

1. 定义链表

       正如前面看到:list_head本身没有意义,它需要被嵌入到你自己的数据结构中才能生效。如:

struct student {
    int    number;          // 学号
    struct list_head list;
};

2. 初始化链表 

       如果结构是在运行时动态创建的,则需要运行时初始化链表。运行时初始化链表,调用函数:

void INIT_LIST_HEAD(struct list_head *list);

       例程如下:

#define STUDENT_NUM    9

struct student *pstudent_class1[STUDENT_NUM]; // 班级1的学生
struct student *pstudent_class2[STUDENT_NUM]; // 班级2的学生

for (i = 0; i < STUDENT_NUM; i++)             // 班级1的学生组成链表
{
    pstudent_class1[i]         = (struct student *)malloc(sizeof(struct student));
    pstudent_class1[i]->number = i + 1;
    INIT_LIST_HEAD(&pstudent_class1[i]->list);
}

for (i = 0; i < STUDENT_NUM; i++)             // 班级2的学生组成链表
{
    pstudent_class2[i]         = (struct student *)malloc(sizeof(struct student));
    pstudent_class2[i]->number = 10 + i;
    INIT_LIST_HEAD(&pstudent_class2[i]->list);
}

       如果结构是在编译期静态创建的,而你需要在其中给出一个链表的直接引用,下面是最简单的方式:

struct student student1 = {
    number = 1;
    LIST_HEAD_INIT(student1.list);
}

3. 链表头

        内核链表实现中最杰出的特性就是:我们的student节点都是无一差别的——每一个都包含一个list_head指针,于是我们可以从任何一个节点开始遍历链表,直到我们看到所有节点。这种方式确实很优美,不过有时候我们确实需要一个特殊的指针索引到整个链表,而不从一个链表节点出发。有趣的是,这个特殊的索引节点也是一个常规的list_head:

LIST_HEAD(student_class1_list_head);
LIST_HEAD(student_class2_list_head);

       该函数定义并初始化了一个名为student_class1_list_head和student_class2_list_head的链表头。

4. 向链表插入节点

1)向指定链表节点的后面插入节点

void list_add(struct list_head *new, struct list_head *head)

       该函数向指定链表head节点的后面插入new节点。因为链表是循环的,而且通常没有首尾节点的概念,所以可以把任何一个节点当成head。如果把“最后”一个节点当作head的话,那么该函数可以实现一个栈。

        回到前面的例子,把数组pstudent_class1/pstudent_class2里的struct student插入到student_class1_list_head/student_class2_list_head的后面,可以这样做:

for (i = 0; i < STUDENT_NUM; i++) {
    list_add(&pstudent_class1[i]->list, &student_class1_list_head);
}

for (i = 0; i < STUDENT_NUM; i++) {
    list_add(&pstudent_class2[i]->list, &student_class2_list_head);
}

        如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

               9 8 7 6 5 4 3 2 1 

       如果从student_class2_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

              18 17 16 15 14 13 12 11 10 

2)向指定链表节点的前面插入节点

void list_add_tail(struct list_head *new, struct list_head *head)
       该函数向指定链表head节点的前面插入new节点。和list_add类似,因为链表是循环的,而且通常没有首尾节点的概念,所以可以把任何一个节点当成head。如果把“第一个”节点当作head的话,那么该函数可以实现一个队列。

       回到前面的例子,把数组pstudent_class1/pstudent_class2里的struct student插入到student_class1_list_head/student_class2_list_head的前面,可以这样做:

for (i = 0; i < STUDENT_NUM; i++) {
    list_add_tail(&pstudent_class1[i]->list, &student_class1_list_head);
}

for (i = 0; i < STUDENT_NUM; i++) {
    list_add_tail(&pstudent_class2[i]->list, &student_class2_list_head);
}

        如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

               1 2 3 4 5 6 7 8 9  

       如果从student_class2_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

              10 11 12 13 14 15 16 17 18 

 5. 从链表中删除节点

       从链表中删除节点,调用函数:

void list_del(struct list_head *entry)  或者 void list_del_init(struct list_head *entry)

       该函数从链表中删除entry元素。注意,该操作不会释放entry或释放包含entry的数据结构的内存。该函数只是把entry元素从链表中移走。所以,该函数被调用后,通常还需要撤销包含entry的数据结构和其中的entry项。

       函数list_del_init除了要再初始化entry外,其它都和list_del类似。这样做是因为:虽然链表不再需要entry项,但是还可以再次使用包含entry项的数据结构体。

      回到前面的例子,如果要从链表student_class1_list_head中删除第一个元素,可以这样做:

list_del(&pstudent_class1[0]->list); 或者 list_del_init(pstudent_class1[0]->list);       

       如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

               2 3 4 5 6 7 8 9  

 6. 替换链表节点

       替换链表节点,调用函数:

void list_replace(struct list_head *old, struct list_head *new) 或者 

void list_replace_init(struct list_head *old, struct list_head *new) ;

       该函数用new元素替换old元素,并把old元素从链表中删除。list_replace_init除了要再初始化old外,其它都和list_replace类似。

       接上面的例子,把删除第一个元素后的链表student_class1_list_head中的第一个元素用已删除的第一个元素替换,可以这样做:

       list_replace(&pstudent_class1[1]->list, &pstudent_class1[0]->list); 或者

       list_replace_init(&pstudent_class1[1]->list, &pstudent_class1[0]->list);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

               1 3 4 5 6 7 8 9  

 7. 移动链表节点

       移动链表节点,调用函数:

void list_move(struct list_head *list, struct list_head *head); 或者

void list_move_tail(struct list_head *list, struct list_head *head);;

       函数list_move把元素list移动到元素head的后面。函数list_move_tail把元素list移动到元素head的前面。

       接前面的例子,如果要把链表的最后一个元素移动到链表的最前面,可以这样做:

list_move(&pstudent_class1[STUDENT_NUM-1]->list,  &student_class1_list_head);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

       9 1 2 3 4 5 6 7 8        

       如果要把链表的第一个元素移动到链表的最后面,可以这样做:

list_move_tail(&pstudent_class1[0]->list, &student_class1_list_head);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

     2 3 4 5 6 7 8 9 1

 8. 旋转链表节点

       旋转链表节点,调用函数:

void list_rotate_left(struct list_head *head);

       该函数把元素head与head的next元素位置对调。比如,接前面的例子,执行下面语句:

list_rotate_left(&pstudent_class1[0]->list);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

        2 1 3 4 5 6 7 8 9

 9. 合并链表

      合并链表,调用函数:

void list_splice(const struct list_head *list, struct list_head *head);

void list_splice_init(struct list_head *list, struct list_head *head);

void list_splice_tail(struct list_head *list, struct list_head *head);

void list_splice_tail_init(struct list_head *list, struct list_head *head);

       函数list_splice把第一个链表的list元素后面的所有元素加入到第二个链表的head元素的后面(注意:元素list不会加入到第二个链表中,它将变为空链表!)。比如,接前面的例子,执行下面语句:

list_splice(&student_class2_list_head, &student_class1_list_head);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

       10 11 12 13 14 15 16 17 18 1 2 3 4 5 6 7 8 9 

       除了list_splice_init把元素list重新初始化外,函数list_splice_init和list_splice类似。

       函数list_splice_tail把第一个链表的list元素后面的所有元素加入到第二个链表的head元素的前一个元素的后面(注意:元素list不会加入到第二个链表中,它将变为空链表!)。比如,接前面的例子,执行下面语句:

list_splice_tail(&student_class2_list_head, &student_class1_list_head);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

       1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 

       除了list_splice_tail_init把元素list重新初始化外,函数list_splice_tail_init和list_splice_tail类似。

 10. 分割链表

       分割链表,调用函数:

void list_cut_position(struct list_head *list, struct list_head *head, struct list_head *entry);

       该函数把元素head所在的链表从元素entry的位置开始分割,元素entry后的元素归属于元素head所在的链表,元素entry前的元素归属于元素list所在的链表。比如,接上面合并链表的例子,分割list_splice_tail合并后的链表,可以用这样做:

list_cut_position(&student_class2_list_head, &student_class1_list_head, &pstudent_class1[STUDENT_NUM-1]->list);

如果从student_class1_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

        10 11 12 13 14 15 16 17 18

如果从student_class2_list_head出发,向前遍历链表并打印出struct student里的number,将得到下面结果:

         1 2 3 4 5 6 7 8 9

 11. 判断链表是否空

       判断链表是否空,调用函数:

int list_empty(const struct list_head *head);  或者

int list_empty_careful(const struct list_head *head)

12. 判断链表是否只包含一个节点

       判断链表是否只包含一个节点,调用函数:

int list_is_singular(const struct list_head *head);

13. 判断节点是否为链表的最后一个节点

int list_is_last(const struct list_head *list, const struct list_head *head);

       该函数判断元素list是否为head所在链表的最后一个节点。

14. 遍历链表

1)list_for_each(pos, head) 宏和 list_for_each_prev(pos, head)宏

       list_for_each宏使用两个list_head类型的参数。第一个参数pos用来指向当前项,这是一个你必须提供的临时变量。第二个参数head是需要遍历的链表的以头节点形式存在的list_head。

       list_for_each宏沿next方向向前依次获取head的下一个元素至head的前一个元素,并存取于pos中。用法如下:

struct list_head *plist_head;

list_for_each(plist_head, &student_class1_list_head)
{
    /* plist_head指向链表中的元素 */
}

       list_for_each_prev宏沿prev方向向后依次获取head的前一个元素至head的下一个元素,并存取于pos中。

       好了,实话实说,其实一个指向链表结构(list_head)的指针通常是无用的:我们需要的是一个指向包含list_head的结构体的指针。比如前面student结构体的例子,我们需要的是指向每个student结构体的指针,而不是指向student结构体中list成员的指针。我们可以使用list_entry宏,来获得包含给定list_head的数据结构,比如:

struct list_head *plist_head;
struct student   *pstudent;

list_for_each(plist_head, &student_class1_list_head)
{
    pstudent = list_entry(plist_head, struct student, list);
}

       按照list_for_each遍历链表的方式,如果从student_class1_list_head出发,遍历链表并打印出struct student里的number,将得到下面结果:

       1 2 3 4 5 6 7 8 9 

       按照list_for_each_prev遍历链表的方式,如果从student_class1_list_head出发,遍历链表并打印出struct student里的number,将得到下面结果:

       9 8 7 6 5 4 3 2 1 

2)list_for_each_entry(pos, head, member)宏和list_for_each_entry_reverse(pos, head, member)宏

       list_for_each_entry宏的功能相当于list_for_each加list_entry的功能。参数pos是一个指向包含list_head的结构体的指针。参数head是需要遍历的链表的以头节点形式存在的list_head,即遍历的起始位置。参数member是pos中list_head结构的变量名。

        list_for_each_entry宏沿next方向向前依次获取head的下一个节点至head的前一个节点,并存取于pos中。用法如下:

list_for_each_entry(pstudent, &student_class1_list_head, list)     
{
    /* pstudent指向student结构体 */
}

      按照list_for_each_entry遍历链表的方式,如果从student_class1_list_head出发,遍历链表并打印出struct student里的number,将得到下面结果:

       1 2 3 4 5 6 7 8 9 

      list_for_each_entry_reverse宏沿prev方向向后依次获取head的前一个节点至head的下一个节点,并存取于pos中。

      按照list_for_each_entry_reverse遍历链表的方式,如果从student_class1_list_head出发,遍历链表并打印出struct student里的number,将得到下面结果:

       9 8 7 6 5 4 3 2 1  

3)list_for_each_entry_continue(pos, head, member)宏和list_for_each_entry_continue_reverse(pos, head, member)宏

       list_for_each_entry_continue宏和list_for_each_entry宏的参数和用法类似,区别在于list_for_each_entry_continue宏是沿next方向向前依次获取pos的下一个节点至head的前一个节点的所有节点,并存取于pos中。比如:

pstudent = pstudent_class1[0];

list_for_each_entry_continue(pstudent, &student_class1_list_head, list)    
{
       /**/
}

按上面方式遍历链表并打印出struct student里的number,将得到下面结果:

       2 3 4 5 6 7 8 9

       list_for_each_entry_continue_reverse宏是沿prev方向向后依次获取pos的前一个节点至head的下一个节点的所有节点,并存取于pos中。比如:

pstudent = pstudent_class1[STUDENT_NUM-1];

list_for_each_entry_continue_reverse(pstudent, &student_class1_list_head, list)
{
       /* */
}

按上面方式遍历链表并打印出struct student里的number,将得到下面结果:

       8 7 6 5 4 3 2 1 

4)list_for_each_entry_from(pos, head, member)宏

       list_for_each_entry_from宏和list_for_each_entry_continue宏的参数和用法类似,区别在于list_for_each_entry_from宏沿next方向向前依次获取pos节点至head的前一个节点的所有节点,并存取于pos中。比如:

pstudent = pstudent_class1[0];

list_for_each_entry_from(pstudent, &student_class1_list_head, list)
{
     /* */
}

按上面方式遍历链表并打印出struct student里的number,将得到下面结果:

       1 2 3 4 5 6 7 8 9

4)list_for_..._safe宏

       list.h中有一些带safe结尾的宏,这类宏的参数和用法与相应的不带safe的宏是类似的,区别在于safe结尾的宏允许遍历链表的同时删除节点。

       标准的链表遍历方法在你遍历链表的同时删除节点是不行的。因为标准链表的操作方法是建立在不会改变链表项的前提上的。所以如果当前项在遍历时被删除,那么接下来的遍历就无法获得next或prev指针。这其实是循环处理的一个常见范式,开发人员通过在潜在的删除之前存储next或prev指针到一个临时变量中,以便能执行删除操作。比如:

       list_for_each_entry_safe(pos, n, head, member) ;

       你可以按照list_for_each_entry的方式来使用上面的宏,只需要提供n指针,n和pos是同样的类型。list_for_each_entry_safe启用n指针来将下一项存进表中,以使得能安全删除当前项。

一、linux内核链表 1、普通链表的数据区域的局限性 之前定义数据区域时直接int data,我们认为我们的链表需要存储的是一个int类型的数。但是实际上现实编程链接的节点不可能这么简单,而是多种多样的。 一般实际项目链表,节点存储的数据其实是一个结构体,这个结构体包含若干的成员,这些成员加起来构成了我们的节点数据区域。 2、一般性解决思路:即把数据区封装为一个结构体 (1)因为链表实际解决的问题是多种多样的,所以内部数据区域的结构体构成也是多种多样的。 这样也导致了不同程序当链表总体构成是多种多样的。 我们无法通过一套泛性的、普遍适用的操作函数来访问所有的链表,意味着我们设计一个链表就得写一套链表的操作函数(节点创建、插入、删除、遍历……)。 (2)实际上深层次分析会发现 不同的链表虽然这些方法不能通用需要单独写,但是实际上内部的思路和方法是相同的,只是函数的局部地区有不同。 实际上链表操作是相同的,而涉及到数据区域的操作就有不同 (3)问题 能不能有一种办法把所有链表操作方法里共同的部分提取出来用一套标准方法实现,然后把不同的部分留着让具体链表的实现者自己去处理。 3、内核链表的设计思路 (1)内核链表实现一个纯链表的封装,以及纯链表的各种操作函数 纯链表就是没有数据区域,只有前后向指针; 各种操作函数是节点创建、插入、删除、遍历。 这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。 4、list.h文件简介 (1)内核核心纯链表的实现在include/linux/list.h文件 (2)list.h就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。 二、内核链表的基本算法和使用简介 1、内核链表的节点创建、删除、遍历等 2、内核链表的使用实践 (1)问题:内核链表只有纯链表,没有数据区域,怎么使用? 使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。类似于公司收购,实现被收购公司的功能。 这里面要借助container_of宏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值