高级IO涉及的编程模型

五种IO模型

引入

在这里插入图片描述

IO模型

阻塞IO

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
在这里插入图片描述

非阻塞IO

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.
在这里插入图片描述

信号驱动IO

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
在这里插入图片描述

O多路转接

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
在这里插入图片描述

异步IO

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
在这里插入图片描述

IO重要概念

同步通信 vs 异步通信

同步和异步关注的是消息通信机制.

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

多进程多线程, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.

阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

其他高级IO

非阻塞IO,纪录锁,系统V流机制, I/O多路转接(也叫I/O多路复用) ,readv和writev函数以及存储映射IO(mmap),这些统称为高级IO

非阻塞IO

fcntl

一个文件描述符, 默认都是阻塞IO.
在这里插入图片描述

基于fcntl将文件描述符设置为非阻塞

bool SetNonBlock(int fd)
{
    
    
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if (fl < 0)
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}
  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

轮询方式读取标准输入

bool SetNonBlock(int fd)
{
    
    
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if (fl < 0)
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}

int main()
{
    
    
    // 将0号文件描述符设置为非阻塞
    SetNonBlock(0); //只要设置一次,后续就都是非阻塞了

    char buffer[1024];
    while (true)
    {
    
    
        sleep(1);
        errno = 0;
        // 非阻塞的时候,我们是以出错的形式返回,告知上层数据没有就绪:通过errno来辨别出错类型
        // 0号设置为非阻塞的时候,read不再阻塞等待键盘输入内容,而是会直接返回
        ssize_t s = read(0, buffer, sizeof(buffer) - 1); 

        // s > 0 代表读取到数据了,否则代表出错了,出错包含两种,一种代表正常continue再次循环,一种代表异常
        // 出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因,可以通过errno来确定出错类型
        if (s > 0)
        {
    
    
            buffer[s-1] = 0;
            std::cout << "echo# " << buffer << " errno[---]: " << errno << " errstring: " << strerror(errno) << std::endl;
        }
        else
        {
    
    
            // 如果失败的errno值是11,代表其实没错,只不过是底层数据没就绪,非阻塞直接返回而已
            // EWOULDBLOCK 和  EAGAIN 就是 11
            if(errno == EWOULDBLOCK || errno == EAGAIN)
            {
    
    
                std::cout << "当前0号fd数据没有就绪, 请下一次再来试试吧" << std::endl;
                continue;
            }
            // EINTR 也是代表正常
            else if(errno == EINTR)
            {
    
    
                std::cout << "当前IO可能被信号中断,在试一试吧" << std::endl;
                continue;
            }
            else
            {
    
    
                //进行差错处理
            }
        }
    }
    return 0;
}

I/O多路转接之select

初识select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select函数

原型

在这里插入图片描述

参数timeout取值

在这里插入图片描述

fd_set结构及输入输出型参数

在这里插入图片描述

select编写需要第三方数组

在这里插入图片描述

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
  • SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

异常就绪

  • socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(TCP协议头中, 有一个紧急指针的字段),

select优缺点

在这里插入图片描述

代码

I/O多路转接之poll

poll函数接口

在这里插入图片描述
在这里插入图片描述

参数说明

在这里插入图片描述

参数fds及poll优缺点

在这里插入图片描述

代码

I/O多路转接之epoll

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

epoll的相关系统调用

在这里插入图片描述

epoll_create

创建一个epoll的句柄.

  • 自从linux2.6.8之后, size参数是被忽略的.
  • 用完之后, 必须调用close()关闭.

epoll_ctl

epoll的事件注册函数.

  • 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
  • 第一个参数是epoll_create()的返回值(epoll的句柄).
  • 第二个参数表示动作,用三个宏来表示.
  • 第三个参数是需要监听的fd.
  • 第四个参数是告诉内核需要监听什么事.

第二个参数的取值:

  • EPOLL_CTL_ADD :注册新的fd到epfd中;
  • EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL :从epfd中删除一个fd;

struct epoll_event结构如下:
在这里插入图片描述

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

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.

epoll_wait

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

收集在epoll监控的事件中已经发送的事件.

  • 参数events是分配好的epoll_event结构体数组.
  • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
  • maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
  • 参数timeout是超时时间 (毫秒, 0会立即返回, -1是永久阻塞).
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.

epoll工作原理

原理

在这里插入图片描述

  • 当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.

在这里插入图片描述
在这里插入图片描述

细节理解

在这里插入图片描述
在这里插入图片描述

epoll的优点(和 select 的缺点对应)

在这里插入图片描述

注意

epoll中使用了内存映射机制
内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.

这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的

epoll工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
在这里插入图片描述
对比LT和ET
在这里插入图片描述

epoll的使用场景

在这里插入图片描述

epoll中的惊群问题

参考 http://blog.csdn.net/fsmiy/article/details/36873357

代码

猜你喜欢

转载自blog.csdn.net/weixin_54183294/article/details/129951088
今日推荐