Linux内核数据结构2

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; \
})

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

五、出队列(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() 遍历并处理队列中的任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值