1、1、非阻塞IO
1.阻塞与非阻塞
(1)阻塞:就是当前的函数要执行的话,需要某些条件,但是没有达到,就被阻塞住,内核挂起,当前进程暂停。CPU被拿去运行别的进程了。比如父进程执行wait这个阻塞函数,等待子进程结束后,去回收子进程剩余的8KB内存资源,如果这个时候子进程没有结束,父进程的wait就会被阻塞,因为条件没有达到,这个时候父进程就暂停了,内核就挂起了,CPU的时间就去执行别的进程了,不在父进程这里耗了,当子进程结束的时候,OS会给子进程的父进程发送一个SIGCHILD信号,这时父进程就被唤醒了,wait函数执行就将子进程回收掉了
(2)常见的阻塞:
wait、sleep、pasue等函数;
read或write写文件的时候
(3)阻塞的好处:有利于操作系统的性能的发挥。因为阻塞住了进程会暂停,CPU可以去做别的事情,并且要完成的任务在一定情况下也会被完成的。
(4)但是一个进程中,进行多路IO操作的时候,有时阻塞式的就不好了,比如你当前进程排在前面的IO,由于因为你被阻塞住了,导致你的当前进程陷入了休眠状态,虽然你将CPU的时间让给了别的进程了,但是对于你自己的进程来说,你自己进程的剩下那些IO操作就要等你前面的IO操作完了之后才能进行,这样就会降低了你当前的一个进程中的工作效率了,因为你排在前面的IO操作没有进行完,你排在后面的IO操作是不肯能得到运行的,都是在一个进程中的。这个时候阻塞就是不好的了,所以有时也需要非阻塞式的操作。
(5)非阻塞式的操作,当你一个进程中进行多路IO的操作的时候,你排在前面的IO操作,由于你没有用阻塞式的操作,而是非阻塞式的操作,所以即使你的条件没有达到你也不会让整个进程陷入沉睡暂停状态,而是直接返回,直接执行这个进程的下面的内容,这样对于当前进程的别IO来说,别的进程的执行效率就加大了,这种情况往往是在IO操作设备文件的时候,比如你在读鼠标这个设备文件,就因为你这读这个设备文件的时候被阻塞住了,而影响到了其他的设备文件的IO操作,这样可想而知,真的不好
(6)如何实现非阻塞呢,就是在打开文件的时候加上O_NONBLOCK这个标志属性和fcntl,fcntl可以读出一个文件的flag,然后加上O_NONBLOCK这个标志,在将这个flag放回去
2、用read读取键盘,来看阻塞的感觉
(1)因为键盘是标准输入设备,对应的文件描述符是0,在每个进程中都是默认被打开的,所以我们直接read(0, buf, 2);直接读这个键盘就行,读取两个字节放到buf中,我们执行这个时候,当我们键盘没有输入东西完毕按回车的时候(因为Linux的控制台是行缓冲的,你没有按下回车的时候,只是把你输入的东西放到控制台上显示,还没有送到read中,你按下回车后才会被输入进去。),当我们没有输入东西按回车的时候,发现是被阻塞住的,被卡住了,不动了,这就是阻塞的感觉,这个时候当前的进程被暂停了,因为没有等待条件,read读取不到东西,当我们输入东西按下回车的时候,当前的进程就被唤醒了,read读取到东西了
3、read读取鼠标
(1)首先鼠标这个设备文件是需要我们打开的,才能得到文件描述符fd,鼠标的设备文件在/dev/input/mouse0或者可能是/dev/input/mouse1
4、先读鼠标在键盘的情况
(1)我们程序在设计的时候,是先读取鼠标这个设备文件,在读取键盘这个标准输入文件,我们会发现,由于我们是阻塞式的,所以我们在鼠标没有读取成功的时候,键盘输入了东西敲回车也不好使,只有当鼠标读取完了之后,才会去读取键盘,这就是阻塞式的坏处,解决方法就是非阻塞
5、并发式IO的解决方案(就是一个进程中对多个IO进行操作时引发的问题,就是上面的4的问题)
(1)解决方案有三个
(2)非阻塞式IO
@1:首先将键盘这个文件设置成非阻塞式的,因为键盘这个标准输入,是在每一个进程都打开的,所以我们无法直接用open这个函数去打开这个文件,将其中的flag加上O_NONBLOCK这个标志属性
所以我们要用fcntl这个函数来去利用文件描述符fd,去将这个文件的属性进行改成非阻塞的。
int fcntl(int fd, int cmd, ... /* arg */ );
@2:第一个参数是文件描述符
@3:第二个参数是要的功能,我们的cmd用的宏是F_GETFD (void) F_SETFD (int),这两个。
@4:利用F_GETFD这个cmd宏,将fd这个文件的flag读取出来,之后用F_SETFD这个cmd宏将O_NONBLOCK这个非阻塞式的属性添加上。
@5:先flag = fcntl(0, F_GETFD);获取原来的这个文件的flag,然后 flag |= O_NONBLOCK;位或一个O_NONBLOCK。然后在fcntl(0, F_SETFD, flag);将属性在写到这个fd中
6、多路复用IO
(1)IO多路复用原理(IO multiplexing),作用和非阻塞IO有点像,是解决并发式IO的问题的
(2)什么时候用多路复用IO呢,在多路IO都是非阻塞式的情况下。为了应对多路非阻塞IO,CPU要一直不停的去轮询这些非阻塞IO,因为是轮询,所以必然要消耗时间,有空档。所以有了多路复用IO。
(3)多路复用IO涉及到的两个函数:select和poll
(4)IO多路复用的实现原理就是:外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。
@1:外部阻塞式的意思是说,select和poll这两个函数本身是阻塞式的
@2:内部非阻塞式,意思是说,select和poll内部在轮询几路IO的时候是非阻塞式的。
@3:select类似于一个监听的函数,比如,当A这个鼠标IO,和B这个键盘IO,我们用select去监听这两个IO,我们的鼠标IO,和键盘IO,都是阻塞式的,并没有设置成非阻塞式的,当我们调用select去监听的时候,当鼠标IO事件和键盘IO事件都没有发生的情况,select本身就是阻塞式的,select会一直在这里等待他监听的两个IO事件其中至少发生一个,select去监听这个两个IO的时候,是以非阻塞式的方式去轮询监听的,这就是外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。select函数表现为对外是阻塞式的,对内部监听的A和B这两个阻塞式IO表现为非阻塞式的方式去轮询A和B这两个阻塞式IO。只要A或者B有一个IO中来数据了,select就会被唤醒,select就会向外面汇报,在没数据时,select是阻塞式的,select所在的进程变成了暂停状态
(5)select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval
*timeout);
@1:第一个参数表示文件描述符的个数,这个值我们应该设置select内部要监听的多路IO中的那个文件描述符最大的那一个的fd+1,因为文件描述符是从0开始的,所以要加1。
@2:第二个参数中的fd_set是多个文件描述符的集合,是一个结构体。select这个函数在运行的时候,是区分文件描述符所要进行的操作的,所以第二个参数是传递了一个要读的文件描述符,是要对多路文件描述符中那些要读的文件描述符。第三个参数是传递多路文件描述符中要写的那些文件描述符。第四个参数是那些发生异常的文件描述符,都是输出型参数。第五个参数是设置超时时间的,因为select是阻塞式的,所以当监听的IO没有IO事件时,select就一直被阻塞了,卡住了,当前进程就一直暂停了,所以有时不想select一直被阻塞住,所以有了这个第五个参数,用来设置超时的,时间到了,select就会被唤醒返回
@3:返回值,当select成功的时候,就select监听的内部有IO事件发生的时候,select返回,这个返回值就是表示select内部有几个IO事件发生了,如果返回值是0就表示是超时了,没有监听到任何的IO事件发生自己超时后返回了,返回值是0,如果返回值是-1,就表示select函数运行错误了
@4:void FD_CLR(int fd, fd_set *set);//将fd,从fd_set中清除出去
int FD_ISSET(int fd, fd_set *set);//判断fd,在fd_set中有没有被置位,发生了IO事件对应fd
//会被置位返回值是1,返回值0表示没有发生这个fd的IO
//事件
void FD_SET(int fd, fd_set *set);//将fd,添加到fd_set中
void FD_ZERO(fd_set *set); //是用来将fd_set这个集合中文件描述符置位的清除置位
这四个函数是用来操作fd_set这个文件描述符集合的
(6)poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
@1:第一个参数是输出型参数,是文件描述符的集合,第二个参数是poll函数内部监听的多路IO的最大的文件描述符fd+1,第三个参数是超时时间,是毫秒级别的
@2: struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
我们给poll传递第一个参数的时候,要设置这个结构体的fd和events,fd表示我们要监测哪个fd,events表示要监测这个fd的什么事件
@3:events如果是
POLLIN 表示监测的是读
POLLOUT 表示监测的是写
@4:revents是内核设置的,当监测的IO事件发生POLLIN或者POLLOUT,我们可以判断events和revents是否相等来确定是否发生我们想要的IO事件
@5:返回值,当poll成功的时候,返回值是一个正数,非零,表示内核帮我们设置了一个revents,表示有IO事件发生,有几路IO事件发生了,返回值就是几,超时时返回0,返回-1表示poll函数运行错误了,和select的返回值定义是一样的
7、多路复用IO的代码实战
(1)select函数实现的读取键盘和鼠标的IO多路复用
(2)poll函数实现的读取键盘和鼠标的IO多路复用
8、异步通知(异步IO)
(1)什么是异步IO,可以认为是操作系统用软件实现的一种软件中断响应系统
(2)异步IO的工作方法就是,当前进程在做自己该做的事情,当有一个异步IO的信号SIGIO的时候,就会去执行这个异步IO信号对应的信号处理函数。
(3)所以就是一个进程中向操作系统注册了一个异步IO的信号对应的信号处理函数,用signal或者sigaction注册了一个SIGIO对应的信号处理函数,当前进程可以做想做的事情,当有异步IO事件发生了的时候,操作系统会给当前进程发一个SIGIO的异步信号,进程接受到这个SIGIO信号后就会去执行这个信号对应的信号处理函数。就跟被中断了一样,去执行了中断处理函数一样,只不过这个中断是由信号来产生的。
(4)异步IO涉及到的函数:
fcntl(主要是设置一些异步通知,F_GETFL,F_SETFL,O_ASYNC,F_SETOWN)
@1:F_GETFL来获取flag属性信息
@2:F_SETFL来设置flag属性信息
@3:O_ASYNC来指示当前的文件描述符可以被接受异步通知,也是一个flag文件属性,将文件描述符添加这个属性
@4:F_SETOWN用这个告诉当异步IO事件发生的时候让OS通知谁,fcntl带上这个参数后,后面一个变参要加上当前进程的pid,可以getpid来获得,表示发生异步IO事件的时候OS通知当前的这个进程
signal或者sigaction(绑定SIGIO对应的信号处理函数的)
8、存储映射IO
(1)mmao函数
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
把一个文件和一个内存区域映射起来。
(2)在LCD显示的时候和IPC共享内存通信的时候会用到
比如,你的LCD要显示的图片的话,是和驱动层的一个LCD驱动中划分出来的一个物理内存中的显存区域,将这个内存区域设置到LCD控制器中的一个参数中,当这个内存区域有一个图片的数据的时候,硬件就会自动刷新到LCD屏幕去显示,但是我们现在是在应用层下,我们在文件系统中的一个打开了一个图片文件,得到了这个图片的数据,我们要将这个打开的图片的数据,也就是要将一块内存复制到LCD的显存的那一个内存区域中,这样每次都直接复制是很浪费CPU时间的,所以就可以mmap将LCD的显存内存区域和我们的这个打开这个文件时的内存区域映射起来,这样就相当于关联起来了,实际上就变成了一个内存区域了,