阻塞型IO
对于 Linux设备驱动程序第三章——字符驱动scull 中的 read
和 write
,我们并没有考虑他是否能理解响应用户空间的调用请求。
比如,调用 read
时,此时并没有数据;调用 write
时,此时空间已满,已经没办法再写入新的数据。对于这些情况,我们的驱动程序应该默认阻塞该调用进程,将其置于 休眠 的状态,直到满足相应的条件再被唤醒继续执行。
1.休眠的简单介绍
当一个进程进入休眠之后,它会被标记为一种特殊的状态,并且从调度器的运行队列中移除。直到某些情况下,这个状态被改变,这个进程才能继续被CPU调度,也就是运行。
进程安全休眠方式的原则:
1.不能在原子上下文中进入休眠。
- 驱动程序不能在拥有自旋锁,seqlock,RCU锁时休眠;
- 如果禁止了中断,也不能休眠;
- 在拥有信号量的时候休眠是合法;
2.不能假定进程被唤醒后的状态,对于被唤醒后的进程必须检查以确保等待的条件确实是真实的。
为了唤醒休眠的进程,Linux内核提供了一种 等待队列,等待某一个条件的进程都可以被加入到这个等待队列中,直到这个指定的条件为真,该队列中休眠的进程才会被唤醒。
等待队列 wait_queue_head_t
被定义在 <linux/wait.h>
头文件中,其定义和初始化方法如下:
//静态定义初始化
DECLARE_WAIT_QUEUE_HEAD(name)
//动态定义初始化
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
2.简单休眠
当进程休眠时,它会等待某个条件为真而被唤醒。前面提到过,当一个进程被唤醒时,它必须检查所等待的条件的确为真。
Linux内核提供的实现进程休眠的方式是 wait_event
及其类似的几个宏。它在实现休眠的同时,也在检查等待的条件。其API如下:
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
queue
是等待队列的头,通过 值传递 传入。
condition
是等待的条件,这个条件可能被多次计算,所以要求其是不能有 副作用。
wait_event
和 wait_event_interruptible
区别就是,后者收到一个中断信号之后能够直接返回,驱动程序一般返回 ERESTARTSYS
这个值。而前者不会处理中断信号。
加了 timeout
的版本就是只会等待指定的 timeout
时间,然后就返回。
用来唤醒休眠进程的函数是 wake_up
,其相关的API如下:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
一般约定:
- 使用
wait_event
休眠,就使用wake_up
唤醒; - 使用
wait_event_interruptible
,就使用wake_up_interruptible
唤醒;
3.阻塞和非阻塞型操作
显式的非阻塞IO由 filp->f_flags
中的 O_NONBLOCK
标志决定。这个标志在 <linux/fcntl.h>
头文件中定义。
在默认的情况下是阻塞IO。也就是当调用 read
或 write
时,如果没有数据或空间已满,那么调用的进程就会阻塞,直到能够成功读取或写入数据。
如果指定了 O_NONBLOCK
标志,read
和 write
的行为就会有所不同。如果在没有数据时调用 read
或 没有空间时调用 write
,此时并不会阻塞,而是直接返回一个 -EAGAIN
值。
所以在用户态指定了 O_NOBLOCK
标志之后,对于返回的结果,要始终检查对应的 errno
,查看当前到底是什么情况。
对于会阻塞很长时间的 open
而言,O_NONBLOCK
标志对其也是有意义的。
需要注意的是,O_NONBLOCK
标志只对 read
,write
,open
等方法有效。
4.一个阻塞IO示例
这个例子来自 scullpipe
驱动程序,它是 scull
实现类管道设备的特殊形式。
该设备驱动程序使用了一个包含 两个等待队列 和 一个缓冲区 的设备结构。其结构如下:
static ssize_t scull_p_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
struct scull_pipe *dev = filp->private_data;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
//此时数据为空,没有数据可读
while(dev->rp == dev->wp){
up(&dev->sem); //释放信号量
//设置了非阻塞IO,此时又无数据可读,直接返回
if(filp->f_flags & O_NONBLOCK)
return -EAGAIN;
PDEBUG("[%s] reading: going to sleep\n", current->comm);
//休眠当前进程,直到有数据可以读再被唤醒
if(wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
return -ERESTARTSYS;
//此时进程已经被唤醒了,准备读取数据,但是首先需要先获取到信号量
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
//此时有数据可读
if(dev->wp > dev->rp)
count = min(count, (size_t)(dev->wp - dev->rp));
else
count = min(count, (size_t)(dev->end - dev->rp));
//把数据拷贝到用户态指针所指向的内存
if(copy_to_user(buf, dev->rp, count)){
up(&dev->sem);
return -EFAULT;
}
dev->rp += count;
if(dev->rp == dev->end)
dev->rp = dev->start;
up(&dev->sem);
//唤醒write进程,此时已经有多余的空间可以写入数据了
wake_up_interruptible(&dev->outq);
PDEBUG("[%s] did read %li bytes\n",current->comm, (long)count);
return count;
}
测试非阻塞IO
test.c
:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int set_socket_nonblocking(int fd) {
// 获取当前文件描述符的标志
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}
// 设置为非阻塞模式
flags |= O_NONBLOCK; // 将 O_NONBLOCK 标志添加到现有标志中
// 设置文件描述符的新标志
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
return -1;
}
return 0;
}
int main(void)
{
int fd;
fd = open("/dev/scull_pipe0", O_RDWR);
if (fd < 0) {
printf("failed to open scull_pipe0: %s!\n", strerror(errno));
exit(-1);
}
//设置非阻塞IO
if(set_socket_nonblocking(fd)){
exit(-1);
}
int buf[128] = {0};
int ret = 0;
/*
此时 scull_pipe0 设备里还没有数据,但是套接字设置了 O_NONBLOCK
所以返回值 ret = -1, errno = EAGAIN, 也就是会打印下面这段话
[read] there is no data to read!
*/
ret = read(fd, buf, sizeof(buf));
if(ret < 0){
if(errno == EAGAIN){
printf("[read] there is no data to read!\n");
}
}
close(fd);
return 0;
}
测试结果:
测试阻塞IO
如果把 test.c
中的这一句 set_socket_nonblocking(fd)
注释掉,此时 scull_pipe0
中并没有数据,所以 read
函数会一直阻塞,直到有数据,或者接收一个中断信号再退出。
test.c
:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int set_socket_nonblocking(int fd) {
// 获取当前文件描述符的标志
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}
// 设置为非阻塞模式
flags |= O_NONBLOCK; // 将 O_NONBLOCK 标志添加到现有标志中
// 设置文件描述符的新标志
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
return -1;
}
return 0;
}
int main(void)
{
int fd;
fd = open("/dev/scull_pipe0", O_RDWR);
if (fd < 0) {
printf("failed to open scull_pipe0: %s!\n", strerror(errno));
exit(-1);
}
//设置非阻塞IO
// if(set_socket_nonblocking(fd)){
// exit(-1);
// }
int buf[128] = {0};
int ret = 0;
/*
此时 scull_pipe0 设备里还没有数据,所以此时调用read会被阻塞
下方之后打印 read before! 这一句
*/
printf("read before!\n");
ret = read(fd, buf, sizeof(buf));
if(ret < 0){
if(errno == EAGAIN){
printf("[read] there is no data to read!\n");
}
}
printf("read end!\n");
close(fd);
return 0;
}
结果:
如图,这个进程是出于休眠的状态,所以执行 ctrl + c
可以发出一个中断信号,驱动程序 read
收到这个中断信号就会返回,接着用户态这个 app
进程就执行完毕然后退出。
5.高级休眠
wait_queue_head_t
实际是是由 一个自旋锁 和 一个链表 组成。
- 链表中保存的是一个等待队列的入口,该入口类型为
wait_queue_t
。这个结构中包含了休眠进程的相关信息以及期望被唤醒的相关细节信息。
进程休眠的大致步骤:
1.分配并初始化一个 wait_queue_t
结构,然后将其将入到对应的等待队列中。完成这项工作后,无论是谁唤醒该进程,都能找到正确的进程并唤醒。
2.设置进程的状态,将其设置为休眠状态。有两个状态说明进程处于休眠状态:TASK_INTERRUPTIBLE
和 TASK_UNINTERRUPTIBLE
。
3.检查休眠等待的条件,然后放弃处理器,调用 shedule
让出CPU。
手动休眠
Linux内核提供了一种手动休眠的方式,其步骤如下:
1.声明并初始化一个等待队列的入口
//静态
DEFINE_WAIT(my_wait);
//动态
wait_queue_t my_wait;
init_wait(&my_wait);
2.将等待队列入口 my_wait
添加到等待队列中,并设置进程的状态。这两个任务都由下面的函数实现:
void prepare_to_wait(wait_queue_head_t *queue,
wait_queue_t *wait,
int state);
queue
是等待队列的头;wait
是进程的入口;state
是进程的新状态。这个值应该是TASK_INTERRUPTIBLE
或TASK_UNINTERRUPTIBLE
。
3.调用 shedule
让出CPU。
4.调用 finish_wait
让当前进程退出等待状态,重新进入调度状态。其函数如下:
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
示例代码
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{
while (spacefree(dev) == 0) { //当前空间已满
DEFINE_WAIT(wait);
up(&dev->sem);
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);
if (spacefree(dev) == 0)
schedule(); //让出CPU
finish_wait(&dev->outq, &wait); //结束等待状态,重新进入调度状态
if (signal_pending(current))
return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
return 0;
}
//返回空闲空间大小
static int spacefree(struct scull_pipe *dev)
{
if (dev->rp == dev->wp)
return dev->buffersize - 1;
return ((dev->rp + dev->buffersize - dev->wp) % dev->buffersize) - 1;
}
static ssize_t scull_p_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_pipe *dev = filp->private_data;
int result;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
result = scull_getwritespace(dev, filp);
if (result)
return result;
count = min(count, (size_t)spacefree(dev));
if (dev->wp >= dev->rp)
count = min(count, (size_t)(dev->end - dev->wp));
else
count = min(count, (size_t)(dev->rp - dev->wp - 1));
PDEBUG("Going to accept %li bytes to %p from %p\n", (long)count, dev->wp, buf);
if (copy_from_user(dev->wp, buf, count)) {
up (&dev->sem);
return -EFAULT;
}
dev->wp += count;
if (dev->wp == dev->end)
dev->wp = dev->buffer;
up(&dev->sem);
wake_up_interruptible(&dev->inq);
if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
PDEBUG("\"%s\" did write %li bytes\n",current->comm, (long)count);
return count;
}