Linux 内核链表 list.h 的使用

Stella981
• 阅读 1099

C 语言本身并不自带集合(Collection)工具,当我们需要把结构体(struct)实例串联起来时,就需要在结构体内声明指向下一实例的指针,构成所谓的“链表”。而为了实现对链表的操作,我们需要另外实现一系列的函数,例如添加、删除、搜索、复制等等。而利用 Kernel 源代码中自带的 list.h,则可以方便地实现任意类型结构体的串联。

编程需求


假设我有一个表示学生资料的结构体:

#define MAX_STRING_LENGTH 50

typedef struct student {
  char first_name[MAX_STRING_LENGTH];
  char last_name[MAX_STRING_LENGTH];
  unsigned int age;
} student_t;

传统的做法,当我们需要将一系列学生的数据串联起来,那我们需要在该结构体内部添加一枚指针:

typedef struct student {
  char first_name[MAX_STRING_LENGTH];
  char last_name[MAX_STRING_LENGTH];
  unsigned int age;
  struct student *next; /* Look at dis ;p */
} student_t;

几乎每位 C 语言学习者都有在教科书中见过此类实现方法。但是这样做的缺点是:我们需要额外编写一系列函数,实现对该类型链表的操作。然而,稍微复杂点的项目内,有个十几个结构体那是很平常的事情。如果我们想为它们都实现链表功能,那我们就需要为每个类型的结构体编写一套函数,然后我们就累 shi 了。

有没有一种更为通用的办法呢?不需要顾及结构体本身的类型,就可以简简单单地把它们串起来。有点类似 Java 中的 LinkedList,如果我需要把一堆 Student 类的对象管理起来,那就很简单:

LinkedList<Student> lstStudents = new LinkedList<Student>();

完事儿!接着直接把对象往 lstStudents 里面放就可以了。

list.h 简介


Linux 内核的源代码中有一个工具头文件 list.h,里面定义了一系列的宏(macro),帮助我们实现对任意类型结构体的串联。然而原始版本的 list.h 是供内核使用的,并不能直接被使用在用户空间(user space)的程序内。好在有网友对其进行了改写,使得其可以被使用在一般应用程序内。因为博客系统的限制,我无法将源代码一并贴上,大家可以自行下载:list.h 。较之 Github 上最新版的 list.h,该修改版提供的宏不是很全面,你可以根据自己的需要,从 Github 摘取、添加新宏。

list.h 的使用


为了利用 list.h 实现链表功能,首先我们需要对我们的结构体做一点小小的改动:

#include "list.h"

typedef struct student {
  char first_name[MAX_STRING_LENGTH];
  char last_name[MAX_STRING_LENGTH];
  unsigned int age;
  struct list_head node_student;
} student_t;

首先毫无悬念的,我们需要将 list.h 包含到我们的程序,接着在结构体内,我们添加一个类型为 struct list_head 的字段 node_student。这个字段就像是链条上的环扣,一环一环地将所有实例串联起来。接下去我们需要一个函数,来创建该结构体的实例:

student_t *make_student(const char *fn, const char *ln, unsigned int a) {
  student_t *stu = NULL;

  if ((stu = calloc(1, sizeof(struct student))) == NULL) {
    return NULL; /* Failed to allocate memory. */
  }
  if (strlen(fn) > (MAX_STRING_LENGTH - 1)) {
    return NULL; /* First name too long. */
  }
  if (strlen(ln) > (MAX_STRING_LENGTH - 1)) {
    return NULL; /* Last name too long. */
  }

  strcpy(stu->first_name, fn);
  strcpy(stu->last_name, ln);
  stu->age = a;
  return stu;
}

函数代码比较简单,就不费口舌解释了。

添加元素

现在在我们的 main 函数内,我们将创建一系列 student_t 结构体的实例,并把它们串联起来:

struct list_head class;
INIT_LIST_HEAD(&class);

student_t *stu = NULL;

/* Create students. */
if ((stu = make_student("Pierre", "Dupont", 16)) == NULL) {
  fprintf(stderr, "Failed to create Pierre.\n");
  return -1;
}
list_add_tail(&stu->node_student, &class);

if ((stu = make_student("Hugot", "Badeaux", 18)) == NULL) {
  fprintf(stderr, "Failed to create Hugot.\n");
  return -1;
}
list_add_tail(&stu->node_student, &class);

if ((stu = make_student("Celine", "Brousseau", 17)) == NULL) {
  fprintf(stderr, "Failed to create Celine.\n");
  return -1;
}
list_add_tail(&stu->node_student, &class);

这里我们首先创建一个链表 class,并用宏 INIT_LIST_HEAD 对其进行初始化。紧接着我们创建三名学生,分别叫:Pierre.Dupont,Hugot.Badeaux 和 Celine.Brousseau。每创建一名学生,我们就用宏 list_add_tail,将其添加到链表 class 内。list_add_tail 取两个参数,首先是结构体内用于串联实例的“环扣”node_student 的指针,其次是链表本身的指针,即 class。

链表的遍历

完成对链表的初始化之后,我们试着打印出链表内所有的学生信息:

/* Print all students in class. */
printf("All the students in class.\n");
list_for_each_entry(stu, &class, node_student) {
  printf("First name: %s\n", stu->first_name);
  printf("Last name: %s\n", stu->last_name);
  printf("Age: %u\n\n", stu->age);
}

对链表的遍历是通过宏 list_for_each_entry 来实现的,它取三个参数,分别是:一个结构体类型的指针,链表的指针,以及结构内“环扣”的名称,即 node_student。程序编译时,该宏会被替换成一个 for 循环,遍历链表内所有的元素,并且打印在终端上。

导出链表

现在学校组织一次春游,为此我们利用宏 LIST_HEAD 添加了另一个新的链表 bus。使用 LIST_HEAD 来创建空链表相对简单,不需要先声明,再用 INIT_LIST_HEAD 进行初始化。现在,我们需要把班级上所有的学生都移到公车 bus 内:

LIST_HEAD(bus);

/* Moving all students into bus. */
printf("Moving all the students into the bus.\n");
list_splice_init(&class, &bus);
if (list_empty(&class)) {
  printf("No more student in class.\n");
}
printf("\n");
  
/* Print all student in bus. */
list_for_each_entry(stu, &bus, node_student) {
  printf("%s %s (%u) is in the bus.\n", stu->first_name, stu->last_name, stu->age);
}
printf("\n");

宏 list_splice_init 取两个参数,分别为原链表指针和新链表指针。使用该宏之后,class 链表内的所有元素就会被导出到 bus 链表内。接着我们再用宏 list_empty 测试链表 class 是否为空。顺利完成对学生的移动之后,我们再一次使用 list_for_each_entry,逐一打印出新链表 bus 内的所有学生。不出意外的话,我们会发现 class 链表已为空,且所有学生都被成功转移到了新链表 bus 内。

删除元素

新的麻烦来了,在校车即将出发之际,妹子 Celine 突然决定不去了,要回家。那我们只能将 Celine 从校车中移出,然后再重新清点学生人数。

student_t *tmp = NULL;

/* Celine wanna go home. */
printf("Celine wants to go home, remove her.\n");
list_for_each_entry_safe(stu, tmp, &bus, node_student) {
  if (strcmp(stu->first_name, "Celine") == 0) {
    list_del(&stu->node_student);
    free(stu);
  }
}
printf("\n");

/* Print remaining students in bus. */
printf("Remaining students in bus.\n");
list_for_each_entry(stu, &bus, node_student) {
  printf("%s %s (%u) is in the bus.\n", stu->first_name, stu->last_name, stu->age);
}
printf("\n");

我们首先用宏 list_for_each_entry_safe 遍历链表 bus 内的所有学生,当我们找到 Celine 的时候,则调用宏 list_del 将其从链表内删除。list_del 只取一个参数,即目标元素内“环扣”的指针。此外 list_for_each_entry 和 list_for_each_entry_safe 的唯一区别在于,list_for_each_entry_safe 可以在遍历的过程当中删除链表内的元素,且不会造成错误。较之 list_for_each_entry,list_for_each_entry_safe 需多取一个参数 tmp,其同样为结构体类型的指针,用于在遍历过程,临时保存链表元素。在删除 Celine 之后,我们发现 Pierre 和 Hugot 还老老实实地坐在校车内。

程序的最后,校车成功抵达目的地,我们将所有剩余的学生(Pierre 和 Hugot)从校车内移出,并释放系统资源:

/* End of demo. */
printf("End of demo, free all resources.\n");
list_for_each_entry_safe(stu, tmp, &bus, node_student) {
  printf("Removing student: %s %s\n", stu->first_name, stu->last_name);
  list_del(&stu->node_student);
  free(stu);
}

return 0;

编译完成之后,试着运行程序,终端输出为:

All the students in class.
First name: Pierre
Last name: Dupont
Age: 16

First name: Hugot
Last name: Badeaux
Age: 18

First name: Celine
Last name: Brousseau
Age: 17

Moving all the students into the bus.
No more student in class.

Pierre Dupont (16) is in the bus.
Hugot Badeaux (18) is in the bus.
Celine Brousseau (17) is in the bus.

Celine wants to go home, remove her.

Remaining students in bus.
Pierre Dupont (16) is in the bus.
Hugot Badeaux (18) is in the bus.

End of demo, free all resources.
Removing student: Pierre Dupont
Removing student: Hugot Badeaux

一切正如我们预期的那样,我们实现了对链表的添加、遍历、导出和删除。

注意事项

最后,使用 list.h 时需要注意代码的可读性,先看看下面这个例子:

typedef struct school {
  struct list_head list_students;
  struct list_head list_teachers;
  struct list_head list_guards;
  struct list_head list_classrooms;
  struct list_head list_bus;
} school_t;

这个结构体的本意很明显,就是想包含一所学校内所有的人员和器材等等。但是单凭这样一个结构体,我们无法知道每一个链表内元素的具体类型。尤其是当你把一个项目丢给新接手的开发人员时,他看到这样一个结构体,会比较头疼。而且很多项目内,结构体的名字并非都那么浅显易懂。

当他想使用宏 list_for_each_entry 或者 list_for_each_entry_safe 遍历链表时,如果没有具体注解,他甚至不知道该用什么结构体类型。所以,一定要有详细的注解:

typedef struct school {
  struct list_head list_students; /* struct student */
  struct list_head list_teachers; /* struct teacher */
  struct list_head list_guards; /* struct guard */
  struct list_head list_classrooms; /* struct classroom */
  struct list_head list_bus; /* struct bus */
} s

综上所述,我个人的意见:list.h 很好用,但是要慎用。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这