Linux系统编程(九)

编程入门 行业动态 更新时间:2024-10-18 22:31:29

Linux<a href=https://www.elefans.com/category/jswz/34/1770742.html style=系统编程(九)"/>

Linux系统编程(九)

文章目录

    • 1 问题提出
    • 2 fd_set容器
      • 2.1 fd_set的实现
      • 2.2 操作fd_set容器
    • 3 select函数
      • 3.1 参数说明:
      • 3.2 select返回值
      • 3.3 select与信号
    • 4 poll函数
    • 5 epoll函数
      • 5.1 IO事件
      • 5.2 select 与 poll的缺点
      • 5.3 使用epoll
      • 5.4 epoll_create
      • 5.5 epoll_create
      • 5.6 epoll_wait
    • 6 epoll触发模式
      • 6.1 两种触发
      • 6.2 边沿触发 + 非阻塞

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 返回 0sprintf(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) {// errorPERR("read");}else if (n == 0) {// peer closesprintf(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;// 打印 pidprintf("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) {// 如果被信号打断的,不让程序退出,直接 continueif (errno == EINTR) {perror("select");continue;}// 其它情况的错误,直接让程序退出PERR("select");}else if (res == 0) {// timeoutcontinue;}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) {// errorPERR("read");}else if (n == 0) {// peer closesprintf(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) {// errorif (errno == EINTR) {perror("poll");continue;}PERR("poll");}else if (res == 0) {// timeoutcontinue;}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) {// errorPERR("read");}else if (n == 0) {// peer closesprintf(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) {// errorif (errno == EINTR) {perror("epoll_wait");continue;}PERR("epoll_wait");}else if (res == 0) {// timeoutcontinue;}// 开始处理所有事件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);// 开始循环 readwhile (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 使用了边沿触发模式。

更多推荐

Linux系统编程(九)

本文发布于:2024-03-04 19:49:44,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1710248.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:系统   Linux

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!