首先了解一下什么是流,什么是I/O
- 可以进行I/O操作的内核对象。
- 比如文件,管道,套接字等都会有流的概念。
- 流的入口一般是文件描述符,在Linux中一切皆文件。
- 文件的读写需要通过流来进行,而对流的读写操作即为I/O
阻塞
在处理问题时我们一般选择阻塞的方式,节省CPU资源。但不是绝对的。
- 阻塞等待:此期间不占用CPU的时间。
- 非阻塞轮询:所谓轮询是指不停的询问,占用CPU资源。
当然阻塞的缺点也很明显,比如说我等待的资源刚好这一时刻只来了一个需要我处理的资源,我结束等待处理完成皆大欢喜。但是资源并不是一个接一个的,比如取快递来说,快递没来我在阻塞等待,突然100个快递一起来了,那我就有的忙了。这就是阻塞式等待的缺点。
IO多路复用
因为上述方法不能满足用户需求,所以就有了IO多路复用。
它既有阻塞等待CPU资源节约的优点,还能兼顾同一时刻响应多路请求。
select函数
select函数在并发请求时的作用:继续上述的取快递为例,快递没来之前我CPU处于阻塞休息的状态,突然有100个快递来了,这时候select监听到了有快递来了,但是它监听的数量有限一般就是1024个。select监听到了有快递来告诉我(CPU)要去取快递,但是select不会告诉我具体是哪个快递到了,所以我只能知道有快递到了,这时我再去便利这所有的快递挨个询问是哪个到了,把到了的快递取了(处理请求)。
具体工作如下:
while(1){
select(流[]);阻塞
for(int i = 0; i < maxSize; ++i){
if(i == 数据){
处理
}
}
}
epoll
虽然说我可以休息避免了不停的问,但是如果只来了3个快递,我还要挨个打电话去问,其余的1021次就是在浪费时间。
所以就有了epoll();
epoll就强大了,它的监听工作和select一样,但是他会同时告诉我有几个快递到了并且具体到是哪个快递。这样我CPU的工作量就大大减轻了。而且epoll所监听的个数往往比1024还要大很多。
epoll的伪代码如下:
while(1){
需要处理的流[] = epoll_wait(epoll_fd); // 阻塞
for(int i = 0; 需要处理流.size; ++i){
CPU处理 = 需要处理的流[i];
}
}
epoll虽强但只是Linux独有的。而select是平台无关的。
像其他有用到epoll的都是对原生Linux C中epoll的封装。
什么是epoll
- 与select和poll一样,对 I /O多路复用的技术。
- 只关心“活跃的连接”,无需遍历不需要CPU处理的描述符。
- 能够处理大量的连接请求。(系统能够打开最大的文件个数)(linux下可以用 cat /proc/sys/fs/file-max 查看 一般远远大于1024)
epoll API
-
创建epoll:int epoll_create(int size);
-
控制epoll:int epoll_ctl(int epfd, int op, struct epoll_event *event);
-
等待epoll:int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout),
-
C语言epoll编程思路
-
epoll的两种触发模式:水平触发(LT)和边缘触发(ET)
水平触发:内核会不断的抛出已经被激活的事件,直到该事件被用户处理完才能返回结果。类似于TCP是比较安全的,不会丢包的处理方式。
边缘触发:内核只会抛出用户需要处理的事件,只会通知用户一次,后面不管用户处理还是不处理内核都不管。类似于UDP只管发不管你收到没收到,可能丢包的处理方式。
常见服务器的设计模型
单线程Accept
只适合学习的demo练习,实际企业不会使用这种设计模型。
单线程Accept + 多线程读写业务
类比与上一种优化了多个客户端的响应,但是线程之间的切换成本很高,也不实用。
单线程多路 I / O 复用
优点:解决了同时监听多个客户端的读写模型,该模型是阻塞式、非忙轮询的,不浪费CPU资源,对CPU的利用率较高。
缺点:虽然是监听了多个用户,但是同一时刻下正在处理的业务只能是一个,并发为1。当多个客户端同时访问时,由于该模型是串行处理的方式,会造成排队延迟的问题。
在客户端比较少的情况下可以使用。
单线程多路 I / O 复用 + 多线程读写业务(工作池)
较上一种模型降低了排队延迟,因为业务的处理交给了线程处理,主线程只负责分配任务然后响应客户端的读写请求。但是读写的处理并发为1,所以这种模型的并发量不是特别高,依然会出现排队延迟的问题。比单纯使用 IO多路复用的效率要高一些。
单线程多路 I / O 复用 + 多线程多路 I / O 复用(连接线程池)
优点:这种模型将读写的监听以及读写业务的处理都交给一个线程去做,主线程只负责分配资源,这样同一时刻的并发量就是N (线程池中线程的数量)。并发量相比于上述几种要高很多很多。CPU的利用率大大提高。如果线程池数量和CPU核数适配那么可以尝试将CPU核心与线程进行绑定,从而降低线程切换频率,极大的利用了CPU的资源。
缺点:虽然并发量显著提高,但是最高也不过是N,当有多个客户端请求同一个线程时依然会有排队等待的现象。实际上该模型就是 N × (单线程多路 I / O复用)。
目前大部分企业服务器的设计都用的该模型。
单线程多路 I / O 复用 + 多线程多路 I / O 复用(线程池)+ 多线程
模型分析:
①Server在启动监听之前,开辟固定数量(N)的线程,用Thead Pool线程池管理
②主线程main thread创l建listenFd之后,采用多路/O复用机制(如:select、epal)进行IO状态阻塞监控。有一个客户端Connect请求,I/O复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd分发给Thread Pool中的某个线程进行监听。
③Thread Pool中的每个thread都启动多路 I/O复用机制(select、epoll,用来监听main thread建立成功并且分发下来的socket套接字。一旦其中某个被监听的客户端套接字触发 I/O读写事件,那么,会立刻开辟一个新线程来处理 I/O读写业务
④当某个读写线程完成当前读写业务,如果当前套接字没有被关闭,那么将当前客户端套接字如:ConnFd重新加回线程池的监控线程中,同时,自身线程自我销毁。
优点:在上一模型基础上,除了能够保证同时响应最高的并发数,又能够解决读写并行通道的局限问题。
同一时刻的读写并行通道,达到了最大化极限,一个客户端可以对应一个单独的执行流程处理读写业务,读写并行通道与客户端的数量1∶1关系。
缺点:过于理想化。因为要求CPU核心数数量足够大。
如果硬件CPU数量可数,那么该模型就造成大量的CPU切换的成本浪费。因为为了保证读写并行通道和客户端是1:1的关系,就要保证server开辟的thread的数量与客户端一致。
综上,
最适合企业服务器的设计模型就是单线程多路 I / O 复用 + 多线程多路 I / O 复用(连接线程池)