Linux怎样处理网络请求——彻底理解IO多路复用

常见的网络IO模型

网络 IO 模型分为四种:同步阻塞 IO、同步非阻塞IO、IO 多路复用、异步非阻塞 IO(Async IO, AIO),其中AIO为异步IO,其他都是同步IO

同步阻塞IO

同步阻塞IO:在线程处理过程中,如果涉及到IO操作,那么当前线程会被阻塞,直到IO处理完成,线程才接着处理后续流程。如下图,服务器针对客户端的每个socket都会分配一个新的线程处理,每个线程的业务处理分2步,当步骤1处理完成后遇到IO操作(比如:加载文件),这时候,当前线程会被阻塞,直到IO操作完成,线程才接着处理步骤2。

同步阻塞IO 演示图

实际使用场景

在Java中使用线程池的方式去连接数据库,使用的就是同步阻塞IO模型。

模型的缺点

因为每个客户端存都需要一个新的线程,势必导致线程被频繁阻塞和切换带来开销。

同步非阻塞 IO-NIO(New IO)

同步非阻塞IO:在线程处理过程中,如果涉及到IO操作,那么当前的线程不会被阻塞,而是会去处理其他业务代码,然后等过段时间再来查询 IO 交互是否完成。如下图:Buffer 是一个缓冲区,用来缓存读取和写入的数据;Channel 是一个通道,负责后台对接 IO 数据;而 Selector 实现的主要功能,是主动查询哪些通道是处于就绪状态。Selector复用一个线程,来查询已就绪的通道,这样大大减少 IO 交互引起的频繁切换线程的开销。

实际使用场景

Java NIO 正是基于这个 IO 交互模型,来支撑业务代码实现针对 IO 进行同步非阻塞的设计,从而降低了原来传统的同步阻塞 IO 交互过程中,线程被频繁阻塞和切换带的开销。

NIO使用的经典案例是Netty框架,Elasticsearch底层实际上就是采用的这种机制。

IO多路复用

  • IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程

所以,每个客户端和服务器的socket 连接就可以看做”一路“,多个客户端和该服务器的socket连接就是”多路“,从而,IO多路就是多个socket连接上的输入输出流,复用就是多个socket连接上的输入输出流由一个线程处理。 因此 IO多路复用可以定义如下:

Linux中的 IO多路复用是指:一个线程处理多个IO流

IO多路复用3种实现方式

select/pool/epool

基本socket模型

先看下socket模型,以便与下面几种实现方式对比:

listenSocket = socket() // 系统调用socket(),创建一个主动socket

bind(listenSocket) // 给主动socket绑定地址和端口

listen(listenSocket) // 将默认的主动socket转换为服务器的被动socket(也叫监听socket)

while(true) {

    connSocket = accept(listenSocket) // 接受客户端连接,获取已链接socket

    recv(connSocket) // 从客户端读取数据,只能同时处理一个客户端

    send(connSocket) // 往客户端发送数据,只能同时处理一个客户端
}

实现网络通信流程如下图

基础的socket模型,能够实现服务器端和客户端的通信,但程序每调用一次accept函数,只能处理一个客户端请求,当有大量客户端连接时,这种模型处理性能较差,因此linux提供了高性能的IO多路复用机制来解决这种困境。

select机制

select是最古老的I/O多路复用机制,可以同时监听多个文件描述符的读写事件。它使用的fd_set数据结构来存储待监听的文件描述符集合,并通过select()函数将fd_set集合传递给内核,等待内核返回文件描述符的状态变化。

fd_set数据结构 (bitmap)

typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
/**
*  参数说明
*  监听的文件描述符数量__nfds、
*  被监听描述符的三个集合*__readfds,*__writefds和*__exceptfds
*  监听时阻塞等待的超时时长*__timeout
*  返回值:返回一个socket对应的文件描述符
*/
int select(int __nfds, fd_set * __readfds, fd_set * __writefds, fd_set * __exceptfds, struct timeval * __timeout)

select实现网络通信流程如下图:

 缺点

1、select使用的fd_set数据结构对单个进程能监听的文件描述符是有限制的,默认是1024

2、select()函数返回后,需要遍历文件描述符集合,才能找到就绪的描述符,遍历过程会产生一定开销,降低性能。

poll机制

poll与select类似,也可以同时监听多个文件描述符的读写事件。它使用的pollfd数据结构来存储待监听的文件描述符集合,并通过pool()函数将pollfd集合传递给内核,等待内核返回文件描述符的状态变化。相对于select,poll没有fd_set集合大小的限制,但并没有解决轮询获取就绪fd的问题,效率也不高。

pollfd结构体的定义

struct pollfd {
    int fd;         //进行监听的文件描述符
    short int events;       //要监听的事件类型
    short int revents;      //实际发生的事件类型
};

poll实现网络通信流程如下图:

 epoll机制

epoll 是对 select 和 poll 的改进,解决了“性能开销大”和“文件描述符数量少”这两个缺点,是性能最高的多路复用实现方式,能支持的并发量也是最大。

总结下,epoll 相关的函数里内核运行环境分两部分:

  • 用户进程内核态。进行调用 epoll_wait  等函数时会将进程陷入内核态来执行。这部分代码负责查看接收队列,以及负责把当前进程阻塞掉,让出 CPU。

  • 硬软中断上下文。在这些组件中,将包从网卡接收过来进行处理,然后放到 socket 的接收队列。对于 epoll 来说,再找到 socket 关联的 epitem,并把它添加到 epoll 对象的就绪链表中。这个时候再捎带检查一下 epoll 上是否有被阻塞的进程,如果有唤醒之。

epoll 的特点是:

1)使用红黑树存储一份文件

2)通过异步 IO 事件找到就绪的文件描述符,而不是通过轮询的方式;

3)使用队列存储就绪的文件描述符,且会按需返回就绪的文件描述符,无须再次遍历;

epoll接口

图片

1、epoll_create 用于创建一个 eventpoll 对象

epoll 实例内部维护了3个结构,

图片

具体如代码所示:

// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
   
    /* 就绪的描述符的双链表. 
      当有的连接数据就绪的时候,内核会把就绪的连接放到 rdllist 链表里。
      这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。
    */
    struct list_head rdlist;

    /* 一颗红黑树,通过这棵树来管理用户进程下添加进来的所有socket连接。*/
    struct rb_root  rbr;


   /* wq: 等待队列链表。软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。存放阻塞的进程*/
    wait_queue_head_t wq;      

    ......
};

2、epoll_ctl 负责把服务端和客户端建立的 socket 连接注册到 eventpoll 对象里,会做三件事:

1)创建一个红黑树节点对象 epitem ,主要包含两个字段,分别存放 socket fd 即连接的文件描述符,和所属的 eventpoll 对象的指针;

struct epitem {

    //红黑树节点
    struct rb_node rbn;

    //socket文件描述符信息
    struct epoll_filefd ffd;

    //所归属的 eventpoll 对象
    struct eventpoll *ep;

    //等待队列
    struct list_head pwqlist;
}

2)添加等待事件到socket的等待队列中,其回调函数是将回调函数ep_poll_callback,ep_poll_callback通过回调default_wake_up_function唤醒用户进程

3)将 epitem 对象插入红黑树

3、epoll_wait 用于等待其管理的连接上的 IO 事件

1) 检查 eventpoll 对象的就绪的连接 rdllist 上是否有数据到达;

2) 如果没有就把当前的进程描述符加入到 eventpoll 的进程等待队列里,然后阻塞当前进程,等待数据到达时通过回调函数被唤醒

图片

 3) 当 eventpoll 监控的连接上有数据到达时,通过下面几个步骤唤醒对应的进程处理数据:

1)socket 的数据接收队列有数据到达,会通过进程等待队列的回调函数 ep_poll_callback 唤醒红黑树中的节点 epitem;

2)ep_poll_callback 函数将有数据到达的 epitem 添加到 eventpoll 对象的就绪队列 rdllist 中;

3)ep_poll_callback 函数检查 eventpoll 对象的进程等待队列上是否有等待项,通过回调函数 default_wake_func 唤醒这个进程,进行数据的处理;

4)当进程醒来后,继续从 epoll_wait 时暂停的代码继续执行,把 rdlist 中就绪的事件返回给用户进程,让用户进程调用 recv 把已经到达内核 socket 等待队列的数据拷贝到用户空间使用。

软中断回调的时候回调函数也整理一下
sock_def_readable:sock 对象初始化时设置的

=>

ep_poll_callback : epoll_ctl 时添加到 socket 上的

=>

default_wake_function: epoll_wait 是设置到 epoll上的

图片

  • 在socket等待队列中,其回调函数是ep_poll_callback,另外其 private 没有用了,指向的是空指针 null。
  • 在 eventpoll 的等待队列项中,回调函数是 default_wake_function。其 private 指向的是等待该事件的用户进程。

ET模式与LT模式的区别
  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
  • ET模式(边缘触发): epoll_wait 遍历 ready_list 的时候,会把 socket 从 ready_list 里面移除,然后读取这个 scoket 的事件。
  • LT模式(水平触发)下,会把 socket 从 ready_list 里面移除,然后读取这个 scoket 的事件,如果这个 socket 返回了感兴趣的事件那么当前这个 socket 会再被加入到 ready_list 中,这样下次调用 epoll_wait 的时候,还能拿到这个 socket。

举个栗子:

如果此时一个客户端同时发来了 5 个数据包,按正常的逻辑,只需要唤醒一次 epoll ,把当前 socket 加一次到 ready_list 就行了,不需要加 5 次。然后用户程序可以把 socket 接收队列的所有数据包都读完。

但假设用户程序就读了一个包,然后处理报错了,后面不读了,那后面的 4 个包咋办?

如果是 ET 模式,就读不了了,因为没有把 socket 加入到 ready_list 的触发条件了。除非这个客户端发了新的数据包过来,这样才会再把当前 socket 加入到 ready_list,在新包过来之前,这 4 个数据包都不会被读到。

而 LT 模式不一样,因为每次读完有感兴趣的事件发生之后,会把当前 socket 再加入到 ready_list,所以下次肯定能读到这个 socket,所以后面的 4 个数据包会被访问到,不论客户端是否发送新包。

因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

3种机制底层实现的区别

select和poll都是通过轮询的方式,即内核每次要遍历监听的文件描述符集合,判断每个文件描述符是否有I/O事件发生;

而epoll底层实现是基于事件通知的方式,即当文件描述符状态发生变化时,内核会向应用程序发起事件通知,这种方式避免了无效的遍历,从而提高了效率。

在epoll中,使用epoll_wait函数进行事件监听时,内核将发生的事件文件描述符加入到一个就绪队列中,等待应用程序处理。如果就绪队列中没有任何文件描述符,则epoll_wait函数会阻塞,直到有文件描述符加入就绪队列,这种方式实现了I/O事件的高效处理和调度。

selectpollepoll
数据结构bitmap数组红黑树
最大连接数1024无上限无上限
fd拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
工作效率轮询:O(n)轮询:O(n)回调:O(1)

参考资料:

https://juejin.cn/post/6844904200141438984

IO多路复用机制详解 - 知乎

select poll epoll 区别 和 底层实现-掘金

https://www.cnblogs.com/88223100/p/Deeply-learn-the-implementation-principle-of-IO-multiplexing-select_poll_epoll.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值