您好,欢迎来到图艺博知识网。
搜索
您的当前位置:首页网络初阶——高效IO & 多路转接

网络初阶——高效IO & 多路转接

来源:图艺博知识网

一、什么是高效 IO

1、什么是 IO

在网络中,向网络读或写是都会遇到阻塞问题的,因此如何使服务器读得更快就是一个问题。而我们知道,无论是从网络读写,还是普通的文件读写,当我们IO(调用 write 或 read 函数)时,本质都是操作系统先等有没有数据或有没有空间,然后再让上层的进程把用户缓冲区的数据拷到 OS 里或让进程把 OS 缓冲区里的数据拷到用户缓冲区里。所以,其实 IO 的过程就是等 + 读写数据的过程。

2、再谈高效 IO

由上面可知,无论是 read 还是 write,本质上都要经历等待 + 读/写。而什么是高效 IO 呢?其实就是在单位时间内等的时间比重变少,把尽可能多的时间都给到读/写上。

二、IO 的 5 大模型

  • 单进程阻塞 IO:等数据到了后,才进行 IO,然后才干自己的事。
  • 单进程非阻塞 IO:一个进程 a.先干自己的事,干完后再自己看看收到数据没有。
  • 单进程信号驱动式 IO:一个进程 a.没收到信号时干自己的事,收到后才进行 IO。
  • 单进程多路转接/多路复用:一个进程 a.但有多个文件描述符,一旦OS缓冲区有数据,直接读。
  • 多进程异步 IO:一个 a 进程、一个 b 进程。b 进程参与 IO,但 a 进程不参与 IO,只拿 IO 数据。

1、阻塞 IO VS 非阻塞 IO

其实阻塞和非阻塞的等待时间是一样长的,只不过非阻塞在等的同时可以做其他事,所以效率就高了。

2、同步 IO VS 异步 IO

同步 IO 就是进程本身参与了 IO 的过程,即进程自己参与了等待或把 OS 缓冲区的数据读上来。而异步 IO 就是进程本身不参与 IO 的过程,只会从用户缓冲区拿数据,或向用户缓冲区写数据。

3、什么是多路复用

一个进程,但管理着多个文件描述符。利用多个文件描述符读写数据。

4、多路复用的 3 个模式

4.0. 就绪事件

对于一个文件来说,它的用途就只有三种:读、写、异常。

对于读,当 OS 缓冲区有数据后才能读,这时就叫读事件就绪;而如果 OS 缓冲区没有数据,那么文件就读不出数据,此时就叫读事件未就绪。

对于写,当 OS 缓冲区有空间后才能写,这时就叫写事件就绪;而如果 OS 缓冲区没有空间存数据,那么文件就读不出数据,此时就叫写事件未就绪。

4.1. select 模式

4.1.1. select 模式原理
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
            struct timeval *timeout);

struct timeval 
{
    long tv_sec;   // 秒(Seconds)
    long tv_usec;  // 微秒(Microseconds)
};

在规定的时间内阻塞等待,时间一到,返回已经就绪的 fd 和就绪 fd 的数量。

4.1.2. set_fd

set_fd 类型其实是一个位图。最低位对应的 fd 为 0,次低位对应的 fd 为 1……

对 set_fd 的相关操作:

#include <sys/select.h>

void FD_CLR(int fd, fd_set *set); // 从位图里移除 fd

void FD_SET(int fd, fd_set *set); // 把 fd 加入到位图里

int FD_ISSET(int fd, fd_set *set); // 看看 fd 是否在位图里

void FD_ZERO(fd_set *set); // 给位图清零
4.1.3. select 模式的缺点 
  •  耗时严重。因为 readfds/writefds/exceptfds 都是输入输出型参数。因此  readfds/writefds/exceptfds 是一定会被改变的,所以我们要自己额外创建 fd 数组,里面填的都是需要被关心的 fd,然后每次调完 select 后都要重新设置位图。而每次都要重新设置位图这个操作是 O(n) 的,还是很费时间的。除此之外,内核检测 fd 就绪,也是要遍历的。
  • 用户要额外使用设置数组来保存需要关心的 fd,非常麻烦。
  • 等待的 fd 数量有限。因为 select 用的是位图,而位图的大小已经被系统定死了,最多就 1024 位,用户是没法控制的。
  • 输出型参数太多,数据拷贝的频率太高。每调一次 select 前就要把用户维护的 fd 数组拷到位图里,然后调完后还要将用户的数据拷回用户维护的 fd 数组。

4.2. poll 模式

4.2.1. poll 模式原理
#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd 
{
    int fd;       /* file descriptor */
    short events;   /* events to look for */
    short revents;  /* events returned */
};

参数介绍:

  • fds:需要关心的 fd 数组。
  • nfds:表示 fds 数组中的元素个数。
  • timeout:超时时间(单位:毫秒),用来控制等待的时间长度。timeout > 0:等待指定的毫秒数。timeout == 0:立即返回,不阻塞。timeout < 0:无限期等待,直到有 fd 就绪。
  • 返回值:就绪的 fd 数量。 

用户传需要关心的 fd 数组,然后系统会在规定的时间内不断对数组进行遍历,当某个 fd 就绪后,系统只会改 pollfd 的 revents,并不会改 pollfd 的 events,因此我们就不用在每次调完 poll 函数之后重置数组了;当时间到了后,就会通过 pollfd 数组和就绪 fd 数量。

常见事件类型(宏)

POLLIN

有数据可读
POLLOUT有空间可写
POLLERR有异常
4.2.2. poll 模式的优点
4.2.3. poll 模式缺点

poll 函数内部还是通过遍历 fds 数组来看哪些 fd 就绪,时间复杂度还是 O(n)。

4.3. epoll 模式

4.3.1. epoll 模型

图中的红黑树是用来存需要关心的 fd 和 fd 的事件类型。就绪队列就是存已经就绪了的 fd。而就绪队列 + 红黑树就组成了一个 epoll 模型。然后 struct file 中有一个指针指向这个 epoll 模型,于是这个进程就可以通过 fd 来访问这个 epoll 模型啦。 

4.3.2. epoll 模型原理

一旦网卡上有数据,网卡就会通过硬件中断给操作系统发信号,同时把数据输入到网卡的驱动层(数据链路层)。然后操作系统就会调这个网卡对应的回调函数。

4.3.3. 回调函数

其实上面提到的回调函数,每个 fd 都会关联一个回调函数。操作系统一旦收到某个硬件的硬件中断后,就会调这个硬件对应的回调函数。然后这个回调函数就会把硬件驱动层的数据拷到上层的接收缓冲区(即 TCP 的那个接收队列);接着回调函数就会去 epoll 模型的红黑树找自己的 fd 关心的事件是不是 EPOLLIN 或 EPOLLOUT,如果和 fd 状态匹配,那么就为这个 fd 构造就绪节点,插入到就绪队列中。

所以 epoll 的检测就绪的效率是比 select 和 poll 要高的。因为 select 和 poll 都要遍历 fd 数组,但 epoll 可以理解成一旦有 fd 收到数据,fd 就会直接通知操作系统,然后操作系统直接调这个 fd 的处理方法,根本不用遍历所有的 fd 。

4.3.3. epoll 的优势

a. 检测就绪的时间复杂度为 O(1),获取就绪的时间复杂度为 O(n)。因为检测就绪就看就绪队列是不是空的就行了,所以是 O(1);但获取就绪要把就绪队列的节点拷到数组里,因此获取就绪是 O(n) 的。不过这已经比 select 和 poll 高效了。因为 select 和 poll 检测就绪都是通过遍历被关心的 fd 数组来查看有哪些 fd 是就绪的;但 epoll 是通过每个 fd 都有的回调函数直接构建就绪队列的节点,并不用遍历红黑树来看哪些 fd 就绪,因此是比 select 和 poll 高效的。

b. 因为就绪队列和红黑树都是由节点组成的,因此能管理的 fd 和 event 是可以无限多的。

c. 因为返回值是就绪队列的节点数,再加上 epoll_wait 函数输出就绪的 fd 数组。所以我们只需要遍历这个就绪 fd 数组可以拿到所有就绪的 fd 了。但 poll 和 select 都要遍历整个被关心的 fd 数组才能知道全部就绪的 fd ,而就绪 fd 数组的大小是远远小于整个被关心的 fd 数组的。

4.3.4. 函数介绍
4.3.4.1. 创建 epoll 模型
int epoll_create(int size);

参数介绍

  • size:该参数已经被废弃了,传一个大于 0 的数进去就行了。 
  • 返回值:成功时,返回 epoll 模型的 fd;失败返回 -1.
4.3.4.2. 获取就绪队列
#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);


附1:
struct epoll_event 
{
    uint32_t events; // 就绪的事件类型,如 EPOLLIN, EPOLLOUT 等
    epoll_data_t data; // 用户数据,通常存储文件描述符或指针
};

附2:
typedef union epoll_data 
{
    void    *ptr;     // 存储任意指针
    int      fd;      // 存储文件描述符
    uint32_t u32;     // 存储 32 位无符号整数
    uint_t u;     // 存储  位无符号整数
} epoll_data_t;

此类型为联合体,一般用这里面 4 个数据中的一个

参数介绍

  • epfd:epoll 模型对应的 fd。
  • events:可以理解成存需要关心的 fd 和 fd 事件类型的数组
  • maxevents:events 数组的元素个数。
  • timeout: 等待事件的超时时间,单位为毫秒(ms)。timeout = -1:阻塞等待,直到有事件发生。timeout = 0:立即返回,无论是否有事件发生。timeout > 0:等待指定的超时时间后返回。
  • 返回值:就绪 fd 的数量
4.3.4.3. 操作 epoll 模型的红黑树
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数介绍

  • epfd:epoll 模型对应的 fd。
  • op:操作类型,
    EPOLL_CTL_ADD:向 epoll 实例中注册新的文件描述符。        
    EPOLL_CTL_MOD:修改已注册文件描述符的监听事件。

    EPOLL_CTL_DEL:从 epoll 实例中删除指定的文件描述符。
  • fd:你要操作的 fd。
  • event:输入型参数。fd 的事件类型。不过如果 op 为 EPOLL_CTL_DEL 时,传 nullptr。
  • 返回值:成功为 0,失败为 -1.

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- huatuoyibo.net 版权所有 湘ICP备2023021910号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务