笔记地址:https://note.youdao.com/ynoteshare1/index.html?id=09f9fa91b90576adac4640f48b9210a3&type=note
前面第一节我们学习了对IO的open、read、write等阻塞式文件操作,这一篇我们将会学习对IO的一些高级操作。 2019/04/30 20:51
目录
一、非阻塞IO的引入
1、阻塞与非阻塞
- a) 函数操作文件时,因文件类型而阻塞,阻塞与函数本身无关
·读某些文件:由于数据不存在会导致调用者永远阻塞
读管道文件:管道是进程间通信用的特殊文件,读管道时,如果管道中并无数据会导致
对管道的读操作会阻塞。
- b) 写某些文件:在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致
- c) 某些函数本身就是阻塞的
wait、pause、sleep等函数以及某些进程间通信的函数(如当消息队列的消息接受函数设置了阻塞时),这些函数调用本身就是阻塞的
2、如何修改阻塞为非阻塞
通过前面的学习,我们知道某些文件默认打开后默认对文件的操作就是阻塞的,但是利用对文件描述符设置,可将其操作设置为非阻塞的,主要的方法有如下两种:
- 1)打开文件时指定非阻塞,以非阻塞的方式打开标准输入文件(O_NONBLOCK函数,指定为了非阻塞)
|
int main(void){ int fd = -1; fd = open("/dev/stdin", O_RDONLY|O_NONBLOCK); if(fd < 0){ perror("open stdin is fail"); exit(-1); } return 0; } |
- 2)将已经打开了的文件描述符设置为非阻塞的
(用fcntl函数进行设置,具体可查看第一节对fcntl函数的梳理)
|
int main(void) { int fd = -1, flag = -1; /* F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态 * STDIN_FILENO:指向标准输入文件的文件描述符0 */ flag = fcntl(STDIN_FILENO, F_GETFL); flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志 //将修改后的包含了非阻塞标志的新状态重新设置回去 fcntl(STDIN_FILENO, F_SETFL, flag); return 0; } |
二.阻塞式IO的困境(举例)
1、程序中读取键盘
|
int main(void) { // 读取键盘 // 键盘就是标准输入,故为0号文件描述符,stdin char buf[100]; memset(buf, 0, sizeof(buf)); printf("before read.\n"); read(0, buf, 5); printf("读出的内容是:[%s].\n", buf); return 0; } |
2、程序中读取鼠标
我们鼠标的驱动文件如下:当查看mouse1,移动鼠标,并没有数据,当查看mouse0时,有乱码的数据,故我们的鼠标设备文件为:mouse0
|
int main(void) { // 读取鼠标 int fd = -1; char buf[200]; fd = open("/dev/input/mouse0", 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、程序中同时读取键盘和鼠标
|
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; } |
4、问题分析
(程序上设计是先读取鼠标,再读取键盘),如果顺序用反了,会怎么样???
运行程序时由于先读的是鼠标,它阻塞了键盘,所以我们先键盘敲入数据是没有用的,当移动鼠标后,鼠标数据打印出来,这时进程又阻塞在了读鼠标处,所以此时移动鼠标是没有用的,这时必须键盘敲入数据,才能回到读鼠标处(标准输入处),由于鼠标数据是整形的坐标值,所打印出来是乱码。
所以,我们先输入键盘后鼠标,则会出错,这就是阻塞式IO的问题
那么怎么解决这个问题纳,那就得引入并发式IO的解决方案了。
三.并发式IO的解决方案
1、非阻塞式IO
上面我们知道,键盘和鼠标的读导致了相互的阻塞,我们输入时并不通畅,现在我们将它们都改为非阻塞的,那么它们就不会相互阻塞,输入就会变得通畅
|
int main(void) { // 读取鼠标 int fd = -1; char buf[200]; int flag=-1; int ret=-1; fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open:"); return -1; } //F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态 // STDIN_FILENO:指向标准输入文件的文件描述符0 flag = fcntl(STDIN_FILENO, F_GETFL); flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志 fcntl(STDIN_FILENO, F_SETFL, flag);//将修改后的包含了非阻塞标志的新状态重新设置回去
while(1) { //读鼠标 memset(buf, 0, sizeof(buf)); //printf("before 鼠标 read.\n"); ret=read(fd, buf, 50); if(ret>0) //>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; } |
这么设置后,鼠标和键盘不再相互阻塞,所以运行这个程序时不必再忌讳谁先输入的问题了,谁先输入都可以。
只是这种非阻塞会导致进程时刻都处在循环的,这种轮询的机制会非常的消耗cpu资源,并不提倡这方法
2、利用两个进程实现同时读键盘和鼠标
|
int main(void) { char buf[100] = {0}; int fd = -1, ret = -1;
/* 开两个进程,父进程读鼠标,子进程读键盘 */ ret = fork(); if(ret == 0)//为0,表示还没有接收到东西 { while(1) { /* 读键盘 */ memset(buf, 0, sizeof(buf)); ret=read(0, buf, 5); if(ret > 0) {printf("键盘读出的内容是:[%s].\n", buf);} } } else if(ret > 0) { fd = open("/dev/input/mouse0", O_RDONLY |O_NONBLOCK); if(fd < 0) { perror("open error"); return -1; } while(1) { /* 读鼠标 */ memset(buf, 0, sizeof(buf)); ret = read(fd, buf, 50); if(ret > 0) {printf("鼠标读出的内容是:[%s].\n", buf); } } } return 0; } |
我们打开两个进程,一个进程读键盘,一个进程读鼠标,由于进程本身就是迸发同时向前运行的,所以这里再也不需要将键盘和鼠标设置为非阻塞,这种多进程的方法,进程之间切换也是很耗费资源的,并且当程之间需要相互共享资源的话,这就需要加入进程间通信机制,这就会使得我们的程序变得更加的复杂。
3、多路复用IO
-
IO多路复用原理
其基本思想是构造一个文件描述符的表,在这个表里面存放了阻塞的文件描述符,然后调用多路复用函数,该函数每隔一段时间会去检查一次,看表中是否有某个或几个文件描述符有动作,没有就休眠一段时间,再隔一段再去检查一次,如果其中的一个或多个文件名描述符有了动作,函数返回不在休眠,将分别对其有动作的文件描述符做相应操作。
实际上多路复用本身也存在轮询检查过程,但是绝大部分时间却是在休眠,所以就避免了一般的轮询机制,以及多进程实现所带来的相当的资源损耗。多路复用原理如下图所示:
注意:一般情况下集合中都设置阻塞的文件描述符,设置非阻塞的描述符是没有意义的。
外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO
·select机制;
-
select函数
1)、函数原型和所需头文件
|
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); |
2)、函数功能
这两个函数都是为了实现多路复用,但是这两个函数都能够被信号中断,pselect函数能够通过sigmask参数屏蔽掉那些我们不希望中断pselect系统函数的信号。
3)、函数参数
·int nfds:readfds, writefds, exceptfds这三个集合中最大描述符+1(因为描述符是从0算起的),用以说明需要关心描述符的范围,这个范围必须包含所有集合中的文件描述符。
|
·fd_set *readfds:读集合,设置读会阻塞的描述符。 ·fd_set *writefds: 写集合,设置写会阻塞的描述符。 ·fd_set *exceptfds: 设置异常描述符的集合。 ·struct timeval *timeout:成员结构如下: struct timeval { long tv_sec; /* seconds(秒) */ long tv_usec; /* microseconds (微秒)*/ }; |
(1)该参数填NULL表示不设置超时,这种情况下如果没有描述符响应,同时也没有 信号中断的话则永远阻塞。
(2)如果需要设置超时,则需填写设置了时间结构体的地址,如果没有描述符响应,但设置的时间却到了,立即返回而不再阻塞。
4)、函数返回值
·返回-1:说明函数调用失败,errno被设置。
·返回0:超时时间到并且没有一个描述符有响应,返回0说明没有一个描述符准备好。
·返回值>0:返回有响应的文件描述符的数量。
-
select函数测试用例
|
int main(void) { // 读取鼠标 int fd = -1; char buf[200]; int ret=-1; fd_set myset; struct timeval tim; fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open:"); return -1; } // 当前有2个fd,一个是fd:用于鼠标,一个是标准输入0 键盘 /* 设置读集合,设置操作必须放在循环内 */ FD_ZERO(&myset); //清空读集合 FD_SET(fd, &myset); //将fd设置到读集合中 FD_SET(0, &myset);//将标准输入0也设置到读集合中 /* 设置超时时间,设置必须放在循环内 */ tim.tv_sec= 3; //秒 tim.tv_usec= 0; //微秒
/* -如果集合中没有描述符响应 * 1.如果第四个参数被设为NULL,select将一直阻塞知道集 * 合中描述符有响应为止 * 2.如果第四个参数设置了超时时间,时间到则函数超时返回 * -如果集合中有一个或多个描述符响应,则集合被内核重新设 * 置,用于存放有响应的描述符,设置的超时时间也被清空*/ ret = select(fd+1, &myset, NULL, NULL, &tim); if (ret < 0)//说明函数调用失败,errno被设置 { perror("select: "); return -1; } else if (ret == 0)//返回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机制;
-
poll函数
实际上poll机制,与select差不多,只是具体调用的实现不一样,与select 不同,poll不是为每个条件构造一个描述符集,而是构造一个 pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
1)、函数原型和所需头文件
|
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask); |
2)、函数功能
·这两个函数都是为了实现多路复用,这两个函数都能够被信号中断,但是ppoll函数能够通过sigmask参数屏蔽掉那些我们不希望中断ppoll系统调用的信号。
3)、函数参数
·struct pollfd *fds:struct pollfd结构体数组,每个数组成员设置了我们需要多路IO操作的每个描述符,该结构体成员结构如下:
|
struct pollfd { int fd; /* file descriptor:文件描述符 */ short events; /* requested events:设置我们希望发生的事件 */ short revents; /* returned events :实际发生的事件*/ }; |
·nfds_t nfds:结构体数组struct pollfd *fds的成员数量。
·int timeout:超时时间,单位为毫秒,如填写为3000,表示3秒超时,如果不希望设置超时,该参数填写负数(如-1)即可。
poll的events和revents标志的设置如下表:
4)、函数返回值
·返回-1:说明函数调失败,errno被设置。
·返回0:超时时间到并且没有文件描述符有响应。
·返回值>0:返回有响应的文件描述符的数量。
-
poll函数测试用例
|
int main(void) { // 读取鼠标 int fd = -1, ret = -1; char buf[200]; struct pollfd myfds[2] = {0}; fd = open("/dev/input/mouse0", O_RDONLY| O_NONBLOCK);//打开鼠标 if (fd < 0) { perror("open:"); return -1; } /*向数组中设置设置需要多路监听的描述符,设置只需要设置一次就行 */ // 初始化我们的pollfd myfds[0].fd = 0; // 键盘 标准输入IO myfds[0].events = POLLIN; // 等待读操作 myfds[1].fd = fd; // 鼠标 myfds[1].events = POLLIN; // 等待读操作
/* myfds:struct pollfd结构提数组,fd+1:需要多路监听的数量,3000:超时时间 3秒*/ ret = poll(myfds, fd+1, 3000); if (ret < 0)//说明函数调失败,errno被设置。 { 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; } |
当描述符有响应时,revents会被内核填写响应的类型,如events==revents,说明这个响应是我们希望的响应,利用该文件描述符实现相应操作,否则就不是我们希望的响应。
4、异步通知(异步IO)
-
何为异步IO
所谓异步io就是,当某个事件准备好,进程会被发送一个SIGIO的异步信号,进程受到这个信号的通知后,会调用信号处理函数去处理事件,在事件没有准备好时,进程并不需要轮询事件或者阻塞等待事件,进程可以忙自己的事情直到等到别人发送异步信号SIGIO通知某事件发生。
异步IO有点类似于中断响应系统,而发送异步信号SIGIO通知某事件发生====响应中断并执行中断服务函数。
-
异步IO设置的步骤如下:
(1) 调用signal或sigaction为该信号建立一个信号处理程序。
(2) 以命令F_SETOWN调用fcntl来设置接收信号进程PID和进程组GID。
(3) 以命令F_SETFL调用fcntl设置O_ASYNC状态标志,使在该描述符上可以进行异步I/O。第3步仅用于指向终端或网络的描述符
|
char buf[200]; int myfd = -1;
void func(int sig); /*绑定SIGIO信号,在函数内部处理异步通知事件,类似于硬件中断服务函数*/ void func(int sig) { if(sig !=SIGIO) { return; } memset(buf, 0, sizeof(buf)); printf("before 鼠标 read.\n"); read(myfd, buf, 50); printf("鼠标读出的内容是:[%s].\n", buf); }
int main(void) { // 读取鼠标 int flag = -1; myfd = open("/dev/input/mouse0", O_RDONLY); if (myfd < 0) { perror("open:"); return -1; } //设置异步IO通知 //①把鼠标的文件描述符设置为可以接收异步IO flag = fcntl(myfd, F_GETFL); flag |= O_ASYNC; fcntl(myfd, F_SETFL, flag); //②设置当前进程获取SIGIO信号 fcntl(myfd, F_SETOWN, getpid()); //③设置当前进程的捕获SIGIO信号 signal(SIGIO, func); /*有了以上的设置,则对鼠标的异步IO处理设置已经搞定 */ while(1) { // 读键盘 memset(buf, 0, sizeof(buf)); printf("before 键盘 read.\n"); read(0, buf, 5); printf("键盘读出的内容是:[%s].\n", buf); } return 0; } |
5、存储映射IO
-
1、存储映射的好处
存储映射I/O使一个磁盘文件与虚拟存储空间中的一段缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。
可以理解为,建立一个缓存区,这个缓存区就是真实需要访问的数据区的影子,不占用任何实际物理空间,当对这个“影子缓存区操作”,即是对真实文件的读写操作
-
2、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算
-
3、存储映射IO需要使用mmap函数,这里,我们暂不学习。
参考博文注明:https://www.cnblogs.com/tshua/p/5763744.html