Linux系统编程(九)--高级IO-多路复用

1 问题提出

// fd1, fd2, fd3 分别是以只读的方式打开的三个不同有名管道的描述符(a.fifo, b.fifo, c.fifo)

while (1) {
    n = read(fd1, buf, 64);
    write(STDOUT_FILENO, buf, n);

    n = read(fd2, buf, 64);
    write(STDOUT_FILENO, buf, n);

    n = read(fd3, buf, 64);
    write(STDOUT_FILENO, buf, n);
}

假设读写管道,只要写端没有写入数据,读端就永远阻塞。意味着上面的程序将会在三个 read 中任何一个或多个发生阻塞。

多进程和多线程可以解决,需要同步,异步 IO 可以搞定,使用非阻塞 IO也可以(轮循,浪费资源)

使用IO多路复用来解决。IO多路复用(I/O Multiplexing)就是一条 IO 通道,被划分成了多个 IO 通道,允许同时进行多个 IO 读写。

2 fd_set容器

2.1 fd_set的实现

对于 fd_set 来说,0 号隔间只能放 0 号描述符,1 号隔间只能放 1 号描述符……

实际上,fd_set 类型是利用整型数组实现的,每个元素中的每个 bit 位被置 1 就表示该位置保存了描述符,如果为 0 就表示没有该描述符。

请添加图片描述

一种 fd_set 的实现,fd_set 中保存了 5 号和 14 号描述符。

2.2 操作fd_set容器

#include <sys/select.h>

// 判断描述符 fd 是否在集合中
int FD_ISSET(int fd, fd_set *fdset);

// 将描述符 fd 从集合中删除
int FD_CLR(int fd, fd_set *fdset);

// 将描述符 fd 添加到集合中
int FD_SET(int fd, fd_set *fdset);

// 将集合清空(所有 bit 置 0)
int FD_ZERO(fd_set *fdset);

3 select函数

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

3.1 参数说明:

中间三个参数都是 fd_set 类型

  • readfds,监听集合中的描述符是否有数据可读。

  • writefds,监听集合中的描述符是否可写,写 IO 也会发生阻塞,比如缓冲区满了。

  • exceptfds,你想监听这个集合中的描述符是否发生异常。

如果三个集合都为空,nfds = 0,只给时间参数传值,相当于 sleep 函数,提供微秒精度。

参数 nfds ,传入参数的那三个集合中,最大的描述符的值 + 1。

最后一个参数是等待时间

struct timeval {
    long    tv_sec;         /* 秒 */
    long    tv_usec;        /* 微秒 */
};

如果传空,表示永远等待,直到三个集合中的描述符有事件(有数据可读、有数据可写、有异常事件发生)到来。不为值,表示最长愿意等待多久。

3.2 select返回值

select 函数返回值体现在两方面:1、函数返回值;2、修改三个集合参数

1、函数返回值

对于函数返回值来说,主要有 3 种情况:

  • 返回值 < 0,表示函数执行出错,比如使用了不可用的描述符。select 被信号打断,也会返回 < 0。因为 select 函数是不支持自动重启动的,被信号打断会立即返回,然后把 errno 的值设置成 EINTR.。
  • 返回值 = 0,超时时间到了,还没有事件发生。
  • 返回值 > 0,表示监听的描述符中,有几个事件发生,累计。

2、修改参数

如何知道哪些描述符上发生了事件,修改三个传入的描述符集合。如果某个集合中的描述符上有事件到来,select 返回时,会保留该描述符,未发生事件的描述符清除。

对于超时参数来说,如果在超时时间到达前发生异常或有事件到来,该参数被被更新为剩余时间。

实验

程序 select.c 从标准输入、a.fifo 和 b.fifo 中读数据并打印到屏幕。

程序 writepipe.c 主要向管道文件写数据。

  • select.c
// select.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

int process(char* prompt, int fd) {
    int n;
    char buf[64];
    char line[64];
    n = read(fd, buf, 64);
    if (n < 0) {
        // read 执行出错
        PERR("read");
    }
    else if (n == 0) {
        // 如果对端关闭,read 返回 0
        sprintf(line, "%s closed\n", prompt);
        puts(line);
        return 0;
    }
    else if (n > 0) {
        buf[n] = 0;
        sprintf(line, "%s say: %s", prompt, buf);
        puts(line);
    }
    return n;
}

int main() {
    int n, res;
    char buf[64];

    fd_set st;
    FD_ZERO(&st);

    int fd0 = STDIN_FILENO;
    int fd1 = open("a.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fd1);
    int fd2 = open("b.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fd2);

    FD_SET(fd0, &st);
    FD_SET(fd1, &st);
    FD_SET(fd2, &st);

    // 最后一个 open 的描述符值是最大的
    int maxfd = fd2 + 1;

    while (1) {
        // 因为 tmpset 参数会被 select 修改,所以要重新赋值。
        fd_set tmpset = st;
        res = select(maxfd, &tmpset, NULL, NULL, NULL);

        if (res < 0) {
            // select 执行出错,对于被信号中断的,需要单独处理,这里暂时不考虑,后面的文章会讲
            PERR("select");
        }
        else if (res == 0) {
            // 超时,先不用管
            continue;
        }

        // 判断返回的集合中是否包含对应的描述符,如果包含,说明的事件(可读)到来。
        if (FD_ISSET(fd0, &tmpset)) {
            n = process("fd0", fd0);
            // 如果返回值为 0,表示对端关闭,后面的也一样。
            if (n == 0) FD_CLR(fd0, &st);
        }
        if (FD_ISSET(fd1, &tmpset)) {
            n = process("fd1", fd1);
            if (n == 0) FD_CLR(fd1, &st);
        }
        if (FD_ISSET(fd2, &tmpset)) {
            n = process("fd2", fd2);
            if (n == 0) FD_CLR(fd2, &st);
        }
    }
}
  • writepipe.c

    writepipe 程序主要向管道文件写数据。它从命令行接收管道文件的名字。

// writepipe.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: %s <fifoname>\n", argv[0]);
        return 1;
    }
    char buf[64];
    int n;
    int fd = open(argv[1], O_WRONLY);
    if (fd < 0) {
        perror("open pipe");
        return 1;
    }
    while (1) {
        n = read(STDIN_FILENO, buf, 64);
        write(fd, buf, n);
    }
    return 0;
}

编译

$ mkfifo a.fifo
$ mkfifo b.fifo

打开三个终端,分别运行:

$ ./select

$ ./writepipe a.fifo

$ ./writepipe b.fifo

请添加图片描述

3.3 select与信号

select 函数可能会返回错误,比如使用了错误的描述符,或者被信号打断。返回值 < 0,同时 errno 会被置成 EINTR。使用 select 函数的时候,要处理被信号中断的情况。

// select_sig.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

void handler(int sig) {
    if (sig == SIGALRM)
        puts("Hello SIGALRM");
}

int process(char* prompt, int fd) {
    int n;
    char buf[64];
    char line[64];
    n = read(fd, buf, 64);
    if (n < 0) {
        // error
        PERR("read");
    }
    else if (n == 0) {
        // peer close
        sprintf(line, "%s closed\n", prompt);
        puts(line);
        return 0;
    }
    else if (n > 0) {
        buf[n] = 0;
        sprintf(line, "%s say: %s", prompt, buf);
        puts(line);
    }
    return n;
}

int main() {
    int n, res, fd0, maxfd;
    char buf[64];
    struct sigaction sa;
    fd_set st;

    // 打印 pid
    printf("pid = %d\n", getpid());

    // 安装信号处理函数
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, NULL);

    // 为了简化程序,这里只管理一个描述符
    FD_ZERO(&st);
    fd0 = STDIN_FILENO;
    FD_SET(fd0, &st);

    maxfd = fd0 + 1;

    while (1) {
        fd_set tmpset = st;
        res = select(maxfd, &tmpset, NULL, NULL, NULL);

        if (res < 0) {
            // 如果被信号打断的,不让程序退出,直接 continue
            if (errno == EINTR) {
                perror("select");
                continue;
            }
            // 其它情况的错误,直接让程序退出
            PERR("select");
        }
        else if (res == 0) {
            // timeout
            continue;
        }

        if (FD_ISSET(fd0, &tmpset)) {
            n = process("fd0", fd0);
            if (n == 0) FD_CLR(fd0, &st);
        }
    }
}
  • 编译
$ gcc select_sig.c -o select_sig1
  • 运行

启动 select_sig 后,在另一个终端给它发 SIGALRM 信号。

请添加图片描述

4 poll函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数是struct pollfd 数组,第二个参数是数组大小,第三个参数是超时时间。

struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 监听的事件,比如可读事件,可写事件 */
    short revents;    /* poll 函数的返回结果,是可读还是可写 */
};

select将可读事件、可写事件描述符单独放进两个不同的集合,poll 函数分配一个结构体,一次性监听。

fd 表示要监听哪个描述符,如果是负数,poll 函数忽略。

events,使用 bit 位来保存你要监听什么事件,用“位或”操作为其赋值,可选值如下:

  • POLLIN: 监听是描述符是否可读。

  • POLLPRI:监听是否有紧急数据可读。

  • POLLOUT:监听是描述符是否可写。

  • POLLRDHUP:监听流式套接字对端是否关闭右半关闭。

如果有监听的事件到来,将事件类型保存到 revents 成员中,比如监听到了有数据可读,则 revents 的 bit 位中会保存 POLLIN。

一些异常事件不主动监听它也会发生,并保存到 revents 中:

  • POLLERR:硬件问题,少见。

  • POLLHUP:对端挂断,比如对于有名管道,其中一端关闭了。

  • POLLNVAL:使用了未打开的描述符

timeout 参数

  • = -1,表示永远等待,直到有事件发生。

  • = 0,不等待,立即返回。

  • > 0,等待 timeout 毫秒。

poll 与 select对比

所做的事件是一样的,但是它们也有区别:

  1. select 使用 fd_set 来存放描述符,poll 使用结构体数组。
  2. select 能够一次监听的描述符数量是受 fd_set 集合的限制的,通常最多放1024个描述符。poll 一次能够监听的描述符个数是数组大小决定的,要看 nfds_t 被定义成什么类型,如果是 unsigned long,4字节宽,poll 能监听 232−1个描述符。

实验

程序 poll.c 只是对前面的 select.c 做了一点点修改,将 select 替换成了 poll 函数。另外,为了能演示异常事件,程序使用了一个未打开的描述符 fd3。poll 函数同样会被信号打断。

// poll.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

void handler(int sig) {
    if (sig == SIGINT) {
        puts("hello SIGINT");
    }
}

int process(char* prompt, int fd) {
    int n;
    char buf[64];
    char line[64];
    n = read(fd, buf, 64);
    if (n < 0) {
        // error
        PERR("read");
    }
    else if (n == 0) {
        // peer close
        sprintf(line, "%s closed\n", prompt);
        puts(line);
        return 0;
    }
    else if (n > 0) {
        buf[n] = 0;
        sprintf(line, "%s say: %s", prompt, buf);
        puts(line);
    }
    return n;
}

int main() {
    int i, n, res;
    char buf[64];

    struct pollfd fds[4];

    if (SIG_ERR == signal(SIGINT, handler)) {
        PERR("signal");
    }

    int fd0 = STDIN_FILENO;
    int fd1 = open("a.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fd1);
    int fd2 = open("b.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fd2);
    int fd3 = 100;

    fds[0].fd = fd0;
    fds[1].fd = fd1;
    fds[2].fd = fd2;
    fds[3].fd = fd3;

    for (i = 0; i < 4; ++i) {
        fds[i].events = POLL_IN;
    }


    while (1) {
        res = poll(fds, 4, -1);

        if (res < 0) {
            // error
            if (errno == EINTR) {
                perror("poll");
                continue;
            }
            PERR("poll");
        }
        else if (res == 0) {
            // timeout
            continue;
        }

        for (i = 0; i < 4; ++i) {
            if (fds[i].revents & POLLIN) {
                sprintf(buf, "fd%d", i);
                n = process(buf, fds[i].fd);
                if (n == 0) fds[i].fd = -1;
            }
            if (fds[i].revents & POLLERR) {
                printf("fd%d Error\n", i);
                fds[i].fd = -1;
            }
            if (fds[i].revents & POLLHUP) {
                printf("fd%d Hang up\n", i);
                fds[i].fd = -1;
                if (fds[i].revents & POLLNVAL) {
                    printf("fd%d Invalid request\n", i);
                    fds[i].fd = -1;
                }
            }
        }
    }
}
  • 编译
$ gcc poll.c -o poll
  • 运行

请添加图片描述

5 epoll函数

5.1 IO事件

事件是对于缓冲区来说的。如果缓冲区中的数据发生变化,说明有 IO 事件产生。

5.2 select 与 poll的缺点

1、需要自己判断哪个描述符发生事件

select 或 poll 返回后,需要一个一个去查询是哪个描述符上发生了 IO 事件。epoll 能将所有发生事件的描述符保存到数组中,没有发生事件的描述符不保存。

2、描述符复制

使用 select 或 poll ,每一次都需要将想要监听的描述符传递给它。每一次调用都需要将这些描述符复制到内核空间。而 epoll 只要事先复制一次。

5.3 使用epoll

使用 epoll 函数的步骤通常如下:

  1. 首先创建一个 epoll 对象,返回该对象的描述符

  2. 通过该对象的描述符,将要监听的描述符复制到内核

  3. 开始监听事件

三个步骤对应三个函数

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

5.4 epoll_create

int epoll_create(int size);

创建一个 epoll 对象,返回对象的描述符。参数 size表示你想监听几个描述符。

5.5 epoll_create

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

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

struct epoll_event {
    uint32_t     events;      /* Epoll 事件 */
    epoll_data_t data;        /* 用户数据 */
};

参数epfd

epoll 对象的描述符,由 epoll_create 函数返回。

参数op

决定向 epoll 对象中添加、修改还是删除描述符。取值如下

含义
EPOLL_CTL_ADD将参数fd指定的描述符添加到 epoll 对象中,同时将其关联到一个epoll 事件对象,即参数 event 所指定的值
EPOLL_CTL_MOD修改描述符fd所关联的事件对象 event
EPOLL_CTL_DEL将描述符fd从epoll对象中移除

参数 event

该参数是 struct epoll_event 结构体指针。event 参数关联到参数 fd 上,表示想监听描述符 fd 上的哪种 IO 事件,比如可读事件,可写事件。结构体成员 events有下面的值:

含义
EPOLLIN监听 fd 是否可读
EPOLLOUT监听 fd 是否可写
EPOLLRDHUP监听流式套接字对象是否关闭或半关闭
EPOLLPRI监听是否有紧急数据可读
EPOLLET触发模式

EPOLLET默认情况下为水平触发(Level Triggered),另一种为边沿触发(Edge Triggered)。

5.6 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

作用:监听所有描述符上是否有事件发生。这些描述符之前都由 epoll_ctl 添加。

如果都没有 IO 事件发生,阻塞,直到有事件到来。一旦有事件到来,epoll_wait 会返回发生事件的个数,所有发生的事件保存到数组events中,数组大小为 maxevents。 events是一个输出参数,充当返回值。如果发生事件的个数比maxevents大,下次再传。返回的 events 数组中,每个元素都表示一个事件,假设 epoll_wait 返回值是 3,就表示有 3 个事件发生,events[0]、events[1] 和 events[2]

用户数据data中的描述符 fd 表示在哪个描述符上发生

一些异常事件,即使不主动监听,如果发生了也会主动通知你:

含义
EPOLLERR描述符有错误,这少见,比如硬件问题
EPOLLHUP关联的描述符有一端挂断,比如管道一端关闭

参数 timeout,超时参数。

  • timeout = -1,永远等待。
  • timeout = 0,立即返回。
  • timeout > 0,最长等待 timeout 毫秒。

返回值

  • > 0,表示有几个事件发生。

  • = 0,表示超时时间到了。

  • < 0,则出错,同时设置 errno 的值。

实验

程序 epoll.c 仍然使用 select 和 poll 中的那个案例,这里只是改成了 epoll 的方式。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

// 信号处理函数,验证 epoll_wait 会被信号打断
void handler(int sig) {
    if (sig == SIGINT) {
        puts("hello SIGINT");
    }
}

// 处理描述符上发生的事件
int process(char* prompt, int fd) {
    int n;
    char buf[64];
    char line[64];
    n = read(fd, buf, 63);
    if (n < 0) {
        // error
        PERR("read");
    }
    else if (n == 0) {
        // peer close
        sprintf(line, "%s closed\n", prompt);
        puts(line);
        return 0;
    }
    else if (n > 0) {
        buf[n] = 0;
        sprintf(line, "%s say: %s", prompt, buf);
        puts(line);
    }
    return n;
}

int main() {
    int i, n, res;
    char buf[64];
    int fds[3];
    int fd;

    if (SIG_ERR == signal(SIGINT, handler)) {
        PERR("signal");
    }

    fds[0] = STDIN_FILENO;
    fds[1] = open("a.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fds[1]);
    fds[2] = open("b.fifo", O_RDONLY);
    printf("open pipe: fd = %d\n", fds[2]);


    // 事件数组 evts 用来保存 epoll_wait 返回的事件
    struct epoll_event evts[4];

    // 创建一个 epoll 实例对象
    int epfd = epoll_create(4);

    // 添加你所关心的描述符到 epoll 实例对象中
    for (i = 0; i < 3; ++i) {
        struct epoll_event ev;
        ev.data.fd = fds[i]; // 注意这个值必须要指定,不然 epoll_wait 返回了你也不知道是谁发生了事件
        ev.events = EPOLLIN; // 想监听可读事件,因为没有指定 EPOLLET 选项,所以默认是水平触发
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev) < 0) {
            PERR("epoll_ctl");
        }
    }


    while (1) {
        // 开始等待事件发生,res 表示发生了事件的个数
        res = epoll_wait(epfd, evts, 4, -1);
        printf("res = %d\n", res);

        if (res < 0) {
            // error
            if (errno == EINTR) {
                perror("epoll_wait");
                continue;
            }
            PERR("epoll_wait");
        }
        else if (res == 0) {
            // timeout
            continue;
        }

        // 开始处理所有事件
        for (i = 0; i < res; ++i) {
            // 这个 fd 就是你一开始通过 event 的 data 成员传进去的。
            fd = evts[i].data.fd;
            if (evts[i].events & EPOLLIN) {
                sprintf(buf, "fd%d", fd);
                process(buf, fd);
            }
            // 这里我们根据没有监听可写事件,所以这种情况不会发生。
            if (evts[i].events & EPOLLOUT) {
                printf("fd%d can write\n", i);
            }
            // 下面这两个事件就算你没有监听,也可能会产生,需要单独处理
            if (evts[i].events & EPOLLERR) {
                printf("fd%d Error\n", i);
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
            }
            if (evts[i].events & EPOLLHUP) {
                printf("fd%d Hang up\n", i);
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
            }
        }
    }
}
  • 编译
gcc epoll.c -o epoll
  • 运行

请添加图片描述

6 epoll触发模式

6.1 两种触发

默认为水平触发,另一种为边沿触发。

如果为 edge-triggered 方式,只有在缓冲区发生变化的情况下,epoll_wait 函数才会返回。

如果为 level-triggered 方式,那么只要缓冲区有数据可读,或者缓冲区有空位可写, epoll_wait 就返回。

如何设置为edge-triggered 触发方式:ev.events |= EPOLLET;

实验

演示两种触发方式的不同,这里使用上一篇文章的代码,然后修改两处:

  • 第一个地方,添加 EPOLLET 触发模式
for (i = 0; i < 3; ++i) {
    ev.data.fd = fds[i];
    ev.events = EPOLLIN;
    // 添加下面这一行,我们通过命令行传参的方式来控制是使用还是不使用 edge-triggered 方式
    if (argc > 1) ev.events |= EPOLLET;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev) < 0) {
        PERR("epoll_ctl");
    }
}
  • 第二个地方,process 函数
int process(char* prompt, int fd) {
    int n;
    // 用作 read 参数的 buf 缓冲区大小改成了 4 字节
    char buf[4];
    char line[64];
    // 最多读 3 个字节,第 4 字节是用来保存 '\0' 字符的。
    n = read(fd, buf, 3);
    //...
}
  • 运行,level-triggered 实验

请添加图片描述

在实验中,管道 a.fifo 的写端首先写入he这个单词,epoll_et 中最后通过 read 函数将缓冲区中的这两个字符完全读取,并打印。

接下来,又写入helloword,此时描述符 fd3 对应的内核缓冲区中就有了 helloword 这一串。在 epoll_et 程序中,立即触发了 IO 事件,epoll_wait 返回了。进入了 process 函数。

因为 read 接收缓冲区 buf 大小只有 4,所以 read 的时候最多读入 3 个字节的数据,buf 第 4 字节是用来保存 \0 字符的,所以不能被占用。从内核缓冲区读取 hel 三个字符后到 buf 后并打印在屏幕上,process 函数结束。

接下来又回到 epoll_wait,前方高能,因为此时还没有任何人向管道写数据,所以描述符 fd3 对应的内核缓冲区中还是只有数据 loword,根据 level-triggered 的规则,只要缓冲区中有数据,就触发 IO 事件,因此 epoll_wait 又立即返回,后面的事件,其实都差不多了,就不重复了。

  • edge-triggered 实验

要使用 edge-triggered,需要在启动 epoll_et 的时候传入一个参数,随便什么都行,我就在后面加了个数字 1。

看图 3,这个过程比 level-triggered 方式要复杂,一步一步来看。首先,向 a.fifo 写入两个字符 he,然后 epoll_et 中 epoll_wait 返回,读取 he 打印。

第二次,向 a.fifo 写入了helloworld,此时 fd3 对应的内核缓冲区由空变成了有数据 helloworld,触发了 IO 事件,因此 epoll_wait 函数返回,进入 process 函数后,从缓冲区读取了三个字符,但是,此时缓冲区中还有字符loworld,接下来又返回到了 epoll_wait,前方高能,虽然缓冲区中有数据,但是此时的触发方式是 edge-triggered!数据没有产生变化,也就是没有增多,因此不会触发 IO 事件,epoll_wait 阻塞了!

请添加图片描述

接下来,向管道仅仅写入一个字母 a,fd3 对应的内核缓冲区产生变化了,再由 loworld 变成了 loworlda的那一瞬间之前(数据 a 在进入缓冲区的过程中,但是还没进入,凭什么这样说,待会儿有图 5 中可以看到),触发了 IO 事件,epoll_wait 函数返回,从缓冲区中读了 3 个字符 low,接下来,epoll_wait 又阻塞了。

看图 5,后面又连续一个一个的输入 b, c, d, e,可以看到,最后一次输入 e 的时候,并不是打印 de,而是只打印了个 d,从这一点也可以证明,e 在进入缓冲区之前,IO 事件就已经触发了。

请添加图片描述

6.2 边沿触发 + 非阻塞

因为 read 的接收缓冲区太小,每次缓冲区的数据读取不完,从而在下次数据到来前,即使缓冲区还有剩余数据未读取,epoll_wait 函数也会阻塞。

解决方法:

使用 while 循环反复从缓冲区非阻塞的方式读,如果 read 返回值 < 0 同时 errno = EAGAIN 或 errno = EWOULDBLOCK 了,就说明缓冲区读完了,退出循环。不使用非阻塞读,一旦读完了,read 就会发生阻塞。

实验:

上一篇文章的代码只需要修改两个地方。

  • 修改以非阻塞的方式打开管道
fds[1] = open("a.fifo", O_RDONLY | O_NONBLOCK);
printf("open pipe: fd = %d\n", fds[1]);
fds[2] = open("b.fifo", O_RDONLY | O_NONBLOCK);
  • 修改 process 函数,以循环方式 read
int process(char* prompt, int fd) {
    int n;
    char buf[4];
    char line[64];
    sprintf(line, "%s say: ", prompt);

    // 开始循环 read
    while (1) {
        n = read(fd, buf, 3);
        if (n < 0) {
            // 如果 errno 的值是 EAGAIN 或 EWOULDBLOCK,说明缓冲区数据读完了。
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;
            PERR("read");
        }
        else if (n == 0) {
            // 对端关闭
            sprintf(line, "%s closed\n", prompt);
            puts(line);
            return 0;
        }
        else if (n > 0) {
            buf[n] = 0;
            // 把从 read 读到的内容串接到 line 后面。
            strcat(line, buf);
        }
    }
    puts(line);
    return n;
}

请添加图片描述

两种触发模式的优缺点
假设每次 read 都不能一次性缓冲区数据读完。

首先从程序运行的角度上看,水平模式+阻塞读,只要缓冲区还有数据,每次都会触发 epoll_wait 函数返回。

边沿模式+非阻塞读,只触发一次 epoll_wait 返回,然后 read 循环读。

那么效率就体现在

  • while { epoll_wait + read }

  • epoll_wait + while {read}

目前还没有谁证明过后者比前者快。有很多大名鼎鼎的网络库或框架,都使用了水平触发模式,而 Nginx 使用了边沿触发模式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值