阻塞和非阻塞I/O
阻塞和非阻塞I/O是设备访问的两种不同的模式,驱动可以灵活地支持这两种用户空间对设备的访问方式。
![进程的状态转化图](https://img-blog.csdn.net/20161108223852788)
阻塞操作:在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作条件为止。
被挂起的进程进入睡眠状态,被从调度器的运行队列移走。
阻塞的唤醒往往是由中断发起的,因为当某个硬件资源获得的时候往往会伴随着中断
非阻塞操作:在不能进行设备操作的时候,并不挂起,它要么放弃,要么不停的查询直到可以操作为止
//阻塞
char buf;
fd = open("/dev/ttyS1", O_ARDWR); //阻塞
...
res = read(fd, &buf,1); //当串口上有输入的时候才返回
//非阻塞
char buf;
fd = open("/dev/ttyS1", O_ARDWR | O_NONBLOCK); //非阻塞
...
while (read(fd, &buf, 1) != 1)
continue; //串口上无输入也返回,因此要循环尝试读取串口
在文件打开的时候可以指定是阻塞还是非阻塞方式,也可以在打开之后用ioctl和fcntl来
改变文件的属性阻塞或者非阻塞。
等待队列
Linux内核中可以用等待队列来实现阻塞进程的唤醒。等待队列很早就作为一个基本的功能单位出现在Linux内核中,信号量也是依赖等待队列来实现的
1. 定义等待队列头部
wait_queue_head_t my_queue;
wait_queue_head_t 是__wait_queue_head结构体的一个typedef
2.初始化等待队列的头部
init_waitqueue_head(&my_queue);
DECLARE_WAIT_QUEUE_HEAD宏可以作为定义并初始化等待队列的头部的快捷方式
DECLARE_WAIT_QUEUE_HEAD (name)
3. 定义等待队列元素
DECLARE_WAITQUEUE(name, tsk)
该宏用来定义并初始化一个名为name的等待队列元素
4. 添加和移除等待队列
add_wait_queue
remove_wait_queue
5. 等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
queue:作为等待队列头部的队列被唤醒
condition: 必须满足,否则继续阻塞
interruptible: 意味着可以被信号打断
timeout: 意味着阻塞等待的超时时间,在超时时间到了之后,无论condition满足与否,均返回
6. 唤醒队列
wake_up(wait_queue_head_t *queue)
wake_up_interruptible (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的进程。
7. 在等待队列上睡眠
sleep_on(wait_queue_head_t *q)
interruptible_sleep_on(wait_queue_head_t *q)
sleep_on函数的作用就是将目前进程的状态置为TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,并将其挂到等待队列头部所指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒
interruptible_sleep_on与sleep_on函数类似,作用是将目前进程的状态置为TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得或者进程收到信号,此时q队列指向的进程被唤醒
sleep_on应该与wake_up成对使用,interruptible_sleep_on应该与wake_up_interruptible成对使用
static ssize_t xxx_write()
{
...
DECLARE_WAITQUEUE(wait, current); //定义等待队列的元素
add_wait_queue(&xxx_wait, &wait); //添加元素到等待队列
//等待设备缓冲区可写
do {
avail = device_writable(...);
if(avail < 0) {
if (file->f_flags & O_NONBLOCK) { //非阻塞
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); //改变进程的状态
schedule(); //调度其他进程执行
if (signal_pending(current)) { //如果是因为信号量唤醒
ret = -ERESTRATSYS;
goto out;
}
}
}while (avail < 0);
//写设备缓冲区
device_write(...)
out:
remove_wait_queue(&xxx_wait, &wait); //将元素移出xxx_wait指引的队列
set_current_state(TASK_RUNNING); //设置进程状态为TASK_RUNNING
return ret;
}
这段函数对于理解Linux间进程状态切换特别重要,DECLARE_WAITQUEUE会申明一个等待队列的元素(wait),这个元素绑定了一个task_struct(current),然后将该元素插入到等待队列中。
然后看下设备缓冲区是否可写,如果不可写,且是非阻塞的,则直接返回EAGAIN;如果是非阻塞地,则先将进程的状态设为TASK_INTERRUPTIBLE,然后调用schedule,切换到别的进程执行。
如果是信号量唤醒则返回ERESTARTSYS
当写设备的资源得到之后,则写入设备缓冲区中,然后将之前的等待队列元素移除,并设置进程的状态为TASK_RUNNING。
轮询操作
非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞访问。
select和poll系统调用最终会调用设备驱动中的poll函数,所以两个系统调用的本质一样
应用程序中的轮询编程
最广泛应用的是select系统调用:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout)
其中readfds、writefds、exceptfds分别是select监视的读写和异常处理的文件描述符集合
numfds的值是需要监视的号码最高的fd的值加1。
readfds文件集中任何一个文件变得可读,select返回;同理writefds文件集中任何一个文件变得可写,select返回
如果对n个文件描述符进行监视,只要满足要求,select都是会返回的;如何没有任何文件描述符满足要求,则select进程阻塞且睡眠。由于调用select的时候,每个驱动的poll接口都会被调用。实际上调用select的进程被挂到了每个驱动的等待队列上,可以被任何一个驱动唤醒,
timeout参数是一个指向struct timeval类型的指针,可以使得select在等待timeout时间后若仍然没有文件描述符准备好则超时返回。
文件描述符集合的设置、清除、判断等操作
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) //判断文件描述符是否被置位(在select轮询实例中有提到)
poll系统调用
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
当多路复用的文件数量庞大、I/O流量频繁的时候,一般不适用select和poll。
通常都是使用epoll,它的最大好处就是不会随着fd的增加而降低效率,select会随着fd的增加性能
下降明显
int epoll_create(int size)
创建一个epoll的句柄,size用来告诉内核要监听多少的fd。注意:
当创建好epoll句柄后,它本身也会占用一个fd,所以在使用epoll之后需要调用close关闭
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
告诉内核要监听什么事件
epfd:是epoll_create的返回值
op: 表示动作包含如下的:
EPOLL_CRL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DAL:从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是告诉内核需要监听的事件类型
struct epoll_event{
__uint32_t events; //epoll events
epoll_data_t data; //User data variable
};
events可以是下面几个宏的或:
EPOLLIN:表示对应的文件描述符可以读
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
(这里表示的是有socket带外数据到来 这个是什么东西)
EPOLLERROR:表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述被挂断
EAPOLLET:将epoll设为边缘触发模式,这是相对于水平触发(默认的触发方式)来说的
水平触发:内核告诉用户一个fd是否就绪,之后用户可以对这个就绪的fd进程IO操作。但是
如果用户不做任何操作,该事件并不会丢失。
边缘触发:高速工作模式下,fd从未就绪变为就绪的时候,内核通过epoll告诉用户,
然后它会假设用户已经知道了fd已经就绪,并不会为那个fd发送更多的就绪通知
EPOLLONESHOT: 意味着一次性监听,当监听完这次事件之后,如果还需要继续监听这个fd的话,需要再次
把这个fd加入到epoll队列中
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
等待事件的产生
其中events参数是输出参数,用来从内核中得到事件的集合。
maxevents: 告诉内核本次最多接受多少事件,不能大于epoll_create的时候size
timeout: 超时时间
该函数返回值是需要处理的事件的数目,如果返回0,代表超时
驱动设备中的poll函数原型
unsigned int(*poll)(struct file *filp, struct poll_table* wait)
filp: file文件结构体指针
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_wait并不会引起阻塞。poll_wait是把当前进程添加到wait参数指定的等待列表(poll_table)中,
实际作用是让唤醒参数queue对应的等待队列可以唤醒因select而睡眠的进程
poll函数应该返回设备资源的可获取状态其中POLLIN意味着设备可以无阻塞的读,POLLOUT意味着设备可以无阻塞的写,函数模板如下
.... poll(struct file *filp, poll_table *wait)
{
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 //表示数据可获得
return mask;
}
轮询操作的实例:
select:
#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
void main(void)
{
int fd,num;
char rd_ch[BUFFER_LEN];
fd_set rfds, wfds; //读写文件描述符集
//以非阻塞方式打开/dev/globalfifo
fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
if (fd != -1)
{
//FIFO 清0
if (ioctl(fd, FIFO_CLEAR, 0) < 0)
printf("ioctl command failed\n");
while (1) {
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(fd, &rfds);
FD_SET(fd, &wfds);
select (fd + 1, &rfds, &wfds, NULL, NULL);
//数据可获得
if (FD_ISSET(fd, &rfds))
printf(...);
//数据可写入
if (FD_ISSET(fd, &wfds))
printf(...);
}
} else {
printf("Device open failed!");
}
}
epoll 监控globalfifo可读状态
#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
void main(void)
{
int fd;
fd = open("dev/globalfifo", O_RDONLY | O_NONBLOCK);
if (fd != -1) {
struct epoll_event ev_globalfifo;
int err;
int epfd;
if (ioctl(fd, FIFO_CLAEAR, 0) < 0)
printf(...)
epfd = epoll_create(1);
if (epfd < 0) {
perror(...);
return;
}
bzero(&ev_globalfifo, sizeof(struct epoll_event));
ev_globalfifo.events = EPOLLIN | EPOLLPRI;
err = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev_globalfifo);
if (err < 0) {
perrof(...);
return;
}
err = epoll_wait(epfd, &ev_globalfifo, 1, 15000);
if (err < 0) {
perror(...);
} else if (0 == err) {
printf(...)
} else {
//有事件监听到
}
err = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev_globalfifo);
if (err < 0)
perror("epoll_ctl()");
}else {
printf("Device open failed!\n");
}
}
总结:
poll_wait :
将设备结构体中的等待队列的头部(queue_head)添加到等待列表中(poll_table),
意味着因调用selecte而阻塞的进程可以被等待队列唤醒
在设备驱动中,阻塞IO一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序
使用非阻塞IO的应用程序,也可以借助轮询函数select poll 或者epoll接口来调用设备驱动提供poll函数。
可访问或者超时
PS:http://www.cnblogs.com/Anker/p/3265058.html 这篇文章挺全的,有时间可以好好钻研下。