Linux多路IO复用:epoll

1. epoll

        epoll是为克服select、poll每次监听都需要在用户、内核空间反复拷贝,以及需要用户程序自己遍历发现有变化的文件描述符的缺点的多路IO复用技术。

epoll原理

创建内核空间的红黑树;

将需要监听的文件描述符上树;

内核监听红黑树上文件描述符的变化;

返回有变化的文件描述符。

epoll优点

        ① 无需在用户、内核空间反复拷贝数据;

        ② 内核返回发生变化的文件描述符,无需用户遍历所有文件描述符。


2. epoll API

(1)epoll_create 创建红黑树

#include

int epoll_create(int size);
/*
功能:
    创建内核中的epoll红黑树;
参数:
    size:监听的文件描述符上限,kernel 2.6版本后写1即可,会自动扩展。
返回值:
    成功:返回红黑树的句柄(相当于操作树的入口)。
    失败:-1,会设置errno。
*/

(2)epoll_ctl 上树、下树、修改节点的监听事件

#include

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/*
功能:
    上树、下树、修改节点。
参数:
    epfd:红黑树的句柄,epoll_create的返回值。
    op:对文件描述符fd的操作
        EPOLL_CTL_ADD:将文件描述符fd上树
        EPOLL_CTL_MOD:修改文件描述符fd的事件
        EPOLL_CTL_DEL:将文件描述符fd下树
    fd:op要操作的文件描述符
    event:用于对特定的文件描述符事件进行设置。
返回值:
    成功:
    失败:
*/

struct epoll_event {
    uint32_t events;    // 监听的事件
    epoll_data_t data;  // 需要监听的文件描述符(共用体中的fd)
}
/*
参数 events:
    EPOLLIN:读事件
    EPOLLOUT:写事件
*/

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

(2)epoll_wait 监听

#include

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
/*
功能:
    监听红黑树上文件描述符的变化;
参数:
    epfd:红黑树的句柄(epoll_create的返回值);
    events:接收发送变化的文件描述符的数组地址
    maxevents:数组元素的个数
    timeout:
        > 0:监听超时时间(多久监听一次);
        0:无文件描述符变化则立即返回;
        -1:阻塞监听到有文件描述符变化才返回
返回值:
    成功:0表示没有文件描述符发生变化;否则返回发生变化的文件描述符的个数
    失败:-1,调用错误,会设置errno
*/

3. epoll使用示例

(1)监听管道

        子进程每3s向管道写数据,父进程使用epoll监听管道,有数据可读则读出管道中的数据。

#include
#include
#include
#include
#include


int main(int argc, const char* argv[]) {

    int fd[2];

    pipe(fd);

    pid_t pid;
    pid = fork();

    if (0 == pid) { // 子进程
        close(fd[0]);
        char buf[5];
        char ch = 'a';
        while (1) {
            memset(buf, ch++, 5);
            write(fd[1], buf, 5);
            sleep(3);
        }
    } else {    // 父进程
        close(fd[1]);

        // 创建红黑树
        int epfd = epoll_create(1);

        // 上树
        struct epoll_event ev;
        ev.data.fd = fd[0];
        ev.events = EPOLLIN;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &ev); // 将fd[0]上树,同时使用ev结构体来设置监听fd[0]的读事件。

        // 监听
        struct epoll_event evs[1]; // 接收从内核返回的有变化的文件描述符的数组。
        while (1) {
            int n = epoll_wait(epfd, evs, 1, -1);
            if (1 == n) {
                char buf[64] = "";
                n = read(fd[0], buf, 64);
                if (n <= 0) {
                    printf("子进程关闭了写端");
                    close(fd[0]);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd[0], &ev); // 下树
                    break;
                } else {
                    printf("读到子进程写的内容:%s\n", buf);
                }
            }
        }
    }
    return 0;
}

运行结果:

Linux多路IO复用:epoll_第1张图片

(2)epoll实现简单并发服务器示例:

#include
#include
#include"wrap.h"

int main(int argc, const char* argv[]) {

    // 1.创建socket,绑定
    int lfd = tcp4bind(8888, NULL);

    // 2.监听
    Listen(lfd, 128);

    // 3.创建树
    int epfd = epoll_create(1);

    // 5.将lfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    // 4.while epoll_wait监听
    struct epoll_event evs[1024];
    while (1) {
        int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
        if (n < 0) {  // 调用出错
            perror("epoll_wait");
            break;
        } else if (0 == n) {
            continue;
        } else {  // 有文件描述符变化
            for (int i = 0;i < n;i++) {
                // 若lfd有读事件
                if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
                    printf("新连接到来:IP = %s, port = %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                        ntohs(cliaddr.sin_port));

                    ev.data.fd = cfd;
                    ev.events = EPOLLIN;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件

                } else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
                    char buf[1024] = "";
                    int n = read(evs[i].data.fd, buf, 1024);
                    if (n < 0) { // 出错
                        perror("read");
                        close(evs[i].data.fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                    } else if (0 == n) {  // 客户端关闭
                        printf("客户端关闭.\n");
                        close(evs[i].data.fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                    } else {
                        write(STDOUT_FILENO, buf, 1024); / printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
                    }
                }
            }
        }
    }
    return 0;
}

运行结果:


4.  epoll的两种工作方式

epoll有两种工作方式:水平触发(LT)、边缘触发(ET)。

(1)水平触发

        如监听文件描述符的读缓冲区时,只要读缓冲区有数据就会触发epoll_wait。例如读缓冲区有数据,只要没读干净,就会触发epoll_wait。

        如监听文件描述符的写缓冲区时,只要可写就会触发epoll_wait,因此监听写缓冲区时推荐使用边缘触发。

(2)边缘触发

        如监听文件描述符的读缓冲区时,读缓冲区有数据到来才会触发epoll_wait;与水平触发不一样,缓冲区数据没读干净且无数据到来,则下次不会再触发epoll_wait,因此要求一次性将读缓冲区数据读干净

        如监听文件描述符的写缓冲区时,写缓冲区数据从有到无才会触发epoll_wait。

epoll默认工作方式为水平触发,但推荐使用边缘触发,以减少epoll_wait系统调用次数


5. epoll的边缘触发使用示例

使用边缘触发,主要两点:1. 监听的事件加上边缘触发的属性;2. 只要触发就一次性将事情处理完。

1. 监听的事件加上边缘触发的属性

无需将监听的文件描述符设置为边缘触发,而是将与客户端通信的文件描述符设置为边缘触发,需将上面的 “epoll实现简单并发服务器示例” 代码第44行:

ev.events = EPOLLIN;

 改为如下,即加上边缘触发的属性。

ev.events = EPOLLIN | EPOLLET;

2. 只要触发就一次性将事情处理完

        以读事件为例,将上面的 “epoll实现简单并发服务器示例” 一次性读取字节数由1024B变为4B,则大多数情况下无法一次read调用就读完缓冲区中所有数据因此循环读取缓冲区,直至读完

        由于是循环读取,直至读完,因此文件描述符cfd需要设置为非阻塞,否则循环到最后一次无数据可读时,read函数将阻塞,无法返回继续监听。

        而水平触发时,结合上面代码,read是阻塞的,但通常不会阻塞住。因为是只要有数据可读就会触发epoll_wait,无数据就不会触发,因此不会阻塞住。

上述 "epoll实现简单并发服务器示例" 改为边缘触发:

#include
#include
#include
#include"wrap.h"

int main(int argc, const char* argv[]) {

    // 1.创建socket,绑定
    int lfd = tcp4bind(8888, NULL);

    // 2.监听
    Listen(lfd, 128);

    // 3.创建树
    int epfd = epoll_create(1);

    // 5.将lfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    // 4.while epoll_wait监听
    struct epoll_event evs[1024];
    while (1) {
        int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
        if (n < 0) {  // 调用出错
            perror("epoll_wait");
            break;
        } else if (0 == n) {
            continue;
        } else {  // 有文件描述符变化
            for (int i = 0;i < n;i++) {
                // 若lfd有读事件
                if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取

                    /* 设置cfd非阻塞 */
                    int flag = fcntl(cfd, F_GETFL);
                    flag |= O_NONBLOCK;
                    fcntl(cfd, F_SETFL, flag);

                    printf("新连接到来:IP = %s, port = %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                        ntohs(cliaddr.sin_port));

                    ev.data.fd = cfd;
                    ev.events = EPOLLIN | EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件

                } else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
                    while (1) { // 循环读取
                        char buf[4] = "";
                        /*缓冲区无数据时,以阻塞的方式读取,则会阻塞等待;若以非阻塞的方式读取,则返回-1,并且设置errno为EAGAIN*/
                        int n = read(evs[i].data.fd, buf, 4);
                        if (n < 0) { // 出错
                            if (EAGAIN == errno) { // 缓冲区被读干净,则继续下一次监听
                                break;
                            }
                            perror("read");
                            close(evs[i].data.fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                            break;
                        } else if (0 == n) {  // 客户端关闭
                            printf("客户端关闭.\n");
                            close(evs[i].data.fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                            break;
                        } else {
                            write(STDOUT_FILENO, buf, 4); // printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
                        }
                    }
                }
            }
        }
    }
    return 0;
}

运行结果:

你可能感兴趣的:(Linux高并发服务器编程,epoll,IO多路复用,Linux网络编程,Linux高并发服务器编程)