Linux驱动设计——阻塞和同步
阻塞和非阻塞是设备访问的两种基本方式,阻塞和非阻塞驱动程序使用时,经常会用到等待队列。
阻塞和非阻塞
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。
对于同步调用来说,许多时候当前线程还是激活的,只是逻辑上当前函数没有返回而已。
阻塞的进程被唤醒的最大可能是通过中断,因为硬件资源获得的同时往往伴随着一个中断。
分别使用阻塞和非阻塞的方式读取串口的一个字符代码比较
/*阻塞方式*/ char buf; fd = open("/dev/ttyS1", O_RDWR); ... res = read(fd,&buf,1); /* 当串口上有输入时才返回 */ if(res==1) printf("%c\n", buf); /*非阻塞方式*/ char buf; fd = open("/dev/ttyS1", O_RDWR| O_NONBLOCK); //区别在这里 ... while(read(fd,&buf,1)!=1) continue; /* 串口上无输入也返回,所以要循环尝试读取串口 */ printf("%c\n", buf);
非阻塞方式即使没有数据也会返回,所以要读取串口应该使用循环,一次次的访问串口。
完整实例程序分析
等待队列(可以控制进程的休眠与唤醒)
等待队列机制使等待的进程暂时睡眠,当等待的信号到来时,便唤醒等待队列中进程继续执行。
等待队列的基本数据结构是一个双向链表,这个链表可以存储睡眠的进程。等待队列也与进程调度机制紧密结合,能够实现内核中异步事件通知机制。
等待队列在中断处理、进程同步、定时等场合有重要的用处。
等待队列的实现
等待队列通过等待队列头来管理
每个等待任务都被抽象成一个wait_queue,并且挂载到wait_queue_head上。
等待队列的使用
1. 定义和初始化等待队列头
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
//或者使用宏进行初始化
DECLARE_WAIT_QUEUE_HEAD (name)
2. 定义等待队列
DECLARE_WAITQUEUE(name, tsk) //此宏用于定义并初始化一个名为name的等待队列
3. 添加和移除等待队列
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
//add_wait_queue()用于将等待队列wait 添加到等待队列头q 指向的等待队列链表中 void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
//remove_wait_queue()用于将等待队列wait 从附属的等待队列头q 指向的等待队列链表中移除。
4. 等待事件
wait_event(queue, condition) //不可以被信号打断;queue,等待队列头(值传递);
//condition:休眠前后都要对该表达式求值,条件为假继续睡眠,为真被唤醒 wait_event_interruptible(queue, condition) //可以被信号打断 wait_event_timeout(queue, condition, timeout)
//加上_timeout 后的宏意味着阻塞等待的超时时间,以jiffy 为单位,在第3 个参数的timeout到达时,不论condition 是否满足,均返回。
wait_event_interruptible_timeout(queue, condition, timeout)
等待第1 个参数queue 作为等待队列头的等待队列被唤醒,而且第2 个参数condition 必须满足,否则继续阻塞。
5. 唤醒等待队列
void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue);
void wake_up_nr(wait_queue_head_t *queue, int nr); //唤醒nr个独占等待进程,nr=0则唤醒所有的独占等待进程
void wake_up_interrupible_nr(wait_queue_head_t *queue, int nr);
void wake_up_interruptible_sync(wait_queue_head_t *queue); //被唤醒后强制调度重新执行原休眠进程,前面几个实际上是没有让进程立即执行
唤醒以queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。
wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE 的进程。
进程的睡眠和唤醒过程中可能存在竞争,如:
A、B进程都等待在等待队列上,且在监视同一个唤醒条件flag,此时如果C进程调用唤醒函数wake_up_interruptible,A被唤醒,检查条件flag!=0成立,此时调度到B进程,B进程也检查到flag!=0成立,这样就会一个事件唤醒两个进程,产生竞态。
解决方法:使用原子操作
6. 在等待队列上睡眠(无条件休眠,老版本,建议不使用)[与wait_event的功能相同]
sleep_on(wait_queue_head_t *q ); //将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒。
interruptible_sleep_on(wait_queue_head_t *q ); //将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒或者进程收到信号。
sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。
在内核中使用set_current_state()函数或_ _add_current_state()函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句也是可行的。通常而言,set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于_ _add_current_state()。
因此,在许多设备驱动中,并不调用sleep_on()或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换:
1 static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, 2 loff_t *ppos) 3 { 4 ... 5 DECLARE_WAITQUEUE(wait, current); /* 定义等待队列 */ //wait:等待队列名, 6 add_wait_queue(&xxx_wait, &wait); /* 添加等待队列 */ 7 8 ret = count; 9 /* 等待设备缓冲区可写 */ 10 do { 11 avail = device_writable(...); 12 if (avail < 0) 13 _ _set_current_state(TASK_INTERRUPTIBLE);/* 改变进程状态 */ 14 15 if (avail < 0) { 16 if (file->f_flags &O_NONBLOCK) {/* 非阻塞 */ 17 if (!ret) 18 ret = - EAGAIN; 19 goto out; 20 } 21 schedule(); /* 调度其他进程执行 22 if (signal_pending(current)) {/* 如果是因为信号唤醒 */ 23 if (!ret) 24 ret = - ERESTARTSYS; 25 goto out; 26 } 27 } 28 }while (avail < 0); 29 30 /* 写设备缓冲区 */ 31 device_write(...) 32 out: 33 remove_wait_queue(&xxx_wait, &wait);/* 将等待队列移出等待队列头 */ 34 set_current_state(TASK_RUNNING);/*设置进程状态为TASK_RUNNING*/ 35 return ret; 36 }
几个要点如下:
① 如果是非阻塞访问(O_NONBLOCK 被设置),设备忙时,直接返回“-EAGAIN”。
② 对于阻塞访问,会进行状态切换并显式通过“schedule()”调度其他进程执行;
③ 醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠,因此唤醒它的有可能是信号,因此,我们首先通过“signal_pending(current)”了解是不是信号唤醒的,如果是,立即返回“- ERESTARTSYS”。
设置进程休眠的内部细节
1. 分配并初始化一个wait_queue_t结构
包括休眠进程的信息,以及期望被唤醒的相关细节
2. 设置进程的状态,将其标记为休眠状态
TASK_INTERRUPTIBLE (wait_event()后的状态)
TASK_UNINTERRUPTIBLE
void set_current_state(int new_state); //手动设置进程状态
current->state=TASK_INTERRUPTIBLE; //老版本内核设置方式
3. 让出处理器
if(!condition)
schedule(); //执行调度。让出处理器
阻塞方式
在阻塞驱动程序中,read实现方式:如果进程调用read,但是设备没有数据或数据不足,进程阻塞。当新数据到达,唤醒被阻塞进程。
write实现类似。
阻塞IO实例
独占等待
与普通休眠的不同
等待队列入口设置了 WQ_FLAG_EXCLUSIVE标志时,则会被添加到等待队列的尾部。而没有这个标志的入口会被添加到等待队列的头部。
在某个等待队列上调用wake_up时,他会唤醒第一个具有WQ_FLAG_EXCLUSIVE标志的进程之后停止唤醒其他独占进程。
使进程进入独占等待函数(有自己特定的函数)
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
注意:使用wait_event的变种函数都无法使用独占等待。
支持阻塞操作的globalfifo设备驱动
使用等待队列实现同步机制
To be continue...