【Linux驱动开发】021 非阻塞IO

一、前言

阻塞IO和非阻塞IO是Linux驱动开发中常见的两种访问设备的模式,这里的IO指的是Input/Output,也就是输入、输出。上一篇文章我们学习了阻塞访问,来回顾一下:

阻塞访问:当进程访问的驱动不可用或数据未准备好时,进程进入interruptible的休眠状态,也就是将当前进程添加到等待队列,等到按键按下进入中断,在中断中将线程唤醒(如果通过定时器消抖也可以在定时器中将线程唤醒),读取按键值。


二、非阻塞IO

1、非阻塞定义

当在应用程序以非阻塞形式访问设备文件时,如果设备不可用或数据未准备好,会立即向内核返回一个错误码,表示数据读取失败,应用程序会再次进行读取,如此重复循环,直至数据读取成功。

2、轮询

应用程序以非阻塞形式访问设备文件,驱动程序就需要相应的非阻塞处理方式,也就是轮询。应用程序中进行非阻塞访问的函数有三个:select、poll、epoll函数,这三个函数的主要作用就是:在应用程序中查询设备是否可以操作,如果可以操作就从设备读取或写入数据。

注意:以下三个函数都是在应用程序中使用。

0)三个非阻塞访问函数对比

详见,这里

属性\函数selectpollepoll
事件集合传入3个参数以区分可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪时间。统一处理所有事件类型。通过pollfd.events传入感兴趣事件,内核通过pollfd.revemts反馈其中就绪的事件。内核通过事件表直接管理用户感兴趣的所有事件。
应用程序索引就绪文件描述符的时间复杂度O(n)O(n)O(1)
最大支持文件描述符数10246553565535

内核实现和工作效率

采用轮询方式检测就绪事件,复杂度O(n)

采用轮询方式检测就绪事件,复杂度O(n)

采用回调方式检测就绪事件,复杂度O(n)

1) select 函数

int select( int             nfds,   
            fd_set          *readfds,   
            fd_set          *writefds, 
            fd_set          *exceptfds,   
            struct timeval  *timeout);

※ nfds:三类文件描述符集合中最大文件描述符加1,可通过以下代码理解:

fd_set fds;

FD_ZERO(&fds);//清空集合
FD_SET(fd1, &fds);//设置描述符
FD_SET(fd2, &fds);//设置描述符
maxfdp = fd1 + 1;//描述符最大值加1,假设fd1 > fd2

※ readfds、writefds 和 exceptfds:需监视文件读、写、异常描述符集合。

比如writefds用于监视指定描述符集的写变化,也就是监视这些文件是否可以写入,只要文件集合中有一个文件可以写入那么select函数就会返回一个大于0的值,表示文件可以写入。如果没有文件可以写入,就会根据timeout参数来判断是否超时。当然,如果只需要监视文件读状态,其他两个可以设置为NULL。

※ timeout:超时时间,timeval结构体定义如下:

struct timeval { 
    long        tv_sec;   // 秒
    long        tv_usec;  // 微秒 
}; 

     一般超时时间有三种情况如下:

  • NULL:将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止。
  • 0:select变成纯粹的非阻塞函数,立即返回继续执行,无文件满足要求返回0,有返回正值。
  • 大于0: 在timeout内阻塞,timeout时间内有事件到来就返回大于0的值,否则就堵塞进程,超时后无论怎样一定返回0,并继续向下执行。

※ 返回值:

  • 0: 超时返回,无文件描述符可以进行操作。
  • -1 :发生错误。
  • 大于0: 有返回数量的文件描述符可以进行操作。

2) poll 函数

int poll(  struct pollfd    *fds,   
           nfds_t           nfds,   
           int              timeout   );

※ fds:pollfd结构体数组,pollfd结构体包含文件描述符,以及对对应文件的监视事件。pollfd结构体定义如下,其中events是要监视的fd文件描述符的事件,revents是内核设置的返回事件。

struct pollfd { 
      int   fd;            /*  文件描述符    */ 
      short events;        /*  监视的事件    */ 
      short revents;       /*  返回的事件    */ 
}; 

events可监视的事件类型如下:

POLLIN       有数据可以读取。 
POLLPRI      有紧急的数据需要读取。 
POLLOUT      可以写数据。 
POLLERR      指定的文件描述符发生错误。 
POLLHUP      指定的文件描述符挂起。 
POLLNVAL     无效的请求。 
POLLRDNORM   等同于 POLLIN 

※ nfds:要监视的文件描述符的数量,也就是上面结构体数组的元素个数。

※ timeout:超时时间,单位是ms。

 返回值:

  • 0:超时。
  • -1:发生错误。
  • 大于0:返回revents 域中不为0的pollfd 结构体个数。

3) epoll 函数

select和poll函数随着监听的fd数量的增加会出现效率低下的问题,epoll函数由此而生,epoll是为了处理大并发准备的,一般在网络编程中使用epoll函数。在应用程序中使用epoll函数,需要怎么操作呢?

🍉 首先,需要创建一个epoll句柄:

int epoll_create(int size);

※ size:从 Linux2.6.8以后该参数无意义,大于0即可。

※ 返回值:epoll句柄,返回-1表示创建失败。

🍉 然后,使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件,epoll_ctl函数声明如下:

int epoll_ctl(  int                   epfd,   
                int                   op,   
                int                   fd, 
                struct epoll_event    *event );

※ epfd:epoll_create 函数创建的epoll 句柄。 

※ op:要对epoll 句柄进行的操作,主要有以下几种:

EPOLL_CTL_ADD   向epfd 添加文件参数fd 表示的描述符。 
EPOLL_CTL_MOD   修改参数fd 的event 事件。 
EPOLL_CTL_DEL   从epfd 中删除fd 描述符。 

 fd:要监视的文件描述符。

※ event:要监视的事件类型,epoll_event结构体指针,epoll_event结构体定义如下:

struct epoll_event { 
       uint32_t          events;          /* epoll 事件     */ 
       epoll_data_t      data;            /*  用户数据      */ 
}; 

上面的结构体成员events可监视的事件如下:

EPOLLIN       有数据可以读取。 
EPOLLOUT      可以写数据。 
EPOLLPRI      有紧急的数据需要读取。 
EPOLLERR      指定的文件描述符发生错误。 
EPOLLHUP      指定的文件描述符挂起。 
EPOLLET       设置epoll 为边沿触发,默认触发模式为水平触发。 
EPOLLONESHOT  一次性的监视,当监视完成以后还需要再次监视某个fd,
              那么就需要将fd重新添加到epoll里面。 

※ 返回值:0,成功;-1,失败。

🍉 最后,在上面都设置好后,需要使用epoll_wait函数等待事件的发生,epoll_wait声明如下:

int epoll_wait(   int                  epfd,   
                  struct epoll_event   *events, 
                  int                  maxevents,   
                  int                  timeout );

※ epfd:要等待的epoll 句柄。 

※ events:指向epoll_event结构体的指针(结构体数组首地址)。当有事件发生Linux内核会将相应的事件写入events,使用者只需要读取该events就可以判断发生了哪些事件。

※ maxevents:events数组大小,必须大于0。

timeout:超时时间,单位为 ms。

返回值:0,超时;-1,错误;大于0的整数,发生事件的文件描述符数量。        

epoll主要用在大规模并发服务器上,因为这种场合select和poll函数效率低下。本章我们使用select、poll函数完成实验。

3、驱动程序poll操作函数

当在应用程序中调用以上三个函数时,驱动程序中file_operations函数集中poll函数就会执行,因此我们还需要实现驱动程序中的poll函数。poll函数圆形如下:

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);

※ filp:要打开的设备文件。

※ wait:poll_table_struct结构体指针,由应用程序传递过来,会将此参数传递给poll_wait函数。

※ 返回值:向应用程序返回设备或者资源状态,可返回值如下:

POLLIN           有数据可以读取。 
POLLPRI          有紧急的数据需要读取。 
POLLOUT          可以写数据。 
POLLERR          指定的文件描述符发生错误。 
POLLHUP          指定的文件描述符挂起。 
POLLNVAL         无效的请求。 
POLLRDNORM       等同于 POLLIN,普通数据可读 

在驱动程序的poll函数(上面的这个函数)内部需要调用poll_wait函数,该函数不会引起阻塞,主要作用是将相应的等待队列注册到poll_table中。poll_awit函数原型如下:

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

※ wait_address:要添加到poll_table的等待队列头。

※ p: poll_table,上面file_operations中的poll函数的wait参数。


三、程序框图

上图为个人理解,如有错误,请在评论区指出。 


2022/06/06于武汉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值