操作系统IO模式(理解)

我通过后面的文章总结如下:

 

IO多路复用三种模式:

select:

(1)他会遍历所有文件描述符,不管是否活跃

(2) 它仅仅返回说有数据了,不会告诉你哪个sock有数据,你还需要自己遍历一遍文件描述符

(3)最多支持1024个连接,资源有限,有效限制。

(4)线程不安全。执行一个sock,当另一个sock来了会扔掉第一个sock

poll

 (1)在select改进了连接数限制,资源不限制。

(2) 还是不知道哪个sock有数据,你还得自己遍历一下。

 (3)遍历所有的,不管是否活跃

   (4) 线程不安全。

epoll:

(1) 线程安全了。

(2)告诉进程哪个sock有数据。

  (3)只是遍历活跃的文件描述符

(4)连接数随便,不设限制。

                                                             

%%%关于如何理解IO多路复用三种模式,select,poll,epool加黑体的是我画出的重点。

作者:码农的荒岛求生
链接:https://www.zhihu.com/question/32163005/answer/1644076216
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

I/O多路复用三剑客

本质上select、poll、epoll都是阻塞式I/O,也就是我们常说的同步I/O。

select:初出茅庐

在select这种I/O多路复用机制下,我们需要把想监控的文件描述集合通过函数参数的形式告诉select,然后select会将这些文件描述符集合拷贝到内核中,我们知道数据拷贝是有性能损耗的,因此为了减少这种数据拷贝带来的性能损耗,Linux内核对集合的大小做了限制,并规定用户监控的文件描述集合不能超过1024个,同时当select返回后我们仅仅能知道有些文件描述符可以读写了,但是我们不知道是哪一个,因此程序员必须再遍历一边找到具体是哪个文件描述符可以读写了。

因此,总结下来select有这样几个特点:

  • 我能照看的文件描述符数量有限,不能超过1024个
  • 用户给我的文件描述符需要拷贝的内核中
  • 我只能告诉你有文件描述符满足要求了,但是我不知道是哪个,你自己一个一个去找吧(遍历)

因此我们可以看到,select机制的特性在高性能网络服务器动辄几万几十万并发链接的场景下无疑是低效的。

poll:小有所成

poll和select是非常相似的,poll相对于select的优化仅仅在于解决了文件描述符不能超过1024个的限制,select和poll都会随着监控的文件描述增加而出现性能下降,因此不适合高并发场景。

epoll:独步天下

在select面临的三个问题中,文件描述数量限制已经在poll中解决了,剩下的两个问题呢?

针对第一个epoll使用的策略是各个击破共享内存

实际上文件描述符集合变化的频率比较低,select和poll频繁的拷贝整个集合,内核都快要烦死了,epoll通过引入epoll_ctl很体贴的做到了只操作那些有变化的文件描述符,同时epoll和内核还成为了好朋友,共享了同一块内存,这块内存中保存的就是那些已经可读或者可写的的文件描述符集合,这样就减少了内核和程序的内存拷贝开销。

针对第二点,epoll使用的策略是“当小弟”。

在select和poll机制下,进程要亲自下场去各个文件描述符上等待,任何一个文件描述可读或者可写就唤醒进程,但是进程被唤醒后也是一脸懵逼并不知道到底是哪个文件描述符可读或可写,还要再从头到尾检查一遍。

但epoll就懂事多了,主动找到进程要当小弟替大哥出头。

 

 

参考https://www.zhihu.com/question/32163005/answer/76577586
 

要弄清问题 先要知道问题的出现原因
原因:
由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I/O(read,write,accept等等),进程可能阻塞,此时进程就阻塞
在这个调用上,不能执行其他操作.阻塞很正常. 接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端调用write(sockfd,but,size)发来数据. 在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一客户的数据到达套接字sockfd2时,服务器不能处理,仍然阻塞在read(sockfd1,...)上;此时问题就出现了,不能及时处理另一个客户的服务,咋么办?I/O多路复用来解决!I/O多路复用:继续上面的问题,有多个客户连接,sockfd1,sockfd2,sockfd3..sockfdn同时监听这n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后就调用read读取收到消息的sockfd,然后又循环回select阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息

Q:
那这样子,在读取socket1的数据时,如果其它socket有数据来,那么也要等到socket1读取完了才能继续读取其它socket的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程IO有什么区别呢?

A:
1.CPU本来就是线性的 不论什么都需要顺序处理 并行只能是多核CPU
http://2.io多路复用本来就是用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题,跟多线程没关系.
3.跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源. 而I/O多路复用不需要切换线/进程,效率相对较高,特别是对高并发的应用nginx就是用I/O多路复用,故而性能极佳.但多线程编程逻辑和处理上比I/O多路复用简单.而I/O多路复用处理起来较为复杂.

 

 

 

参考:https://www.zhihu.com/question/32163005/answer/55772739
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.

第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)

其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:

                                                

于是大部分人都直接联想到"一根网线,多个sock复用" 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的

重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。

 

是不是听起来好拗口,看个图就懂了.

 


在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。

 

什么,你还没有搞懂“一个请求到来了,nginx使用epoll接收请求的过程是怎样的”, 多看看这个图就了解了。提醒下,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

------------------------------------------
了解这个基本的概念以后,其他的就很好解释了。

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

select 被实现以后,很快就暴露出了很多问题。

  • select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  • select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
  • select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
  • select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.

“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气

于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如

  • poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
  • poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。

其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:

  • epoll 现在是线程安全的。
  • epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

 

epoll 当年的patch,现在还在,下面链接可以看得到:
/dev/epoll Home Page

贴一张霸气的图,看看当年神一样的性能(测试代码都是死链了, 如果有人可以刨坟找出来,可以研究下细节怎么测的).


横轴Dead connections 就是链接数的意思,叫这个名字只是它的测试工具叫deadcon. 纵轴是每秒处理请求的数量,你可以看到,epoll每秒处理请求的数量基本不会随着链接变多而下降的。poll 和/dev/poll 就很惨了。

 

可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。

其实有些国内知名厂商把epoll从安卓里面裁掉这种脑残的事情我会主动告诉你嘛。什么,你说没人用安卓做服务器,尼玛你是看不起p2p软件了啦。

而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。

详细的在这里:
Connection processing methods

PS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。 如果像是在欧朋数据中心里面的转码服务器那种动不动就是几万几十万的并发,不用epoll我可以直接去撞墙了

 

 

 

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

 

转载自https://blog.csdn.net/d12345678a/article/details/53944791?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-1&spm=1001.2101.3001.4242

 

%%%%%%%%%这个讲的清楚%%%%%%%%%%%%

 

目前IO模型主要经历了以下五种:
1)阻塞IO
2)非阻塞IO
3)IO复用(select和poll)
4)信号驱动IO(sigio)
5)异步IO(aio_)
 
内核空间和用户空间:
由于操作系统都包括内核空间和用户空间(或者说内核态和用户态),内核空间主要存放的是内核代码和数据,是供系统进程使用的空间。而用户空间主要存放的是用户代码和数据,是供用户进程使用的空间。目前Linux系统简化了分段机制,使得虚拟地址与线性地址总是保持一致,因此,Linux系统的虚拟地址也是0~4G。Linux系统将这4G空间分为了两个部分:将最高的1G空间(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,即为“内核空间”,而将较低的3G空间(从虚拟地址 0x00000000到0xBFFFFFFF)供用户进程使用,即为“用户空间”。同时由于每个用户进程都可以通过系统调用进入到内核空间,因此Linux的内核空间可以认为是被所有用户进程所共享的,因此对于一个具体用户进程来说,它可以访问的虚拟内存地址就是0~4G。另外Linux系统分为了四种特权级:0~3,主要是用来保护资源。0级特权最高,而3级则为最低,系统进程主要运行在0级,用户进程主要运行在3级。
 
一般来说,IO操作都分为两个阶段,就拿套接口的输入操作来说,它的两个阶段主要是:
1)等待网络数据到来,当分组到来时,将其拷贝到内核空间的临时缓冲区中
2)将内核空间临时缓冲区中的数据拷贝到用户空间缓冲区中
 
1、阻塞IO
默认情况下,所有套接口都是阻塞的。
假如recvfrom函数是一个系统调用:


说明:任何一个系统调用都会产生一个由用户态到内核态切换,再从内核态到用户态切换的过程,而进程上下文切换是通过系统中断程序来实现的,需要保存当前进程的上下文状态,这是一个极其费力的过程。
 
2、非阻塞IO
当我们把套接口设置成非阻塞时,就是由用户进程不停地询问内核某种操作是否准备就绪,这就是我们常说的“轮询”。这同样是一件比较浪费CPU的方式。


 
3、IO复用
我们常用到的IO复用,主要是select和poll。这里同样是会阻塞进程的,但是这里进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的IO操作上。
另外还有一点不同于阻塞IO的就是,尽管看起来与阻塞IO相比,这里阻塞了两次,但是第一次阻塞在select上时,select可以监控多个套接口上是否已有IO操作准备就绪的,而不是像阻塞IO那种,一次性只能监控一个套接口。


 
4、信号驱动IO
信号驱动IO就是说我们可以通过sigaction系统调用注册一个信号处理程序,然后主程序可以继续向下执行,当我们所监控的套接口有IO操作准备就绪时,由内核通知触发前面注册的信号处理程序执行,然后将我们所需要的数据从内核空间拷贝到用户空间。


 
5、异步IO
异步IO与信号驱动IO最主要的区别就是信号驱动IO是由内核通知我们何时可以进行IO操作了,而异步IO则是由内核告诉我们IO操作何时完成了。具体来说就是,信号驱动IO当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后内核直接通知可以进程后续操作了。


 
综上所述,我们发现 前四种IO模型的主要区别是在第一阶段,因为它们的第二阶段都是在阻塞等待数据由内核空间拷贝到用户空间;而异步IO很明显与前面四种有所不同,它在第一阶段和第二阶段都不会阻塞。具体参考如下:


 
最后,总结下同步IO与异步IO的区别:
1)同步IO操作会引起进程阻塞直到IO操作完成。
2)异步IO操作不引起进程阻塞。

因此,由上面定义可以看出,阻塞IO、非阻塞IO、IO复用、信号驱动IO都是属于同步IO,而异步IO模型才与异步IO定义所匹配

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jack Ju

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值