什么是内核链表
内核链表是 Linux 内核中非常常用的一种数据结构,它是一种线性表 (linear list),但并不像数组或线性表那样需要一段连续的内存空间来存储所有元素,而是采用链式存储结构,将若干个节点串联起来,每个节点除了自身的数据外,还包含一个指向下一个节点的指针。
内核链表的好处在于,它可以高效地在链表头部和尾部插入和删除元素,而不会涉及原有内存块的移动和重新分配;同时,链表也可以有不确定的长度,可以动态地添加和删除元素。
内核链表的实现
Linux中的内核链表实际上是封装了一个名为"list_head"数据结构,常用的操作函数包括:
list_add:在一个节点之后插入一个新节点;
list_add_tail:在链表尾部插入一个新节点;
list_del:从链表中删除某个节点;
list_empty:判断链表是否为空。
内核链表可以包含任意类型的数据,只需要在每个节点中定义一个该类型的数据结构,然后将这个数据结构与“list_head”结构融合在一起,就可以利用“list_head”操作链表。使用内核链表的步骤如下:
定义一个内核链表节点结构和数据结构,如:
struct item {
int data;
struct list_head list;
};
定义该内核链表的头结点,如:
struct list_head my_list;
在程序中使用“list_add”或“list_add_tail”来插入新节点;使用“list_del”来删除节点。
/* 创建节点 */
struct item *new_item = kmalloc(sizeof(*new_item), GFP_KERNEL);
new_item->data = 10;
INIT_LIST_HEAD(&new_item->list); /* 初始化节点 */
/* 在链表尾部插入新节点 */
list_add_tail(&new_item->list, &my_list);
/* 从链表中删除节点 */
list_del(&new_item->list);
内核链表实例
以内核链表实现简单的LIFO队列为例,LIFO队列是指最后进入队列的元素最先被处理,其实现步骤如下:
定义队列头(在内核链表中表示为“list_head”结构)和数据结构(在本例中,使用大小为100的字符数组作为数据);
#define QUEUE_SIZE 100
struct queue {
char data[QUEUE_SIZE];
struct list_head list;
};
struct list_head my_list;
构建一个 LIFO 队列;
/* 创建节点并添加至队列头部 */
int enqueue(char *data, struct list_head *my_list){
int ret = 0;
struct queue *new_item = kmalloc(sizeof(*new_item), GFP_KERNEL);
if(new_item == NULL) {
ret = -ENOMEM;
goto out;
}
snprintf(new_item->data, QUEUE_SIZE, "%s", data);
INIT_LIST_HEAD(&new_item->list);
list_add(&new_item->list, my_list);
//...
}
弹出队列头元素;
/* 删除队列头结点并取出其中的数据 */
char *dequeue(struct list_head *my_list) {
struct queue *p = list_entry(my_list->prev, struct queue, list);
memcpy(result, p->data, QUEUE_SIZE);
list_del(&p->list);
kfree(p);
return result;
}
这样,我们就可以构建一个基于内核链表实现的简单的LIFO队列了。
内核链表的应用实例
内核链表被广泛应用于 Linux 内核中,常见的应用包括:
系统调用路由映射链表
Linux 内核提供了一套与系统调用相关的内部结构,称为“系统调用处理程序数组”。为了转换系统调用号到正确的处理程序函数的指针,内核中需要使用链表映射调用号。此处采用内核链表来实现该路径映射。例如,系统调用号“1”的处理程序被映射为链表中的第一个节点,调用号“2”的处理程序被映射为链表中的第二个节点。内核定时器链表
内核定时器需要封装定时器任务并将其添加到相应的内核链表中。在定时器到期后,内核依次处理链表中的每个任务。