Skip to content

LV010-阻塞IO

来详细了解一下阻塞IO?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、阻塞IO

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。

image-20250122163424347

二、等待队列

1. 什么是等待队列?

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。

Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。 等待队列是内核实现阻塞和唤醒的内核机制, 以双循环链表为基础结构, 由链表头和链表项两部分组成, 分别表示等待队列头和等待队列元素, 如下图

image-20250122163519063

1.1 等待队列头

如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head_t 来表示:

c
struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;

1.2 等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。 等待队列项是等待队列元素,等待队列项使用结构体 wait_queue_entry_t 来表示:

c
/*
 * A single wait-queue entry structure:
 */
struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

typedef struct wait_queue_entry wait_queue_entry_t;

2. 相关API函数

2.1 定义并初始化等待队列头

2.1.1 DECLARE_WAIT_QUEUE_HEAD()

DECLARE_WAIT_QUEUE_HEAD() 定义如下:

c
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {					\
	.lock		= __SPIN_LOCK_UNLOCKED(name.lock),			\
	.head		= { &(name).head, &(name).head } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

参数 name 表示要定义的队列头名字。 通常以全局变量的方式定义,如下所示 :

c
DECLARE_WAIT_QUEUE_HEAD(head);
2.1.2 init_waitqueue_head()

init_waitqueue_head()定义如下:

c
#define init_waitqueue_head(wq_head)						\
	do {									\
		static struct lock_class_key __key;				\
										\
		__init_waitqueue_head((wq_head), #wq_head, &__key);		\
	} while (0)

参数 wq_head 表示需要初始化的队列头指针。 使用宏定义如下所示:

c
wait_queue_head_t head;     // 等待队列头
init_waitqueue_head(&head); // 初始化等待队列头指针

2.2 创建等待队列项

2.2.1 DECLARE_WAITQUEUE()

DECLARE_WAITQUEUE() 定义如下:

c
#define DECLARE_WAITQUEUE(name, tsk)						\
	struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

第一个参数 name 是等待队列项的名字, 第二个参数 tsk 表示此等待队列项属于哪个任务(进程) , 一般设置为 current。 在 Linux 内核中 current 相当于一个全局变量, 表示当前进程。创建等待队列项如下所示:

c
DECLARE_WAITQUEUE(wait, current); // 给当前正在运行的进程创建一个名为wait的等待队列项。
add_wait_queue(wq, &wait);        // 将 wait 这个等待队列项加到 wq 这个等待队列当中

2.3 添加/删除队列项

当设备没有准备就绪(如没有可读数据) 而需要进程阻塞的时候, 就需要将进程对应的等待队列项添加到前面创建的等待队列中, 只有添加到等待队列中以后进程才能进入休眠态。 当设备可以访问时(如有可读数据) , 再将进程对应的等待队列项从等待队列中移除即可。

2.3.1 add_wait_queue()

add_wait_queue() 函数定义如下:

c
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	__add_wait_queue(wq_head, wq_entry);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue);

该函数(通过等待队列头)向等待队列中添加队列项。

参数含义:

  • wq_head:表示等待队列项要加入等待队列的等待队列头 。
  • wq_entry:表示要加入的等待队列项。

返回值】无

2.3.2 remove_wait_queue()

remove_wait_queue() 函数定义如下:

c
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	unsigned long flags;

	spin_lock_irqsave(&wq_head->lock, flags);
	__remove_wait_queue(wq_head, wq_entry);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(remove_wait_queue);

该函数从等待队列头中移除一个等待队列项。

参数含义:

  • wq_head:要删除的等待队列项所处的等待队列头。
  • wq_entry:要删除的等待队列项。

返回值】无

2.4 等待唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数。

2.4.1 wake_up()

wake_up() 定义如下:

c
#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)

void __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
			int nr_exclusive, void *key)
{
	__wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}
EXPORT_SYMBOL(__wake_up);

这是一个宏,实际是执行 __wake_up() 函数,该函数会将这个等待队列头中的所有进程都唤醒。(wake_up() 函数可以唤醒处于 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE状态的进程)

参数含义:

  • x:表示要唤醒的等待队列的等待队列头。

返回值】无

2.4.2 wake_up_interruptible()

wake_up_interruptible() 定义如下:

c
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

void __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
			int nr_exclusive, void *key)
{
	__wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}
EXPORT_SYMBOL(__wake_up);

这是一个宏,实际是执行 __wake_up() 函数,只是传入的参数不同,它用于唤醒可中断的休眠进程 。wake_up_interruptible() 只能唤醒处于 TASK_UNINTERRUPTIBLE 状态的进程。

参数含义:

  • x:表示要唤醒的等待队列的等待队列头。

返回值】无

2.5 等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。这个就相当于是直接定义了一个等待队列项,并添加到了等待队列头,当标志变为真时就会

2.5.1 wait_event()

wait_event() 定义如下:

c
#define wait_event(wq_head, condition)						\
do {										\
	might_sleep();								\
	if (condition)								\
		break;								\
	__wait_event(wq_head, condition);					\
} while (0)

该宏会使调用进程进入不可中断的阻塞等待, 即让调用进程进入不可中断的睡眠状态, 在等待队列里面睡眠,直到 condition 变成真, 被内核唤醒。 此函数会将进程设置为 TASK_UNINTERRUPTIBLE 状态。

参数说明

  • wq_head:wait_queue_head_t 类型变量。
  • condition : 等待条件, 为假时才可以进入休眠。 如果 condition 为真, 则不会休眠
2.5.2 wait_event_timeout()

wait_event_timeout() 定义如下:

c
#define wait_event_timeout(wq_head, condition, timeout)				\
({										\
	long __ret = timeout;							\
	might_sleep();								\
	if (!___wait_cond_timeout(condition))					\
		__ret = __wait_event_timeout(wq_head, condition, timeout);	\
	__ret;									\
})

wait_event_timeout() 功能和 wait_event() 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition为假。为 1 的话表示 condition 为真,也就是条件满足了。

如果所给的睡眠时间为负数则立即返回,如果在睡眠期间被唤醒,且 condition 为真则返回剩余的睡眠时间,否则继续睡眠直到到达或 超过给定的睡眠时间,然后返回 0。

2.5.3 wait_event_interruptible()

wait_event_interruptible() 定义如下:

c
#define wait_event_interruptible(wq_head, condition)				\
({										\
	int __ret = 0;								\
	might_sleep();								\
	if (!(condition))							\
		__ret = __wait_event_interruptible(wq_head, condition);		\
	__ret;									\
})

可中断的阻塞等待, 让调用进程进入可中断的睡眠状态, 直到 condition 变成真被内核唤醒或信号打断唤醒。 此函数将进程设置为 TASK_INTERRUPTIBLE ,就是可以被信号打断。

2.5.4 wait_event_interruptible_timeout()

wait_event_interruptible_timeout() 定义如下:

c
#define wait_event_interruptible_timeout(wq_head, condition, timeout)		\
({										\
	long __ret = timeout;							\
	might_sleep();								\
	if (!___wait_cond_timeout(condition))					\
		__ret = __wait_event_interruptible_timeout(wq_head,		\
						condition, timeout);		\
	__ret;									\
})

此函数也将进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。如果在睡眠期间被信号打断则返回 ERESTARTSYS 错误码。

2.5.5 wait_event_interruptible_exclusive()

wait_event_interruptible_exclusive() 定义如下:

c
#define __wait_event_interruptible_exclusive(wq, condition)			\
	___wait_event(wq, condition, TASK_INTERRUPTIBLE, 1, 0,			\
		      schedule())

#define wait_event_interruptible_exclusive(wq, condition)			\
({										\
	int __ret = 0;								\
	might_sleep();								\
	if (!(condition))							\
		__ret = __wait_event_interruptible_exclusive(wq, condition);	\
	__ret;									\
})

wait_event_interruptible_exclusive() 宏同样和wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程。

2.5.3 总结

上面那些函数,调用的时要确认 condition 值是真还是假, 如果调用 condition 为真, 则不会休眠。

3. 等待队列使用方法

(1)初始化等待队列头, 并将条件置成假(condition=0)。

(2)在需要阻塞的地方调用 wait_event(), 使进程进入休眠状态。

(3)当条件满足时, 需要解除休眠, 先将条件(condition=1),然后调用 wake_up 函数唤醒等待队列中的休眠进程。

三、阻塞IO demo

1. demo源码

08_IO_model/02_io_block

2. 开发板验证

这里就直接看图吧:

image-20250124171009296

我是在app中循环读的,当执行没有数据写入的时候,会阻塞,当有数据写入的时候,唤醒进程,下一次循环的时候依然还是阻塞状态。相关的测试命令可以看这个脚本:08_IO_model/02_io_block/drivers_demo/module_load.sh

shell
./app_demo.out /dev/sdevchr 1 0 &
./app_demo.out /dev/sdevchr 2 0 abcdefg