什么是I/O?
I/O是指input和output,也就是数据的读取(接收)或写入(发送)操作。一个用户进程完成一次I/O操作需要经历两个阶段:
- 用户空间<==>内核空间
- 内核空间<==>设备缓冲区
什么是阻塞I/O和非阻塞I/O?
阻塞I/O:进程发起I/O系统调用后,若不能获得资源,则进程会被阻塞挂起,被挂起后将进入休眠状态(放弃CPU),直到资源准备好了,进程才会被唤醒。
非阻塞I/O:进程发起I/O系统调用后,若不能立即获得资源,则进程立即返回一个错误(或进程不断地查询),而不会被阻塞挂起。
二者的区别可以看用户程序的调用是否立即返回!
注意:因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的挂了。在实际使用中,唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
阻塞I/O的实现——等待队列
在Linux 驱动程序中,可以使用等待队列(wait queue)来实现进程的休眠和唤醒。阻塞I/O通常是由等待队列来实现的。
等待队列数据结构
等待队列涉及到两个比较重要的数据结构:__wait_queue_head和__wait_queue。
__wait_queue_head描述了等待队列的链表头,其包含了一个原子锁和一个链表,结构体定义如下:
struct __wait_queue_head
{
spinlock_t lock; /* 保护等待队列的原子锁 */
struct list_head task_list; /* 等待队列 */
};
typedef struct __wait_queue_head wait_queue_head_t;
__wait_queue是抽象的描述了一个等待任务,每个等待任务都会抽象成一个wait_queue并挂在wait_queue_head上,__wait_queue结构定义如下:
struct __wait_queue
{
unsigned int flags;
void *private; /* 通常指向当前任务控制块 */
/* 任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function */
wait_queue_func_t func; //唤醒阻塞任务的函数 ,决定了唤醒的方式
struct list_head task_list; /* 挂入wait_queue_head链表 */
};
Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务(任务被唤醒后,会先检查等待条件),唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从等待队列中删除,把任务加入运行队列。被唤醒的任务会先检查等待条件是否为真,若已经为真则不再休眠,若还是为假,则继续调度休眠加入等待队列,并从运行队列删除。
等待队列的使用
1.定义等待队列头
wait_queue_head_t my_queue;
2.初始化等待队列
init_waitqueue_head(&my_queue);
3.加入等待事件
(1)wait_event()宏:进程会被置为TASK_UNINTERRUPTIBLE进入睡眠,直到condition变量变为真。每次进程被唤醒的时候都会检查condition的值。
(2)wait_event_interruptible()函数:和wait_event()的区别是调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE状态.在每次被唤醒的时候,首先检查condition是否为真,如果为真则返回,否则检查如果进程是否是被信号唤醒,会返回-ERESTARTSYS错误码。如果是condition为真,则返回0。
(3)wait_event_timeout()宏: 也与wait_event()类似。不过如果所给的睡眠时间为负数则立即返回。如果在睡眠期间被唤醒,且condition为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0。
(4)wait_event_interruptible_timeout()宏:与wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回ERESTARTSYS错误码。
(5) wait_event_interruptible_exclusive()宏:同样和wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程。
4.唤醒等待事件
(1)wake_up()函数:可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERUPTIBLE状态的进程。与wait_event/wait_event_timeout成对使用。
(2)wake_up_interruptible()函数:和wake_up()唯一的区别是它只能唤醒TASK_INTERRUPTIBLE状态的进程。与wait_event_interruptible/wait_event_interruptible_timeout/ wait_event_interruptible_exclusive成对使用。
一般使用:在I/O读的时候,若没有资源,则进入休眠。当资源就绪后,会在另一个任务(常常是中断)中唤醒休眠的读进程。
疑问:如果同时唤醒多个进程,那么谁先获得资源呢?
比如,写两个进程分别读同一个KFIFO,但这个KFIFO为空,那么这两个进程都将会休眠,再使用另一个进程去写KFIFO,将在写完后唤醒两个读进程,此时哪个进程先读出来呢?
经测试,两个进程都可能读出来!!!也就是说,有时候是进程1读出来并打印,而进程2继续休眠;有时候是进程2读出来并打印,而进程1继续休眠。
原因:两进程可能不是运行在同一CPU上!
将两个读进程绑在同一个核心上再测试,则哪个进程先读,便先获得资源!
非阻塞I/O的实现
非阻塞I/O的实现十分简单,只需要在资源未准备好的时候判断open()函数中flags标志即可。
标志有:
O_RDONLY:只读打开。
O_WRONLY:只写打开。
O_RDWR:读写打开。
O_CREAT:若文件不存在,则创建它。
O_NONBLOCK:设置文件访问为非阻塞模式。
比如:open(/dev/xxx, O_RDWR | O_NONBLOCK)
If(kfifo_is_empty(&my_kfifo))
{
If(file->f_flags & O_NONBLOCK)
{
return -1;
}
}