文章目录
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对比
所做的事件是一样的,但是它们也有区别:
- select 使用 fd_set 来存放描述符,poll 使用结构体数组。
- 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 函数的步骤通常如下:
-
首先创建一个 epoll 对象,返回该对象的描述符
-
通过该对象的描述符,将要监听的描述符复制到内核
-
开始监听事件
三个步骤对应三个函数
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 使用了边沿触发模式。