I/O模型剖析

欠伸展肢体,吟咏心自愉。这篇文章主要讲述I/O模型剖析相关的知识,希望能为你提供帮助。
1.服务端处理网络请求过程
IO分为:
网络IO:本质是socket文件读取; 上图中红线部分
磁盘IO:上图中蓝线部分
由上图可知:IO都分为了两个阶段

1.将数据从文件先加载至内核内存空间(缓冲区)--时间长
2.将数据从内核缓冲区复制到用户空间的进程的内存中--时间短

服务端处理网络请求的过程
1.客户端发起请求,数据发送到网卡
2.将网卡中数据通过DMA机制复制到内核缓冲区
3.内核缓冲区复制到用户空间(比如要:GET index.html)
-------------------
4.web服务器收到请求数据,因为应用程序无法与硬件打交道,所以向内核空间发起系统调用
5.内核空间收到信息,cpu发出指令,让DMA设备去磁盘获取index.html,并拷贝到内核缓冲区
6.内核缓冲区再复制到用户空间
-------------------
7.web服务器收到数据,然后构建响应报文,再发送给内核空间的socket buffer
8.socket buffer 在发送给网卡
9.通过网卡把响应报文发送给客户端

2.什么是DMADMA:直接内存访问

PIO 程序的输入输出模型,--所有操作都需要CPU参与
DMA 直接内存访问; 只需要cpu发送指令,让DMA设备去把磁盘数据拷贝到内存,降低CPU的使用率

3.IO模型
同步,异步,阻塞,非阻塞的理解

比如上图:A是领导B是员工
现在A让B去做一件事情

同步就是:领导自己去询问事情是否做完
异步就是:员工主动告知领导事情的进度

阻塞就是:事情非常紧急需要马上解决,领导必须盯着员工把事情做完,不能干其他的
非阻塞:领导在员工做事情时,可以去喝喝茶,打打高尔夫...等等

A对应:应用程序
B对应:内核
只有让内核多做事情,应用程序少做,这样才能实现高并发

衍生出
同步阻塞,同步非阻塞,异步阻塞,异步非阻塞
场景:用烧水壶烧水

同步阻塞就是:买的烧水壶是无声的,为了把水烧开,并且不能让水喷出来,就只能守着烧

同步非阻塞:买的烧水壶也是无声的,只需要把水烧开就行,不管开水会不会喷出,所以我就可以在烧水时,去看看电视
偶尔去看看水是否烧开就行

异步阻塞:这次买的烧水壶可以语音提示,烧开了会提醒,如果我此时还守着,不是有病嘛,所以这没意义

异步非阻塞:水烧开自动提醒我,在烧水时我可以开心看电视

4.五中IO模型阻塞型、非阻塞型、复用型、信号驱动型、异步
4.1.阻塞型

理解场景:钓鱼

去钓鱼,那人必须守着
数据准备好:就好比鱼儿上钩,这个过程是很漫长的
复制完成:好比当鱼儿上钩,把鱼儿放到桶里,这个过程很快

在钓鱼的过程中,为了不让鱼儿跑掉,你必须一直守着,不能干其他的;所以一直是阻塞的

4.2.非阻塞型

理解场景: 钓鱼,钓鱼旁边有个麻将馆

钓鱼时,把鱼竿搞好,就去麻将馆打麻将了...
打一局,我就去看看鱼儿上钩没有...
当鱼儿上钩了,这个时候我必须在场把鱼儿放在桶里

在鱼儿上钩过程中,我都跑去打麻将了,只是偶尔去看看鱼儿是否上钩,也只有把鱼放在桶里这阶段我在场,所以
只有这段时间是阻塞的

4.3.复用型

理解场景:我雇了一只猫去守着钓鱼,而且这次拿了很多钓竿

让猫在河边守着,但是呢,我又怕猫偷吃,所以我也只能盯着猫不能去打麻将了,这阶段我是阻塞的
当有钓竿鱼上钩了,猫就叫我,然后我就过去把鱼放在桶里,所以我从头到尾一直都是阻塞的....

4.4.信号驱动型

理解场景:我买了一个带语音提示的鱼竿

这次,我把鱼竿搞好,我也依然去旁边的麻将馆打麻将了,
当鱼儿上钩了,就语音提示我,主人,主人,鱼上钩了,然后我就去把鱼儿放在桶里

只有把鱼放在桶里,我需要去河边,这段时间我是阻塞的

4.5.异步

理解场景:由于前几次都是空军,这次我就聘请了一个钓鱼达人去钓

钓鱼达人在钓鱼,我就去打麻将..
我麻将打完了,钓鱼达人,就把鱼儿给我了

5.五种I/O模型比较
阻塞型:一直都阻塞
非阻塞:只有鱼儿放桶里是阻塞
复用型:也是一直阻塞
信号驱动:只有鱼儿放桶里是阻塞
异步:完全不阻塞

6.模型实现的方式
Select:#Linux实现对应I/O复用模型BSD4.2最早实现,POSIX标准,一般操作系统均有实现
Poll:#Linux实现,对应I/O复用模型System V unix最早实现
Epoll:#Linux特有,对应I/O复用模型具有信号驱动I/O模型的某些特性

Kqueue:#FreeBSD实现,对应I/O复用模型具有信号驱动I/O模型某些特性
/dev/poll:#SUN的Solaris实现对应I/O复用模型具有信号驱动I/O模型的某些特性
Iocp#Windows实现,对应第5种(异步I/O)模型

7.select/poll/epoll

7.1.select
Select:POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或
者检查存放fd标志位的数据结构来进行下一步处理

#缺点
1)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义FD_SETSIZE,
再重新编译内核实现,但是这样也会造成效率的降低
2)单个进程可监视的fd数量被限制,默认是1024,修改此值需要重新编译内核
3)对socket是线性扫描,即采用轮询的方法,效率较低
4)select 采取了内存拷贝方法来实现内核将 FD 消息通知给用户空间,这样一个用来存放大量fd的数据结构,
这样会使得用户空间和内核空间在传递该结构时复制开销大

7.2.poll
1)本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态
2)其没有最大连接数的限制,原因是它是基于链表来存储的
3)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
4)poll特点是"水平触发",如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

7.3.epoll
epoll:在Linux 2.6内核中提出的select和poll的增强版本
支持水平触发LT和边缘触发ET,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次
使用"事件"的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激
活该fd,epoll_wait便可以收到通知

#优点:
1)没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口),具体查看
/proc/sys/fs/file-max,此值和系统内存大小相关
2)效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,
即epoll最大的优点就在于它只管理"活跃"的连接,而跟连接总数无关
3)内存拷贝,利用mmap(Memory Mapping)加速与内核空间的消息传递;即epoll使用mmap减少复制开销

边缘触发:只通知一次
8.什么是零拷贝传统Linux中 I/O 的问题
传统的Linux系统的标准 I/O 接口(read、write)是基于数据拷贝的,也就是数据都是 copy_to_user 或者
copy_from_user,这样做的好处是,通过中间缓存的机制,减少磁盘 I/O 的操作,
但是坏处也很明显,大量数据的拷贝,用户态和内核态的频繁切换,会消耗大量的 CPU 资源,严重影响数据传输的
性能
统计表明,在Linux协议栈中,数据包在内核态和用户态之间的拷贝所用的时间甚至占到了数据包整个处理流
程时间的57.1%

零拷贝就是上述问题的一个解决方案,尽量避免拷贝操作来缓解 CPU 的压力。
零拷贝并没有真正做到"0"拷贝,它更多是一种思想,很多的零拷贝技术都是基于这个思想去做的优化

8.1.原始数据拷贝操作

8.2.MMAP:Memory Mapping

8.3.SENDFILE

8.4.DMA 辅助的 SENDFILE

9.Httpd MPM【I/O模型剖析】HTTP中的三种请求处理模式(MPM)的区别
(1)prefork(预派生模式):多进程I/o模型,每个进程相应一个请求#--> > 进程模型,两级结构
主进程--子进程--线程--请求
|--子进程--线程--请求
|--子进程--线程--请求
优点:稳定
缺点:慢,占用资源(内存),不适用于高并发场景
使用select 模型,最大并发1024


(2)worker:复用的多进程I/O模型,多进程多线程 #--> > 线程模型,三级结构
主进程--子进程--线程--请求
|--线程--请求
|--线程--请求
|--子进程--线程--请求
|--线程--请求
|--线程--请求
优点:相比prefork,占用内存较少,可以同时处理更多的请求
缺点:使用keep-alive的长连接方式,某个线程会一直被占据,即使没有传输数据,也需要一直等待到超时才会被释放。


(3)event 事件驱动模型(epoll),增加了一个监听线程 #--> > 线程模型,三级结构
监听线程:用于向工作线程分配任务并和客户端保持会话连接,超时之后监听线程会删除socket,工作线程只处理用户请求,处理完之后将会话保持交于监听线程,自己去处理新的请求,不再负责会话保持
主进程--子进程--监听线程--请求1 空请求不被分配 请求3
||--工作线程--请求1
||--工作线程--请求3
||--工作线程处理完成请求,将keep-alived交给监听线程,自己开始等待处理新请求
与worker进程很像,最大的区别在于,它解决了keep-alive场景下,长期被占用的线程的资源浪费问题
优点:单线程响应多请求,占据更少的内存,高并发下表现更优秀
缺点:没有线程安全控制


    推荐阅读