poll函数应用背景
文件阻塞、非阻塞操作对单个文件可能影响不大,但当我们以阻塞的方式打开多个文件时,有时会出现一种不好的现象,下面利用伪代码演示一下:
#include <···>
int fd1,fd2;
int main(void)
{
fd1=open("/dev/sd1",ORDWR);
fd2=open("/dev/sd2",ORDWR);
read(fd1,user,4) //读取第一个文件,如果这里阻塞,影响后面文件的读取
·····
read(fd2,user,4) //读取第二个文件只有第一个文件成功返回才能读取第二个文件
}
上面的情况,当读取fd1
时可能fd1
没有数据造成进程阻塞,即使fd2
有数据可读,我们也无法获取,那么有没有一种方法来判断查询文件是否有数据可以读取呢?如果有就读取,没有就略过。达到类似下面的目标:
#include <···>
int fd1,fd2;
int main(void)
{
fd1=open("/dev/sd1",ORDWR);
fd2=open("/dev/sd2",ORDWR);
if(fd1有数据可以读)
read(fd1,user,4) //读取第一个文件
·····
if(fd2有数据可以读)
read(fd2,user,4) //读取第二个文件
}
linux内核poll接口
当应用程序需要对多个文件进行读写时,若某个文件没有准备好,则系统会处于读写阻塞状态,并影响了其他文件的读写。为避免这种情况,在必须使用多输入输出流又不想阻塞在他们任何一个设备读写操作上时,linux
系统提供了几个应用编程API
函数来解决这个问题。这几个API
函数是poll
、select
、epoll
这些函数返回调用时,会给出一个文件此时是否可读写的标志状态,应用程序根据这些不同的标志来读写相应的文件,实现阻塞方式打开但是非阻塞方式执行的读写效果。
这些调用都需要来自设备驱动中poll
方法的支持,poll
返回不同的标志,告诉主程序文件是否可以读写。
poll接口原型
poll
接口原型在file_operation
结构体总可以找到
struct file_operations {
····
unsigned int (*poll) (struct file *, struct poll_table_struct *);
···
};
函数原型:
unsigned int xxx_poll(struct file *pfile, struct poll_table_struct *wait)
功能:
a:调用poll_wait()
函数,将进程添加到等待队列上:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
//判断_qproc是否实现,有实现才调用
//_qproc其实是selec.c文件中_pollwait函数
//_pollwait调用后会把当前进行添加到等待队列中
//并且 do_poll函数中先执行do_pollfd函数,该函数内部通过函数指针调用到驱动的poll接口函数
//第一次 do_pollfd函数执行时,pt->_qproc存在,就是_pollwait函数
//do_pollfd对一个fd执行完成一次后,会把pt->_qproc=NULL这样下次调用驱动的pol l函数时候就不会重读添加到进程到等待队列了
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
内核定义下面类型
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
wait
参数类型其实和p
参数类型本质上是一致的。poll_wait
函数与poll
函数公用两个参数,而wait_queue_head_t * wait_address
参数我们看着很熟悉,这其实就是等待队列头,我们定义过在编写poll
函数加进来就可以,这样两者的函数架构就清晰了。
b:返回表示是否能对设备进行无阻塞读或写的掩码,没有数据读写则要求返回0
;
参数:
pfile: struct file
文件指针由上层传递。
wait:轮训表结构指针,由内核传入。
两个参数都不需要驱动编写者做任何处理,原样使用
返回值:
返回表示是否能对设备进行无阻塞读或写的掩码,没有数据读写则要求返回0
;
标志 | 含义 |
---|---|
POLLIN | 如果设备无阻塞的读,就返回该值 |
POLLRDNORM | 通常的数据已经准备好,可以读了,就返回该值。通常的做法是返回(POLLLIN | POLLRDNORA) |
POLLRDBAND | 如果可以从设备读出带外数据(网络方面的驱动),就返回该值,返回该值,它只可在linux内核的某些网络代码中可以使用,通常不用在设备驱动中 |
POLLPRI | 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select把带外数据当做异常处理 |
POLLHUP | 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select功能描述,调用select的进程被告知进程是可读的 |
POLLERR | 如果设备发生错误,就返回该值 |
POLLOUT | 如果设备可以无阻塞的写,就返回该值 |
POLLWRNORM | 设备已经准备好,可以写了,就返回该值,通常的做法是(POLLOUT | POLLWRNORM ) |
POLLWRBAND | 作用与POLLRDBAND类似 |
常用掩码: POLLRDNORM,POLLIN,POLLOUT,POLLWRNORM
设备可读,通常返回:(POLLIN | POLLRDNORM)
设备可写,通常返回:(POLLLOUT | POLLWRNORM)
常用组合: POLLIN | POLLRDNORM,POLLOUT | POLLWRNORM
在上面按键驱动的基础上编写我们底层poll函数
static unsigned int xxx_poll(struct file*pfile,struct poll_table_struct *wait)
{
unsigned int mask=0; //掩码一定初始化为0
//将当前进程加入等待队列
poll_wait(pfile,&wq,wait);
//判断是否有动作,如果有动作返回可读掩码
if(press)
mask=POLLIN|POLLRDNORM;
//否则返回0 表示不可读
return mask;
}
文件操作结构体中挂接我们的poll
函数
struct file_operation xxxdriver_fops={
·····
poll=xxx_poll,
};
应用编程的poll接口
头文件#include<poll.h>
int poll(struct pollfd fd[],nfds_t nfds,int timeout)
功能:
可以阻塞/非阻塞的监控多个文件的读、写、错误事件的发生。poll
函数退出后,struct pollfd
变量fd
,events
值被清零,需要重新设置。revents
变量包含了监测结果。
参数:
1)第一个参数fd
:
该参数是一个struct pollfd
结构数组,struct pollfd
结构如下:
struct pollfd{
int fd; //文件描述符
short events:; //请求的事件
short revents; //返回的事件
};
使用struct pollfd
来表示被监视的文件描述符。
events
和revents
是通过对代表各种事件的标志进行逻辑或运算组合而成。
events
包括要监视的事件,poll
用已经发生的事件标志设置revents
。通过查询revents
被设置标志就可以知道发生哪些事件
如果fd
小于0,则events
字段被忽略,而revents
被设置为0
poll
函数的事件标志符值
常量 | 说明 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读(紧迫的数据可以读) |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
注意:后三个只能作为描述字的返回结果存储在revents
中,而不能作为测试条件用于events
中
2)第二个参数nfds
:
要监视的描述符的数目
3)第三个参数timeout:
是一个用毫秒表示的时间,是指定poll
在返回前没有接收事件时应该等待的时间,如果它的值是-1
,poll
永远不会超时,如果整数值为32
个比特,那么最大的超时时间大约是30
分钟。
timeout
值说明:
-1:永远等待
0:立即返回,不阻塞进程
>0:等待指定数目的毫秒数
返回值:
>0:fd
数组中准备好读,写或错误的那些文件描述符号的总数量(我们关心的情况)
=0:超时
<0:调用函数失败
app函数修改
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
int main(int argc,char *argv[])
{
int i,file_fp,ret;
unsigned char btn[4]={"0000"},cur[4]={"0000"};
struct pollfd fds[1];
//file_fp = open(argv[1],O_RDWR|O_NONBLOCK); //非阻塞方式
file_fp = open(argv[1],O_RDWR); //阻塞方式
while(1)
{
fds[0].fd =fd;
fds[0].events =POLLIN;
ret=poll(fds,1,2000);
//判断查询结果
if(ret<0)
{
perror("poll");
exit(0);
}else if(ret==0)
{
printf("timeout\r\n");
}else
{
if(fds[0].revents & POLLIN)
{
read(file_fp,cur,4);
for(i=0;i<4;i++)
{
if(cur[i]!=btn[i])
{
btn[i]=cur[i];
if(cur[i]=='1')
printf("按键%d 按下\r\n",i+1);
else
printf("按键%d 弹起\r\n",i+1);
}
}
}
}
}
close(file_fp);
}
应用程序接口select()函数
在linux
中,select
函数实现I/O
端口的复用,和前面介绍的poll
功能类似,他也对应设备驱动的poll
接口,这个系统调用来监测设备是否可读写,或出错。
select函数说明
需要包含头文件
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
函数原型:
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
功能:
在设定时间内监测所设置的文件状态是否发生变化。当函数返回时候会清空readset
,writeset
,exceptset
三个集合中的fd
,所以如果想监测他们,则需要返回后再次添加。
参数说明:
ndfs:select
监视的所有文件描述符最大值+1
readfds:select
所监视的可读文件描述符集合
writefds:select
监视的可写文件描述符集合
exceptfds:select
监视的异常文件描述符集合
timeout:本次select
的超时结束时间
返回值:
>0:执行成功返回文件描述符状态已改变的个数;
==0:代表已超过timeout
时间,文件描述符状态还没有发生改变;
==-1:函数有错误发生,错误原因存于reeno
,此时参数readfds
,writefds
,exceptfds
和timeout
的值变成不可预测,错误值可能为:
EBADF:文件描述符无效或文件已经关闭
EINTR:此调用被信号中断
EINVAL:参数n
为负值
ENOMEM:核心内存不足
select()函数参数详细说明
具体解释select
的参数:
nfds:是指集合中所有的文件描述符的范围,即所有的文件描述符最大值+1
,不能错。至于为什么这样,这与底层函数相关,后面进行介绍。
示例:
程序中打开了5
个设备文件,文件描述符分别是fd1~fd5
;
现在要检测fd4
,fd2
这两个文件的读状态。则nfds
值应该是fd2
,fd4
这两个文件描述符中较大的那一个值+1
假设fd2
>fd4
,则fd2+1
为nfds
的值
假设fd2
<fd4
,则fd4+1
为nfds
的值
readfd:这个集合是要监视文件描述符可读状态,可以传入NULL
值,表示不关心任何文件的读变化
writefds:这个集合是要监视的文件描述符可写状态,可以传入NULL
值,表示不关心任何文件的写变化
errorfds:同上面两个参数的意图,用来监视文件错误异常变化
对于fd_set
类型的变量系统提供了以下几个宏来操作它:
void FD_CLR(int fd,fd_set*set);
把set
集合中值为fd
监测对象移除(原来想监测,后来改变主意)
void FD_SET(int fd,fd_set*set);
把fd
添加到set
集合中,表示要监测这个fd
void FD_ZERO(fd_set*set);
把set集合全部清空,表示不监测任意fd
,一般在初始化时使用
int FD_ISSET(int fd,fd_set*set);
判断set
集合中fd
的状态是否发生变化(可读,可写,发生错误)
使用方法:
1):先使用FD_ZERO
清空集合
2):使用FD_SET
添加要监控的对象
3):调用select
函数轮询每个fd状态
4):使用FD_ISSET
判断每一个监测对象状态是否发生了变化
timeout:是select
超时时间,使用struct timeval
表示时间,根据传递的内部不同有以下几种情况:
timeout
设置为NULL
:将select
设置为阻塞状态,直到文件描述符集合中有文件描述符发生变化为止;
timeout
成员都为0
:就变成一个非阻塞状态,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0
,有变化返回正值;
timeout
:成员值大于0
,select
在timeout
时间内阻塞,超时时间之内有文件描述符发生变化就返回了,否则在超时后返回。
时间结构体定义如下:
struct timeval{
long tv_sec; //秒
long tv_usec;//微妙
}
程序示例:
底层驱动函数不变只需改动上层app
函数即可验证,下面是一个小例程:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include<sys/select.h>
#include<sys/time.h>
//int select(int nfds,fd_set *readset,fd_set *writeset,fd_set *exceptset,struct timeval *timeout)
int main(int argc,char *argv[])
{
fd_set readset; //定义监测读集合
int i,file_fp,ret;
unsigned char btn[4]={"0000"},cur[4]={"0000"};
//file_fp = open(argv[1],O_RDWR|O_NONBLOCK); //非阻塞方式
file_fp = open(argv[1],O_RDWR); //阻塞方式
while(1)
{
FD_ZERO(&readset); //清空监测读集合
FD_SET(fd,&readset); //将监测对象添加进读队列
//进行轮询查询,如果监测多个文件还需要进行文件描述符的比较
//超时时间设为NULL,在没有监测事件到来之前,进程阻塞
ret=select(fd+1,&readset,NULL,NULL,NULL);
//判断查询结果
if(ret<0)
{
perror("select");
exit(0);
}else if(ret==0)
{
printf("timeout\r\n");
}else
{
//可读状态发生 读取结果
if(FD_ISSET(fd,&readset))
{
read(file_fp,cur,4);
for(i=0;i<4;i++)
{
if(cur[i]!=btn[i])
{
btn[i]=cur[i];
if(cur[i]=='1')
printf("按键%d 按下\r\n",i+1);
else
printf("按键%d 弹起\r\n",i+1);
}
}
}
}
}
close(file_fp);
}
其余超时时间可以自行尝试。
select函数内部机制理解
理解select
模型的关键在于理解fd_set
,为说明方便,取fd_set
长度为1
个字节,fd_set
中每一bit
可以对应一个文件描述符fd
。则1
字节长的fd_set
最大可对应8
个fd
。(这里取一个字节指示为了说明方便,实际fd_set
比这个长)
typedef __kernel_fd_set fe_set;
typedef struct{
unsigned long fds_bits[__FDSET_LONGS];
}__kernel_fd_set;
从上面的定义可以知道,fd_set
实际上是一个unsigned long
型数组,表示一片连续的内存空间。内核使用这片内存空间的每一个位表示一个fd
,比如:fd=5
,这个文件描述符,则使用第5个二进制位表示,把第5位设置为1,表示要监测的fd值为5的文件。
FD_SET(fd,&readset)
->``readset
第fd
个位置设置为1
当select
函数监测到fd
状态发生了变化,会保留fd
位为1
,没有发生变化fd
位对应变为0
.
FD_ISSET(fd,&readset)``->
判断第fd
位是否是1
,是1
表示状态发生变化
示例:
1.分配fd_set set
:FD_ZERO(&set)
:则set
位是0000 0000
.
2.若fd1=5
,执行FD_SET(fd1,&set);
后set
变为0001 0000
(第5
位置1
)。
再加入fd2=2
,执行FD_SET(fd2,&set)
;
再加入fd3=1
,执行FD_SET(fd3,&set)
;
后set
变为0001 0110
;
执行select(5+1,&set,NULL,NULL,NULL)
;阻塞等待
3.若fd=1
,fd=2
上发生可读事件,则select
返回,此时set
值变为0000 0110
.没有发生状态变化的fd=5
,第5
位变为0
,被清空。
4.判断set
哪一位为1
,就知道哪一个fd
状态发生变化了。
if(FD_ISSET(fd5,&set))
{
判断fd5;
}
if(FD_ISSET(fd2,&set))
{
判断fd2;
}
if(FD_ISSET(fd1,&set))
{
判断fd1;
}