浅析 Linux 五种IO模型

在Linux驱动开发中,应用程序通过循环读取或者中断的方式都会使得CPU的占用率很高。本文介绍五种IO模型,可以用来优化文件读写方式,降低CPU的使用率
在这里插入图片描述

1. 阻塞式I/O模型

阻塞式I/O模型是最常用、最简单的模型。当应用程序对设备驱动进行操作时,若不能获取到设备资源,阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止

阻塞就是进程被休息, CPU处理其它进程去了。如下图,应用程序进行recefrom系统调用,操作系统收到recefrom系统调用请求,经过等待数据准备好和内核将数据从内核缓冲区复制到用户缓冲区这两个阶段后,调用返回,应用程序解除阻塞

在这里插入图片描述

阻塞访问代码示例:

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); 		/* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); 	/* 读取数据 */
2. 非阻塞式I/O模型

对于非阻塞IO,当设备不可用或数据未准备好时会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功

非阻塞就是轮询的方式,在该模型中,I/O操作不会立即完成。例如下图示,应用程序进行recefrom系统调用,操作系统收到recefrom系统调用请求,若无数据会立刻返回一个错误状态;应用程序则需要不断轮询,直到内核缓冲区数据准备好
在这里插入图片描述

非阻塞访问代码示例:

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); 	/* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); 				/* 读取数据 */
3. I/O复用模型

由于非阻塞I/O方式需要不断轮询,会消耗大量CPU时间,而后台又可能有多个任务在同时轮询,为此人们想到了一种方式:循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它

在这里插入图片描述

IO多路复用有三个特别的系统调用select、poll和epoll

  • select函数:能够监视的文件描述符数量最大为1024
/********************select函数原型***************************************/
int select( int nfds,
			fd_set *readfds,
			fd_set *writefds,
			fd_set *exceptfds,
			struct timeval *timeout )
// nfds:所要监视的三类文件描述集合中,最大文件描述符加1
// readfds:用于监视这些文件是否可以读取
// writefds:用于监视些文件是否可以写操作
// exceptfds:用于监视这些文件的异常
// timeout:超时时间
// 返回值:0,超时发生;-1,发生错误;其他值,表示可进行操作的文件描述符个数
/************************************************************************/
/****** fd_set类型变量的每一个位都代表一个文件描述符 *****/
// 例如:从一个设备文件中读取数据,需要定义一个fd_set变量,并传递给参数readfds
void FD_ZERO(fd_set *set)			//将fd_set变量的所有位都清零
void FD_SET(int fd, fd_set *set)	//将fd_set变量的某个位置1,即向变量中添加文件描述符fd
void FD_CLR(int fd, fd_set *set)	//将fd_set变量的某个位清零,即从变量中删除文件描述符fd
int FD_ISSET(int fd, fd_set *set)	//测试一个文件fd是否属于某个集合
/****** timeval结构体定义 *****/
struct timeval {
	long tv_sec; 	//秒 
	long tv_usec; 	//微妙
};

使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例:

void main(void) {
	int ret, fd; 			/* 要监视的文件描述符 */
	fd_set readfds; 		/* 读操作文件描述符集 */
	struct timeval timeout; /* 超时结构体 */
	
	fd = open("dev_xxx", O_RDWR | O_NONBLOCK); 	/* 非阻塞式访问 */
	FD_ZERO(&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);
	switch (ret) {
		case 0: 		/* 超时 */
			printf("timeout!\r\n");
			break;
		case -1: 		/* 错误 */
			printf("error!\r\n");
			break;
		default: 		/* 可以读取数据 */
			if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
				/* 使用 read 函数读取数据 */
			}
			break;
	}
}
  • poll函数:能够监视的文件描述符数量没有限制
/********************poll函数原型***************************************/
int poll(struct pollfd *fds,
		 nfds_t nfds,
		 int timeout)
// fds:要监视的文件描述符集合以及要监视的事件,pollfd结构体类型
// nfds:要监视的文件描述符数量
// timeout:超时时间(ms)
// 返回值:0,超时发生;-1,发生错误;其他值,发生事件或错误的文件描述符数量
/****** pollfd 结构体定义 *****/
struct pollfd {
	int fd; 		//文件描述符:若fd无效,则events监视事件也无效,revents返回0
	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); /* 非阻塞式访问 */
	/* 构造结构体 */
	fds.fd = fd;
	fds.events = POLLIN; 	/* 监视数据是否可以读取 */

	ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
	if (ret) { 	/* 数据有效 */
		......
		/* 读取数据 */
		......
	} else if (ret == 0) { 	/* 超时 */
		......
	} else if (ret < 0) { 	/* 错误 */
		......
	}
}
  • epoll函数:selcet 和 poll 函数会随着所监听的 fd 数量的增加,出现效率低下的问题,epoll 就是为处理大并发而准备的
/************************************************************************/
/****** 使用时应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄 ******/
int epoll_create(int size)   
// size 为大于0的数值
// 返回值:epoll句柄;返回-1,表示创建失败
/************************************************************************/
/** 句柄创建后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件 **/
int epoll_ctl(int epfd,
			  int op,
			  int fd,
			  struct epoll_event *event)
// epfd:要操作的epoll句柄
// op:对epfd进行的操作(包括EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL)
// fd:要监视的文件描述符
// event:要监视的事件类型,为 epoll_event 结构体类型指针
// 返回值: 0,成功; -1,失败,并且设置 errno 的值为相应的错误码
/****** epoll_event 结构体定义 *****/
struct epoll_event {
	uint32_t events; 	/* epoll 事件 */
	epoll_data_t data; 	/* 用户数据 */
};
/****** events可选的事件如下 ******/
EPOLLIN 	//有数据可以读取。
EPOLLOUT 	//可以写数据
EPOLLPRI 	//有紧急的数据需要读取。
EPOLLERR 	//指定的文件描述符发生错误。
EPOLLHUP 	//指定的文件描述符挂起。
EPOLLET 	//设置 epoll 为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT //一次性的监视,若监视完成后还需要再次监视某个fd,就需要将fd重新添加到epoll里
/************************************************************************/
/**** 上述步骤设置好后应用程序就可以通过 epoll_wait 函数来等待事件的发生 *****/
int epoll_wait (int epfd,
				struct epoll_event *events,
				int maxevents,
				int timeout)
// epfd:要等待的epoll
// events:指向epoll_event结构体的数组
// maxevents:events数组大小,必须大于 0
// timeout:超时时间,单位为ms
// 返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量
4. 信号驱动式I/O模型

信号类似于硬件上使用的"中断",只不过信号是软件层面上的。可理解为软件层次上对中断的一种模拟,驱动通过主动向应用程序发送可访问的信号,应用程序获取到信号后即可从驱动设备中读取或写入数据了

例如下图示,应该程序进行read系统调用,进程继续运行不会阻塞,立即返回,等待内核缓冲区数据准备好后,通过SIGIO信号通知应用程序,应用程序再进行read系统调用,内核将内核缓冲区中的数据拷贝到用户缓冲区,调用完成。整个过程中应用程序没有去查询驱动设备是否可以访问,是由驱动设备通过SIGIO信号告诉给应用程序的

在这里插入图片描述

信号驱动模型的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,如下所示:

#define SIGHUP 		1 		/* 终端挂起或控制进程终止 */
#define SIGINT 		2 		/* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 	3 		/* 终端退出(Ctrl+\组合键) */
#define SIGILL 		4 		/* 非法指令 */
#define SIGTRAP 	5 		/* debug 使用,有断点指令产生 */
#define SIGABRT 	6 		/* 由 abort(3)发出的退出指令 */
#define SIGIOT 		6 		/* IOT 指令 */
#define SIGBUS 		7 		/* 总线错误 */
#define SIGFPE 		8 		/* 浮点运算错误 */
#define SIGKILL 	9 		/* 杀死、终止进程 */
#define SIGUSR1 	10 		/* 用户自定义信号 1 */
#define SIGSEGV 	11 		/* 段违例(无效的内存段) */
#define SIGUSR2 	12 		/* 用户自定义信号 2 */
#define SIGPIPE 	13 		/* 向非读管道写入数据 */
#define SIGALRM 	14 		/* 闹钟 */
#define SIGTERM 	15 		/* 软件终止 */
#define SIGSTKFLT 	16 		/* 栈异常 */
#define SIGCHLD 	17 		/* 子进程结束 */
#define SIGCONT 	18 		/* 进程继续 */
#define SIGSTOP 	19 		/* 停止进程的执行,只是暂停 */
#define SIGTSTP 	20 		/* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 	21 		/* 后台进程需要从终端读取数据 */
#define SIGTTOU 	22 		/* 后台进程需要向终端写数据 */
#define SIGURG 		23 		/* 有"紧急"数据 */
#define SIGXCPU 	24 		/* 超过 CPU 资源限制 */
#define SIGXFSZ 	25 		/* 文件大小超额 */
#define SIGVTALRM 	26 		/* 虚拟时钟信号 */
#define SIGPROF 	27 		/* 时钟信号描述 */
#define SIGWINCH 	28 		/* 窗口大小改变 */
#define SIGIO 		29 		/* 可以进行输入/输出操作 */
#define SIGPOLL 	SIGIO	/* #define SIGLOS 29 */
#define SIGPWR 		30 		/* 断点重启 */
#define SIGSYS 		31 		/* 非法的系统调用 */
#define SIGUNUSED 	31 		/* 未使用信号 */

这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能

使用中断时需要设置中断处理函数,同样的,在应用程序中使用信号,就必须设置信号所使用的信号处理函数,在应用程序中使用signal函数来设置指定信号的处理函数,其函数原型如下所示:

/***********************signal函数原型*****************************/
sighandler_t signal(int signum, sighandler_t handler)
// signum:要设置处理函数的信号
// handler:信号的处理函数
// 返回值:成功,返回信号的前一个处理函数;失败,返回 SIG_ERR
/***********************信号处理函数原型***************************/
typedef void (*sighandler_t)(int)
5. 异步I/O模型

相对于同步IO,异步IO不是顺序执行。例如下图示,用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的

在这里插入图片描述

6. 五种I/O模型的比较

对于Liunx的五种I/O模型,主要在等待数据和数据复制这两个时间段不同,它们的区别详见下表

在这里插入图片描述

在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安迪西嵌入式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值