Linux驱动开发阻塞和非阻塞IO

Posted XXX_UUU_XXX

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux驱动开发阻塞和非阻塞IO相关的知识,希望对你有一定的参考价值。

  • IO:应用程序对设备驱动进行输入/输出操作。
  • 阻塞IO:当资源不用时,将应用程序对应的线程挂起,当资源可用时,唤醒任务。
  • 非阻塞IO:当资源不可用时,应用程序轮询等待放弃。具有超时处理机制

设备驱动文件默认读取方式:阻塞式

应用程序阻塞读取数据

int fd;
int data = 0;

fd = open("/dev/xxx", O_RDWR);
ret = read(fd, &data, sizeof(data));

应用程序阻塞读取数据,添加参数O_NONBLOCK

int fd;
int data = 0;

fd = open("/dev/xxx", O_RDWR | O_NONBLOCK);
ret = read(fd, &data, sizeof(data));

阻塞—等待队列

当设备文件不可操作时,进程进入休眠状态,将CPU资源让出来。在设备文件可以操作时,唤醒进程,一般是在中断函数中唤醒

Linux内核使用等待队列实现阻塞进程的唤醒。

等待队列头

等待队列头是等待队列的头部。

使用结构体wait_queue_head_t定义一个等待队列头,然后用init_waitqueue_head函数初始化等待队列头。或使用宏DECLARE_WAIT_QUEUE_HEAD一次性定义初始化等待队列头。

等待队列项

每个访问设备的进程都是一个队列项,当设备不可用的时候,将这些进程对应的等待队列项添加到等待队列。

使用结构体wait_queue_t表示等待队列项,使用宏DECLARE_WAITQUEUE定义并初始化一个等待队列项。

DECLARE_WAITQUEUE(name, tsk)
  • name:等待队列项名字。
  • tsk:等待队列属于的进程,一般设置为currentLinux内核中current是一个全局变量,表示当前进程

将等待队列项添加到等待队列头

当设备不可访问时,将进程对应的等待队列项添加到之前创建的等待队列头中,只有添加到等待队列头后,进程才能进入休眠态

add_wait_queue函数原型如下:

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
  •  q:等待队列项要加入的等待队列头。
  • wait:等待队列项。
  • 返回值:无。

移除等待队列头

当设备可以访问时,将进程对应的等待队列项从等待队列头中删除。

remove_wait_queue函数原型如下:

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
  •  q:要删除的等待队列项所处的等待队列头。
  • wait:要删除的等待队列项。
  • 返回值:无。

等待唤醒

当设备可用时,唤醒进入休眠态的进程。wake_up函数唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程。wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程。

void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_heat_t *q);
  • q:要唤醒的等待队列头。
  •  返回值:无。

等待事件

除了主动唤醒,还可以设置等待某个事件,当事件满足后自动唤醒等待队列中的进程。

wait_event(wq, condition)

wait_event函数,condition条件为真时,以wq为等待队列头的等待队列被唤醒,否则一直阻塞,会将进程设置为TASK_UNINTERRUPTIBLE状态。

wait_event_timeout(wq, condition, timeout)

wait_event_timeout函数,和wait_event函数类似,可以添加超时时间,以jiffies为单位,函数返回0,超时时间到,condition为假;返回1,条件满足,condition为真。

wait_event_interruptible(wq, condition)

wait_event_interruptible函数,和wait_event函数类似,将进程设置为TASK_INTERRUPTIBLE状态,可以被信号打断。

wait_event_interruptible_timeout(wq, condition, timeout)

wait_event_interruptible_timeout函数,和wait_event_timeout函数类似,将进程设置为TASK_INTERRUPTIBLE状态,可以被信号打断。

非阻塞—轮询

应用程序以非阻塞方式访问设备,设备驱动程序要提供非阻塞的处理方式。

  • 应用程序通过selectepollpoll函数查询设备是否可以操作,如果可以操作的话,从设备读取或向设备写入数据。
  • 设备驱动程序执行poll函数。

应用程序

select

int select(int            nfds,
           fd_set         *readfds,
           fd_set         *writefds,
           fd_set         *exceptfds,
           struct timeval *timeout);
  • nfds:要监视的三类文件描述符集合中,最大文件描述符加1。 
  • readfds、writefds和exceptfds:都是fd_set类型,fd_set类型变量的每一个位都代表一个文件描述符。readfds用于监视文件描述符集是否可以读取,如果集合中有一个文件可以读取,返回一个大于0的值表示可读,如果没有文件可以读取,根据timeout参数判断是否超时。如果readfds设置为NULL表示不关心文件的读变化。writefds用于监视文件是否可以进行写操作。exceptfds用于监视文件的异常。
  • timeout:超时时间。当timeout设置为NULL表示无限期等待。
  • 返回值:0,超时发生,但是没有任何文件描述符可以进行操作;-1,发生错误;其他值,进行操作的文件描述符个数。

当定义一个fd_set类型变量,可以进行使用宏进行操作。

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
  • FD_ZERO用于将fd_set变量的所有位都清零。
  • FD_SET用于将fd_set变量的某个位置1,也就是向 fd_set添加一个文件描述符,参数fd就是要加入的文件描述符。
  • FD_CLR用于将 fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd就是要删除的文件描述符。
  • FD_ISSET 用于测试一个文件是否属于某个集合,参数fd就是要判断的文件描述符。

select函数非阻塞访问示例

void main(void) 
    int ret, fd;             /* 要监视的文件描述符 */
    fd_set readfds;          /* 读操作 文件描述符 */
    struct timeval timeout;  /* 超时时间 */

    fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞访问 */
    
    FD_ZERO(&readfds);       /* 清楚readfds */
    FD_SET(fd, &readfds);    /* 将fd添加到readfds */

    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500 ms */

    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);

    switch(ret) 
        case 0: /* 超时 */
            printf("timeout\\r\\n");
            break;
        case -1: /* 错误 */
            printf("error\\r\\n");
            break;
        default: /* 读数据 */
            if(FD_ISSET(fd, &readfds))  /* 判断是否为文件描述符 */
                /* read函数读取数据代码 */
            
            break;            
    

poll

在单个线程中,select函数能够监视的文件描述符最大数量为1024,poll函数没有最大文件描述符限制

struct pollfd 
    int    fd;      /* 文件描述符 */
    short  events;  /* 请求的事件 */
    short  revents; /* 返回的事件 */
;

int poll(struct pollfd *fds;
         nfds_t        nfds,
         int           timeout)
  • fd:要监视的文件描述符,fd无效则events监视事件无效,revents返回0。
  • events:要监视的事件。events可监视事件的类型
    • POLLIN              有数据可以读取
    • POLLPRI           有紧急的数据需要读取
    • POLLOUT          可以写数据
    • POLLERR          指定的文件描述符发生错误
    • POLLHUP          无效的请求
    • POLLNVAL         指定的文件描述符挂起
    • POLLRDNORM  等同于POLLIN
  • revents:返回的事件,由内核设置具体的返回事件。
  • fds:要监视的文件描述符集合及要监视的事件,fds为一个数组,数组元素都是结构体pollfd类型。
  • nfds:poll函数要监视的文件描述符数量。
  • timeout:超时时间,单位ms。
  • 返回值:返回发生事件或错误的文件描述符数量,0,超时;-1,发生错误并设置errno为错误类型。

poll函数非阻塞访问示例

void main(void) 
    int ret, fd;       /* 要监视的文件描述符 */
    struct pollfd fds; /* pollfd结构体 */ 

    fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞访问 */
    
    fds.fd = fd;
    fds.events = POLLIN; /* 监视数据是否可读 */
    
    ret = poll(&fds, 1, 500);  /* 轮询文件是否可操作, 超时500ms */
    if (ret) 
        /* 读取数据代码 */
     else if (ret == 0) 
        /* 超时 */
     else if (ret == -1) 
        /* 错误 */
    

epoll

selectpoll函数都会随着监听fd数量的增加,出现效率低下的问题,poll函数每次必须遍历所有描述符来检查就绪的描述符,浪费时间。涉及的文件描述符较少时适合用select和poll函数

epoll函数为处理大并发而准备的,常用于网络编程

应用程序先使用epoll_create创建一个epoll句柄。

int epoll_create(int size)
  • size:Linux版本2.6.8开始size参数没有意义,写一个大于0的值即可。 
  • 返回值:epoll句柄,-1表示创建失败。

epoll句柄创建成功后使用epoll_ctl向其中添加要监视的文件描述符和监视的事件。

struct epoll_event 
    uint32_t     event; /* epoll事件 */
    epoll_data_t data;  /* 用户数据 */
;

int epoll_ctl(int                epfd,
              int                op,
              int                fd,
              struct epoll_event *event)
  • events:要监视的事件。
    • EPOLLIN 数据可读。
    • EPOLLOUT 可写数据。
    • EPOLLPRI 有紧急的数据需要读取。
    • EPOLLERR 指定的文件描述符发生错误。
    • EPOLLHUP 指定的文件描述符挂起。
    • EPOLLET设置epoll为边沿触发,默认为水平触发。
    • EPOLLONESHOT 一次性监视,当监视完成后还需要再次监视某个fd,需要将fd重新添加到epoll。
  • epfd:epoll_create创建的句柄。
  • op:对epfd进行的操作。
    • EPOLL_CTL_ADD 向epfd添加参数fd表示的描述符。
    • EPOLL_CTL_MOD 修改参数fd的event事件。
    • EPOLL_CTL_DEL 从epfd中删除fd文件描述符 。
  • fd:要监视的文件描述符。
  • event:要监视的事件类型。
  • 返回值:0,成功;-1,失败,并设置errno的值为相应的错误码。

应用程序使用epoll_wait等待事件发生。

int epoll_wait(int                 epfd;
               struct epoll_event *events;
               int                 maxevents;
               int                 timeout)
  • epfd:要等待的epoll。 
  • events:指向epoll_event结构体的数组,当有事件发生时Linux内核会填写events。
  • maxevents:events数组大小,必须大于0。
  • timeout:超时时间,单位ms。
  • 返回值:0,超时;-1,错误;其他值,准备就绪的文件描述符数量。

设备驱动程序—poll操作函数

应用程序调用select或poll函数对驱动程序进行非阻塞访问时,驱动程序file_operations操作集中poll函数就会执行。

unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait)
  • filp:要打开的设备文件,即文件描述符。
  • wait:poll_table_struct类型指针,应用程序传进来,wait参数传给poll_wait函数。
  • 返回值:向应用程序返回设备状态。
    • POLLIN              有数据可以读取
    • POLLPRI           有紧急的数据需要读取
    • POLLOUT          可以写数据
    • POLLERR          指定的文件描述符发生错误
    • POLLHUP          无效的请求
    • POLLNVAL         指定的文件描述符挂起
    • POLLRDNORM  等同于POLLIN

驱动程序的poll函数中调用poll_wait函数,poll_wait不会引起阻塞,只是将应用程序添加到poll_table中。

void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
  •  filp:要打开的设备文件,即文件描述符。
  • wait_address:要添加到poll_table中的等待队列头。
  • p:file_operation中poll函数的wait参数。

以上是关于Linux驱动开发阻塞和非阻塞IO的主要内容,如果未能解决你的问题,请参考以下文章

Linux 阻塞和非阻塞IO 实验

嵌入式Linux开发27——Linux阻塞和非阻塞IO

Linux设备驱动中的IO模型---阻塞和非阻塞IO

Linux设备驱动基础03之阻塞与非阻塞IO

Linux设备驱动基础03之阻塞与非阻塞IO

Linux设备驱动基础03之阻塞与非阻塞IO