一、前言
在第三章中,我们已经讨论了如何实现驱动程序的read和write方法。现在谈论另一种重要问题:如何驱动程序无法立即满足要求,该如何响应?调用程序通常不会关心此类问题,程序员只会简单调用read和write,然后等待必要的工作结束后返回调用,因此,在这种情况下,我们驱动程序应该(默认)阻塞该进程,将其置入休眠状态直至请求可继续。
我么介绍过的函数可以满足许多驱动程序的休眠请求。但是在某些情况下,我们需要Linux的等待队列机制有更深的理解。复杂的锁定以及性能需求会强制驱动程序用底层的函数实现休眠。
二、阻塞IO和高级休眠
1、休眠的简单介绍
当一个进程被置入休眠时,它会被标记为一种特殊状态并从调度器的运行队列中移走。休眠中的进程会搁置在一边,等待将来的某个事件发生。
对linux设备驱动程序来讲,让一个进程进入休眠状态很容易,需牢记两个原则:
(1)永远不要在原子上下文中进入休眠。
(2)当我们被唤醒时,我们永远无法知道休眠了多长时间,或者休眠期间都发生了什么事情,因此必须检查以确保我们等待的条件真正为真。
2、简单休眠
linux内核中最简单的休眠方式为wait_event的宏,在实现休眠的同时,它也检查休眠条件。如下:
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
在 Linux 中, 一个等待队列由一个wait_queue_head_t 结构体来管理,其定义在中:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
唤醒休眠:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);//驱动中大部分情况使用这个
3、阻塞和非阻塞操作
显示的非阻塞IO由filp->f_flags的O_NONBLOCK标志决定。这个标志在默认时要被清除,因为等待数据的进程一般是休眠。
只有read、open和write文件操作受非阻塞标志的影响。
4、高级休眠
(1)手动休眠
/* ①创建并初始化一个等待队列。通常由宏定义完成:*/
DEFINE_WAIT(my_wait);
/*其中name 是等待队列入口项的名字. 也可以用下面两个步骤来做:*/
wait_queue_t my_wait;
init_wait(&my_wait);
/*在实现休眠的循环前放置 DEFINE_WAIT 通常更容易实现*/
/* ②添加等待队列入口到队列,并设置进程状态:*/
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
/*其中,queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)。*/
/* ③在调用pre_pare_to_wait之后,若进程仍有必要等待,则调用 schedule*/
schedule();
/* ④一旦schedule 返回,就到了清理时间*/
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
/*⑤代码测试其状态,并判断是否需要重新等待*/
(2)独占等待
当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2 个重要的不同:
①当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。
②当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒.但内核仍然每次唤醒所有的非独占等待。
执行独占等待的进程每次只会被唤醒其中一个。但是,内核每次仍然会唤醒所有非独占等待进程。
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
三、实例
scull_pipe.h
#ifndef _SCULL_H_
#define _SCULL_H_
#include <linux/ioctl.h>
#define DEBUG
//#undef PDEBUG
#ifdef DEBUG
//# ifdef __KERNEL__
# define PDEBUG(fmt,args...) printk(KERN_DEBUG "scull:" fmt,## args)
//# else
//# define PDEBUG(fmt,args...) fprintf(stderr,fmt,## args)
//# endif
#else
# define PDEBUG(fmt,args...) /*no printf*/
#endif
#ifndef SCULL_MAJOR
#define SCULL_MAJOR 0
#endif
#ifndef SCULL_P_NR_DEVS
#define SCULL_P_NR_DEVS 4
#endif
#ifndef SCULL_P_BUFFER
#define SCULL_P_BUFFER 4000
#endif
#define TYPE(minor) ((minor)>>4 && 0xff)
#define NUM(minor) ((minor) & 0xf)
extern int scull_major; /*from main.c*/
extern int scull_p_buffer; /*for pipe.c*/
int scull_p_init(dev_t dev);
void scull_p_cleanup(void);
int scull_access_init(dev_t dev);
void scull_access_cleanup(void);
/* * Ioctl definitions
* */
/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* * S means "Set" through a ptr,
* * T means "Tell" directly with the argument value
* * G means "Get": reply by setting through a pointer
* * Q means "Query": response is on the return value
* * X means "eXchange": switch G and S atomically
* * H means "sHift": switch T and Q atomically
* */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_