阻塞型IO
当驱动程序无法立即满足请求,该如何响应?如当我们想要写入的时候,设备对应的缓冲区已满,或者是当我们想要读的时候当前缓冲区是空的。为了提高CPU的效率,我们的驱动程序应该阻塞等待该进程,将其置于休眠状态直到请求可继续。
休眠(sleep)对于进程来讲意味着什么?当一个进程被置入休眠时,他会被标记为一种特殊状态并从调度器的运行队列中移走,直到某些情况下修改了这个状态,进程才会在任意CPU上调度,也就是运行该进程。休眠中的进程会被搁置到一边,等待将来某个事件的发生。
在linux设备驱动中,将一个进程置于休眠状态很简单,但是怎么才能安全的将进程置于休眠状态呢?需要注意以下三点:
- 永远不要在原子上下文中进入休眠
原子上下文:在执行多个步骤时,不能有任何的并发访问,即将要休眠的程序不能拥有自旋锁、seqlock或者RCU锁时休眠。如果我们已经禁止了中断也不能休眠。但是在拥有信号量的时候休眠是合法的,但是必须仔细检查拥有信号量时休眠的代码。如果代码在拥有信号量时休眠,任何其他等待该信号量的线程也会休眠,因此任何拥有信号量而休眠的代码必须很短,并且还要确保拥有信号量并不会阻塞最终会唤醒我们的那个进程。 - 当进程从休眠状态被唤醒之后,应该重新检查进程所等待的条件(因为可能有多个多个进程都在等待这同一个条件)
- 我们要确保其他的代码会在其他地方唤醒我们,否则不能休眠。完成唤醒任务的代码必须能够找到我们的进程,之后才能唤醒休眠的进程。为了确保唤醒发生,需要整体理解我们的代码,并清楚地知道对每个休眠而言那些事件序列会结束休眠。
鉴于以上的三个点,我们需要维护一个称为等待队列的数据结构,在这个结构中包含了等待某个特定事件的所有进程。
在linux中,一个等待队列通过一个等待队列头来管理,等待队列头是一个类型为wait_queue_head_t的结构体,定义在<linux/wait.h>中,如下所示:
struct __wait_queue_head {
/**
* 由于等待队列可能由中断处理程序和内核函数修改,所以必须对双向链表进行保护,以免对其进行同时访问。
* 其同步是由lock自旋锁达到的。
*/
spinlock_t lock;
/**
* 等待进程链表的头。
*/
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
我们可以通过两种方法来初始化这个wait_queue_head_t结构体
- 静态方法
DECLARE_WAIT_QUEUE_HEAD(name);
/**
* 该宏定义一个新等待队列的头,它静态的声明了一个叫name的等待队列的头变量并对该变量的lock和task_list
* 字段进行初始化
*/
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = SPIN_LOCK_UNLOCKED, \
.task_list = { &(name).task_list, &(name).task_list } }
- 动态方法
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
/**
* 用来初始化动态分配的等待队列的头变量
*/
static inline void init_waitqueue_head(wait_queue_head_t *q)
{
q->lock = SPIN_LOCK_UNLOCKED;
INIT_LIST_HEAD(&q->task_list);
}
/**
* 创建一个新的链表。是新链表头的占位符,并且是一个哑元素。
* 同时初始化prev和next字段,让它们指向list_name变量本身。
*/
#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)
linux中的休眠函数
以下函数可以实现休眠并且可以不断检查条件,直到条件为真停止休眠。
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
queue:等待队列头
condition:任意一个布尔表达式,在条件为假的时候,进程会一直休眠(休眠过程中该条件可能会被求值多次,因此对该表达式求值不能带来副作用)
timeout:jiffies的个数,一个tick的时间长取决于内核的CONFIG_HZ的大小。比如CONFIG_HZ=200,则一个jiffies对应5ms时间。
第一个函数是不可以被中断的,而第二个是可以被信号中断的,第三和第四个函数在第一和第二个函数的基础上增加了timeout参数,如果给定时间到达时,这两个宏都会返回0值,而无论condition如何求值。
linux中的唤醒函数
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
wake_up会唤醒等待在给定queue上的所有进程,而wake_up_interruptible只会唤醒那些执行可中断休眠的进程。在实践中,约定做法是在使用wait_event时使用wake_up,而在使用wait_event_interruptible时使用wake_up_interruptible。
文件IO模型-阻塞(休眠)
阻塞:当进程在读取外部设备的资源(数据),资源没有准备好,进程就会进入休眠状态 linux应用中,大部分接口都是阻塞,例如scanf(); read(); write(); accept();
实现步骤
1) 将当前进程加入等待队列头中
add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
2) 将当前进程状态设置为TASK_INTERRUPTIBLE,可以响应信号。
set_current_state(TASK_INTERRUPTIBLE);
3) 让出调度-休眠
schedule(void);
一个更加智能的接口是(可以实现上面三个的功能)
wait_event_interrupt(wq, condition);
非阻塞:在读写的时候,如果没有数据,立即返回,并且返回一个出错码(用的比较少,因为比较消耗资源)
open(“/dev/key0”, O_RDWR | O_NOBLOCK);驱动中需要去区分,当前模式是阻塞还是非阻塞。
阻塞和非阻塞型操作
有时调用进程会告诉我们他不想阻塞,而不管其IO是否可以继续,显式的非阻塞IO由filp->f_flags中的O_NOBLOCK标志决定的。他可以在打开时指定。O_NDELAY标志是O_NONBLOCK的另一个名字,是为了保持和system V代码的兼容性而设计的。这个标志在默认情况下是被清除的,因为等待数据的进程一般只是休眠,在执行阻塞型操作的情况下,应该实现下面的动作:
1)如果一个进程调用了read但是还没有数据可读,此进程必须阻塞,数据到达时进程被唤醒,并把数据返回给调用者。即使数据数据少于count参数指定的数据也是这样
2)如果一个进程调用了write但缓冲区没有空间,此进程必须阻塞,而且必须休眠在与读取进程不同的等待队列上,当向硬件设备写入一些数据,从而腾出了部分输出缓冲区后,进程即将被唤醒,write调用成功,即使缓冲区中可能没有所要求的的count字节的空间而只是写入了部分数据,也是如此。
3)在驱动程序中实现输出缓冲区可以提高性能,这得益于减少了上下文的切换和用户级/内核级转换的次数。
如果指定了O_NOBLOCK 标志,read和write的行为就会有所不同,如果在数据没有就绪时调用read或是在缓冲区没有空间时调用write,则该调用简单地返回-EAGAIN。
非阻塞型操作会立即返回,使得应用程序可以查询数据,在处理非阻塞型文件时,应用程序调用stdio函数必须非常小心,因为很容易把一个非阻塞返回错误认为是EOF,所以必须检查errno
只有read,write和open文件操作会受到非阻塞标志的影响。