Linux内核链表分析与移植
链表简介
链表是一种常用的数据结构,它通过指针将一系列数据节点连接成一条数据链。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失
回忆我们当初在C语言学习的时候所接触到的链表类型。链表最基本的组成元素是节点,一个节点至少包含如下两个部分:数据域和指针域。数据域存放具体的数据(或数据地址),指针域将若干个节点串联起来,形成一个链表。如下是几种常见的链表示意图:
(1)单向链表示意图:
(2)双向链表示意图:
(3)双向循环链表示意图:
Linux内核链表是一种双向循环链表。而Linux内核链表和传统的双向循环链表有什么区别呢?传统的双向循环链表的指针域next_ptr是指向下一个节点的起始地址(prev_prt同理),而指针域的指针类型则必须和节点类型相匹配,而节点类型(Struct)一般随着项目的不同而不同,因此导致指针的类型不一样,从而无法实现统一的链表操作接口。
而Linux内核链表则很好的解决了指针域类型不统一的问题。Linux内核链表的指针域并不指向节点的起始地址,而将其指向节点中的指针域,从而可以定义统一的指针域类型:
include/linux/types.h
struct list_head {
struct list_head *next, *prev;
};
Linux内核链表示意图如下:
而访问链表最关键的是访问链表的数据域,但是Linux的内核链表仅仅描述了各个节点之间指针域之间的关系,换句话说,我们能很方便的找到链表中某个节点的指针域,而如何通过该指针域访问所对应的数据域呢?稍后将详细讲解。
链表操作函数
Linux内核链表的操作函数主要有下面几个,它们都在include/linux/list.h文件中实现:
函数名称 | 功能 |
---|---|
INIT_LIST_HEAD | 创建链表 |
list_add | 在链表头插入节点 |
list_add_tail | 在链表尾插入节点 |
list_del | 删除节点 |
list_entry | 取出节点 |
list_for_each | 遍历链表 |
(1)创建链表INIT_LIST_HEAD
链表的创建也就是初始化一个链表头结构(指针域结构),也就是将节点的next和prev指向节点本身。具体代码实现及示意图如下:
include/linux/list.h
static inline void INIT_LIST_HEAD(struct list_head *list){
list->next = list;
list->prev = list;
}
(2)在链表头插入节点
在双向链表头插入节点即在双向链表的表头和表头的next节点之间插入一个新的节点。主要处理分为4步,插入前和插入后的示意图如下图所示:
(3)在链表尾插入节点
在双向链表尾部插入节点即在双向链表的表头和表头的prev节点之间插入一个新的节点。主要处理分为4步,插入前和插入后的示意图如下图所示:
在链表头部和尾部插入节点的代码如下:
include/linux/list.h
/*
* Insert a new entry between two known consecutive entries.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
#ifndef CONFIG_DEBUG_LIST
static inline void __list_add(struct list_head *n,
struct list_head *prev,
struct list_head *next)
{
next->prev = n;
n->next = next;
n->prev = prev;
prev->next = n;
}
#else
extern void __list_add(struct list_head *n,
struct list_head *prev,
struct list_head *next);
#endif
/**
* list_add - add a new entry
* @new: new entry to be added
* @head: list head to add it after
*
* Insert a new entry after the specified head.
* This is good for implementing stacks.
*/
static inline void list_add(struct list_head *n, struct list_head *head)
{
__list_add(n, head, head->next);
}
/** * list_add_tail - add a new entry
* @new: new entry to be added
* @head: list head to add it before
*
* Insert a new entry before the specified head.
* This is useful for implementing queues.
*/
static inline void list_add_tail(struct list_head *n, struct list_head *head)
{
__list_add(n, head->prev, head);
}
(4)删除节点
从双向链表中删除一个节点,主要处理分为3步,处理代码及删除前和删除后的示意图如下:
include/linux/list.h
/*
* Delete a list entry by making the prev/next entries
* point to each other.
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
/**
* list_del - deletes entry from list.
* @entry: the element to delete from the list.
* Note: list_empty() on entry does not return true after this, the entry is
* in an undefined state.
*/
#ifndef CONFIG_DEBUG_LIST
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
#else
extern void __list_del_entry(struct list_head *entry);
extern void list_del(struct list_head *entry);
#endif
(5)取出节点
Linux内核链表的精妙之处在于如何通过链表节点的指针域访问所对应的数据域。该操作通过宏list_entry获得。该宏函数中ptr表示节点中指针域的地址,type表示节点所属结构体的类型,member表示该指针域在节点所属结构体中的成员名称。
include/linux/kernel.h
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
#ifndef container_of
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
#endif
include/linux/list.h
/**
* list_entry - get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
container_of宏函数在文件include/linux/kernel.h文件中实现,其实现算法如下图所示:
由上图可知,已知一个结构体中指针域的地址ptr,要推算出该结构体的首地址,我们可以用ptr的值减去图中灰色部分的偏移的大小就可以得到该结构体的起始地址。而这个偏移量怎么计算呢?如右图所示,将0地址强制类型转换为(TYPE*)0,然后通过指针访问结构体成员的方法获取到偏移量的大小,即&((TYPE *)0)->MEMBER,最后即可算出结构体的首地址(type *)((char *)__mptr - offsetof(type, member))。
(6)遍历链表
遍历链表的宏函数是list_for_each,其中参数pos是事先定义好的struct list_head *类型的指针,用于在遍历的过程中指向当前节点,head是链表头,要获取当前节点的数据域,只需要通过list_entry宏函数即可获得。
include/linux/list.h
/**
* list_for_each - iterate over a list
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)
内核链表的移植
Linux内核链表完全由C语言实现,因此Linux内核链表可以很容易的移植到应用程序中。下面的代码示例了应用程序中如何使用Linux内核链表。
list_demo.c
#include <stdio.h>
#include "list.h"
/* 链表节点定义 */
struct score{
int num; /* 学号 */
int english; /* 英语成绩 */
int math; /* 数学成绩 */
struct list_head node;
};
struct list_head score_head; /* 链表头(指向链表的第一个节点) */
struct score stu1, stu2, stu3; /* 链表中的3个节点 */
struct list_head *pos; /* 遍历链表使用(遍历过程中指向节点的指针域) */
struct score *tmp;
int main()
{
/* 创建链表(实际上就是对链表头的初始化) */
INIT_LIST_HEAD(&score_head);
/* 向链表插入节点 */
stu1.num = 1;
stu1.english = 90;
stu1.math = 98;
list_add_tail(&(stu1.node), &score_head);
stu2.num =2;
stu2.english = 93;
stu2.math = 91;
list_add_tail(&(stu2.node), &score_head);
stu3.num =3;
stu3.english = 94;
stu3.math = 95;
list_add_tail(&(stu3.node), &score_head);
list_for_each(pos, &score_head)
{
tmp = list_entry(pos,struct score,list); /* 获取链表节点(数据域) */
printf("no %d,english is %d,math is %d\n", tmp->num, tmp->english, tmp->math);
printf("HHH\n");
}
list_del(&stu1.node);
list_del(&stu2.node);
printf("This is a test!\n");
getchar();
return 0;
}