为什么我执意要先把NIO基础原理放到最开始来讲,主要是因为,只有理解好了原理,才能更好的去理解NIO。
一、用户空间和内核空间
在linux操作系统,会把内存分为两块,一块是内核空间,一块用户空间。关于这一部分的知识,不做详细分析,可以去看一下这个:Linux的用户空间与内核空间
我们都知道,为了OS的安全性等的考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。我们每次请求IO设备的时候,我们都要先请求内核,内核操作IO设备,把数据读取到内核Buffer中,然后内核再把Buffer中的数据读取到用户进程的内存中。
这就像我们去银行取钱一样,三个角色,用户(用户),收银员(内核),金库(IO设备),用户肯定是不能直接去金库中取钱,先要经过收银员,然后收银员去金库中取钱,再把钱交给用户。
二、UNIX下的五种IO模型
-
阻塞IO
-
非阻塞IO
-
IO多路复用
-
信号驱动IO
-
异步IO
1、阻塞IO
用户线程发起调用read发起IO读操作,用户线程阻塞,内容等待数据从IO设备中读入 Buffer,完成后将数据拷贝至用户线程内存中,完成read操作。
这里可以看成小磊(用户线程)去银行取钱
1.小磊:我要取钱(发起read请求)(阻塞,只能一直等,等到收银员把钱准备好)
2.收银员:你稍等,我叫人(IO 设备)帮你准备钱(等待数据到达,也就是从IO设备把数据读到内核Buffer中)
3.(取完钱了回来了)收银员:钱取好了,你收好(数据到达了,等待用户拿钱)
4.小磊把钱装进包里(数据拷贝,完成read请求),小磊很开心,可以直接花钱了。
2、非阻塞IO模型
用户发起调用read请求发起IO读操作,系统不会立刻阻塞用户线程,而是立刻返回一个错误标识ewouldblock,用户进程判断该错误标识是ewouldblock,就能够知道数据还在准备,然后会进行其他的操作,用户线程可以再次发送read请求,可以不断轮询,直到数据准备完毕,然后讲数据拷贝至用户线程的内存中,完成read操作。
好了,我们再来取一次钱:
1.小磊:我要取钱(发起read请求)
2.收银员:好的,我叫人(IO 设备)帮你准备钱,你可以做点别的事情(小磊可以去做一些其它的操作,喝杯咖啡,打两把王者啊)
3.小磊打完游戏后(当然也可以什么都不做),再次询问:钱取好了吗?4.收银员:没好
5.小磊又可以继续干其它事情,或者再次询问(不断轮询),直到收银员说准备好了(数据到达了)
6.准备好了后,小磊把钱装进包里(数据拷贝,IO完成)
3、IO多路复用
多路复用函数 select把一些文件描述符(下面有文件描述符的定义)集合在一起某个文件描述符的状态发生变化 比如进入"写就绪"或者 "读就绪"状态 ,函数select就会立即返回并且通知进程读取或者写入数据如果没有I/O操作到达进程就会阻塞直到函数select超时退出为止。
IO多路复用就是基于select函数实现,用户进程调用了select,整个线程就会被阻塞,此时内核就会监视所有select负责的socket,一旦其中有一个socket中的数据准备好了,select就会返回,然后用户在调用read操作。
多路复用的优缺点:缺点:多路复用比阻塞IO还多了一个select的系统调用,在处理连接数 不高的情况下,可能性能还会低于阻塞IO,优点:可以同时处理多个socket
OK,原理性的东西完毕了,是时候取钱了,但是这次取钱不一样了,小磊他想出国玩,他需要人民币买机票,又需要美元在国外使用,当然,这家银行也挺给力,提供取美元的服务,不过美元和人民币在不同的金库(socket)中。
1.小磊:我要取人民币和美元。(select请求)
2.收银员:我去叫人(不同的socket)帮你准备,你稍等(小磊必须等待了,阻塞),收银员不断问准备钱的人取好了吗或者等待准备钱的人告诉收银员钱准备好了(对应下面的三种模型)
3.一旦存在任何一个(美元或者人民币准备好了,也就是socket中有数据了)准备好了,立马通知小磊拿钱(数据拷贝)
4.接着小磊就把钱装进口袋里了(数据拷贝完成)
此时,你就取了一种纸币了,接着你可以继续请求(select())取另外一种纸币
文件描述符fd
Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个filedescriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性)。那么我们的应用程序对文件的读写就通过对描述符的读写完成。
每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引,索引对应的位置有一个指向已打开文件的指针。例如图中的stdin,文件描述符是1,它的指针指向键盘文件,stdout,文件描述是2,它的指针指向显示器文件 ,指向文件是一个结构体 ,结构体中保存了一些相关信息。
通过上面应该大概能知道IO多路复用的大概流程了吧,但是,有一个点很重要的地方,内核是如何检测IO准备完成的(收银员如何确认钱准备好的)。
于是UNIX操作系统提供以下三种模型:
select
基本原理:select 函数监视的fd分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有fd就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
缺点:
FD限制,32位系统1024个,64位2048
遍历方式为轮询
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
poll和select基本上没太大区别,主要区别在于他存储fd是基于链表的形式,所以理论上来说没有最大连接限制,但是他同样的会在大量连接的时候,效率非常低。
epoll
epoll是select和poll的增强版本。
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epollctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epollwait便可以收到通知。
没有最大连接的限制
效率高,随着fd的增加不会降低效率
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
底层数据结构是红黑树,效率非常高
小结:三种模式,select和poll都是轮询的方式,在连接数量高的时候效率极低,epoll是基于事件驱动的,在连接数量高的时候效率不会降低
4.信号驱动异步IO
这种io模型用的比较少,我就不介绍太多了,他是一种非阻塞IO模型,用户进程发起IO请求后,可以继续执行其他操作,等待内核准备好数据,会递交一个SIGIO信号给用户进程,用户进程收到该信号后就知道数据准备好了,然后再次发起IO请求,进行数据拷贝。
5、异步IO
当用户进程发起读操作的时候,会立刻得到一个返回,用户进程会继续执行其他操作,内核会等待数据到达,并且将数据拷贝到用户进程的缓冲区中,拷贝完成后,内核会发送一个信号给用户进程,用户进程收到这个信号后,可以直接使用已经拷贝好的数据。
这异步IO的方式可以理解为收银员不仅帮你把钱准备好了,还会主动塞进你口袋里。
三、同步、异步、阻塞和非阻塞对比
同步IO和异步IO:他们最主要的区别就是同步IO还需要用户进程去拷贝数据,这段时间用户进程会阻塞,而异步IO就不需要去拷贝数据了,收到信号后,数据已经被拷贝完成了。
阻塞和非阻塞:阻塞是指用户进程必须等,非阻塞是指用户进程可以去执行其他操作。