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

相关推荐
云飞云共享云桌面2 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq2 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮3 小时前
Linux 使用中的问题
linux·运维
dsywws4 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零5 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
小林熬夜学编程6 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
做网站建设制作设计小程序推广7 小时前
如何建购物网站提升用户体验
经验分享
程思扬7 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
sun0077007 小时前
拷贝 cp -rdp 和 cp -a
linux·运维·服务器
wowocpp7 小时前
ubuntu 22.04 server 安装 anaconda3
linux·运维·ubuntu