目录
IO模型
阻塞IO和非阻塞IO
阻塞IO
非阻塞IO
IO复用模型
异步IO
mmap
IO复用之select、poll、epoll模型
select
poll
epoll
IO模型
阻塞IO和非阻塞IO
阻塞IO
所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。
非阻塞IO
所谓非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。
术语:非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。
IO复用模型
如果在并发的环境下,可能会N个人向应用B发送消息,这种情况下我们的应用就必须创建多个线程去读取数据,每个线程都会自己调用recvfrom 去读取数据
那么问题来了,这么多的线程不断调用recvfrom 请求数据,先不说服务器能不能扛得住这么多线程,就算扛得住那么很明显这种方式是不是太浪费资源了,线程是我们操作系统的宝贵资源,大量的线程用来去读取数据了,那么就意味着能做其它事情的线程就会少
所以,有人就提出了一个思路,能不能提供一种方式,可以由一个线程监控多个网络请求,这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。
异步IO
到此,还有没有更好的办法,通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。
应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
mmap
mmap技术,也就是内存映射,可以直接将磁盘文件映射到内核缓冲区。首先还是会发生一次内核态到用户态的切换,这个过程是基于DMA引擎拷贝的,磁盘文件拷贝到内核缓冲区。
接下来,从内核态切换回用户态,此时不会把内核缓冲区的数据拷贝到用户空间。用户缓冲区会跟内核缓冲区建立一个映射,共享一块内存数据,这就不需要从内核缓冲区拷贝到用户缓冲区了。
然后,要把数据通过socket发送出去,需要从用户态切换到内核态,在内核态直接把内核缓冲区的数据拷贝到socket缓冲区里去,然后基于DMA技术把socket缓冲区的数据拷贝到网卡,发送出去。
完成以上步骤之后,就会从内核态切换回用户态。
总结来说,需要4次用户态和内核态之间的切换,3次数据的拷贝,比普通的io操纵少了一次数据拷贝,但切换次数并不减少。
IO复用之select、poll、epoll模型
select
数组
应用进程想要通过select 去监控多个连接(也就是fd)的话需要经向大概如下的流程:
1、在调用select之前告诉select 应用进程需要监控哪些fd可读、可写、异常事件,这些分别都存在一个fd_set数组中。
2、然后应用进程调用select的时候把3个fd_set传给内核(这里也就产生了一次fd_set在用户空间到内核空间的复制),内核收到fd_set后对fd_set进行遍历,然后一个个去扫描对应fd是否满足可读写事件。
3、如果发现了有对应的fd有读写事件后,内核会把fd_set里没有事件状态的fd句柄清除,然后把有事件的fd返回给应用进程(这里又会把fd_set从内核空间复制用户空间)。
4、最后应用进程收到了select返回的活跃事件类型的fd句柄后,再向对应的fd发起数据读取或者写入数据操作。
通过上面的图我想你已经大概了解了select的工作模式,select 提供一种可以用一个进程监控多个网络连接的方式,但也还遗留了一些问题,这些问题也是后来select面对高并发环境的性能瓶颈。
1、每调用一次select 就需要3个事件类型的fd_set需从用户空间拷贝到内核空间去,返回时select也会把保留了活跃事件的fd_set返回(从内核拷贝到用户空间)。当fd_set数据大的时候,这个过程消耗是很大的。
2、select需要逐个遍历fd_set集合 ,然后去检查对应fd的可读写状态,如果fd_set 数据量多,那么遍历fd_set 就是一个比较耗时的过程。
3、fd_set是个集合类型的数据结构有长度限制,32位系统长度1024,62位系统长度2048,这个就限制了select最多能同时监控1024个连接。
poll
链表
select模式下 fd_set 长度限制就开始成为了致命的缺陷。
吸取了select的教训,poll模式就不再使用数组的方式来保存自己所监控的fd信息了,poll模型里面通过使用链表的形式来保存自己监控的fd信息,正是这样poll模型里面是没有了连接限制,可以支持高并发的请求。
和select还有一点不同的是保存在链表里的需要监控的fd信息采用的是pollfd的文件格式,select 调用返回的fd_set是只包含了上次返回的活跃事件的fd_set集合,下一次调用select又需要把这几个fd_set清空,重新添加上自己感兴趣的fd和事件类型,而poll采用的pollfd 保存着对应fd需要监控的事件集合,也保存了一个当返回于激活事件的fd集合。 所以重新发请求时不需要重置感兴趣的事件类型参数。
epoll
红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
不同于select 和poll的直接调用方式,epoll采用的是一组方法调用的方式,它的工作流程大致如下:
1、创建内核事件表(epoll_create)。
这里主要是向内核申请创建一个fd的文件描述符作为内核事件表,这个描述符用来保存应用进程需要监控哪些fd和对应类型的事件。
2、添加或移出监控的fd和事件类型(epoll_ctl)。
调用此方法可以是向内核的内核事件表 动态的添加和移出fd 和对应事件类型。
3、epoll_wait 绑定回调事件
内核向事件表的fd绑定一个回调函数。
当监控的fd活跃时,会调用callback函数把事件加到一个活跃事件队列里;
最后在epoll_wait 返回的时候内核会把活跃事件队列里的fd和事件类型返回给应用进程。
从epoll整体思路上来看,采用事先就在内核创建一个事件监听表,后面只需要往里面添加移出对应事件,因为本身事件表就在内核空间,所以就避免了向select、poll一样每次都要把自己需要监听的事件列表传输过去,然后又传回来,这也就避免了事件信息需要在用户空间和内核空间相互拷贝的问题。
然后epoll并不是像select一样去遍历事件列表,然后逐个轮询的监控fd的事件状态,而是事先就建立了fd与之对应的回调函数,当事件激活后主动回调callback函数,这也就避免了遍历事件列表的这个操作,所以epoll并不会像select和poll一样随着监控的fd变多而效率降低,这种事件机制也是epoll要比select和poll高效的主要原因。