Linux第86步_了解“阻塞和非阻塞IO”以及相关处理函数

2024-04-11 12:52

本文主要是介绍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”以及相关处理函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/894129

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级