Linux内核数据结构2(基于Linux6.6)---队列介绍
一、概述
在 Linux 内核中,队列(Queue)是一种非常常用的基础数据结构,用于管理一系列待处理的任务或事件。与链表不同,队列通常遵循 先进先出(FIFO, First In First Out)的原则,也就是说,第一个进入队列的元素会最先被处理。
Linux 内核中的队列常常用来管理各种类型的资源、任务、请求等,例如 I/O 请求、进程调度、信号处理等。内核为不同的需求设计了多种队列实现方式。
1.1、队列的基本类型
在 Linux 内核中,队列的实现方式主要有两种:链表队列和基于环形缓冲区的队列。
-
链表队列(Linked List Queue):通过链表来实现队列,支持任意长度的队列,不需要预先分配固定大小的内存空间。
-
环形队列(Circular Queue):通常用于需要循环访问的数据结构,例如网络包缓存、内存池等。环形队列的特点是队列的尾部指针会回绕到头部。
1.2、Linux 内核中常见的队列实现
1.2.1、 任务调度队列(Scheduling Queue)
Linux 内核使用队列来管理进程的调度。例如,**就绪队列(Ready Queue)和睡眠队列(Sleep Queue)**都采用了队列来组织进程或线程。
- 就绪队列:用来存储所有待运行的进程。当 CPU 空闲时,调度器会从就绪队列中选择一个进程进行调度。
- 睡眠队列:当进程需要等待某些条件(例如 I/O 操作完成)时,会被放入睡眠队列。
这些队列通常使用链表来实现,其中每个节点表示一个进程或线程。
1.2.2、 I/O 请求队列
I/O 请求队列是 Linux 内核中另一个重要的队列实现,尤其在块设备的 I/O 调度中。内核会将 I/O 请求(如磁盘读写)按照一定的规则(例如优先级、先来先服务)插入到 I/O 请求队列中。调度器会从队列中取出请求并进行处理。
- 块设备 I/O 调度队列:如磁盘调度器使用的队列,来管理磁盘的读写请求。
- 网络协议栈的队列:用于处理网络数据包的接收和发送。
1.2.3、工作队列(Work Queue)
工作队列是 Linux 内核中处理延迟任务的机制,允许内核异步执行任务。在内核中,工作队列常用于延迟任务的执行,内核通过队列将任务排队等待处理。工作队列在内核线程中运行,可以避免直接在中断上下文中执行复杂的操作。
- 工作队列通常包含一个任务队列,每个任务都有一个执行的函数。当任务被添加到队列时,它会在适当的时候由内核线程处理。
1.2.4、软中断和任务队列
Linux 内核中的软中断处理机制也使用队列。软中断(Softirq)和任务队列常用于延迟处理或高优先级任务的执行。软中断队列用于处理网络、块设备等高频事件。
- 软中断队列:通过软中断机制,内核可以处理一些延迟任务,避免在硬中断上下文中过多的处理逻辑。
- 任务队列:任务队列与工作队列类似,通常用于处理延迟的工作任务。
1.2.5、信号队列(Signal Queue)
Linux 内核也使用队列来管理进程的信号。每个进程都有一个信号队列,存储该进程的待处理信号。信号队列确保信号的有序传递,避免信号的丢失或乱序。
下图是一个标准队列:
二、Linux内核队列(kfifo)
Linux内核通用队列实现称为kfifo。
struct kfifo
include/linux/kfifo.h
/*
* define compatibility "struct kfifo" for dynamic allocated fifos
*/
struct kfifo __STRUCT_KFIFO_PTR(unsigned char, 0, void);
#define __STRUCT_KFIFO_PTR(type, recsize, ptrtype) \
{ \
__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
type buf[0]; \
}
#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \
union { \
struct __kfifo kfifo; \
datatype *type; \
const datatype *const_type; \
char (*rectype)[recsize]; \
ptrtype *ptr; \
ptrtype const *ptr_const; \
}
enqueue、dequeue
入队操作 (enqueue
)
list_add()
:将一个元素插入到链表的头部(即队列的前端)。list_add_tail()
:将一个元素插入到链表的尾部(即队列的末端)。
#include <linux/list.h>
// 初始化队列
struct list_head queue;
INIT_LIST_HEAD(&queue);
// 定义一个元素
struct task_struct {
struct list_head list;
int task_id;
};
// 创建并初始化元素
struct task_struct *task = kmalloc(sizeof(struct task_struct), GFP_KERNEL);
task->task_id = 1;
// 入队操作:将任务插入队列尾部
list_add_tail(&task->list, &queue);
list_add()
:该操作将元素添加到链表的头部,用于将元素插入队列的前端。list_add_tail()
:该操作将元素添加到链表的尾部,用于将元素插入队列的末端。
出队操作 (dequeue
)
list_del()
:删除链表中的一个元素。list_first_entry()
:获取链表的第一个元素。list_del_init()
:从链表中删除一个元素并将该元素初始化。
// 删除队列头部的任务
struct task_struct *first_task;
// 获取队列头部的第一个任务
first_task = list_first_entry(&queue, struct task_struct, list);
// 出队操作:删除队列头部的任务
list_del(&first_task->list);
// 释放内存
kfree(first_task);
list_first_entry()
:从链表中获取第一个元素。通常用于获取队列头部的元素。list_del()
:将该元素从链表中删除。
- kfifo对象维护了两个偏移量:入口偏移和出口偏移
- 入口偏移是指下一次入队列时的位置
- 出口偏移是指下一次出队列时的位置
- 出口偏移总是小于等于入口偏移,否则无意义,因为那样说明要出队列的元素根本还没有入队列
三、创建队列
创建队列并分配缓冲区(kfifo_init)
该函数创建并初始化一个kfifo对象,其使用buffer指向的size字节大小的内存。
include/linux/kfifo.h
/**
* kfifo_init - initialize a fifo using a preallocated buffer
* @fifo: the fifo to assign the buffer
* @buffer: the preallocated buffer to be used
* @size: the size of the internal buffer, this have to be a power of 2
*
* This macro initializes a fifo using a preallocated buffer.
*
* The number of elements will be rounded-up to a power of 2.
* Return 0 if no error, otherwise an error code.
*/
#define kfifo_init(fifo, buffer, size) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
__is_kfifo_ptr(__tmp) ? \
__kfifo_init(__kfifo, buffer, size, sizeof(*__tmp->type)) : \
-EINVAL; \
})
lib/kfifo.c
int __kfifo_init(struct __kfifo *fifo, void *buffer,
unsigned int size, size_t esize)
{
size /= esize;
if (!is_power_of_2(size))
size = rounddown_pow_of_two(size);
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
fifo->data = buffer;
if (size < 2) {
fifo->mask = 0;
return -EINVAL;
}
fifo->mask = size - 1;
return 0;
}
EXPORT_SYMBOL(__kfifo_init);
动态创建(kfifo_alloc)
include/linux/kfifo.h
/**
* kfifo_alloc - dynamically allocates a new fifo buffer
* @fifo: pointer to the fifo
* @size: the number of elements in the fifo, this must be a power of 2
* @gfp_mask: get_free_pages mask, passed to kmalloc()
*
* This macro dynamically allocates a new fifo buffer.
*
* The number of elements will be rounded-up to a power of 2.
* The fifo will be release with kfifo_free().
* Return 0 if no error, otherwise an error code.
*/
#define kfifo_alloc(fifo, size, gfp_mask) \
__kfifo_int_must_check_helper( \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
__is_kfifo_ptr(__tmp) ? \
__kfifo_alloc(__kfifo, size, sizeof(*__tmp->type), gfp_mask) : \
-EINVAL; \
}) \
)
lib/kfifo.c
int __kfifo_alloc(struct __kfifo *fifo, unsigned int size,
size_t esize, gfp_t gfp_mask)
{
/*
* round up to the next power of 2, since our 'let the indices
* wrap' technique works only in this case.
*/
size = roundup_pow_of_two(size);
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
if (size < 2) {
fifo->data = NULL;
fifo->mask = 0;
return -EINVAL;
}
fifo->data = kmalloc_array(esize, size, gfp_mask);
if (!fifo->data) {
fifo->mask = 0;
return -ENOMEM;
}
fifo->mask = size - 1;
return 0;
}
静态声明kfifo
include/linux/kfifo.h
- 静态声明kfifo比较简单,但不太常用。
- 例如下面会创建一个名称为name,大小为size的kfifo对象。和前面一样,size必须是2的幂。
/**
* DECLARE_KFIFO - macro to declare a fifo object
* @fifo: name of the declared fifo
* @type: type of the fifo elements
* @size: the number of elements in the fifo, this must be a power of 2
*/
#define DECLARE_KFIFO(fifo, type, size) STRUCT_KFIFO(type, size) fifo
/**
* INIT_KFIFO - Initialize a fifo declared by DECLARE_KFIFO
* @fifo: name of the declared fifo datatype
*/
#define INIT_KFIFO(fifo) \
(void)({ \
typeof(&(fifo)) __tmp = &(fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
__kfifo->in = 0; \
__kfifo->out = 0; \
__kfifo->mask = __is_kfifo_ptr(__tmp) ? 0 : ARRAY_SIZE(__tmp->buf) - 1;\
__kfifo->esize = sizeof(*__tmp->buf); \
__kfifo->data = __is_kfifo_ptr(__tmp) ? NULL : __tmp->buf; \
})
四、入队列(kfifo_put)
kfifo_put
include/linux/kfifo.h
/**
* kfifo_put - put data into the fifo
* @fifo: address of the fifo to be used
* @val: the data to be added
*
* This macro copies the given value into the fifo.
* It returns 0 if the fifo was full. Otherwise it returns the number
* processed elements.
*
* Note that with only one concurrent reader and one concurrent
* writer, you don't need extra locking to use these macro.
*/
#define kfifo_put(fifo, val) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(*__tmp->const_type) __val = (val); \
unsigned int __ret; \
size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
if (__recsize) \
__ret = __kfifo_in_r(__kfifo, &__val, sizeof(__val), \
__recsize); \
else { \
__ret = !kfifo_is_full(__tmp); \
if (__ret) { \
(__is_kfifo_ptr(__tmp) ? \
((typeof(__tmp->type))__kfifo->data) : \
(__tmp->buf) \
)[__kfifo->in & __tmp->kfifo.mask] = \
*(typeof(__tmp->type))&__val; \
smp_wmb(); \
__kfifo->in++; \
} \
} \
__ret; \
})
五、出队列(kfifo_get)
kfifo_get
include/linux/kfifo.h
/**
* kfifo_get - get data from the fifo
* @fifo: address of the fifo to be used
* @val: address where to store the data
*
* This macro reads the data from the fifo.
* It returns 0 if the fifo was empty. Otherwise it returns the number
* processed elements.
*
* Note that with only one concurrent reader and one concurrent
* writer, you don't need extra locking to use these macro.
*/
#define kfifo_get(fifo, val) \
__kfifo_uint_must_check_helper( \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(__tmp->ptr) __val = (val); \
unsigned int __ret; \
const size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
if (__recsize) \
__ret = __kfifo_out_r(__kfifo, __val, sizeof(*__val), \
__recsize); \
else { \
__ret = !kfifo_is_empty(__tmp); \
if (__ret) { \
*(typeof(__tmp->type))__val = \
(__is_kfifo_ptr(__tmp) ? \
((typeof(__tmp->type))__kfifo->data) : \
(__tmp->buf) \
)[__kfifo->out & __tmp->kfifo.mask]; \
smp_wmb(); \
__kfifo->out++; \
} \
} \
__ret; \
}) \
)
六、重置队列(kfifo_reset)
kfifo_reset
include/linux/kfifo.h
/**
* kfifo_reset - removes the entire fifo content
* @fifo: address of the fifo to be used
*
* Note: usage of kfifo_reset() is dangerous. It should be only called when the
* fifo is exclusived locked or when it is secured that no other thread is
* accessing the fifo.
*/
#define kfifo_reset(fifo) \
(void)({ \
typeof((fifo) + 1) __tmp = (fifo); \
__tmp->kfifo.in = __tmp->kfifo.out = 0; \
})
七、撤销队列(kfifo_free)
kfifo_free
include/linux/kfifo.h
/**
* kfifo_free - frees the fifo
* @fifo: the fifo to be freed
*/
#define kfifo_free(fifo) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
if (__is_kfifo_ptr(__tmp)) \
__kfifo_free(__kfifo); \
})
八、举例应用
在 Linux 内核中,队列作为一种常见的数据结构,广泛应用于任务调度、I/O 请求、软中断处理、工作队列等多个方面。队列的实现通常基于链表(list_head
),在许多内核模块中起着重要作用。
下面将通过一些实际的例子来详细说明 Linux 内核中队列的应用。
1. 任务调度队列
任务调度是 Linux 内核的核心之一,其中队列的应用非常关键。内核通过多个队列来管理和调度进程,包括就绪队列(ready queue)和等待队列(waiting queue)。
就绪队列(Ready Queue)
Linux 内核使用就绪队列来管理所有可运行的进程。当进程被调度器选中时,它会被放入就绪队列中,等待 CPU 调度。就绪队列通常使用双向链表(list_head
)来实现。
struct list_head ready_queue;
INIT_LIST_HEAD(&ready_queue);
在任务调度时,调度器会遍历这个队列,选择一个最优的进程来运行。
等待队列(Wait Queue)
等待队列用于管理那些等待某个事件发生的进程。一个典型的例子是进程在 I/O 操作完成之前进入等待队列。
struct wait_queue_head my_wait_queue;
INIT_WAITQUEUE_HEAD(&my_wait_queue);
当一个进程执行 sleep()
或 wait_event()
时,它会被添加到等待队列。当某个事件(如 I/O 完成)发生时,内核会将进程从等待队列中移除,重新加入就绪队列。
wait_event_interruptible(my_wait_queue, condition_met);
2. I/O 请求队列
Linux 内核使用队列来管理对设备的 I/O 请求,确保设备的访问是按顺序进行的。I/O 队列通常用于磁盘调度、网络数据传输等场景。
磁盘 I/O 队列
在磁盘调度中,内核将 I/O 请求加入一个队列中,调度器根据不同的调度算法(如 FCFS、C-SCAN 等)选择合适的请求执行。
内核中的 block_layer
就是一个例子,其中所有磁盘 I/O 请求都被放入队列等待处理。
struct request_queue *rq_queue = blk_init_queue(request_fn, &rq_queue_lock);
每个 I/O 请求(struct request
)都会通过 list_add_tail()
被添加到请求队列的尾部,等待磁盘驱动程序处理。
网络数据包队列
在网络协议栈中,网络数据包也需要排队处理。比如,在 TCP 协议中,收到的数据包被放入接收队列,等待被应用程序读取。网络驱动程序将接收到的数据包通过队列传递给上层协议栈。
struct sk_buff *skb;
skb_queue_tail(&sk->sk_receive_queue, skb);
这个队列确保网络数据包按顺序被传递给应用程序。
3. 软中断队列
软中断(softirq)是 Linux 内核的一种中断机制,用于延迟某些操作,以提高系统性能。软中断通常被用来处理网络数据包、定时器事件等。内核通过软中断队列来管理这些任务。
软中断处理
软中断的队列管理通常是由内核定时触发的。软中断任务被插入队列中,然后由内核的软中断处理程序执行。
例如,网络数据包的处理通常由 net_rx_action()
在软中断上下文中处理。内核会将需要处理的数据包添加到软中断队列中,然后在稍后的调度中处理它们。
// 软中断处理
void net_rx_action(struct softirq_action *h)
{
struct sk_buff *skb;
// 获取数据包并处理
skb = skb_dequeue(&skb_queue);
process_packet(skb);
}
4. 工作队列
工作队列(work queue)是 Linux 内核用来延迟执行任务的一种机制。它是通过队列管理任务,在某个内核线程上下文中执行这些任务。工作队列非常适用于需要在内核线程中异步执行的操作,如网络处理、I/O 操作等。
创建工作队列
内核通过 create_workqueue()
创建一个工作队列,每个任务通过 queue_work()
加入到工作队列中。
struct workqueue_struct *my_wq;
INIT_WORK(&my_work, my_work_fn);
my_wq = create_workqueue("my_queue");
// 将任务添加到工作队列
queue_work(my_wq, &my_work);
工作队列的执行
当内核线程空闲时,它会从工作队列中取出任务并执行。内核线程通常在工作队列中循环执行任务,直到队列为空。
void my_work_fn(struct work_struct *work)
{
pr_info("Processing work item\n");
}
工作队列广泛用于延迟任务的处理,尤其在驱动程序和模块中处理 I/O 或定时任务时非常有用。
5. 示例:使用队列进行任务调度
下面是一个简单的示例,演示如何使用队列(list_head
)来模拟一个任务调度系统,任务按优先级排序。
任务结构和队列初始化
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/list.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("A simple example of task queue in Linux Kernel");
struct task {
struct list_head list;
int priority;
char task_name[20];
};
// 初始化队列
static struct list_head task_queue;
INIT_LIST_HEAD(&task_queue);
static int __init queue_example_init(void)
{
struct task *task1, *task2, *task3;
// 创建任务并添加到队列
task1 = kmalloc(sizeof(struct task), GFP_KERNEL);
task2 = kmalloc(sizeof(struct task), GFP_KERNEL);
task3 = kmalloc(sizeof(struct task), GFP_KERNEL);
task1->priority = 1;
strcpy(task1->task_name, "Task 1");
list_add_tail(&task1->list, &task_queue);
task2->priority = 3;
strcpy(task2->task_name, "Task 2");
list_add_tail(&task2->list, &task_queue);
task3->priority = 2;
strcpy(task3->task_name, "Task 3");
list_add_tail(&task3->list, &task_queue);
pr_info("Queue initialized with 3 tasks.\n");
// 调度任务:按优先级从队列中移除任务
struct task *task;
struct list_head *pos, *q;
list_for_each_safe(pos, q, &task_queue) {
task = list_entry(pos, struct task, list);
pr_info("Processing task: %s with priority %d\n", task->task_name, task->priority);
list_del(pos);
kfree(task);
}
return 0;
}
static void __exit queue_example_exit(void)
{
pr_info("Task queue example exited.\n");
}
module_init(queue_example_init);
module_exit(queue_example_exit);
解释
- 该示例创建了一个任务队列,每个任务包含一个优先级和任务名称。
- 任务按照优先级从队列中出队并执行。
list_add_tail()
将任务加入队列尾部,list_for_each_safe()
遍历并处理队列中的任务。