【linux系统】IO多路复用的三个函数select、poll和epoll

1.引出IO多路复用

以下内容参考了https://blog.csdn.net/SkydivingWang/article/details/74917897

我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output),往流中读出数据,系统调用read,写入数据,系统调用write。不过话说回来了 ,计算机里有这么多的流,我怎么知道要操作哪个流呢?对,就是文件描述符,即通常所说的fd,一个fd就是一个整数,所以,对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。

当我们处理高并发时,即有好多个fd需要我们处理时,我们该怎么做呢?我们想到的是开辟多个进程或者线程对许多fd进行处理,但是多进程并发与多线程并发又存在着下面的一些问题:

多进程并发:

  • 进程数量有限制
  • 代价太高——进程的销毁/切换
  • 受限于cpu——一个cpu同一个时间只有一个进程在跑
  • 进程间内存隔离
  • 进程间通信复杂

多线程高并发:

  • 受限于cpu——影响响应能力

  • 阻塞——某个线程啥也不干,影响响应能力 非阻塞——死循环一直读数据,影响响应能力

    (双十一几十万人同时访问淘宝,服务器也不可能开几十万个线程,引入IO多路复用)

于是,我们引入IO多路复用,其有selectpollepoll三个函数,这三个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这三个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了(比如可读),select就会返回套接字可读,我们就可以调用recv来接受数据。我们平时用到的scanf、read、recv等函数都是阻塞IO,但是他们一次只能监测一个fd,对一个fd进行操作。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。

2.select函数——同步IO多路复用

select()函数允许一个进程监控多个文件描述符fd,当有一个或多个文件描述符就绪时(即可读可写等),程序对就绪的fd执行相应的操作(读/写等)。select()函数将分别监听三种独立的fd,可读,可写和异常情况的fd。在此函数处会发生阻塞

 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:三种监听的fd的最大值+1

  • readfds:监听的可读的文件fd集合

  • writefds:监听的可写的文件fd集合

  • exceptfds:监听的可读的文件fd集合

  • timeout:延时

    struct timeval {
                   long    tv_sec;         /* seconds */
                   long    tv_usec;        /* microseconds */
               };
    
    

通过下面这四个函数可以创建、操作集合

void FD_CLR(int fd, fd_set *set);//清除监听集合的某个fd
int  FD_ISSET(int fd, fd_set *set);//判断是否在监听集合中
void FD_SET(int fd, fd_set *set);//创建监听集合
void FD_ZERO(fd_set *set);//对监听集合清0

select()函数会一直发生阻塞,阻塞结束的条件:

  1. 一个文件描述符fd就绪
  2. 调用被信号中断
  3. 延时结束

返回值:

  • retval>0:有retval个fd就绪
  • retval=0:超出延时
  • retval<0:发生错误
//eg
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

    
int main(void){

    fd_set rfds;
    struct timeval tv;
    int retval;

    /* Watch stdin (fd 0) to see when it has input. */
    //设置监听集合
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);//0为stdout

    /* Wait up to five seconds. */

    tv.tv_sec = 5;
    tv.tv_usec = 0;

    retval = select(1, &rfds, NULL, NULL, &tv);//std放在readfds处,监听是否可读
    /* Don't rely on the value of tv now! */

    if (retval == -1)//错误
        perror("select()");
    else if (retval){//有fd就绪
        //此时数据是ready的 还在标准IO的缓冲中,并没有读
        //如果不读,程序结束后,缓冲区的东西到达内核的stdin
        printf("Data is available now.\n");
        //修改部分
        char buff[512] = {0};
        scanf("%s", buff);
        //修改部分
    }
        /* FD_ISSET(0, &rfds) will be true. */
    else//超时
        printf("No data within five seconds.\n");
     
    exit(EXIT_SUCCESS);
}

3.poll函数——等待一个fd上的events

poll()函数执行与select类似的任务,等待一个fd集合中的一个fd变成就绪状态

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:在fds参数中指定要监控的文件描述符

    struct pollfd {
                   int   fd;         /* file descriptor */ //文件描述符
                   short events;     /* requested events */ //要监听的时间 可读/写?
                   short revents;    /* returned events */   //反馈 当可读/写时会通过这个参数反馈
               };
    
  • nfds: 要监听的fd的个数,一般就是struct pollfd结构体集合的大小

  • timeout:延时ms

常用的events:

  • POLLIN:可读
  • POLLOUT:可写
  • POLLERR:错误
  • POLLHUP:挂起

阻塞结束条件和返回值与select()类似

//eg:用poll函数写一个服务器,用来接受多个客户端发来的消息并给客户端发送对应信息
#include"../1.select将connect设置为非阻塞/common/head.h"
#include"../1.select将connect设置为非阻塞/common/common.h"
#include"../1.select将connect设置为非阻塞/common/tcp_server.h"
#include<poll.h>

#define POLLSIZE 100
#define BUFSIZE 1024
//小写->大写
char ch_char(char c){
    if(c >= 'a' && c <= 'z') return c - 32;
    return c;
}

int main(int argc, char **argv){
    if(argc != 2){
        fprintf(stderr, "Usage: %s port!\n", argv[0]);
        exit(1);
    }
    int server_listen, fd;
    
    if((server_listen = socket_create(atoi(argv[1]))) < 0){//将socket bind listen封装
        perror("socket_create");
        exit(1);
    }
    
    struct pollfd event_set[POLLSIZE];
    for(int i = 0; i < POLLSIZE; i++){
        event_set[i].fd = -1;
    }
    //用0号event_set监听server_listen
    event_set[0].fd = server_listen;
    event_set[0].events = POLLIN;
    
    while(1){
        //一次while循环只会加上一个client
        int retval;
        if((retval = poll(event_set, POLLSIZE, -1)) < 0){
            perror("poll");
            return 1;
        }
        //如果server_listen就绪,需要向结构体数组里加入客户端fd,以便进行监听
        if(event_set[0].revents & POLLIN){
            if((fd = accept(server_listen, NULL, NULL)) < 0){
                perror("accept");
                continue;
            }
            retval--;
            //找到最小的下标存放fd
            int i;
            for(i = 1; i < POLLSIZE; i++){
                if(event_set[i].fd < 0){
                    event_set[i].fd = fd;
                    event_set[i].events = POLLIN;//对于服务端,要对fd进行读操作
                    break;
                }    
            }
            if(i == POLLSIZE){
                printf("Too many client!\n");
                close(fd);
            }

        }
        //当有fd就绪时,要对就绪的fd进行读操作并且给客户端发送对应的大写信息
        for(int i = 1; i < POLLSIZE; i++){
            if(event_set[i].fd < 0) continue;
            if(event_set[i].revents & (POLLIN | POLLHUP | POLLERR)){
                retval--;
                char buff[BUFSIZE] = {0};
                if(recv(event_set[i].fd, buff, BUFSIZE, 0) > 0){
                    printf("Recv: %s\n", buff);
                    for(int i = 0; i < strlen(buff); i++){
                        buff[i] = ch_char(buff[i]);
                    }
                    send(event_set[i].fd, buff, strlen(buff), 0);
                    
                }else{//当recv失败时,说明这个客户端已经下线,清楚对应的event_set
                    close(event_set[i].fd);
                    event_set[i].fd = -1;
                }
            }
            if(retval < 0) break;
        }
    }

    return 0;
}

4.epoll函数——IO事件通知

epoll()函数执行与poll函数相同的任务:监听多个文件描述符。其有两种模式:

  1. 边缘触发:循环扫描,只有扫描到它才对它监控
  2. 水平触发:当扫描到它时,往后都会对其监控

我们要用到以下3个函数:

  1. int epoll_create(int size);//创建一个新的epoll实例
    //程序是磁盘中的文件在内存的镜像(实例)
    
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //该系统调用对文件描述符epfd引用的epoll(7)实例执行控制操作。 它要求对目标文件描述符fd执行操作op。
    
    • epfd:通过epoll_create()创建的实例

    • op:对epoll的操作

      • EPOLL_CTL_ADD:注册(添加)一个新的fd在epoll实例中
      • EPOLL_CTL_MOD:改变对应fd的event参数(比如由原来监听可读->可写)
      • EPOLL_CTL_DEL:从epoll实例中将对应的fd删除,不再监听
    • fd:要对epoll实例中的哪个实例进行操作

    • event:描述一个fd的状态

      typedef union epoll_data {
                     void        *ptr;
                     int          fd;
                     uint32_t     u32;
                     uint64_t     u64;
                 } epoll_data_t;
      
                 struct epoll_event {
                     uint32_t     events;      /* Epoll events */
                     epoll_data_t data;        /* User data variable */
                 };
      
  3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    //等待epoll实例中fd集合的事件发生
    //执行后epoll实例中所有要监听的fd的状态都放在传出参数events中
    //其中events.data.fd = fd, events.events=可读/写/挂起/错误等
    
    • epfd:创建的epfd实例
    • events:传出参数
    • maxevents:要监听的fd的数量
    • timeout:延时

阻塞结束条件和返回值跟select类似

//eg:用epoll写一个服务器
#include <sys/epoll.h>
#define MAX_EVENTS 10
#include "../1.select将connect设置为非阻塞/common/head.h"
#include "../1.select将connect设置为非阻塞//common/tcp_server.h"
#include "../1.select将connect设置为非阻塞//common/common.h"
#define BUFFSIZE 512

int main(int argc, char **argv) {
    //ev负责向epollfd里面注册
    //events数组用来接受监控的结果
    struct epoll_event ev, events[MAX_EVENTS];
    int listen_sock, conn_sock, nfds, epollfd;
    char buff[BUFFSIZE] = {0};
    if (argc != 2) exit(1);
    listen_sock = socket_create(atoi(argv[1]));
/* Code to set up listening socket, 'listen_sock',
   (socket(), bind(), listen()) omitted */
    epollfd = epoll_create1(0);//创建epoll实例,一开始size=0
    if (epollfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }
//将listen_sock注册进epollfd实例
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;

    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    }

    for (;;) {
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);

        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
		//先找是否有新加入的client
        for (int n = 0; n < nfds; ++n) {
            //来一个client往epollfd注册一个
            if (events[n].data.fd == listen_sock) {
                conn_sock = accept(listen_sock, NULL, NULL);
                if (conn_sock == -1) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                make_nonblock(conn_sock);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = conn_sock;

                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
                    perror("epoll_ctl: conn_sock");
                    exit(EXIT_FAILURE);
                }
            //当epollfd监测的sockfd有数据就绪时,接受转化为大写发送    
            } else {
                //do_use_fd(events[n].data.fd);
                if (events[n].events & (EPOLLIN | EPOLLHUP | EPOLLERR)) {
                    memset(buff, 0, sizeof(buff));
                    if (recv(events[n].data.fd, buff, BUFFSIZE, 0) > 0) {
                        printf("recv: %s", buff);
                        for (int i = 0; i < strlen(buff); i++) {
                            if (buff[i] >= 'a' && buff[i] <= 'z') buff[i] -= 32;
                        }
                        send(events[n].data.fd, buff, strlen(buff), 0);
                        //当没有数据接受时,说明这个sockfd已经关闭,将这个sockfd从epollfd删除
                    } else {
                        if (epoll_ctl(epollfd, EPOLL_CTL_DEL, events[n].data.fd, NULL) < 0) {
                            perror("epoll_ctrl");
                        }
                        close(events[n].data.fd);
                        printf("Logout!\n");
                    }
                }
            }
        }
    }
    return 0;
}

5.三个函数的比较

以下内容参考了https://blog.csdn.net/sinat_36629696/article/details/81186774

  1. select 的参数类型 fd_set 没有将文件描述符和事件绑定 , 只是一个文件描述符集合 , 因此 select 需要提供三个此类型的参数来分别传入和输出可读 , 可写 , 异常等事件 , 这使得 select 不能处理更多类型的事件 , 又因为内核对 fd_set 集合的在线修改 , 下次调用 select 之前必须重置 这 3 个 fd_set 集合

    • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
    • select支持的文件描述符数量太小了,默认是1024
  2. poll 的参数类型 pollfd 将文件描述符和事件绑定 , 任何事件都被统一处理 , 并且内核每次修改的是 pollfd 结构体的 revents 成员 , 而 events 成员保持不变 , 因此下次调用 poll 时无需重置 pollfd 类型的事件集参数

  3. epoll则采用与select和poll完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到o(1)

    epoll 适用于连接数量多 , 但活动连接较少的情况 , 因为当活动连接较多时 , 回调函数的触发过于频繁 , 其效率未必高于 select 和 poll
    在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沙diao网友

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值