文件读写——如何实现非阻塞式IO?

以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。

一、阻塞式IO

1、阻塞式的概念

我们知道,有些函数在调用时(比如网络编程中的recv函数),如果某些条件不满足,则会进入等待状态,直到条件满足才去执行操作并返回。这种函数就叫做阻塞式函数。在文件IO中,阻塞式函数(或者API)有wait、pause、sleep等函数。

另外,使用read或write某些文件时(read和write函数本身不是阻塞式的),文件对应的对象可能是阻塞式的,比如键盘和鼠标就是阻塞式设备

Linux系统默认使用阻塞式。

2、阻塞式的缺点

阻塞式的设计,既有它的好处,也有它的困境。它的好处是简单容易实现,有利于充分发挥CPU的性能。

它的缺点,我们用一个例子说明。上面提及,键盘和鼠标都是阻塞式设备。

1、在程序中读取键盘

比如执行下面这段代码,read函数会等待键盘输入完成(因为shell是行缓冲的,输入回车时表示输入完成,不按下回车就表示输入还没有结束),从而阻塞。

int main(void)
{
	// 读取键盘
	// 键盘就是标准输入,stdin
	
	char buf[100];
	
	memset(buf, 0, sizeof(buf));
	printf("before read.\n");
	read(0, buf, 5);
	printf("读出的内容是:[%s].\n", buf);
	
	return 0;
}

2、 在程序中读取鼠标

比如执行下面这段代码。(这里我们先使用cat命令来查看使用的鼠标对应哪个文件,以便在代码中open时使用。)代码执行后,移动鼠标屏幕有数据输出。

int main(void)
{
	// 读取鼠标
	int fd = -1;
	char buf[200];
	
	fd = open("/dev/input/mouse1", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	memset(buf, 0, sizeof(buf));
	printf("before read.\n");
	read(fd, buf, 50);
	printf("读出的内容是:[%s].\n", buf);
	
	
	return 0;
}

3、 在程序中同时读取鼠标与键盘

我们希望实现动键盘就显示键盘的内容,动鼠标就显示鼠标的内容。

但是下面的程序的现象却是:如果顺着程序,先鼠标再键盘则顺利显示;如果先键盘的话,会被鼠标阻塞住。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(void)
{
	// 读取鼠标
	int fd = -1;
	char buf[200];
	
	fd = open("/dev/input/mouse1", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	memset(buf, 0, sizeof(buf));
	printf("before 鼠标 read.\n");
	read(fd, buf, 50);
	printf("鼠标读出的内容是:[%s].\n", buf);
	
	// 读键盘
	memset(buf, 0, sizeof(buf));
	printf("before 键盘 read.\n");
	read(0, buf, 5);//键盘就是标准输入,stdin,用0表示,它是默认打开的。
	printf("键盘读出的内容是:[%s].\n", buf);
	
	return 0;
}

二、非阻塞式IO

1、实现非阻塞式IO的两种方法

如何实现非阻塞式IO呢?有两种方法:

(1)方法一:打开文件时添加O_NONBLOCK标志。

(2)方法二:普通打开文件后,使用fcntl函数设置文件描述符的属性。

2、代码示例

分别利用O_NONBLOCK标志、fcntl函数, 将上节中阻塞式的鼠标和键盘读取改为非阻塞式的。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	// 读取鼠标
	int fd = -1;
	int flag = -1;
	char buf[200];
	int ret = -1;
	
    //方法一:利用O_NONBLOCK标志将鼠标设置为非阻塞式的  
	fd = open("/dev/input/mouse1", O_RDONLY | O_NONBLOCK);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}

	//方法二:利用fcntl函数设置键盘为非阻塞式的                                   
	// 把0号文件描述符(stdin)变成非阻塞式的    
	flag = fcntl(0, F_GETFL);		// 先获取原来的flag
	flag |= O_NONBLOCK;				// 添加非阻塞属性
	fcntl(0, F_SETFL, flag);		// 更新flag
	// 这3步之后,0就变成了非阻塞式的了
	
	while (1)
	{
		// 读鼠标
		memset(buf, 0, sizeof(buf));
		ret = read(fd, buf, 50);
		if (ret > 0)
		{
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
		
		// 读键盘
		memset(buf, 0, sizeof(buf));
		ret = read(0, buf, 5);
		if (ret > 0)
		{
			printf("键盘读出的内容是:[%s].\n", buf);
		}
	}
	
	return 0;
}

三、并发式IO的解决方案(解决多路阻塞式IO的方案)

由上面的例子引出话题:并发式IO的解决方案,或者说多路阻塞式IO的解决方案。

比如上面的例子中,鼠标与键盘都是阻塞式的IO设备(即多路阻塞),它们可能同时启动与输入数据(即并发式)。在上面例子中,我们使用fcntl函数和O_NONBLOCK标志解决了阻塞的问题,但是性能不是很好。其实还可以利用多路复用IO、异步IO、存储映射IO来解决阻塞的问题。

1、多路复用IO

(1)方法原理

多路复用IO英文为“ IO multiplexing ”,主要用于解决多路阻塞式IO的问题。

它涉及select函数、poll函数,这两个函数的设计思想一样,只是外部特征不一样,我们可以任选其一来完成功能。

它的实现原理是:外部阻塞式(select函数本身是阻塞式的),内部自动轮询多路阻塞式IO(键盘A,鼠标B,这两者本身是阻塞式的),只要AB至少有一个输入,则select由阻塞返回。

(2)代码示例

1)比如使用select函数实现同时读取鼠标键盘。

 int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

在代码中设置了超时时间,即阻塞时间不能太长,如果很久没有IO来激活select,则表明超时了。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>

int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	fd_set myset;
	struct timeval tm;//设置超时时间,即阻塞的时间不能太长,
                      //如果很久都没有IO来激活select,则表明超时了。
	
	fd = open("/dev/input/mouse1", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	// 当前有2个fd,一共是fd一个是0
	// 处理myset
	FD_ZERO(&myset);
	FD_SET(fd, &myset);
	FD_SET(0, &myset);
	
	tm.tv_sec = 10;
	tm.tv_usec = 0;
                  //+1,是因为0~fd,则共有fd+1个文件描述符
	ret = select(fd+1, &myset, NULL, NULL, &tm);
	if (ret < 0)//错误
	{
		perror("select: ");
		return -1;
	}
	else if (ret == 0)//表明超时
	{
		printf("超时了\n");
	}
	else//>0表示有一路IO激活了
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (FD_ISSET(0, &myset))
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		
		if (FD_ISSET(fd, &myset))
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}

	return 0;
}

2)用poll函数实现同时读取键盘鼠标。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>

int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	struct pollfd myfds[2] = {0};
	
	fd = open("/dev/input/mouse1", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	// 初始化我们的pollfd
	myfds[0].fd = 0;		// 键盘
	myfds[0].events = POLLIN;	// 等待读操作
	
	myfds[1].fd = fd;		// 鼠标
	myfds[1].events = POLLIN;	// 等待读操作

	ret = poll(myfds, fd+1, 10000);
	if (ret < 0)
	{
		perror("poll: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (myfds[0].events == myfds[0].revents)
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		
		if (myfds[1].events == myfds[1].revents)
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}

	return 0;
}

2、异步IO

(1)方法原理

几乎可以认为,异步IO就是操作系统用软件实现的一套中断响应系统。

异步IO的工作方法是:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。

主要涉及两个函数:fcntl函数(F_GETFL(获取flag)、F_SETFL、O_ASYNC(表明可以接收异步通知)、F_SETOWN(设置通知谁,一般都是通知当前进程)),以及signal或者sigaction(SIGIO)函数。

(2)代码实践

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

int mousefd = -1;

// 绑定到SIGIO信号,在函数内处理异步通知事件
void func(int sig)
{
	char buf[200] = {0};
	
	if (sig != SIGIO)
		return;

	read(mousefd, buf, 50);
	printf("鼠标读出的内容是:[%s].\n", buf);
}

int main(void)
{
	// 读取鼠标
	char buf[200];
	int flag = -1;
	
	mousefd = open("/dev/input/mouse1", O_RDONLY);
	if (mousefd < 0)
	{
		perror("open:");
		return -1;
	}	
	// 把鼠标的文件描述符设置为可以接受异步IO
	flag = fcntl(mousefd, F_GETFL);
	flag |= O_ASYNC;
	fcntl(mousefd, F_SETFL, flag);
	// 把异步IO事件的接收进程设置为当前进程
	fcntl(mousefd, F_SETOWN, getpid());
	
	// 注册当前进程的SIGIO信号捕获函数
	signal(SIGIO, func);
	
	// 读键盘,在这里是当前进程
	while (1)
	{
		memset(buf, 0, sizeof(buf));
		read(0, buf, 5);
		printf("键盘读出的内容是:[%s].\n", buf);
	}
		
	return 0;
}

3、存储映射IO

该方法体现在mmap函数,将一个文件和一段内存映射起来,比如LCD设备文件和显存的对应,比如IPC之共享内存。

由于共享而不是复制,减少内存操作,处理大文件时效率高(一般用于视频处理)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天糊土

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

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

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

打赏作者

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

抵扣说明:

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

余额充值