Linux高级IO_select、epoll

调用send/write、read/recv这些IO接口进行网络通信时,需要等待IO条件满足(IO事件就绪)才能正常拷贝数据。比如调用send/write需要等待TCP的发送缓冲区有剩余空间才能将数据拷贝到TCP发送缓冲区中,调用read/recv需要等待TCP的接收缓冲区有数据才能将数据拷贝到应用层,即IO=等+数据拷贝,而这些等待是由用户来完成的,高级IO就是降低用户等的时间以提高IO效率。常见的五种IO模型有:阻塞IO、非阻塞IO、信号驱动IO、多路复用/多路转接、异步IO(让操作系统进行IO)。前四种模型是同步IO,最后一种模型是异步IO。同步还是异步取决于进程有没有参与等+数据拷贝的过程。

一.阻塞IO

二.非阻塞IO

我们如果调用read函数从0号文件描述符中读数据时,如果不输入数据,那么程序就会进入阻塞状态。如果想让这个接口进行非阻塞IO,我们可以调用fcntl()系统调用接口将该文件描述符设置为非阻塞状态。
image.png

  1. int fd:想要设置的那个文件描述符
  2. int cmd:
    1. 如果传入F_GETFL :返回一个位图,获取当前文件属性
    2. 如果传入F_SETFL: 可以设置文件的属性,可以设置O_NONBLOCK将该文件描述符的属性设置为非阻塞
  3. 一旦将文件描述符设置为非阻塞状态,那么它的返回状态有4种情况
    1. 成功,返回值大于0
    2. 读到文件末尾,返回值等于0
    3. 读失败,IO事件没有就绪,错误码被设置为11(EAGAIN or EWOULDBLOCK)或者(EINTER)代表这次IO被信号中断,需要重新读取
    4. 读失败,真正读失败,错误码不等于EAGAIN or EWOULDBLOCK or EINTER
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    char buff[128] = {0};

    int sl = fcntl(0, F_GETFL);
    if (sl < 0) 
    {
        perror("fcntl");
        abort();
    }
    fcntl(0, F_SETFL, O_NONBLOCK|sl);

    while (true)
    {
        printf("please enter# ");
        fflush(stdout);
        ssize_t n = read(0, buff, sizeof(buff)-1);
        if (n > 0) 
        {
            buff[n-1] = 0;
            printf("echo# %s\n", buff);  
        }   
        else if (n == 0)
        {
            std::cout << "读到文件末尾!" << std::endl;
            break;
        }
        else
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                sleep(1);
                std::cout << "数据没有准备好 " << std::endl;
                continue;
            }
            else if (errno == EINTR)
            {
                std::cout << "这次IO被信号中断,重新读取 " << std::endl;
                continue;
            }
            else 
            {
                std::cout << "读取失败 " << std::endl;
                break;
            }
        }

    }
    return 0;
}

三.多路复用/多路转接

read/write系统调用,一次只能等待一个文件描述符,而接下来的select,poll、epoll可以一次等待多个文件描述符,这不同于read和write,因为select、poll、epoll只会进行等待,拷贝数据还是io系统调用完成。

3.1 select

如果要使用select进行等待多个文件描述符,我们就必须先得知道哪些文件描述符要被等待,所以我们使用select接口时,必须先设置文件描述符集。

#include <sys/select.h>
void FD_CLR(int fd, fd_set* set); 将set中的fd设置为0
void FD_ISSET(int fd, fd_set* set); 判断set中是否存在fd
void FD_SET(int fd, fd_set* set); 将set中的fd设置为1
void FD_ZERO(fd_set* set); set全部置0

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

  • 返回值:
    • n > 0 ,有n个fd就绪
    • n==0 ,timeout
    • n < 0 等待失败
  • int nfds:等待的多个fd中,最大的fd+1

下面四个参数是输入输出型参数,你可以设置值给系统,操作系统也可以设置值返回给用户。

  • fd_set* readfds:
    • 用户要告诉内核,哪些fd的读事件需要被等待
    • 内核要告诉用户,哪些fd的读事件已经就绪
  • fd_set* writefds:
  • fd_set* exceptfds:

struct timeval
{

time_t tv_sec;
suseconds_t tv_usec:
}

  • struct timeval* timeout:
    • 如果设置为nullptr,则阻塞等待,如果等待的文件描述符中有一个没有就绪,那么select就阻塞。
    • tv_sec:tv_usec = 0:0 ,非阻塞等待
    • tv_sec:tv_usec = n:0 , n秒以内阻塞等待,否则timeout一次
    • 输出:表示剩余时间

缺点:

  1. 每次调用select时,都需要手动设置fd集合,接口使用不方便
  2. 每次调用select时,都需要将fd集合从用户态拷贝到内核态
  3. 每次调用select时,都需要在内核中遍历传递进来的fd集合
  4. select支持的文件描述符数量太少

第二种多路转接方案poll一定程序上解决了select的缺点,将输入输出参数分离,于是就不需要手动设置fd集合,接口使用方便许多,同时也解决了select文件描述符数量太少的缺点。

3.2 poll

image.png

  1. 返回值:
    1. 大于0,几个fd就绪
    2. =0,timeout
    3. <0 ,等待出错

image.pngimage.png

  1. struct pollfd* fds :等待的fd集合,可以设置检测文件描述符的哪些事件,events可以设置关心哪些事件,revents表示哪些事件触发。
    1. POLLIN:读就绪
    2. POLLOUT:写就绪
  2. nfds_t nfds:fds集合的长度
  3. int timeout:单位毫秒
    1. -1:阻塞等待
    2. 0:非阻塞等待
    3. 0:阻塞等待n秒,然后timeout一次

第三种多路复用技术epoll,解决了上述select和poll的缺点。

3.3 epoll
  1. epoll接口

创建epoll模型:image.png

  • int size:设置一个大于0的值就行,这个参数被忽略
  • 返回值:返回一个epoll文件描述符

控制epoll模型:image.png

功能:用户告诉内核哪个fd的什么事件需要被关心

  • int epfd:epoll_create的返回值
  • int op:对epoll模型的操作
    • EPOLL_CTL_ADD
    • EPOLL_CTL_DEL
    • EPOLL_CTL_MOD
  • int fd:操作的目标fd
  • event:关心的事件

等待事件就绪:

image.png

  • int epfd:对某个epoll模型操作
  • struct epoll_event* events:输出型参数,告诉程序员哪些事件就绪
  • int maxevents:events的长度
  • int timeout:效果同timeout

image.png

events:填写下面
image.png

  1. epoll原理

在我们调用epoll_create时,操作系统会为我们创建一个epoll模型,这个模型中有包含三个机制:红黑树、就绪队列、回调机制。所谓epoll模型就是一个结构体,当你调用epoll_create时,os会创建一个结构体,然后创建一个struct file将这个epoll结构体放入这个文件结构体中,给用户返回一个文件描述符,用户就可以通过文件描述符来找到这个epoll模型,进而对这个epoll模型进行操作。这三个机制解决了select和poll的缺点。这颗红黑树就相当于在select和poll中用户定义的第三方fd表,程序员调用epoll_ctl可以操作这棵树;就绪队列保存了已经就绪的文件描述符。当底层有文件就绪时,文件结构体内的回调函数就会被调用,将红黑树中的节点链入就绪队列中,这不像select和poll还需要每次遍历文件描述符集才能确定哪个文件就绪,时间复杂度从n变为了1。当程序员调用epoll_wait接口时,其底层只需要判断就绪队列是否为空即可,也不需要遍历检测。
image.png
让我们回想下select的缺点,1.用户需要每次手动设置关心的fd 2.需要将第三方fd数组拷贝到内核 3.内核在底层要遍历fd集 4.fd数量太少 。其中1和4这两个缺点poll解决了,但是2和3这两个缺点是select和poll都有的,而epoll在底层实现了红黑树解决了缺点2,回调机制解决了缺点3。所以epoll的效率极高。

  1. epoll工作模式

在我们使用select、poll、epoll时,如果有事件就绪,但是没有取走这个数据,那么底层就会一直通知事件就绪。 这种方式是epoll默认的工作模式:LT模式(水平触发),另外epoll还有一种工作模式:ET(边缘触发)

  • ET模式:有效通知只有一次,数据变化时才会通知一次。这种通知方式的效率显然要比LT模式高,因为LT模式在单位时间内,一直在做重复的通知。因为ET会倒逼上层,尽快取走数据,即循环调用recv接口进行非阻塞读取,直到读到的数据小于期望值,那么说明底层数据已经读取完毕。

LT模式,既可以在阻塞模式下工作也可以在非阻塞模式下工作,ET模式就只能在非阻塞模式下工作。对于LT模式和ET模式的IO效率谁高谁低,是要看LT的工作模式是非阻塞还是阻塞。

只要缓冲区中的剩余空间变多,那么TCP报文给对方通知的窗口大小也会变化,对方的滑动窗口也有可能变大,对方发送的数据就会变多,IO效率就会变高

  • ET的应用场景是高IO,LT是要求响应及时的场景
  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值