0


【Linux杂货铺】IO多路复用

🌈前言🌈

    本期【Linux杂货铺】,主要讲解什么是IO多路复用,通过了解IO模式,了解为什么要有多路复用,以及其他的IO模型,学习多路复用的3中模型。

📁 五种IO模型

📂 阻塞IO

    在内核将数据准备好之前,系统调用会一直等待。默认是阻塞方式。

** 📂 非阻塞IO**

    如果内核还没有将数据准备好,系统调用会直接返回,并且返回**EWOULDBLOCK错误码**。此时,就需要特殊判错误码,究竟是异常错误,还是非阻塞IO。

    非阻塞IO常常需要循环式的读写这个文件描述符,这个过程叫做轮询,对CPU是一个巨大的浪费,只有在特定场景下才会使用。

** 📂 信号驱动IO**

    内核将数据准备好,使用SIGIO信号通知应用程序进行IO操作。

** **📂 多路复用

    同时等待多个文件描述符的就绪状态,如果有一个或多个文件描述符事件触发,就会返回。

** **📂 异步IO

    由内核在数据拷贝完成时,通知应用程序,上层只需要完成数据处理即可。但是异步IO往往伴随着代码逻辑复杂,难以理解,编程困难等问题。

    在IO过程中,需要经过两个步骤,1. 等待;2. 拷贝。等待的时间往往高于拷贝时间,因此提高IO效率,最核心的方法就是缩小等待时间。

    此外,**同步和异步关注的消息通知机制**。

    同步,就是发出一个调用时,在没有得到结果之前,该调用就不返回。一旦调用返回,就得到返回值,即由调用者主动等待这个调用结果;

    异步,调用发出之后,这个调用会直接返回,没有返回结果,即一个异步调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过,信号等通知调用者,通过回调函数处理这个调用。

    **阻塞和非阻塞关注的是程序等待调用结果时状态。**

    阻塞调用指的是调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果才会返回。

    非阻塞调用指的是不能立刻得到结果之前,该调用不会阻塞当前线程。

📁 非阻塞IO实现

    文件描述符默认都是阻塞的。使用fcntl系统调用可以修改文件描述为非阻塞。
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd ,int cmd , ...);

fcntl函数有5种功能:

    1. 复制现有的描述符(cmd = F_DUPFD)

    2. 获得/设置文件描述符标记(cmd = F_GETFD 或 F_SETFD)

    3. 获得/设置文件状态标记(cmd = F_GETFL 或 F_SETFL)

    4. 获得/设置异步IO所有权(cmd = F_GETOWN或F_SETOWN)

    5. 获得/设置锁(cmd = F_GETLK,F_SETLK或F_SETLKW)
void SetNoBlock(int fd)
{
    int fl = fcntl(fd , F_GETFL);
    if(fl < 0)
    {
        prror("fcntl");
        return ;
    }
    fcntl(fd , F_SETFL , fl | O_NONBLOCK);
}

📁 select

** 📂 接口使用**

include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
    • 参数 nfds 是需要监视的最大的文件描述符值+1;

    • rdset,wrset,exset 分别对应于需要检测的可读文件描述符的集合, 可写文件描述符的集合及异常文件描述符的集合;

    • 参数 timeout 为结构体 timeval, 用来设置 select()的等待时间。timeout为NULL时,表示select会一直被阻塞,直到事件发生;timeout为0时,仅检测描述符集合的状态,然后立刻返回,并不会等待外部事件的发生;timeout为特定时间值,如果指定时间段内没有事件发生,select会超时返回。

    fd_set结构表示为1个位图,一个bit位表示要监视文件描述符。select系统调用的三个fd_set参数是输入输出型参数,输入表示要监听哪些描述符,输出表示哪些描述符事件就绪。


void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关fd的位

int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关fd的位是否为真

void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关fd的位

void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位
    select系统调用返回值:执行成功返回文件描述符状态已改变的个数;如果返回值为0表示超时;如果返回-1,错误原因存在于errno。

    select系统调用常常搭配一个数组来使用。将需要监听的描述符添加至数组,通过遍历数组来给fd_set赋值。

** 📂 缺点**

    1. select函数,支持的文件描述符数量太小;

    2. select函数,每次都需要手动设置fd集合,使用不方便;

    3. 每次调用select都需要在内核遍历传递进来所有的fd,这个开销在fd很多时也很大。

    4. 每次调用select,都需要吧fd集合从用户态拷贝到内核态,这个开销在fd很多时很大。

📁 poll

** 📂 接口使用**


include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
    • fds 是一个 poll 函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.

    • nfds 表示 fds 数组的长度.

    • timeout 表示 poll 函数的超时时间, 单位是毫秒(ms)

    如果底层事件就绪,revents会被赋值。

events和revents的取值:

    poll函数返回值小于0,表示出错;返回值等于0,表示poll等待超时;返回值小于0,表示由于poll由于文件描述符就绪而返回。 

** 📂 优缺点**

    1. poll没有描述符的数量限制;接口比select使用更方便,pollfd结构体包含了要监视的event和发生的revent,不在使用select输入输出型参数传递方式。

    2. 同select一样,poll返回后,需要遍历pollfd数组来获取就绪的描述符。

    3. poll需要把大量的pollfd结构体从用户态拷贝至内核态。

📁 epoll

** 📂 接口使用**

1. 创建epoll文件描述符(句柄)
int epoll_create(int size);

2.操作epoll模型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

3. 等待时间
int epoll_wait(int epfd , struct epoll_event* events,int maxevents , int timeout);

op参数表示动作,用3个宏来表示:

    • EPOLL_CTL_ADD: 注册新的 fd 到 epfd 中;

    • EPOLL_CTL_MOD: 修改已经注册的 fd 的监听事件;

    • EPOLL_CTL_DEL: 从 epfd 中删除一个 fd;

uint32_t events可以是一下几个宏的集合:

    • EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);

    • EPOLLOUT : 表示对应的文件描述符可以写;

    • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);

    • EPOLLERR : 表示对应的文件描述符发生错误;

    • EPOLLHUP : 表示对应的文件描述符被挂断;

    • EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.

    • EPOLLONESHOT: 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.

    epoll_wait系统调用会将底层已经就绪的事件拷贝至数组中,系统调用返回值就是已经就绪的事件数量n,只需要遍历数组前n个元素即可。

** 📂 底层原理**

** epoll模型中,节点采用了与类型无关的数据结构,即一个节点可以被连入到不同的数据结构中,既可以在红黑树中,也可以在就绪队列中。**

** 当节点既在红黑树中中,又在就绪队列中,就表示该节点监视的文件描述中有事件触发。**

** 📂 工作模式 ET vs LT**

    水平触发(LT):epoll**默认状态下就是LT模式**。当epoll检测到事件就绪,如果没有处理,或者处理不完全,**再次调用epoll_wait时会立即返回并通知事件就绪**,直到事件被处理完成,如缓冲区上所有数据被处理完成,epoll_wait才不会立刻返回。**支持阻塞读写或非阻塞读写。**

    边缘触发(ET):epoll检测到有事件就绪,必须立刻处理。如果没有处理,或者处理不完全,**下次调用epoll_wait不会再通知**,而是等到有新的数据到来,事件再次就绪,再次通知上层。即事件就绪后,只有一次处理机会。ET模式比LT模式性能更高,Nginx默认采用ET模式。**ET模式只支持非阻塞读写。**

    ET模式的epoll,需要将文件描述符设置为非阻塞,这不是接口上的要求,而是“工程实践”上的要求。

    如果面对下面这种情况,ET模式采用阻塞读取是会出现问题的:

• 服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据.

• 客户端要读到服务器的响应, 才会发送下一个请求

• 客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据

    因此,为了解决上述问题,需要将ET模式改为非阻塞轮询式读取底层数据,一次将数据全部读取完毕。

** 📂 优点**

    1. 接口使用方便。

    2. 数据拷贝轻量。

    3. 事件回调机制,避免了使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入就绪队列中。

    4. 没有数量限制。

📁 总结

    以上,就是本期【Linux杂货铺】的主要内容了,主要讲解了五种IO模型,重点讲解了select,epoll接口,学习了epoll底层原理,以及epoll的工作模式,了解了poll接口,其中epoll是尤为重要的多路复用接口。

    如果感觉本期内容对你有帮助,欢迎点赞,关注,收藏Thanks♪(・ω・)ノ

标签: 网络 服务器 linux

本文转载自: https://blog.csdn.net/jupangMZ/article/details/143664963
版权归原作者 秋刀鱼的滋味@ 所有, 如有侵权,请联系我们删除。

“【Linux杂货铺】IO多路复用”的评论:

还没有评论