• select, poll, epoll 学习笔记


    摘要

        在学习 Redis 的过程中,发现 Redis 底层是复用了现成的 I/O多路复用模型(evport, epoll, kqueue, select),本篇博客就总结一下 Linux 内核中提供的三种模型。

    select

    fd_set

        select()函数主要是建立在fd_set类型的基础上的。fd_set 是一组文件描述字(fd)的集合,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个宏来操作:

    void FD_CLR(int fd, fd_set *set)    //置 fd_set 中 fd 对应的位为 0
    int  FD_ISSET(int fd, fd_set *set)  //判断 fd 在 fd_set 对应的位是否为 1 
    void FD_SET(int fd, fd_set *set)    //将 fd_set 中 fd 对应的位置 1
    void FD_ZERO(fd_set *set)           //将 fd_set 中的所有位全部置 0

       首先先看一下 fd_set 的结构体:

    // from linux-3.10, win 下定义可能不同,一般为 4096(FD_SETSIZE = 64, long long int 类型数组,数组大小为 64, 所以 64 * 64 = 4096)
    // posix_types.h
    
    #undef __FD_SETSIZE
    #define __FD_SETSIZE    1024
    
    typedef struct {
        unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
    } __kernel_fd_set;

        可以看出 fd_set 是一个结构体,该结构体包含了一个 unsigned long 类型的一个数组,数组大小为:1024/(8 * sizeof(long)), 在我的系统上(Ubuntu 16.04),sizeof(long) == sizeof(unsigned long) = 8。所以数组的大小为:1024/(8 * 8) = 16; 由于 fd_set 是以位来标识一个 fd 的,所以我们来计算这个数组表示的位数,数组中的每一个元素都是 long 类型,数组元素个数为 16, 所以得:bits = 8 * 8 * 16 = 1024(bit), 故可以标识 1024 个 fd。

        可以通过下面的代码在自己的 Linux 系统上跑一下,参考代码:

    #include <sys/time.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    
    int main()
    {
            printf("fd_set size = %d
    ", sizeof(fd_set));
            printf("long type size: %d
    ", sizeof(long));
            printf("unsigned long type size: %d
    ", sizeof(unsigned long));
            return 0;
    }
    查看类型占用的字节数

    原型

        先看一下 select 原型:

    /*
    * nfds: 监听的最大描述符序号 + 1
    * readfds: 监听的读描述符集合
    * writefds: 监听的写描述符集合
    * exceptfds: 监听的发生异常的描述符集合
    * *timeout: 时间结构体指针,表示阻塞时间
    * returnVal: 满足监听事件的 fd 总个数
    * */

    int select(int nfds, fd_set *readfds, fd_set *writefd, fd_set *exceptfds, struct timeval *timeout);

    示例代码

    // from man 2 select
    #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);
    
        /* Wait up to five seconds. */
        tv.tv_sec = 5;
        tv.tv_usec = 0;
    
        retval = select(1, &rfds, NULL, NULL, &tv);
        /* Don't rely on the value of tv now! */
    
        if (retval == -1)
            perror("select()");
        else if (retval)
            printf("Data is available now.
    ");
            /* FD_ISSET(0, &rfds) will be true. */
        else
            printf("No data within five seconds.
    ");
    
        exit(EXIT_SUCCESS);
    }
    select 用法示例

    执行原理

    理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

    1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,00002)若fd=5,执行FD_SET(fd,&set);后set变为0010,0000(下标为 5 的位被 置 1)  //参考博客中说是:0001,00003)若再加入fd=2,fd=1,则set变为0010,0110                              //参考博客中说是:0001,00114)执行select(6,&set,0,0,0)阻塞等待
    (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0110。注意:没有事件发生的fd=5被清空 //参考博客中说:set 变为 0000,0011

    总结

    (1) 可监控的文件描述符个数取决于 sizeof(fd_set) 的值,我 Ubuntu 16.04 系统上 sizeof(fd_set)=128,所以可监控的描述符个数为:128*8=1024 个;即可监控的文件描述符的个数是有限的;
    (2) select 在内核执行时会修改传入的 fds 的值,所以每次执行 select() 都要重新传入,可以提前将原 dfs 保存下来,但是每次都需要将 fds 从用户态复制到内核态;
    (3) 因为linux系统对select()的实现中会修改参数tv为剩余时间,所以对于select函数中的最后一个参数,需要在循环中设置,每次循环要重新设置;

    Poll

    原型

    struct pollfd {
        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */
    };
    
    /*
     * *fds:      监听的 pollfd 数组
     * nfds:      监听的 pollfd 个数
     * timeout:   监听的阻塞时间
    * returnVal: 返回满足监听事件的 fd 总个数,同 select 的返回值 *
    */ int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    示例代码

    for(int i=0; i<5; i++) {
        memset(&client, 0, sizeof(client));
        addrlen = sizeof(client);
        pollfds[i].fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
        pollfds[i].events = POLLIN;
        sleep(1);
        while(1) {
            puts("round again");
            poll(pollfds, 5, 50000);
    
            for(int i=0; i<5; i++) {
                if(pollfds[i] & POLLIN) {
                    pollfds[i].revents = 0;  // 恢复 revents ,使之可重用
                    memset(buffer, 0, MAXBUF);
                    read(pollfds[i].fd, buffer, MAXBUF);
                    puts(buffer);
                }
            }
        }
    }
    poll 使用示例

    总结

    (1) 监听的描述符个数不仅限于 1024 个了(可以通过 ulimit -a 查看最大可打开的文件个数, 也可进行修改);
    (2) 传入的 pollfds 是可重用的,但是仍然需要用户态到内核态的拷贝,因为我们置位了 revents;
    (3) 仍然需要遍历所以的 pollfds, 来确定是哪个 fd 满足条件,只是根据 revents 来判断而已

    epoll

    原型

        epoll 函数主要包括三个函数 epoll_create, epoll_ctl, epoll_wait

    epoll_create

    /*
     * size: 建议内核监听的事件个数,Since  Linux  2.6.8,  the size argument is
           ignored
     * returnVal: 成功:非负值,失败:-1
     * 作用:在内核中创建一个红黑树,返回值指向这个红黑树的树根(其实返回值是整数,通过这个索引找到数值中存在的指向树根节点的指针)
     * */
    int epoll_create(int size);
    epoll_create

    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 events */
        epoll_data_t data;        /* User data variable */
    };
    /*
     * epfd:      epoll_create() 返回的 fd (指向红黑树的根节点的句柄)
     * op:        操作类型:EPOLL_CTL_ADD/MOD/DEL
     * fd:        将要进行操作的 fd
     * *event:    结构体的地址,event->events  的取值:
     *            EPOLLIN:读事件
     *            EPOLLOUT:写事件
     *            EPOLLERR:异常事件
     *            EPOLLET: 边缘触发(Edge Triggered)模式, 默认为水平触发(Level Triggered)模式
     *            event->data.fd 的取值:同 fd 参数
     *            为了内核执行 epoll_wait() 的时候,把该结构复制进 出参 events 中,进行返回
     * returnVal: 成功:0, 失败:-1
     * */
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll_ctl

    epoll_wait

    /*
     * epfd:        epoll_create() 返回的 fd (指向红黑树的根节点的句柄)
     * *events:     出参:满足监听条件的 poll_event 类型的数组
     * maxevents:   返回满足监听条件的最大 events 个数
     * timeout:     阻塞时间
     * returnVal:   满足监听条件的文件描述符 fd 的个数, 出错返回 -1
     * */
    
    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
    epoll_wait

    触发方式

        水平触发(Level Trigger):默认的触发方式,如果缓冲区中的数据一次没有全部读取完,则 epoll_wait() 会再次触发,直到所有数据全部被取走完成。

     用管道来模拟水平触发方式,参考代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/epoll.h>
    #include <errno.h>
    #include <unistd.h>
    
    #define MAXLINE 10
    
    int main(int argc, char *argv[])
    {
        int efd, i;
        int pfd[2];
        pid_t pid;
        char buf[MAXLINE], ch = 'a';
    
        pipe(pfd);
        pid = fork();
    
        if(pid == 0) {  //子进程 写操作
            close(pfd[0]);  //关闭管道读端
            while(1) {
                //aaaa
    
                for(i=0; i<MAXLINE/2; i++)
                    buf[i] = ch;
                buf[i-1] = '
    ';
                ch++;
                //bbbb
    
                for(; i<MAXLINE; i++)
                    buf[i] = ch;
                buf[i-1] = '
    ';
                ch++;
                //aaaa
    bbbb
    
                write(pfd[1], buf, sizeof(buf));  //把 buff 写入管道
                sleep(5); //sleep 5s
            }
            close(pfd[1]);
        }else if(pid > 0) {  //父进程 读操作
            struct epoll_event event;
            struct epoll_event revents[10];  //epoll_wait 返回的 event 数组
            int res, len;
    
            close(pfd[1]);  //关闭管道写端
            efd = epoll_create(10);
    
            //event.events = EPOLLIN | EPOLLET;  //ET 边缘触发
            event.events = EPOLLIN;           //LT 水平触发(默认)
            event.data.fd = pfd[0];            //监控管道读操作
            epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
    
            while(1) {
                res = epoll_wait(efd, revents, 10, -1);
                printf("res: %d
    ", res);
                if(revents[0].data.fd == pfd[0]) {
                    len = read(pfd[0], buf, MAXLINE/2);  //只读取管道中的一半内容
                    write(STDOUT_FILENO, buf, len);
                }
            }
            close(pfd[0]);
        }
        return 0;
    }
    
    /*
     * 输出结果为:
     * aaaa
     * bbbb
     * sleep(5s)
     * cccc
     * dddd
     * sleep(5s)
     * ....
     * 结果解析:水平触发会持续触发 epoll_wait 函数返回,直到所监视的文件描述符中的所有内容读完为止
     * */
    epoll-LT.c

        边缘触发(Edge Trigger):默认只触发一次,直到有新的触发事件产生才会再次触发。这种方式可以很大程度上减少 epoll_wait() 的触发次数,提高 epoll 的效率。

        用管道来模拟边缘触发方式,参考代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/epoll.h>
    #include <errno.h>
    #include <unistd.h>
    
    #define MAXLINE 10
    
    int main(int argc, char *argv[])
    {
        int efd, i;
        int pfd[2];
        pid_t pid;
        char buf[MAXLINE], ch = 'a';
    
        pipe(pfd);
        pid = fork();
    
        if(pid == 0) {  //子进程 写操作
            close(pfd[0]);  //关闭管道读端
            while(1) {
                //aaaa
    
                for(i=0; i<MAXLINE/2; i++)
                    buf[i] = ch;
                buf[i-1] = '
    ';
                ch++;
                //bbbb
    
                for(; i<MAXLINE; i++)
                    buf[i] = ch;
                buf[i-1] = '
    ';
                ch++;
                //aaaa
    bbbb
    
                write(pfd[1], buf, sizeof(buf));  //把 buff 写入管道
                sleep(5); //sleep 5s
            }
            close(pfd[1]);
        }else if(pid > 0) {  //父进程 读操作
            struct epoll_event event;
            struct epoll_event revents[10];  //epoll_wait 返回的 event 数组
            int res, len;
    
            close(pfd[1]);  //关闭管道写端
            efd = epoll_create(10);
    
            event.events = EPOLLIN | EPOLLET;  //ET 边缘触发
            //event.events = EPOLLIN;           //LT 水平触发(默认)
            event.data.fd = pfd[0];            //监控管道读操作
            epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
    
            while(1) {
                res = epoll_wait(efd, revents, 10, -1);
                printf("res: %d
    ", res);
                if(revents[0].data.fd == pfd[0]) {
                    len = read(pfd[0], buf, MAXLINE/2);  //只读取管道中的一半内容
                    write(STDOUT_FILENO, buf, len);
                }
            }
            close(pfd[0]);
        }
        return 0;
    }
    
    /*
     * 输出结果为:
     * aaaa
     * sleep(5s)
     * bbbb
     * sleep(5s)
     * cccc
     * sleep(5s)
     * dddd
     * sleep(5s)
     * ....
     * 结果解析:
     * step1: buf = aaaa
    bbbb
     触发 epoll_wait(), 读出 aaaa
    , buf 中还有 bbbb
    , 但没有新的触发事件发生
     * sleep(5s)
     * 
     * step2: buf = bbbb
    cccc
    dddd
     触发 epoll_wait(), 读出 bbbb
    , buf 中还有 cccc
    dddd
    , 没有新的触发事件发生
     * sleep(5s)
     * ...
     * */
    epoll-ET.c

    示例代码

    struct epoll_event events[5];
    int epfd = epoll_create(10);
    for(int i=0; i<5; i++) {
        static struct epoll_event ev;
        memset(&client, 0, sizeof(client));
        addrlen = sizeof(client);
        ev.data.fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
        ev.events = EPOLLIN;
        epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
    }
    while(1) {
        puts("round again");
        nfds = epoll_wait(epfd, events, 5, 10000);
        for(int i=0; i<nfds; i++) {
            memset(buffer, 0, MAXBUF);
            read(events[i].data.fd, buffer, MAXBUF);
            puts(buffer);
        }
    }
    epoll 使用示例

    总结

    (1) 与 poll 一样,没有描述符 1024 的限制;
    (2) 只需要从用户态拷贝一次监控的描述符到内核态;
    (3) 返回了就绪的描述符(events 数组),而不是只返回就绪的描述符总个数,不需要遍历全部描述符

    参考资料:

    https://www.cnblogs.com/anker/p/3265058.html

    https://blog.csdn.net/starflame/article/details/7860091

  • 相关阅读:
    奇虎360安全牛人全球挑战赛无线部…
    C经典之14-双向链表存储1-10---ShinePans
    linux杂谈(十五):ftp的企业应用级的配置(二)
    Cocos2d-x 3.0心得(01)-图片载入与混合模式
    Windows装10gRAC须要注意的几个要点(怎样解决PRKH-1010和PRKR-1062的错误)
    query多选下拉框插件 jquery-multiselect(改动)
    SICP 习题 (1.42)解题总结
    电话号码 【trie树】
    hdu 3854 Glorious Array(线段树or树状数组)
    【转载】CGLib动态代理原理及实现
  • 原文地址:https://www.cnblogs.com/zpcoding/p/13128510.html
Copyright © 2020-2023  润新知