Linux内核数据结构1

Linux内核数据结构1(基于Linux6.6)---链表介绍


一、链表概述

在 Linux 内核中,链表(Linked List)是一种广泛使用的数据结构。它是一种灵活的、动态的内存分配结构,可以在不需要连续内存空间的情况下存储数据。链表通过将数据元素(节点)通过指针连接起来,使得元素的插入和删除操作可以非常高效地完成。Linux 内核使用链表来管理各种资源,如任务调度、内存管理、文件系统等。

链表由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的指针。链表的基本结构通常如下:

struct list_head {
    struct list_head *next, *prev;
};
  • next:指向链表中的下一个节点。
  • prev:指向链表中的前一个节点。

通过这两个指针,链表可以方便地实现双向遍历

 

二、链表的种类

2.1、单向链表

只使用 next 指针,忽略 prev 指针。单向链表的优点是节省空间和内存,因为每个节点只需要一个指针。

特点:

  • 单向链表:每个节点只有指向下一个节点的 next 指针。
  • 内存节省:不使用 prev 指针,适合只需要向前遍历的场景。
/* 一个链表中的一个元素 */
struct list_element {
    void *data; /* 有效数据 */
    struct list_element *next; /* 指向下一个元素的指针 */
};

2.2、双向链表

最常见的 Linux 链表类型是双向链表,它使用 struct list_head 作为链表节点,每个节点有两个指针:nextprev,分别指向下一个节点和前一个节点。这使得链表可以支持双向遍历。

特点:

  • 双向链表:每个节点可以通过 nextprev 指针向前向后遍历。
  • 嵌入式链表:用户可以将 list_head 嵌入到自己的结构体中,这使得链表非常灵活。
/* 一个链表中的一个元素 */

struct list_element {

void *data; /* 有效数据 */

struct list_element *next; /* 指向下一个元素的指针 */

struct list_element *prev; /* 指向前一个元素的指针 */

};

2.3、环形链表

循环链表是将链表的最后一个节点的 next 指针指向链表的第一个节点,从而形成一个闭环。在 Linux 内核中,链表通常不会直接使用循环链表,但在一些特定应用中,如任务调度队列、环形缓冲区等,循环链表是非常有用的。

特点:

  • 循环结构:链表的尾部指向头部,形成一个闭环。
  • 无终止节点:循环链表没有“终止”的标志,遍历时需要特定的结束条件。

 

2.4、哈希链表(Hash List)

在某些情况下,链表与哈希表结合使用,形成哈希链表。每个哈希桶内都包含一个链表,这样可以将不同哈希值的数据组织在一起,并利用链表来处理哈希冲突。Linux 内核的 struct hlist_head 就是为了解决这一问题设计的。

特点:

  • 哈希桶:每个桶中包含一个链表,用于存储哈希冲突的元素。
  • 适用于哈希表:通过链表管理哈希冲突。

示例:

struct hlist_node {
    struct hlist_node *next, *pprev;
};
struct hlist_head {
    struct hlist_node *first;
};

2.5、环形链表(Ring Buffer)

环形链表有时被用于实现缓冲区,特别是在需要高效、连续地管理内存区域时。例如,环形缓冲区常用于数据流处理、日志系统等。与普通链表不同,环形链表的尾部指针指向头部,形成一个“圆环”结构。

特点:

  • 环形结构:尾部与头部连接形成闭环。
  • 固定大小:环形链表通常用于固定大小的缓冲区。

2.6、内核队列(List Queue)

内核队列(例如任务调度中的任务队列)常常使用链表作为底层数据结构。队列遵循先进先出(FIFO)原则,链表通过操作 headtail 指针来实现对元素的添加和删除。

特点:

  • FIFO:链表的插入和删除操作遵循先进先出的原则。
  • 多用途:内核中常用于任务调度、I/O 调度等。

2.7、任务链表(Task List)

在 Linux 内核中,任务(进程或线程)也通常通过链表来管理。例如,调度器通过链表维护所有进程的队列,如就绪队列、睡眠队列等。

特点:

  • 进程调度:链表用于实现就绪队列、睡眠队列、等待队列等。
  • 状态管理:可以很方便地将任务状态(就绪、阻塞、运行中等)与链表节点绑定。

2.8、延迟队列(Delayed Queue)

延迟队列是一种特殊的链表类型,用于管理需要延迟执行的任务。任务按照到期时间排序,内核会按顺序执行这些任务。延迟队列通常使用 struct timer_list 等数据结构来实现,但它也基于链表。

特点:

  • 延迟执行:链表中的任务会根据预定时间排序,并按时间顺序执行。
  • 定时任务:常用于定时器和延迟处理。

三、Linux内核链表的实现

  • 相比普遍的链表实现方式,Linux内核的实现可以说独树一帜。
  • Linux内核方式与众不同,它不是将数据结构塞入链表 ,而是将链表节点塞入数据结构。

3.1、Linux的链表数据结构(list_head)

  • 链表代码定义于list.h头文件中,格式如下:
    • next:指向下一个链表节点。
    • prev:指向前一个链表节点。

include/linux/types.h 

struct list_head {

struct list_head *next, *prev;

};

3.2、链表在内核中如何使用

 普通的应用程序来表示一个链表中的节点,则其格式如下:

struct fox {

unsigned long tail_length;

unsigned long_weight;

bool is_fantastic;

struct fox *next; //指向后一个节点

struct fox *prev; //指向前一个节点

};

但是Linux表示一个节点,则用下面的格式:其中list.next:指向下一个元素。其中list.prev:指向前一个元素。

struct fox {

unsigned long tail_length;

unsigned long_weight;

bool is_fantastic;

struct list_head list; //所有fox结构体形成地链表

};

3.3、container_of()宏

使用宏container_of()可以很方便地从链表指针找到父结构中包含的任何变量。这是因为在C语言中,一个给定结构中的变置偏移在编译时地址就被ABI固定下来了 .

tools/include/linux/kernel.h

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

3.4、list_entry()宏

使用container_of()宏,我们定义一个简单的函数便可返回包含list_head的父类型结构体。

依靠list_entry()方法,内核提供了创建、操作以及其他链表管理的各种例程——所有这些方法都不需要知道list_head所嵌入对象的数据结构。

include/linux/list.h

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

3.5、定义/创建一个链表(LIST_HEAD_INIT)

 链表需要在使用前初始化。因为多数元素都是动态创建的(也许这就是需要链表的原因),因此最常见的方式是在运行时初始化链表。

  include/linux/list.h

#define LIST_HEAD_INIT(name) { &(name), &(name) }

3.6、链表头(LIST_HEAD宏)

内核链表最突出的特点就是:每一个链表节点中都包含一个list_head指针,于是我们可以从任何一个节点起遍历链表,直到我们看到所有节点。

以上方式确实很优美,不过有时确实也需要一个特殊指针索引到整个链表,而不从一个链表节点触发。有趣的是,这个特殊的索引节点事实上也就是一个常规的list_head。

LIST_HEAD:该函数定义并初始化了一个链表例程,这些例程中的大多数都只接受一个或者两个参数:头节点或者头节点加上一个特殊链表节点。

 include/linux/list.h

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

3.7、常用链表宏和函数总结

操作宏/函数名描述
初始化链表INIT_LIST_HEAD初始化一个空链表
插入到头部list_add将节点插入到链表头部
插入到尾部list_add_tail将节点插入到链表尾部
删除节点list_del删除链表中的节点
删除并初始化节点list_del_init删除节点并初始化节点
遍历链表list_for_each遍历链表中的每个节点
反向遍历链表list_for_each_reverse反向遍历链表中的每个节点
判断链表是否为空list_empty判断链表是否为空
获取第一个元素list_first_entry获取链表的第一个元素
获取最后一个元素list_last_entry获取链表的最后一个元素

 

四、操作链表(增加、删除、移动)

 内核提供了一组函数来操作链表,这些函数都要使用一个或多个list_head结构体指针作参数。因为函数都是用C语言以内联函数形式实现的,所以它们的原型在文件 include/linux/list.h中。

下面介绍的函数复杂度都为O(1)。这意味着,无论这些函数操作的链表大小如何,无论它们得到的参数如何,它们都在恒定时间内完成。

4.1、检查链表是否为空(list_empty)

检查指定的链表是否为空,为空的话返回非0值;不为空返回0 。

/**
 * list_empty - tests whether a list is empty
 * @head: the list to test.
 */
static inline int list_empty(const struct list_head *head)
{
	return READ_ONCE(head->next) == head;
}

4.2、增加节点(list_add)

/**
 * 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 *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

功能:该函数向指定链表的head节点后插入new节点。

4.3、增加尾节点(list_add_tail)

/**
 * 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 *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

 功能:该函数向指定链表的head令点前插入new节点。

__list_add()函数

/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	if (!__list_add_valid(new, prev, next))
		return;

	next->prev = new;
	new->next = next;
	new->prev = prev;
	WRITE_ONCE(prev->next, new);
}

4.4、删除节点(list_del)

/**
 * 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.
 */
static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

 功能:从链表中删除一个结点。

删除一个节点并重新初始化(list_del_init)

/**
 * list_del_init - deletes entry from list and reinitialize it.
 * @entry: the element to delete from the list.
 */
static inline void list_del_init(struct list_head *entry)
{
	__list_del_entry(entry);
	INIT_LIST_HEAD(entry);
}

__list_del()函数

/*
 * 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;
	WRITE_ONCE(prev->next, next);
}

4.5、节点的移动(list_move)

 从一个链表中移除list项,然后将其加入到另一个链表的head节点后面。

/**
 * list_move - delete from one list and add as another's head
 * @list: the entry to move
 * @head: the head that will precede our entry
 */
static inline void list_move(struct list_head *list, struct list_head *head)
{
	__list_del_entry(list);
	list_add(list, head);
}

节点的移动(list_move_tail)

 从一个链表中移除list项,然后将其加入到另一个链表的head节点的前面。

/**
 * list_move_tail - delete from one list and add as another's tail
 * @list: the entry to move
 * @head: the head that will follow our entry
 */
static inline void list_move_tail(struct list_head *list,
				  struct list_head *head)
{
	__list_del_entry(list);
	list_add_tail(list, head);
}

4.6、链表的合并(list_splice)

 该函数合并两个链表,将list所指的链表插入到指定链表的head元素后面。

/**
 * list_splice - join two lists, this is designed for stacks
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 */
static inline void list_splice(const struct list_head *list,
				struct list_head *head)
{
	if (!list_empty(list))
		__list_splice(list, head, head->next);
}

链表的合并(list_splice_init)

 并重新初始化原来的链表,并重新初始化list链表。

/**
 * list_splice_tail - join two lists, each list being a queue
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 */
static inline void list_splice_tail(struct list_head *list,
				struct list_head *head)
{
	if (!list_empty(list))
		__list_splice(list, head->prev, head);
}

__list_splice

static inline void __list_splice(const struct list_head *list,
				 struct list_head *prev,
				 struct list_head *next)
{
	struct list_head *first = list->next;
	struct list_head *last = list->prev;

	first->prev = prev;
	prev->next = first;

	last->next = next;
	next->prev = last;
}

五、遍历链表

5.1、基本方法(list_for_each)

该宏使用2个参数:

参数1:一个临时变量,遍历时用来指向当前项。

参数2:需要遍历的链表的以头节点形式存在的list_head。

在遍历链表时,第一个参数在链表中不断移动指向下一个元素,直到链表中的所有元素都被访问为止。

 

/**
 * 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; !list_is_head(pos, (head)); pos = pos->next)
  • 演示案例:例如我们遍历前面的fox_list链表,则可以定义以下的代码
struct list_head *p;

struct fox *f;


list_for_each(p,&fox_list){

//每次返回一个struct fox结构体

//list_entry见上面介绍

f=list_entry(p,struct fox,list);

}

5.2、list_for_each_entry()

多数内核代码采用list_for_each_entry()宏遍历链表。该宏内部也使用list_entry()宏,但简化了遍历过程参数:

参数1:指向包含list_head节点对象的指针,可将它看做是list_entry宏的返回值。

参数2:head是一个指向头节点的指针,即遍历开始的位置。

参数3:list_head在结构中的名称。

/**
 * list_for_each_rcu - Iterate over a list in an RCU-safe fashion
 * @pos:	the &struct list_head to use as a loop cursor.
 * @head:	the head for your list.
 */
#define list_for_each_rcu(pos, head)		  \
	for (pos = rcu_dereference((head)->next); \
	     !list_is_head(pos, (head)); \
	     pos = rcu_dereference(pos->next))

5.3、反向遍历链表(list_for_each_entry_reverse)

其和list_for_each_entry类似,不同点在于它是反向遍历链表的。也就是说,不再是沿着next指针向前遍历,而是沿着prev指针向后遍历。

很多原因会需要反向遍历链表。其中一个是性能原因——如果你知道你要寻找的节点最可能在你搜索的起始点的前面,那么反向搜索岂不更快。第二个原因是如果顺序很重要,比如,如果你使用链表实现堆栈,那么你需要从尾部向前遍历才能达到先进/先 出(LIFO)原则。如果你没有确切的反向遍历的原因,就老实点,用list_for_each_entry()宏吧。

/**
 * list_for_each_entry_reverse - iterate backwards over list of given type.
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_head within the struct.
 */
#define list_for_each_entry_reverse(pos, head, member)			\
	for (pos = list_last_entry(head, typeof(*pos), member);		\
	     !list_entry_is_head(pos, head, member); 			\
	     pos = list_prev_entry(pos, member))

5.4、正向遍历的同时删除(list_for_each_entry_safe)

 标准的链表遍历方法在你遍历链表的同时要想删除节点时是不行的。因为标准的链表方法建 立在你的操作不会改变链表项这一假设上,所以如果当前项在遍历循环中被刪除,那么接下来的遍历就无法获取next(或prev)指针了。这其实是循环处理中的一个常见范式,开发人员通过在潜在的删除操作之前存储next(或者previous)指针到一个临时变量中,以便能执行删除操作。好在Linux内核提供了例程处理这种情况:

/**
 * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_head within the struct.
 */
#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_first_entry(head, typeof(*pos), member),	\
		n = list_next_entry(pos, member);			\
	     !list_entry_is_head(pos, head, member); 			\
	     pos = n, n = list_next_entry(n, member))

5.5、反向遍历的同时删除(list_for_each_entry_safe_reverse)

该宏与list_for_each_entry_safe相反,是在反向遍历链表的同时删除它 。

/**
 * list_for_each_entry_safe_reverse - iterate backwards over list safe against removal
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_head within the struct.
 *
 * Iterate backwards over list of given type, safe against removal
 * of list entry.
 */
#define list_for_each_entry_safe_reverse(pos, n, head, member)		\
	for (pos = list_last_entry(head, typeof(*pos), member),		\
		n = list_prev_entry(pos, member);			\
	     !list_entry_is_head(pos, head, member); 			\
	     pos = n, n = list_prev_entry(n, member))

六、举例应用

在 Linux 内核中,链表是一种非常重要的数据结构,广泛应用于任务调度、资源管理、I/O 系统、文件系统等多个领域。下面我会通过几个实际的例子详细说明 Linux 链表的应用。

1. 任务调度(任务队列)

Linux 内核中的任务调度是链表应用的一个经典场景。内核使用链表来管理进程(任务)的状态。比如,就绪队列(Ready Queue)和睡眠队列(Sleep Queue)等。

就绪队列

每当进程被调度执行时,内核会将其从就绪队列中取出,并执行。进程可以处于不同的状态,比如运行、就绪、阻塞等。Linux 使用链表来维护这些状态之间的转换。

代码示例:

内核的就绪队列使用双向链表来管理进程。task_struct 结构体代表每个进程,其中包含一个 list_head 类型的链表节点,用于将该进程加入到链表中。

struct task_struct {
    struct list_head tasks; // 用于进程链表
    ...
};

// 初始化链表
INIT_LIST_HEAD(&task->tasks);

// 将进程插入就绪队列
list_add_tail(&task->tasks, &ready_queue);
  • list_add_tail() 用来将任务插入到链表的尾部。
  • INIT_LIST_HEAD() 是用来初始化链表头的宏。

睡眠队列

当一个进程在等待某个条件(如 I/O 完成)时,它将进入睡眠队列。在满足条件时,内核会将进程从睡眠队列中移除并放入就绪队列中。睡眠队列也是通过链表来管理的。

代码示例:

struct sleep_queue {
    struct list_head list;
    ...
};

// 将进程插入睡眠队列
list_add_tail(&task->tasks, &sleep_queue);

2. 内存管理(伙伴系统和空闲内存块管理)

在内核的内存管理中,链表用于管理空闲的内存块。Linux 内核使用伙伴系统(Buddy System)来分配和释放内存块,这个系统是基于链表实现的。

空闲内存块链表

当内存块被释放时,它们会被放入空闲内存块链表中,等待再次分配。Linux 内核使用多个链表来管理不同大小的内存块。每个空闲块的链表通过 list_head 来组织。

代码示例:

struct free_area {
    struct list_head free_list; // 存储空闲内存块的链表
    ...
};

struct page {
    struct list_head lru; // 用于空闲页面的链表
    ...
};

3. I/O 调度(I/O 请求队列)

Linux 内核使用链表来管理 I/O 请求队列。每当一个 I/O 请求(如磁盘读写)被发起时,内核会将它插入到一个链表中,调度器随后会根据某些策略处理这些请求。

块设备 I/O 请求链表

Linux 使用 struct bio 来表示块设备的 I/O 请求,bio 结构体中包含一个 list_head 类型的链表,表示 I/O 请求的链表。

代码示例:

struct bio {
    struct list_head bi_list; // 用于存储 I/O 请求的链表
    ...
};

void add_bio_to_queue(struct bio *bio, struct bio_queue *queue) {
    list_add_tail(&bio->bi_list, &queue->bi_list);
}

4. 延迟执行(延迟任务队列)

内核中有很多需要延迟执行的任务,例如定时器、延迟信号、延迟处理任务等。这些任务通常会被插入到一个延迟任务队列中,等待到期后执行。延迟任务队列也是通过链表来管理的。

定时器链表

内核定时器(如 timer_list)通常会被放入一个链表中,在定时器到期时,内核会遍历链表并执行相应的定时任务。

代码示例:

struct timer_list {
    struct list_head entry; // 定时器的链表节点
    ...
};

void add_timer_to_list(struct timer_list *timer, struct list_head *queue) {
    list_add_tail(&timer->entry, queue);
}

5. 文件系统(dentry 缓存和 inode 链表)

Linux 文件系统通过链表来管理文件和目录的相关信息。dentry(目录项)和 inode(索引节点)是文件系统中两个非常重要的数据结构,它们通常通过链表来管理。

dentry 缓存

dentry 缓存用于存储文件路径的解析结果。在内核中,每个目录项(如文件或目录)都有一个 dentry 对象,dentry 结构体包含了一个链表,用于管理同一目录下的所有目录项。

代码示例:

struct dentry {
    struct list_head d_u; // 用于目录项的链表
    ...
};

void add_dentry_to_cache(struct dentry *dentry) {
    list_add_tail(&dentry->d_u, &parent_dentry->d_u);
}

inode 链表

inode 结构体用来存储文件的元数据,如文件权限、大小、块地址等。内核通过链表来管理每个文件系统的 inode 对象。

6. 网络协议栈(网络连接管理)

在 Linux 网络协议栈中,链表被用来管理网络连接。每个网络连接(如 TCP 连接)都有一个 sock 结构体,sock 结构体通过链表管理。

TCP 连接链表

在 TCP 协议中,每个连接会维护一个链表来管理连接的状态。内核会根据链表中的信息来调度和管理网络连接。

代码示例:

struct sock {
    struct list_head sk_node; // 用于管理网络连接的链表
    ...
};

void add_sock_to_list(struct sock *sk, struct list_head *list) {
    list_add_tail(&sk->sk_node, list);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值