linux 高级IO

1.非阻塞IO

阻塞读文件

当读某些文件时,如果文件没有数据,会导致读操作阻塞,如:

  1. 读鼠标/键盘等字符设备文件;
  2. 读管道文件(PIPE,FIFO);
#include 
#include 
#include 
#include 
#include 
#include 



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);
	printf("键盘读出的内容是:[%s].\n", buf);
	
	
	return 0;
}


/*
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;
}
*/


/*
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;
}
*/

问题:

1. 读普通文件会阻塞吗?
读普通文件不会阻塞,因为有数据就成功返回,没有数据就返回0,并不会阻塞;

2. 写文件会阻塞吗?
写文件会阻塞,如写管道文件,会需要先从读端把管道文件读取出来。但是写普通文件是不会阻塞的。

阻塞读文件的意义

文件没有数据读取而阻塞,导致线程进入阻塞状态并不会占用CPU,节省CPU资源。当然特殊情况下,如果需要非阻塞读,OS也提供了非阻塞读文件的方式。

如何以非阻塞方式读文件

两种方式,本质都是添加O_NONBLOCK选项到文件状态标志:
1. open文件时指定O_NONBLOCK选项

2. fcntl修改打开文件属性,指定O_NONBLOCK选项

#include 
#include 
#include 
#include 
#include 
#include 



int main(void)
{
	// 读取鼠标
	int fd = -1;
	int flag = -1;
	char buf[200];
	int ret = -1;
	
	fd = open("/dev/input/mouse1", O_RDONLY | O_NONBLOCK);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	
	// 把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));
		//printf("before 鼠标 read.\n");
		ret = read(fd, buf, 50);
		if (ret > 0)
		{
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
		
		// 读键盘
		memset(buf, 0, sizeof(buf));
		//printf("before 键盘 read.\n");
		ret = read(0, buf, 5);
		if (ret > 0)
		{
			printf("键盘读出的内容是:[%s].\n", buf);
		}
	}
	
	return 0;
}


/*
int main(void)
{
	// 读取鼠标
	int fd = -1;
	char buf[200];
	
	fd = open("/dev/input/mouse1", O_RDONLY | O_NONBLOCK);
	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;
}
*/

/*
int main(void)
{
	// 读取键盘
	// 键盘就是标准输入,stdin
	
	char buf[100];
	int flag = -1;
	
	// 把0号文件描述符(stdin)变成非阻塞式的
	flag = fcntl(0, F_GETFL);		// 先获取原来的flag
	flag |= O_NONBLOCK;				// 添加非阻塞属性
	fcntl(0, F_SETFL, flag);		// 更新flag
	// 这3步之后,0就变成了非阻塞式的了
	
	memset(buf, 0, sizeof(buf));
	printf("before read.\n");
	read(0, buf, 5);
	printf("读出的内容是:[%s].\n", buf);
	
	return 0;
}
*/

2.IO多路复用(I/O multiplexing)

前面我们为了实现同时读键盘和鼠标,采用非阻塞或多进程来实现,但是这两种方法都有一定的缺陷,非阻塞会需要轮询,很消耗cpu资源,如果采用多进程时,进程之间切换也是很耗费资源的,并且当程之间需要相互共享资源的话,这就需要加入进程间通信机制,这就会使得我们的程序变得更加的复杂。

多路IO的优势

针对类似于同时读取鼠标/键盘数据的情况而言,

  1. 多进程
    开销太大,不建议使用。

  2. 非阻塞方式
    需要搭配while循环不断轮询,耗费CPU资源,不建议使用。

  3. 多线程
    开销较低,常用方法

  4. 多路IO
    使用多路IO时,由于监听时如果没有动静(没有数据到来),监听线程休眠,开销也很低。

select和poll

多路IO两种实现方式:select, poll。select更常用。

select示例代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	fd_set myset;
	struct timeval tm;
	
	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;

	ret = select(fd+1, &myset, NULL, NULL, &tm);
	if (ret < 0)
	{
		perror("select: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路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;
}

poll示例代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 



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;
}

3.异步IO

所谓异步io就是,当某个事件准备好,进程会被发送一个SIGIO的异步信号,进程受到这个信号的通知后,会调用信号处理函数去处理事件,在事件没有准备好时,进程并不需要轮询事件或者阻塞等待事件,进程可以忙自己的事情直到等到别人发送异步信号SIGIO通知某事件发生。

所谓异步就是,进程接收异步信号的时机完全是随机的,这个时机完全取决于事件发生的时刻,接受信号的进程是没有办法预测的。

使用异步IO方式读取鼠标和键盘

#include 
#include 
#include 
#include 
#include 
#include 
#include 



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));
		//printf("before 键盘 read.\n");
		read(0, buf, 5);
		printf("键盘读出的内容是:[%s].\n", buf);
	}
		
	return 0;
}

4.存储映射IO

普通文件读写方式的缺陷

调用read/write对普通文件进行读写, 由于底层封装多层, 在面对频繁读写大量数据时, 效率低下. 这就引入了存储映射.

存储映射mmap

存储映射(memory map)简称mmap, 是直接将实际存储的物理地址映射到进程空间, 而不使用read/write 函数. 对普通文件存储, 是硬盘地址映射到进程空间. 这样, 省去中间繁杂调用过程, 快速对文件进行大量输入输出.

注意: 使用mmap时, open 不可省, read/write 可省.

存储映射 vs 共享内存

存储映射mmap原理类似于System V共享内存, 不过用途还是有区别:

  1. 共享内存
    主要应用进程间通信, 2个进程的进程工具映射到同一段RAM区域, 从而进行通信.
    共享内存映射到的物理地址是RAM地址.

  2. 存储映射
    mmap主要用于对文件进行大数据量的高效输入输出.
    mmap 映射到的物理地址是硬盘地址.

mmap函数

原型:

#include

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

功能:
将文件所在的磁盘空间映射到进程空间.

返回值:
调用成功, 则返回映射的起始虚拟地址; 失败则返回(void *)-1, errno设置.

你可能感兴趣的:(linux,ubuntu,c语言)