后端开发工程师必须知道的Linux 网络IO模型

本文深入探讨了Linux下的五种网络IO模型,包括阻塞I/O、非阻塞I/O、IO复用、信号驱动IO及异步IO,对比了select、poll和epoll的优劣,为读者提供了在不同场景下选择合适IO模型的指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

同步和异步,阻塞和非阻塞

同步和异步

请求线程是否需要等待结果返回。

同步:请求线程需要等待调用结果的返回。
异步:不需要等待结果,而是通过其他手段比如,状态通知,回调函数等。

在这里插入图片描述

阻塞和非阻塞

请求线程请求后能否做其它事。

阻塞:是指结果返回之前,当前线程被挂起,不做任何事。
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
在这里插入图片描述

五种I/O模型

在这里插入图片描述

阻塞I/O模型

下面图中的进程都可以理解为线程,Linux 中的线程就是用进程实现的。(个人理解)
在这里插入图片描述
需要先等待内核准备好数据(阻塞),再等待数据复制完成返回(同步)。
同步阻塞:进程调用方法后,不能做其它事,且需要等待结果返回。

非阻塞IO模型

可以看到整个等待数据部分都是系统调用,没有 recvfrom 的时候,系统可以安排进程做其它事。虽然在人类看来这个时间很短做不了什么,但是在计算机的世界可以做很多事了。(个人理解)在这里插入图片描述
每隔一段时间调用方法检查内核数据准备状态,再等待数据复制完成返回(同步)。
同步非阻塞:进程调用方法后,可以做其它事,但还是需要等待结果返回。

IO复用模型

在这里插入图片描述
主要是select和epoll两个系统调用,能实现同时对多个IO端口进行监听。
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的是,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
当用户进程调用了select,那么整个进程会被block;而同时,kernel会“监视”所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回。这个时候,用户进程再调用read操作,将数据从kernel拷贝到用户进程。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

NIO 是基于 I/O 复用的,Tomcat(8.5) 是基于 NIO 的。但 Tomcat 的 I/O 模型和 NIO 又有区别。
1个线程 Acceptor 负责接收连接 Channel 到 accept 队列。2个线程负责轮询。轮询到就绪的读/写创建 SocketProcessor 交给线程池 Executor 去执行。在 Executor 的线程中,会完成从 socket 中读取 HttpRequest 解析成 HttpServletRequest 对象,分派到相应的 servlet 并完成逻辑,然后将 Response 通过 socket 发回 client。在从 socket 中读数据和往 socket中写数据的过程,并没有像典型的非阻塞的 NIO 的那样,注册 OP_READ 或 OP_WRITE 事件到主 Selector,而是直接通过 socket 完成读写,这时是阻塞完成的。

信号驱动IO(了解)

在这里插入图片描述
两次调用,两次返回。

异步IO模型(了解)

在这里插入图片描述
用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,当内核收到aio_read后,会立刻返回。然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户控件,然后再通知进程本次IO已经完成。

五个I/O模型的比较

在这里插入图片描述

什么是 select、poll、epoll

服务端需要管理多个客户端连接,而 recv 只能监视单个 socket,这种矛盾下,人们开始寻找监视多个socket的方法。

select

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...)

int fds[] =  存放需要监听的socket

while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

用一个数组fds存放所有需要监视的socket。然后调用select,当前进程A阻塞(进程A进入所有socket的等待队列),直到有一个socket接收到数据(中断程序唤醒A,也就是从所有socket等待队列中移除,放入内核空间中的工作队列),select返回。用户可以遍历fds,判断具体哪个socket收到数据,然后做出处理。

注意:当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为什么select的返回值有可能大于1的原因之一。如果没有socket有数据,进程才会阻塞。

缺点:
1,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
2,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。

poll

和 select 基本一样,有少量改进。

epoll

epoll是select和poll的增强版本。epoll通过以下一些措施来改进效率。

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...);

while(1){
    int n = epoll_wait(...)
    for(接收到数据的socket){
        //处理
    }
}

措施一:功能分离
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升。

对比两段代码可以发现,select 在循环里面;而 epoll 将它分为2步,epoll_ctl 在循环外面,epoll_wait在循环里面。循环里面做的事少了,效率自然就高了。

措施二:就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。

当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象),就绪列表 rdlist 是 eventpoll 的成员。

当程序执行到epoll_ctl方法时,所有需要监听的socket将eventpoll添加到自己的等待队列中。

当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回;如果rdlist为空,阻塞进程(将当前进程A放入eventpoll的等待队列)。

当socket接收到数据,中断程序一方面修改rdlist,添加socket引用;另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(放入工作队列)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。

select、poll、epoll的区别(大厂面试)

1、支持一个进程所能打开的最大连接数
在这里插入图片描述
2、FD剧增后带来的IO效率问题

在这里插入图片描述

3、消息传递方式
在这里插入图片描述

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:

  1. 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

  2. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

补充知识点:
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!
select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值