一、阻塞与非阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回。
非阻塞指不能立刻得到结果之前,该函数不会阻塞当前进程,而会立刻返回。
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用的函数也可以进入阻塞调用。函数select()就是这样一个例子。
二、等待队列
在linux设备驱动程序中,阻塞进程可以使用等待队列来实现。
在内核中,等待队列是有很多用处的,尤其是在中断处理,进程同步,定时等场合,可以使用等待队列实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问。
1、等待队列的实现:
在linux中,等待队列的结构如下:
struct __wait_queue_head {
spinlock_t lock; //自旋锁,用来对task_list链表起保护作用,实现了对等待队列的互斥访问
struct list_head task_list; //用来存放等待的进程
};
typedef struct __wait_queue_head wait_queue_head_t;
2、等待队列的使用
(1)定义和初始化等待队列:
wait_queue_head_t wait;//定义等待队列
init_waitqueue_head(&wait);//初始化等待队列
定义并初始化等待队列:
#define DECLARE_WAIT_QUEUE_HEAD(name) wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
(2)添加或移除等待队列:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);//将等待队列元素wait添加到等待队列头q所指向的等待队列链表中。
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
(3)等待事件:
wait_event(wq, condition);//在等待队列中睡眠直到condition为真。
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition) ;
wait_event_interruptible_timeout(wq, condition, timeout) ;
/*
* queue:作为等待队列头的等待队列被唤醒
* conditon:必须满足,否则阻塞
* timeout和conditon相比,有更高优先级
*/
(4)睡眠:
sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
/*
sleep_on作用是把目前进程的状态置成TASK_UNINTERRUPTIBLE,直到资源可用,q引导的等待队列被唤醒。
interruptible_sleep_on作用是一样的, 只不过它把进程状态置为TASK_INTERRUPTIBLE
*/
(5)唤醒等待队列:
//可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程;
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
//只能唤醒处于TASK_INTERRUPTIBLE状态的进程
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
三、操作系统中睡眠、阻塞、挂起的区别形象解释
首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。
使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。
线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能怪雇工,肯定你这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。
四、阻塞与非阻塞操作
阻塞是指没有获得资源则挂起进程,直到获得资源为止。被挂起的进程进入休眠状态,被调度器的运行队列移走,直到等待条件被满足。
非阻塞是不能进行设备操作时不挂起,或放弃,或反复查询,直到可以进行操作为止。
驱动程序常需要这种能力:当应用程序进行read(),write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应该在设备驱动程序的xxx_read(),xxx_write()等操作中将进程阻直到资源可以获取,以后,应用程序的read(),write()等调用返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read(),xxx_write()等操作应立即返回,read(),write()等系统调用也随即被访问。
阻塞不是低效率,如果设备驱动不阻塞, 用户想获取设备资源只能不断查询,消耗CPU资源,阻塞访问时,不能获取资源的进程将进入休眠,将CPU资源让给其他资源。
阻塞的进程会进入休眠状态,因此,必须确保有一个地方能唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
eg1:阻塞地读取串口一个字符
- 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);//串口上无输入也返回,所以要循环尝试读取串口
- printf("%c\n",buf);
在linux驱动程序中,可使用等待队列(wait queue)来实现阻塞进程的唤醒,以队列为基础数据结构,与进程调度机制紧密结合,用于实现内核中的异步事件通知机制,也可用于同步对系统资源的访问。
定义"等待队列头"
- wait_queue_head_t my_queue;
- init_waitqueue_head(&my_queue);
- DECLARE_WAIT_QUEUE_HEAD (name);
- DECLARE_WAITQUEUE(name,tsk);定义并初始化一个名为name的等待队列
- void fastcall add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
- void fastcall remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
- wait_event(queue,condition);等待以queue为等待队列头等待队列被唤醒,condition必须满足,否则阻塞
- wait_event_interruptible(queue,condition);可被信号打断
- wait_event_timeout(queue,condition,timeout);阻塞等待的超时时间,时间到了,不论condition是否满足,都要返回
- wait_event_interruptible_timeout(queue,condition,timeout)
- void wake_up(wait_queue_head_t *queue);
- void wake_up_interruptible(wait_queue_head_t *queue);
wake_up()与wake_event()或者wait_event_timeout成对使用,
wake_up_intteruptible()与wait_event_intteruptible()或者wait_event_intteruptible_timeout()成对使用。
六,轮询操作
在用户程序中,select()和poll()也是与设备阻塞与非阻塞访问的,使用非阻塞I/O的应用程序常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。
select()和poll()系统调用最终会引发设备驱动中的poll()函数被执行。
应用程序中的轮询编程
- int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,
- struct timeval *timeout);
numfds是需要检查的号码最高的文件描述符加1,timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。
- struct timeval
- {
- int tv_sec;
- int tv_usec;
- };
- FD_ZERO(fd_set *set);//清除一个文件描述符
- FD_SET(int fd,fd_set *set);//将一个文件描述符加入文件描述符集中
- FD_CLR(int fd,fd_set *set);//将一个文件描述符从文件描述符集中清除
- FD_ISSET(int fd,fd_set *set);//判断文件描述符是否被置位
设备驱动中poll()函数:
- unsigned int (*poll)(struct file *filp, struct poll_table *wait);
1.对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table;
2.返回表示是否能对设备进行无阻塞读,写访问的掩码。
用于向poll_table注册等待队列的poll_wait()函数的原型
- void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait);
驱动程序poll()返回设备资源的可获取状态,POLLIN(定义为0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为0x0004)意味着可以无阻塞的写。
eg3:poll()函数典型模板
- static unsigned int xxx_poll(struct file *filp,poll_table *wait)
- {
- unsigned int mask=0;
- struct xxx_dev *dev=filp->private_data;
- ...
- poll_wait(filp,&dev->r_wait,wait);
- poll_wait(filp,&dev->w_wait,wait);
- if(...)//可读
- {
- mask|= POLLIN |POLLRDNORM;//数据可获得
- }
- if(...)//可写
- {
- mask|= POLLOUT|POLLWRNORM;//数据可写入
- }
- ...
- return mask;
- }
- eg4:globalfifo设备驱动的poll()函数
- static unsigned int globalfifo_poll(struct file *filp,poll_table *wait)
- {
- unsigned int mask=0;
- struct globalfifo_dev *dev=filp->private_data;
- down(&dev->sem);
- poll_wait(filp,&dev->r_wait,wait);
- poll_wait(filp,&dev->w_wait,wait);
- //fifo非空
- if(dev->current_len!=0)
- {
- mask|=POLLIN |POLLRDNORM;//标示数据可获得
- }
- //fifo非满
- if(dev->current_len!=GLOBALFIFO_SIZE)
- {
- mask|=POLLOUT |POLLWRNORM;
- }
- up(&dev->sem);
- return mask;
- }