【C/C++】io 并发:select/poll/epoll 编程入门

【C/C++】io 并发:select/poll/epoll 编程入门


本文描述了 select/poll/epoll 三种 io 编程方式,并且阐述了三者的关系

select

在这里插入图片描述

  1. nfds:值最大的 fd,初始化时置为 sockfd + 1,后续根据连接客户端变化而变化
  2. readfds:fd 集合指针(检查可读)
  3. writefds:fd 集合指针(检查可写)
  4. exceptfds:fd 集合指针(检查错误)
  5. timeout:表示阻塞多长时间,为 NULL 表示一直阻塞

类型定义

在这里插入图片描述

fd_set:一共16个long int,即 16 * 8 * 8 个bit位,一共可以表示1024个 fd

方法说明

  1. FD_SET、FD_CLR分别从fd集合(即之前定义的fd_set变量)中设置或移除一个文件描述符,在程序表现上即相应位置置1
  2. FD_ZERO用于初始化fd集合
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main(void) {
    //监听
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2000);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
        printf("bind error %d\n", sockfd);
        return -1;
    }
    listen(sockfd, 5);
    printf("listening...\n");

    //连接
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr);
    fd_set rfds, rset;
    FD_ZERO(&rfds);
    FD_SET(sockfd, &rfds);
    int maxfd = sockfd;
    while (1) {
        rset = rfds;
        int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (FD_ISSET(sockfd, &rset)) {
            printf("accepting...\n");
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("client %d accepted\n", clientfd);
            FD_SET(clientfd, &rfds);
            if (maxfd < clientfd) {
                maxfd = clientfd;
            }
        }
        for (int i = sockfd + 1; i <= maxfd; i++) {
            if (FD_ISSET(i, &rset)) {
                char buffer[1024] = {0};
                int count = recv(i, buffer, sizeof(buffer), 0);
                if (count == 0) {
                    close(i);
                    FD_CLR(i, &rfds);
                    printf("client %d closed\n", i);
                    continue;
                }
                printf("RECV %s\n", buffer);
                send(i, buffer, count, 0);
                printf("SEND %d\n", count);
            }
        }
    }
    return 0;
}

poll

类型定义

在这里插入图片描述
pollfd:
从select的fd集合演变为了在拥有fd的同时给它添加了两个属性,分别是events和revents,这样做的好处是减少了函数所需要的参数

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>

int main(void) {
    //初始化
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2000);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
        printf("bind error %d\n", sockfd);
        return -1;
    }
    listen(sockfd, 5);
    printf("listening...\n");

    //处理
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr);
    struct pollfd fds[1024];
    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) {
            printf("accepting...\n");
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            printf("client %d accepted\n", clientfd);
            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;
            if (maxfd < clientfd) {
                maxfd = clientfd;
            }
        }
        for (int i = sockfd + 1; i <= maxfd; i++) {
            if (fds[i].revents & POLLIN) {
                char buffer[1024] = {0};
                int count = recv(i, buffer, sizeof(buffer), 0);
                if (count == 0) {
                    fds[i].fd = -1;
                    fds[i].events = -1;
                    close(i);
                    printf("client %d closed\n", i);
                    continue;
                }
                printf("RECV %s\n", buffer);
                send(i, buffer, count, 0);
                printf("SEND %d\n", count);
            }
        }
    }
    return 0;
}

select 和 poll 使我们能够在一个线程内解决多个 clientfd 的问题,但是又出现了另一个问题:这两个函数在检查 io 就绪或者发送请求的时候都需要遍历 fd 集合,但实际上在业务开发中,有很多个客户端连接并不代表着有很多的io请求,比如有100万个客户端连接,往往只有1%左右的请求在同时发生,那么我们是否也需要同时遍历这100万个io呢?

epoll

epoll_create构建了一个机制,即将所有的 io 组织在一起,又将就绪组织在一起,然后聘请一个对象统一管理起来,epoll_ctl负责添加、修改、删除 io 连接,epoll_wait 则负责”聘请对象“多久管理一次

epoll 相比较 select、poll 的优势

  1. epoll 利用 epoll_ctl,在 io 数增加时,与select、poll里面的突然增加不同,而是一个一个积累,而出现 io 事件时,处理就绪就可以了,很好地解决了大并发的情况
  2. 在很大数目的 io (例如 100w 个 io)时,让我们更好地去处理就绪的事件
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void) {
    //初始化
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2000);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
        printf("bind error %d\n", sockfd);
        return -1;
    }
    listen(sockfd, 5);
    printf("listening...\n");

    //连接
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(struct sockaddr);
    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];
        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) {
                printf("accepting...\n");
                int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
                printf("client %d accepted\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) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
                    close(connfd);
                    printf("client %d closed\n", connfd);
                    continue;
                }
                printf("RECV %s\n", buffer);
                send(connfd, buffer, count, 0);
                printf("SEND %d\n", count);
            }
        }
    }

    return 0;
}

总结


  1. select 和 poll 的底层原理相同,都是将 fd 所属的结构体拷贝到内核中,遍历其中 io 是否就绪,select 通过参数控制是可读、可写还是错误,而 poll 则是将这些转换为了 fd 的属性,即通过 pollfd 结构体中的 events 属性来判断,这样减少了 poll 函数所需的参数,但两者的性能都类似
  2. epoll 的出现让我们对 io 的管理从之前的 fd 集合的管理转向了对事件的管理。

技术参考


https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值