IO多路复用(select/poll/epoll)——基础

IO多路复用——基础

本文主要介绍 select/poll/epoll 三大 IO 多路复用方法,主要介绍相关的函数使用

一、select

介绍:select 是最早的 I/O 多路复用技术之一,其历史可以追溯到 1980 年代,最早由 Unix 系统引入,作为时代的眼泪,在大量连接下的性能远不如 epoll,但 select 有个独特的优势:跨平台

1.1 相关函数

#include <sys/select.h>
struct timeval {
    time_t tv_sec;        // seconds
    suseconds_t tv_usec;  // microseconds
};

int select(int nfds, // 待测试的fd数量,值为最大fd + 1
           fd_set *readfds,  // 读描述符集合
           fd_set *writefds, // 写描述符集合
           fd_set *exceptfds, // 异常描述符集合
           struct timeval *timeout // 超时时间
          );

// 将fd从set中删除
void FD_CLR(int fd, fd_set *set);
// 判断fd是否在set中
int  FD_ISSET(int fd, fd_set *set);
// 将fd添加到set中
void FD_SET(int fd, fd_set *set);
// 初始化set
void FD_ZERO(fd_set *set);

1.2 代码流程

fd_set rfds, rset;

FD_ZERO(&rfds); // 清空rfds
FD_SET(sockfd, &rfds);

int maxfd = sockfd;
while (1) {
    rset = rfds; // 每次循环都重新设置rset
    int nready = select(maxfd + 1, &rset, NULL, NULL, NULL); // 监测rset中所有fd的读事件
    
    if (FD_ISSET(sockfd, &rset)) {  // 新的连接请求
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        printf("accept finshed: %d\n", clientfd);
        FD_SET(clientfd, &rfds);  
        if (clientfd > maxfd) maxfd = clientfd;
    }

    int i = 0;
    for (i = sockfd + 1; i <= maxfd; ++i) { 
        if (FD_ISSET(i, &rset)) { // 有数据可读
            char buffer[1024] = {0};
            int count = recv(i, buffer, 1024, 0);
            if (count == 0) { 
                printf("client disconnect: %d\n", i);
                close(i);
                FD_CLR(i, &rfds);
                continue;
            }
            printf("RECV: %s\n", buffer);
            count = send(i, buffer, count, 0);
            printf("SEND: %d\n", count);
        }
    }
}

举个例子来理解上述过程,服务器启动,监听的 sockfd 假设为 3,有3个客户端来连接,假设其 fd 分别为 4,5,6;先初始化 fd_set rfds,并将 sockfd 添加进去。设置 maxfd 为 3(即 sockfd);第一个客户端来连接,文件描述符为 4,select 监测到 sockfd(3)上有连接请求,通过 accept 接受连接,返回新的文件描述符 4,将文件描述符 4 添加到 rfds 集合,更新 maxfd 为 4。第二、三个客户端同理,返回新的文件描述符 5,6。并将 5,6 添加到 rfds 集合,更新 maxfd 为 6。然后数据处理阶段,首先检测 fd 4,如果有数据可读,则读取并回发,然后 fd 5 和 fd 6 同理,然后持续监测…

1.3 select 流程图

在这里插入图片描述

二、poll

最早的 IO 多路复用机制是 select 系统调用,它出现在 4.2BSD UNIX中(大约在1983年),尽管 select 很有用,但它有一些限制和缺点。特别是 select 的文件描述符集大小受限于 FD_SETSIZE,并且在处理大量文件描述符时效率较低。为了解决这些问题,SVR3 引入了 poll 系统调用,大约在 1986 年左右。但由于后来大明星 epoll 的到来,poll 的存在感现在其实也不高

之所以这样,poll 可以被认为是 select 的小优化,poll 的机制与 select 类似,与 select 在本质上没有多大差别,使用方法也类似

2.1 相关函数

#include <poll.h>
// pollfd结构体
struct pollfd {
    int   fd;       // 委托内核检测的fd
    short events;   // 委托内核检测fd的什么事件
    short revents;  // fd实际发生的事件 -> 传出
};

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

2.2 代码流程

struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;

int maxfd = sockfd;

while (1) {
    int nready = poll(fds, maxfd + 1, -1);
    if (fds[sockfd].revents & POLLIN) {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        printf("accept finshed: %d\n", clientfd);
        fds[clientfd].fd = clientfd;
        fds[clientfd].events = POLLIN;
        if (clientfd > maxfd) maxfd = clientfd;
    }

    int i = 0;
    for (i = sockfd + 1; i <= maxfd; ++i) {
        if (fds[i].revents & POLLIN) {
            char buffer[1024] = {0};
            int count = recv(i, buffer, 1024, 0);
            if (count == 0) { 
                printf("client disconnect: %d\n", i);
                close(i);
                fds[i].fd = -1;
                fds[i].events = 0;
                continue;
            }
            printf("RECV: %s\n", buffer);
            count = send(i, buffer, count, 0);
            printf("SEND: %d\n", count);
        }
    }
}

代码结构与 select 很接近,应该非常好理解

三、epoll

最后来介绍我们的大明星,大网红,epoll 是使得 Linux 成为主流服务器的关键成员,在Linux操作系统中地位很高

3.1 相关函数

俗称“epoll三板斧”

#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
3.1.1 epoll_create() 函数

epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的 fd 集合。

函数参数 size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。

函数返回值:失败则返回-1,成功则返回一个有效的 fd,通过这个 fd 就可以访问创建的epoll实例了

3.1.2 epoll_ctl() 函数

epoll_ctl() 函数的作用是管理红黑树实例上的节点,进行添加、删除、修改操作。

typedef union epoll_data {
    void        *ptr;
    int          fd;	
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      
    epoll_data_t data;        
};

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

函数参数 epfd:epoll_create() 函数的返回值

函数参数 op:执行什么操作

​ ①EPOLL_CTL_ADD:往epoll模型中添加新的节点

​ ②EPOLL_CTL_MOD:修改epoll模型中已经存在的节点

​ ③EPOLL_CTL_DEL:删除epoll模型中的指定的节点

函数参数 fd:要执行操作的 fd

函数参数event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件

​ ①events:委托epoll检测的事件,有EPOLLIN(读事件)、EPOLLOUT(写事件)和 EPOLLERR(异常事件)

​ ②data:用户数据变量

函数返回值:失败则返回-1,成功则返回0

3.1.3 epoll_wait() 函数

epoll_wait() 函数的作用是检测创建的epoll实例中有没有就绪的 fd。

函数参数epfd:epoll_create() 函数的返回值

函数参数events:传出参数, 存储了已就绪的 fd 的信息

函数参数maxevents:修饰第二个参数

函数参数timeout:

​ ①等于 0:函数不阻塞

​ ②大于 0:函数阻塞对应的毫秒数再返回

​ ③等于 -1:函数一直阻塞

函数返回值:成功有两种情况,等于 0 表示函数是阻塞被强制解除了,没有检测到满足条件的 fd;大于 0 表示检测到的已就绪 fd 的总个

数;失败则返回 -1

3.2 代码流程

int epfd = epoll_create(1);

struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

while (1) {
    struct epoll_event events[1024] = {0};
    int nready = epoll_wait(epfd, events, 1024, -1);
    int i = 0;
    for (i = 0;i < nready; ++i) {
        int connfd = events[i].data.fd;
        if (connfd == sockfd) {
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("accept finshed: %d\n", clientfd);
            ev.events = EPOLLIN;
            ev.data.fd = clientfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
        } else if (events[i].events & EPOLLIN) {
            char buffer[1024] = {0};
            int count = recv(connfd, buffer, 1024, 0);
            if (count == 0) { // disconnect
                printf("client disconnect: %d\n", connfd);
                close(connfd);
                epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
                continue;
            }
            printf("RECV: %s\n", buffer);
            count = send(connfd, buffer, count, 0);
            printf("SEND: %d\n", count);
        }
    }
}

后续会继续更新 epoll 的底层原理等

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值