@在linux驱动开发中有两种常见的设备访问模式,在编写驱动中要考虑到阻塞和非阻塞两种模式。
一:简介
这里的io不是我们所说的gpio引脚,是指input/output,也就是输入/输出,是应用程序对设备驱动的输入/输出操作。
当应用程序访问设备驱动进行操作的时候,如果不能获取设备资源,阻塞式IO就会将应用程序挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么轮询等待,直到资源可以使用,要么直接放弃。
总结:
1->阻塞IO 应用程序对应的线程直接挂起,直到设备资源可以访问获取;
应用程序 (user)------>read(user)------>设备不可用(kernel)------>sleep状态(kernel)------>设备可用(kernel)------>从驱动中read资源(user)
应用程序实现非阻塞访问代码示例:
int fd,data = 0;
fd = open("/dev/xxx_dev",ORDWR | O_NONBLOCK); ret = read(fd,
&data,sizeof(data));
2->非阻塞IO 应用程序对应的线程轮询等待或者放弃访问,直到设备资源可以访问
应用程序 (user)------>read(user)------>设备不可用(kernel)------>返回错误码(kernel)------>读取错误码,继续read(user)......------> 设备可用(kernel)------>从驱动中read资源(user)
应用程序实现非阻塞访问代码示例:
int fd,data = 0;
fd = open("/dev/xxx_dev",ORDWR | O_NONBLOCK); ret = read(fd,
&data,sizeof(data));
3->IO 是指input/output 输入输出设备,不是gpio
二:等待队列
-
等待队列队头
阻塞访问的好处就是设备资源不可以访问时,进程进入休眠状态,这样可以将cpu资源让出来;当设备资源可以访问时,必须唤醒进程,这一过程一般在中断
函数里完成唤醒工作。
kernel提供了等待队列(wait queue)来实现阻塞唤醒工作,如果我们在驱动中使用等待队列,必须创建一个等待队列的列头。结构体 (include/linux/wait.h)
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct _wait_queue_head wait_queue_head_t;
//使用说明:定义号等待列头之后,需要用init_waitqueue_head函数初始化等待队列头,函数原型
void init_queue_head(wait_queue_headt *q) //q为初始化等待队列列头
DECLARE_WAIT_QUEUE_HEAD
- 等待队列项
等待队列头就是等待队列的头部,每个访问设备的进程都是一个等待队列项,当设备资源不可用的时候,需要将这些进程添加到等待队列里面。等待队列项结构体如下
struct __wait_queue {
unsigned int flags;
void * private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
DECLEAR_WAITQUEUE(name,tsk)
//宏定义并初始化一个等待队列项
//name 等待队列项的名字
//tsk 表示等待队列项时属于哪一个进程,一般设置为current。current在kernel中是一个全局变量,表示当前进程。
- 将等待队列项 添加、移除等待队列头
当设备资源不可以访问时,需要将进程对应的等待队列项添加到创建的等待队列头中。(只有添加到等待队列头中 以后 进程才能进入睡眠状态。)当设备可以访问,将等待队列项从等待队列头中移除即可。
函数原型:
void add_wait_queue(wait_quit_head_t *q,
wait_queue_t *wait)
//q 等待队列项要加入的等待队列头
//待加入的等待队列项
void remove_queue(wait_queue_heat_t *q,
wait_queue_t *wait)
- 等待唤醒
当设备资源可以访问时,需要唤醒进入睡眠的进程,可以使用以下两个函数。
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
//这两个函数会将等待队列头中所有的进程都唤醒。
- 等待事件
除了等待唤醒之外,可以设置等待队列的某个时间,进行唤醒。当事件条件满足之后,就会唤醒。
函数原型:
wait_queue(wp,condition)//等待以wp为等待队列头的等待队列,当conditon为true时,可以唤醒。
wait_event_timeout(wp,condition,timeout)//l可以添加超时时间
wait_event_interrputible(wp,condition)//可以被信号打断
wait_event_interrputible(wp,condition,timeout)
三:轮询
应用程序以非阻塞方式访问,设备驱动需要提供非阻塞的访问方式,也就是轮询。poll 、epoll、select可以用于处理轮询,应用程序可以通过poll 、epoll、select函数来查询设备资源是否可以操作。如果可以操作,就读写设备。
当应用程序使用select、poll、epoll函数时,驱动成勋中的poll函数就会执行,因此需要在设备驱动里编写poll函数。
- select 函数
int select( int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *excptfds,
struct timeval *timeout)
参数:
nfds:所监视的三类文件描述集合,最大文件描述符加1
readfds、writrefds、exceptfds:三个指针指向描述符集合,三个参数关心那些描述符,满足那些条件。
例子:比如从一个设备文件读取数据,那么可以先定义一个fd_set 变量传递给readfds。
定义宏:
void FD_ZERO(fd_set *set) //用于将fd_set 变量的所有bit清零。
void FD_SET(int fd,fd_set *set) //FD_SET 用于将fd_set变量的某个位置置1,也就是向fd_set添加一个文件描述符,参数fd,就是要加入的文件描述符。
void FD_CLR(int fd,fd_set *set)//将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd,就是要删除的文件描述符。
int FD_ISSET(int fd,fd_set *set)//用于测试一个文件是否属于某个集合,参数fd就是要判断的文件描述符。
timeout 超时时间,调用select函数等待某些文件描述符可以设置超时时间,超时时间结构使用timeval,结构体定义如下:
struct timeval {
long tv_sec; //s
long tv_usec; //us
};
select 函数使用示例:
//select 非阻塞函数访问示例
void mian{
int ret,fd;
fd_set readfds;
struct timeval timeout;
fd = open("dev",O_RDWR | O_NONBLOCK);
FD_ZERO(&readfds);//清楚readfds
FD_SET(fd,&readfds);//将fd添加到readfds里面
timeout.tv_sec = 0;
timeout.tv_usec = 500000;
ret =select(fd+1 ,&readfds,NULL,NULL,&timeout);
switch(ret){
case 0:
timeout 超时
break;
case -1:
error 错误
break;
default:
if(FD_ISSET(fd,&readfds)){
read()
}
break;
}
}
- poll函数
在单个线程中,select函数监视的文件描述符数量有最大的限制,一般为1024,可以修改内核将监视的文件描述符数量改大,但这样会降低效率。这时候就可以使用poll函数,poll函数的本质上和select没有太大区别,但是poll函数没有最大文件描述符限制。poll函数原型:
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout)
struct pollfd{
int fds;
short events;
short revents;
};
fds//要监视的文件描述符,如果fd无效,events监视事件也无效,并且revents返回0,events是要监视的时间,可以监视的事件类型如下:
POLLIN 有数据可读
POLLPRI 有紧急的数据需要读取
POLLOUT 写数据
POLLERR 错误
POLLHUP 挂起
nfds:poll函数要监视的文件描述符数量
timeout 超时时间,单位ms.
poll函数进行非阻塞访问的示例:
void mian(void)
{
int ret,fd;
struct pollfd fds;
fd = open("dev",O_RDWR | O_NONBLOCK);
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, 1, 500);
if(ret){
read 数据
}else if (ret==0) {
timeout 超时
}else if(ret<0){
error 错误
}
}
- epoll函数
无论是select还是poll函数都会随着监听的fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的文件描述符来检查就绪的设备描述符,这个过程很浪费时间。为此,epoll应出来了,为处理大并发而准备的,常常在网络编程中使用epoll函数。
首先应用程序使用epoll_create函数创建一个epoll,函数原型:
int epoll_create(int size)
size :随便填写一个大于0的值就可以
返回值:为-1创建失败
句柄创建完成之后,要使用epoll_ctl 想其中添加要监视的文件描述符以及监视的事件,函数原型如下:
int epoll_ctl( int epfd,
int op,
int fd,
struct epoll_event *event)
)
函数参数:
epfd :要操作的epoll句柄,也就是epoll_create函数创建的epoll句柄。
op:表示要对epds(epoll句柄)进行操作,可以设置为:
EPOLL_CTL_ADD 向epfd添加参数fd表示的描述符
EPOLL_CTL_MOD 修改参数fd的event事件
EPOLL_CTL_DEL 从epfd中删除fd描述符
fd:要监视的文件描述符。
event:要监视的事件类型,为epoll_event结构体类型指针:
struct epoll_event{
uint32_t events ;//poll事件
//事件可以是EPOLLIN EPOLLOUT EPOLLPRI EPOLLERR
epoll_data_t data; //用户数据
}
返回值-1,失败,0成功
前面设置好以后,应用程序就可以通过epoll_wait 函数来等待事件的发生,雷士select函数。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
返回值 :-1 失败;0 超时;其他值,准备就绪文件的描述符数量。