Linux设备驱动中的阻塞和非阻塞I/O

阻塞和非阻塞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 这篇文章挺全的,有时间可以好好钻研下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值