基于tiny4412按键中断驱动的poll、select函数演示

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函数是pollselectepoll
这些函数返回调用时,会给出一个文件此时是否可读写的标志状态,应用程序根据这些不同的标志来读写相应的文件,实现阻塞方式打开但是非阻塞方式执行的读写效果。
这些调用都需要来自设备驱动中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
int poll(struct pollfd fd[],nfds_t nfds,int timeout)
功能:
可以阻塞/非阻塞的监控多个文件的读、写、错误事件的发生。poll函数退出后,struct pollfd变量fdevents值被清零,需要重新设置。revents变量包含了监测结果。
参数:
1)第一个参数fd
该参数是一个struct pollfd结构数组,struct pollfd结构如下:

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

使用struct pollfd来表示被监视的文件描述符。
eventsrevents是通过对代表各种事件的标志进行逻辑或运算组合而成。
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在返回前没有接收事件时应该等待的时间,如果它的值是-1poll永远不会超时,如果整数值为32个比特,那么最大的超时时间大约是30分钟。
timeout值说明:
-1:永远等待
0:立即返回,不阻塞进程
>0:等待指定数目的毫秒数
返回值:
>0:fd数组中准备好读,写或错误的那些文件描述符号的总数量(我们关心的情况)
=0:超时
<0:调用函数失败

app函数修改

#include 
#include 
#include 
#include 
#include 
#include 
#include 
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
#include
#include
#include

函数原型:
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,exceptfdstimeout的值变成不可预测,错误值可能为:
EBADF:文件描述符无效或文件已经关闭
EINTR:此调用被信号中断
EINVAL:参数n为负值
ENOMEM:核心内存不足

select()函数参数详细说明

具体解释select的参数:
nfds:是指集合中所有的文件描述符的范围,即所有的文件描述符最大值+1,不能错。至于为什么这样,这与底层函数相关,后面进行介绍。

示例:
程序中打开了5个设备文件,文件描述符分别是fd1~fd5;
现在要检测fd4fd2这两个文件的读状态。则nfds值应该是fd2,fd4这两个文件描述符中较大的那一个值+1
假设fd2>fd4,则fd2+1nfds的值
假设fd2 < fd4,则fd4+1nfds的值

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:成员值大于0selecttimeout时间内阻塞,超时时间之内有文件描述符发生变化就返回了,否则在超时后返回。
时间结构体定义如下:

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

程序示例:
底层驱动函数不变只需改动上层app函数即可验证,下面是一个小例程:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include
#include

//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最大可对应8fd(这里取一个字节指示为了说明方便,实际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) ->``readsetfd个位置设置为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;
}

你可能感兴趣的:(个人笔记,随笔,linux函数练习,tiny4412,linux)