内核的睡眠机制
进程通过睡眠机制释放CPU,以便CPU能够处理其他进程。进程进入睡眠状态的原因可能是等待自己需要的资源释放或者数据可用。
内核调度器管理要运行的任务列表,这被称为运行队列。睡眠进程会被从运行队列中移除,不再调度,除非睡眠进程被唤醒。进程一旦进入睡眠,就会被放进等待队列,也就会释放cpu的控制权,一定要确保有条件或者其他进程能够唤醒睡眠队列。在内核中,内核提供了一组函数和数据结构降低了实现睡眠机制的难度。

阻塞I/O:第一阶段进行数据的查询,若外设数据没有就绪,进程就会进入睡眠,放入等待队列,一旦外设数据准备就绪,就会在中断中唤醒对应的进程恢复执行。
非阻塞I/O:第一阶段进程外设数据的查询,若外设数据没有就绪,进程不会进入睡眠,会返回错误,表示数据没有准备好,进程便不再进行读写操作。若进程仍要继续读取数据,就会使用select或者poll、epoll函数进行轮询,若一旦发现外设数据准备就绪,进程就会完成read操作,进行第二阶段的读写操作。
阻塞IO和非阻塞IO都属于同步IO,与之相对的还有异步IO,两者的区别如下:
同步IO:在执行IO过程中,会导致进程被阻塞
异步IO:在执行IO过程中,不会导致进程被阻塞,进程发出异步IO请求后,无论内核中的数据是否准备好,API都会立即返回,应用程序不会阻塞,而是继续执行其他的逻辑代码。当数据在内核中准备好后,内核会将数据从内核空间拷贝到用户空间中进程指定的buffer中,拷贝完成后通过信号或者事件通知进程。应用程序在信号处理程序或者事件处理程序中获取IO操作的结果,对IO操作结果的数据进行处理。
两者主要的区别是:同步IO的IO访问,进程调用的API会查询外设数据是否就绪,并根据这个结果来决定是否进行外设数据的读取。但是异步IO并不依赖所调用的API的返回结果,由内核自动完成对外设数据状态的检查、将外设数据拷贝到进程buffer以及对进程发出通知。
等待队列
等待队列实际上用于处理被阻塞的I/O,以等待特定条件成立,并感知数据和资源的可用性。相关的数据结构定义在include/linux/wait.h中:
这等待队列的结构体中,我们也能看到一个熟悉的老朋友 struct list_head,说明内核也是使用链表来管理睡眠队列。想要进入睡眠的进程都会被放进链表中排队并进入睡眠状态,知道其想要的数据准备就绪,等待队列可以看做简单的进程链表和锁。
处理等待队列的API
(1)静态声明
DECLARE_WAIT_QUEUE_HEAD(name)
(2)动态声明
wait_queue_head_t my_wait_queue;
init_waitqueue_head(&my_wait_queue);
等待队列的动态声明和静态声明与链表的声明方式是类似的,都是内存空间开辟的区别。
(3)阻塞
(4)解除阻塞
wait_event_interruptible、wake_up_interruptible和wake_up_interruptible_all都是可中断的函数,可以被信号打断,因此应当检查他们的返回值,非零值意味着睡眠被某种信号中断,驱动应当返回ERESTARTSYS.
与之相对的是,内核中还有wait_event、wake_up和wake_up_all等待队列处理函数,他们是不会被信号中断的,以独占等待的方式处理队列中的进程,这些函数只能应用在关键任务中。
睡眠的进程只有调用了阻塞解除处理函数才会被唤醒,否则将一直睡眠。
下面是一个等待队列的例子(其中涉及到的运行队列在下一篇博文中学习)