C++中的IO多路复用(select、poll、epoll)总结

目录

什么是IO多路复用

IO多路复用的方式简介

select方式

select运行原理

select函数使用方法

实例

注意的点

poll方式

poll方式运行原理

poll函数使用方法

实例

epoll方式

epoll运行原理

epoll函数使用方法

工作模式

水平模式 

实例

边沿模式

实例

边沿模式需要注意的点

参考资料


什么是IO多路复用

        I/O多路复用(IO multiplexing)是一种并发处理多个I/O操作的机制。它允许一个进程或线程同时监听多个文件描述符(如套接字、管道、标准输入等)的I/O事件,并在有事件发生时进行处理。

        传统的I/O模型中,通常使用阻塞I/O和非阻塞I/O来处理单个I/O操作。如果需要同时处理多个I/O操作,那么需要使用多个线程或多个进程来管理和执行这些I/O操作。这种方式会导致系统资源的浪费,且编程复杂度较高。

        而I/O多路复用通过提供一个统一的接口,如selectpollepoll等,来同时监听多个文件描述符的I/O事件。它们会在任意一个文件描述符上有I/O事件发生时立即返回,并告知应用程序哪些文件描述符有事件发生。应用程序可以根据返回的结果来针对有事件发生的文件描述符进行读取、写入或其他操作。

        I/O多路复用的优点包括:

  • 单个进程或线程可以同时处理多个I/O操作,提高了系统的并发性。
  • 避免了大量的进程或线程切换,节约了系统资源
  • 使用较少的线程或进程,简化了编程模型和维护工作。

IO多路复用的方式简介

        主要的 I/O 多路复用方式有以下几种:

  • selectselect 是最早的一种 I/O 多路复用方式,可以同时监听多个文件描述符的可读、可写和异常事件。通过在调用 select 时传递关注的文件描述符集合,及时返回有事件发生的文件描述符,然后应用程序可以对这些文件描述符进行读写操作。

  • pollpoll 是 select 的一种改进版,也能够同时监听多个文件描述符的可读、可写和异常事件。通过调用 poll 时传递关注的文件描述符数组,返回有事件发生的文件描述符,应用程序执行对应的读写操作。

  • epollepoll 是 Linux 特有的一种 I/O 多路复用机制,相较于 select 和 poll 具有更高的性能,适用于高并发环境。epoll 使用了回调机制来通知应用程序文件描述符上的事件发生,并且支持水平触发(LT,level triggered)和边缘触发(ET,edge triggered)两种模式。

select方式

  select 是一种 I/O 多路复用的机制,用于同时监听多个文件描述符的可读、可写和异常事件。它是最早的一种实现,适用于多平台。select几乎在所有的操作系统上都可用,并且拥有相似的接口和语义。这使得应用程序在多个平台上能够以相似的方式使用 select

select运行原理

  select 函数在阻塞过程中,主要依赖于一个名为 fd_set 的数据结构来表示文件描述符集合。通过向 select 函数传递待检测的 fd_set 集合,可以指定需要检测哪些文件描述符。fd_set 结构一般是通过使用宏函数以及相关操作进行初始化和处理。

  fd_set 结构可以用于传递三种不同类型的文件描述符集合,包括读缓冲区、写缓冲区和异常状态。通过将文件描述符放入相应的集合中,程序员可以选择性地检查特定类型的事件或操作。通过使用传出变量,程序员可以获取与就绪状态对应的文件描述符集合,并相应地处理与就绪内容相关的操作。

        下面两张图展示了select函数在运行时的逻辑(读缓冲区为例

select函数使用方法

select函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监视的最大文件描述符加1,即待监视的文件描述符的最大值加1。
  • readfds:可读性检查的文件描述符集合。
  • writefds:可写性检查的文件描述符集合。
  • exceptfds:异常条件的文件描述符集合。
  • timeout:最长等待时间,也可以设置为 NULL,表示一直阻塞直到有事件发生。

函数返回值如下:

  • 大于 0:返回值为有事件发生的文件描述符的总数。
  • 0:表示超时,没有事件发生。
  • -1:出错,可以通过查看全局变量 errno 来获取错误码。

一些值得注意的小细节:

  • nfds 的值必须是所有待监视文件描述符中最大的值加1。
  • 在某些平台上,select 的文件描述符集大小有可能有限制。
  • 调用 select 会阻塞等待,直到有事件发生,这会导致效率问题。
  • 在多个线程中使用 select 可能需要使用互斥锁来保护传递的文件描述符集。

操作fd_set的API:

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

1. FD_ZERO(fd_set *set):清空指定的文件描述符集合 set,将其所有位都置为0。

2. FD_SET(int fd, fd_set *set):将指定的文件描述符 fd 添加到文件描述符集合 set 中,相应的位将被置为1。

3. FD_CLR(int fd, fd_set *set):将指定的文件描述符 fd 从文件描述符集合 set 中移除,相应的位将被清零(置为0)。

4. FD_ISSET(int fd, fd_set *set):检查指定的文件描述符 fd 是否在文件描述符集合 set 中,如果存在,则返回非零值(true);否则,返回零值(false)。

实例

下面是一个利用select实现的客户端与服务器端相互传输的简单示例:

服务器端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <iostream>

using namespace std;

int main() // 基于多路复用select函数实现的并行服务器
{
    // 1 创建监听的fd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2 绑定
    struct sockaddr_in addr; // struct sockaddr_in是用于表示IPv4地址的结构体,它是基于struct sockaddr的扩展。
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9997);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));

    // 3 设置监听
    listen(lfd, 128);

    // 将监听的fd的状态交给内核检测
    int maxfd = lfd;
    // 初始化检测的读集合
    fd_set rdset;
    fd_set rdtemp;
    // 清零
    FD_ZERO(&rdset);
    // 将监听的lfd设置到集合当中
    FD_SET(lfd, &rdset);

    // 通过select委托内核检测读集合中的文件描述符状态, 检测read缓冲区有没有数据
    // 如果有数据, select解除阻塞返回
    while (1)
    {

        rdtemp = rdset;
        int num = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);

        // 判断连接请求还在不在里面,如果在,则运行accept
        if (FD_ISSET(lfd, &rdtemp))
        {
            struct sockaddr_in cliaddr;
            int cliaddrLen = sizeof(cliaddr);
            int cfd = accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&cliaddrLen);

            // 得到了有效的客户端文件描述符,将这个文件描述符放入读集合当中,并更新最大值
            FD_SET(cfd, &rdset);
            maxfd = cfd > maxfd ? cfd : maxfd;
        }

        // 如果没有建立新的连接,那么就直接通信
        for (int i = 0; i < maxfd + 1; i++)
        {
            if (i != lfd && FD_ISSET(i, &rdtemp))
            {

                // 接收数据,一次接收10个字节,客户端每次发送100个字节,下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次
                //  	循环会一直持续, 知道缓冲区数据被读完位置
                char buf[10] = {0};
                int len = read(i, buf, sizeof(buf));
                cout << "len=" <<len<< endl;

                if (len == 0) // 客户端关闭了连接,,因为如果正好读完,会在select过程中删除
                {
                    printf("客户端关闭了连接.....\n");
                    // 将该文件描述符从集合中删除
                    FD_CLR(i, &rdset);
                    close(i);
                }
                else if (len > 0) // 收到了数据
                {
                    // 发送数据
                    if (len > 2)
                    {
                        write(i, buf, strlen(buf) + 1);
                        cout << "写了一次" << endl;
                        sleep(0.1);
                    }
                }
                else
                {
                    // 异常
                    perror("read");
                    FD_CLR(i, &rdset);
                }
            }
        }
    }

    return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <iostream>

using namespace std;


int main()  //网络通信的客户端
{
    // 1 创建用于通信的套接字
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd==-1)
    {
        perror("socket");
        exit(0);
    }

    // 2 连接服务器
    struct sockaddr_in addr;
    addr.sin_family=AF_INET; //ipv4
    addr.sin_port=htons(9997);// 服务器监听的端口, 字节序应该是网络字节序
    inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr.s_addr);
    int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret==-1)
    {
        perror("connect");
        exit(0);
    }

    //通信
    while (1)
    {
        //读数据
        char recvBuf[1024];
        //写数据
        fgets(recvBuf,sizeof(recvBuf),stdin);
        write(fd,recvBuf,strlen(recvBuf)+1);
      
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值