一,非阻塞IO与阻塞式IO
1. 非阻塞式IO和阻塞式IO的区别
非阻塞式IO是用户发出IO请求后不进行等待,直接获得一个结果,通常使用时用O_NONBLOCK配合fcntl来完成。阻塞式IO是当用户线程发出IO请求之后,内核会去查看资源是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU,常见的阻塞有wait、pause、sleep等函数,read或write某些文件时。
2. 阻塞式IO的好处
对于内核来说,内部大部分默认的IO方式都设置为了阻塞式,这样的好处是为了充分发挥操作系统的性能,让CPU时刻工作在被需要的情况。比如对于A进程来说,它需要满足一定的条件才能继续往后进行,但是可能在短时间内该条件不能够满足,那么该进程会阻塞住,并交出CPU供其他进程使用。等到条件满足时,阻塞的地方解除阻塞,CPU回到该进程继续执行。这样极大程度地提高了CPU的利用率,减少原地踏步的时间,提高了整体系统的效率。
3. 阻塞式IO的困境
但是对于一个进程来说,里面可能有2个或多个阻塞式IO的地方,这就面临着一个问题:先阻塞的地方需要满足条件后才能去执行后阻塞的地方,也就是如果后阻塞的地方虽然达到了条件,但是先阻塞的地方卡住了,后面的结果还是没法得到。
4.举个例子:设置read函数来读取鼠标和键盘输入的内容,先对鼠标进行阻塞式访问,再对键盘进行阻塞式访问,此时先晃动鼠标得到鼠标的内容,再键盘输入得到键盘的内容。但是如果先键盘输入,那么进程会一直阻塞在鼠标输入那里,直到晃动鼠标才能够通过,这就带来了一个输入必须有先后顺序的困扰。
#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);
printf("键盘读出的内容是:[%s].\n", buf);
return 0;
}
注:键盘的标准输入设备,对应的文件描述符是0;鼠标不是标准设备,但是可以ls /dev/input查看,确认当前使用的设备后,open("/dev/input/mouse1", O_RDONLY);打开对应的设备文件即可。
二、并发式阻塞IO的解决
1、非阻塞式IO
最简单的解决方法就是将2个IO位置改变为非阻塞的方式,类似于一种轮询的方式,通过循环读取鼠标和键盘来执行对应的IO操作。
还是以读鼠标和键盘为例子:
#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;
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));
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;
}
实验现象分析:无论是先动鼠标还是先动键盘,都能打印信息,但是CPU被一直耗在这里,其他进程不能得到CPU调度,降低了CPU的使用率。
2、IO多路复用
2.1、IO多路复用的原理
IO多路复用的方式通常需要借助select或poll函数,表现形式为外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。
外部阻塞式的意思是select/poll函数对外表现为阻塞式,也就是最普通的阻塞式方式,两个IO都被封装在了select/poll中。内部非阻塞式自动轮询的意思是,在封装的内部,对于鼠标和键盘这两个输入一直处于自动轮询的方式,谁满足条件谁输出。多路阻塞式IO的意思是鼠标和键盘的封装内部仍然为阻塞式IO。
那么内部仍然是阻塞式IO的话跟之前不久一样了吗,还是会卡住?答案当然不是了,对于是否满足输出条件已经在最外层的select/poll中进行判断了,所以内部IO虽然还是阻塞式的,但是如果判断进来以后说明条件已经满足,即虽然是阻塞式,但是一定会执行。在select内部封装的两个IO相当于并行的,不存在先后顺序,只要满足条件就会到对应的分支去执行对应的操作。
2.2、IO多路复用的使用
(1)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;
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;
}
(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;
}
实验结果都一样:
3、异步IO
1、何为异步IO
异步IO可以理解为操作系统用软件实现的一套中断响应系统。
它的工作方式为:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
2、涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
(2)signal或者sigaction(SIGIO)
3、代码实践
#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;
}
4、存储映射IO
1、mmap函数
mmap是memory map的缩写,意思是存储映射。其实就会将普通文件的硬盘空间的物理地址映射到进程空间的虚拟地址。通常情况下,进程空间的虚拟地址只映射自己底层物理空间的物理地址,但是使用mmap时,他会将文件的硬盘空间的地址也映射到虚拟地址空间,这么一来应用程序就可以直接通过映射的虚拟地址操作文件,根本就不需要read、write函数了,使用地址操作时省去了繁杂的中间调用过程,可以快速对文件进行大量数据的输入输出。
2、IPC之共享内存
存储映射与共享内存在原理上很像,都是进程虚拟内存空间向外做映射。
共享内存原理:共享内存是让不同的进程空间映射到同一片物理内存上,然后通过共享的物理内存来实现进程间通信。
3.对比存储映射和共享内存
(1)存储映射,其实也可以用来实现进程间通信
比如A和B进程都映射到同一个普通文件上,这时A进程往里写数据,B进程从里面读数据,反过来也是一样的,如此就实现了进程间的通信。但是这顶多只算是广义上的通信,所谓广义上的通信就是,只要不是OS提供专门的IPC,就不是专门的进程间通信,只能算是广义的IPC。实际上,我们也不会使用mmap映射普通文件来实现进程间通信,因为操作硬盘的速度相比操作内存来说低了很多,如果你想实现进程间大量数据通信的话,完全可以使用与存储映射原理类似的“共享内存”来实现,而且速度很快。
(2)虽然存储映射和共享内存原理相似,但是各自用途不同
共享内存 实现进程间大量数据通信(共享)。
存储映射 对文件进行大量数的高效输入输出。
4、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算