本文主要是介绍Linux第86步_了解“阻塞和非阻塞IO”以及相关处理函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1、IO
“应用程序”对“驱动设备“进行输入/输出操作,简称IO操作,它是Input和Output的缩写。
2、阻塞IO
阻塞IO是“应用程序”对“驱动设备”进行操作,若不能获取到设备资源,则阻塞IO应用程序的线程会被“挂起”,直到获取到设备资源为止。
“挂起”就是让线程进入休眠,将CPU的资源让出来。线程进入休眠后,当设备文件可以操作时,就必须唤醒这个休眠的线程。通常是在中断函数里完成唤醒工作,Linux内核是采用“等待队列(wait queue)”来完成阻塞线程的唤醒工作。
阻塞IO应用举例:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 以阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
3、非阻塞IO
非阻塞IO是“应用程序”对“驱动设备”进行操作,若不能获取到设备资源,则非阻塞IO应用程序的线程不会被“挂起”,即线程不进入休眠,而是一直“轮询”,直到获取到设备资源为止,或者直接放弃。
非阻塞IO应用举例:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
/* 非阻塞方式打开 */
/*O_NONBLOCK表示如果路径名指向FIFO/块文字/字符文件,则把文件打开和后继I/O设置为非阻塞*/
ret = read( fd, &data, sizeof(data) ); /* 读取数据 */
4、等待队列
wait_queue_head结构体,需要包含头文件“#include <linux/wait.h>”
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
//给“wait_queue_head”起个别名叫“wait_queue_head_t”
//目的是兼容老版本的代码
初始化“等待队列头”
void init_waitqueue_head(struct wait_queue_head *wq_head)
//wq_head是要初始化的“等待队列头”
//使用宏 DECLARE WAIT OUEUE HEAD一次性完成等待队列头的定义和初始化
“等待队列头”是等待队列的头部,每个访问设备的进程都是一个队列项,当不能获取到设备资源的时候,就要将“进程对应的队列项”添加到“等待队列”里面。
wait_queue_entry结构体如下:
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
DECLARE_WAITQUEUE(name, tsk)
//给“当前正在运行的进程tsk”创建并初始化一个“等待队列项name”
//name就是等待队列项的名字
//tsk表示这个等待队列项属于哪个任务(进程),一般设置为current
注意:
在Linux内核中current相当于一个全局变量,表示当前进程;
void add_wait_queue( struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry)
//将“等待队列项wq_entry”添加到“等待队列头wq_head”,允许进程睡眠;
//wq_head:“等待队列项”要加入的“等待队列头”
//wq_entry:要加入的“等待队列项”
void remove_wait_queue( struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry)
//将“等待队列项wq_entry”从“等待队列头wq_head”中删除,允许访问设备
//wq head:要删除的“等待队列项”所处的“等待队列头”
//wq_entry:要删除的“等待队列项”
等待唤醒1
void wake_up(struct wait_queue_head *wq_head)
//由“驱动程序”去唤醒进入休眠的进程,属于主动唤醒
//wq_head:要唤醒的“等待队列头”
等待唤醒2
void wake_up_interruptible(struct wait_queue_head *wq_head)
//由“驱动程序”去唤醒进入休眠的进程,属于主动唤醒
//wq_head:要唤醒的“等待队列头”
设置“等待队列”等待某个事件,当这个事件满足以后,就可以自动唤醒“等待队列的进程”。
等待事件1
wait_event(wq_head, condition)
//由“wq_head为等待队列头的等待队列”去唤醒进程;
//当condition条件满足(为真)时会执行唤醒,否则会一直阻塞。
//唤醒后,会将进程设置为 TASK_UNINTERRUPTIBLE 状态,即进程不能被信号打断。
等待事件2
wait_event_interruptible(wq_head, condition)
//等待以“wq_head为等待队列头的等待队列”被唤醒,属于等待队列唤醒;
//当condition条件满足(为真)时会执行唤醒,否则会一直阻塞。
//唤醒后,会将进程设置为 TASK_INTERRUPTIBLE 状态,即进程可以被信号打断。
等待事件3
wait_event_timeout(wq_head, condition, timeout)
//等待以“wq_head为等待队列头的等待队列”被唤醒,属于等待队列唤醒;
//如果返回值为0,表示超时,且condition为假,阻塞直到超时,再执行唤醒;
//如果返回值为1,表示condition为真,阻塞直到条件满足,再执行唤醒;
等待事件4
wait_event_interruptible_timeout(wq_head, condition, timeout)
//等待以“wq head为等待队列头的等待队列”被唤醒,属于等待队列唤醒;
//如果返回值为0,表示超时,且condition为假,阻塞直到超时,再执行唤醒;
//如果返回值为1,表示condition为真,阻塞直到条件满足,再执行唤醒;
//唤醒后,会将进程设置为TASK_INTERRUPTIBLE状态,即进程可以被信号打断。
5、轮询
在非阻塞处理方式中,poll()、epoll()和select()用于处理轮询。若不能获取到设备资源,就从设备读取或者向设备写入数据。当应用程序调用poll()、epoll()或select()函数时,设备驱动程序中的poll()函数就会被执行,因此需要在设备驱动程序中编写poll()函数。
应用程序需要包含“#include <poll.h>”
驱动程序需要包含“#include <linux/poll.h>”
1)、select()函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
//nfds所要监视的这“三类文件描述集合”中,“最大文件描述符”加1;
/*readfds用于监视指定描述符集的“读变化”,监视这些文件是否可以读取;
若可以读取,则返回大于0的值;若没有文件可以读取,则根据timeout判断是否超时;*/
/*writefds用于监视指定描述符集的“写变化”,监视这些文件是否可以写入;
若文件可以执行写操作,则返回大于0的值;若没有文件可以写入,则根据timeout判断是否超时;*/
//exceptfds用于监视文件的异常
//timeout超时时间
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
}
当timeout为NULL时,表示无限期等待;
缺点:
在单个线程中,select()函数能够监视的“文件描述符数量”有最大的限制,一般为 1024;我们可以修改内核,将监视的“文件描述符数量”改大,但是这样做会降低效率;
void FD_ZERO(fd_set *set)
//将fd_set型变量的所有位都清零,即将所有的文件描述符从fd_set中删除;
void FD_SET(int fd, fd_set *set)
//将fd_set型变量的某个位置1,即向fd_set添加一个文件描述符;
//fd是要加入的文件描述符
void FD_CLR(int fd, fd_set *set)
//将fd_set型变量的某个位置0,即将一个文件描述符从fd_set中删除;
//fd是要删除的文件描述符
int FD_ISSET(int fd, fd_set *set)
//测试一个文件是否属于某个集合
//参数fd就是要判断的文件描述符
select()函数非阻塞读访问举例:
void main(void)
{
int ret, fd; /* 要监视的文件描述符 */
fd_set readfds; /* 读操作文件描述符集 */
struct timeval timeout; /* 超时结构体 */
fd = open("dev_xxx", O_RDWR | O_NONBLOCK);
//打开文件成功,则fd 为“文件描述符”
/* 非阻塞式访问 */
/*O_NONBLOCK表示如果路径名指向FIFO/块文字/字符文件,则把文件打开和后继I/O设置为非阻塞*/
FD_ZERO(&readfds); /* 清除readfds */
//将readfds变量的所有位都清零,即将所有的文件描述符从readfds中删除;
FD_SET(fd, &readfds); /* 将fd添加到readfds里面 */
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /*设置超时时间为500ms */
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
//“fd + 1”表示“最大文件描述符”加1
/*readfds用于监视指定描述符集的“读变化”,监视这些文件是否可以读取;
若可以读取,则返回大于0的值;若没有文件可以读取,则根据timeout判断是否超时;*/
//NULL不关注写
//NULL不关注文件的异常
//timeout设置超时时间为500ms;
switch (ret)
{
case 0: /* ret=0超时 */
printf("timeout!\r\n");
break;
case -1: /* 错误 */
printf("error!\r\n");
break;
default: /* 可以读取数据 */
if( FD_ISSET(fd, &readfds) ) /* 判断是否为fd文件描述符 */
{
/* 使用read函数读取数据 */
}
break;
}
}
2)、poll()函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
//fds是一个pollfd型结构数组
//nfds是要监视的文件描述符数量
//timeout超时时间,单位为ms;
//函数返回值为0,超时;
//函数返回值为-1,发生错误,并且设置errno为错误类型
//成功:函数返回值为revents域中不为0的pollfd结构体个数,也就是发生事件或错误的文件描述符数量;
缺点:poll()函数都会随着所监听的fd数量的增加,出现效率低下,且每次必须遍历“所有的描述符”来检查就绪的描述符,这个过程很浪费时间;
struct pollfd {
int fd; /* 要监视的文件描述符*/
short events; /* 要监视的事件,若文件描述符无效,则监视事件无效*/
short revents; /* 返回的事件*/
};
events事件类型:
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 有数据可以读取,等同于POLLIN;
poll()函数读非阻塞访问应用举例:
void main(void)
{
int ret;
int fd; /* 要监视的文件描述符 */
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK);
//打开文件成功,则fd 为“文件描述符”
/* 非阻塞式访问 */
/*O_NONBLOCK表示如果路径名指向FIFO/块文字/字符文件,则把文件打开和后继I/O设置为非阻塞*/
/* 构造结构体 */
fds.fd = fd; /* 要监视的文件描述符*/
fds.events = POLLIN; /* 监视数据是否可以读取 */
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时500ms */
//fds是一个pollfd型结构数组
//nfds=1是要监视的文件描述符数量
//timeout=500,超时时间,单位为ms;
if (ret) /* 数据有效 */
{
......
/* 读取数据 */
......
}
else if (ret == 0) /* 超时 */
{
......
}
else if (ret < 0) /* 错误 */
{
......
}
}
3)、epoll()函数
int epoll_create(int size)
//size:从Limux2.6.8开始该参数已经没有意义了,设置size>0就可以了;
//返回值:成功返回的是epoll句柄,如果为-1的话,表示创建失败;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
//epfd:要操作的epoll句柄,也就是使用 epoll_create()函数创建的epoll句柄。
//op:表示要对epfd(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; /* epoll事件 */
epoll_data_t data; /* 用户数据*/
};
events事件类型:
POLLIN 有数据可以读取。
POLLOUT 可以写数据。
POLLPRI 有紧急数据需要读取。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
EPOLLET 设置epo1l为边沿触发,默认触发模式为水平触发
EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个fd ,那么就需要将fd重新添加到epoll里面;
注意:
上面这些事件可以进行“或”操作,也就是说可以设置监视多个事件。
//函数返回值为0,成功;
//函数返回值为-1,失败,并且设置 errno 的值为相应的错误码;
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,错误;其他值,准备就绪的文件描述符数量;
注意:
epoll()更多的是用在大规模的“并发服务器”上,因为在这种场合下select()和 poll()并不适合。当设计到的文件描述符(fd)比较少的时候就适合用selcet()和poll();
4)、Linux驱动下的poll()函数
需要包含“#include <linux/delay.h>”
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_wait()函数不会引起阻塞,只是将“应用程序”添加到poll_table中
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参数;
6、绑定信息文档
设备树是用来描述板子上的设备信息,不同的设备其信息不同,反映到设备树中就是属性不同。
在设备树中,添加一个硬件对应的节点,我们从哪里查阅相关的说明呢?
在Linux内核源码中,有详细的TXT文档描述了如何添加节点,这些TXT文档叫做绑定文档,路径为:
Linux 源码目录/Documentation/devicetree/bindings
绑定文档Documentation/devicetree/bindings/gpio/gpio.txt,详细描述了 gpio 控制器节点各个属性信息;
led0 {
compatible = "zgq,led";
status = "okay";
led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
};
key0 {
compatible = "zgq,key";
status = "okay";
key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpiog>;/*指定父中断器为&gpiog*/
interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
/*指定中断号为3,中断类型和触发方式为下降沿触发*/
};
EXTI控制器的设备树绑定信息参考文档 :
Documentation/devicetree/bindings/interrupt-controller/st,stmm32-exti.txt
这篇关于Linux第86步_了解“阻塞和非阻塞IO”以及相关处理函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!