极客时间已完结课程限时免费阅读

22 | 非阻塞I/O:提升性能的加速器

22 | 非阻塞I/O:提升性能的加速器-极客时间

22 | 非阻塞I/O:提升性能的加速器

讲述:冯永吉

时长10:27大小9.56M

你好,我是盛延敏,这里是网络编程实战第 22 讲,欢迎回来。
在性能篇的前两讲中,我分别介绍了 select 和 poll 两种不同的 I/O 多路复用技术。在接下来的这一讲中,我将带大家进入非阻塞 I/O 模式的世界。事实上,非阻塞 I/O 配合 I/O 多路复用,是高性能网络编程中的常见技术。

阻塞 VS 非阻塞

当应用程序调用阻塞 I/O 完成某个操作时,应用程序会被挂起,等待内核完成操作,感觉上应用程序像是被“阻塞”了一样。实际上,内核所做的事情是将 CPU 时间切换给其他有需要的进程,网络应用程序在这种情况下就会得不到 CPU 时间做该做的事情。
非阻塞 I/O 则不然,当应用程序调用非阻塞 I/O 完成某个操作时,内核立即返回,不会把 CPU 时间切换给其他进程,应用程序在返回后,可以得到足够的 CPU 时间继续完成其他事情。
如果拿去书店买书举例子,阻塞 I/O 对应什么场景呢? 你去了书店,告诉老板(内核)你想要某本书,然后你就一直在那里等着,直到书店老板翻箱倒柜找到你想要的书,有可能还要帮你联系全城其它分店。注意,这个过程中你一直滞留在书店等待老板的回复,好像在书店老板这里"阻塞"住了。
那么非阻塞 I/O 呢?你去了书店,问老板有没你心仪的那本书,老板查了下电脑,告诉你没有,你就悻悻离开了。一周以后,你又来这个书店,再问这个老板,老板一查,有了,于是你买了这本书。注意,这个过程中,你没有被阻塞,而是在不断轮询。
但轮询的效率太低了,于是你向老板提议:“老板,到货给我打电话吧,我再来付钱取书。”这就是前面讲到的 I/O 多路复用。
再进一步,你连去书店取书也想省了,得了,让老板代劳吧,你留下地址,付了书费,让老板到货时寄给你,你直接在家里拿到就可以看了。这就是我们将会在第 30 讲中讲到的异步 I/O。
这几个 I/O 模型,再加上进程、线程模型,构成了整个网络编程的知识核心。
按照使用场景,非阻塞 I/O 可以被用到读操作、写操作、接收连接操作和发起连接操作上。接下来,我们对它们一一解读。

非阻塞 I/O

读操作

如果套接字对应的接收缓冲区没有数据可读,在非阻塞情况下 read 调用会立即返回,一般返回 EWOULDBLOCK 或 EAGAIN 出错信息。在这种情况下,出错信息是需要小心处理,比如后面再次调用 read 操作,而不是直接作为错误直接返回。这就好像去书店买书没买到离开一样,需要不断进行又一次轮询处理。

写操作

不知道你有没有注意到,在阻塞 I/O 情况下,write 函数返回的字节数,和输入的参数总是一样的。如果返回值总是和输入的数据大小一样,write 等写入函数还需要定义返回值吗?我不知道你是不是和我一样,刚接触到这一部分知识的时候有这种困惑。
这里就要引出我们所说的非阻塞 I/O。在非阻塞 I/O 的情况下,如果套接字的发送缓冲区已达到了极限,不能容纳更多的字节,那么操作系统内核会尽最大可能从应用程序拷贝数据到发送缓冲区中,并立即从 write 等函数调用中返回。可想而知,在拷贝动作发生的瞬间,有可能一个字符也没拷贝,有可能所有请求字符都被拷贝完成,那么这个时候就需要返回一个数值,告诉应用程序到底有多少数据被成功拷贝到了发送缓冲区中,应用程序需要再次调用 write 函数,以输出未完成拷贝的字节。
write 等函数是可以同时作用到阻塞 I/O 和非阻塞 I/O 上的,为了复用一个函数,处理非阻塞和阻塞 I/O 多种情况,设计出了写入返回值,并用这个返回值表示实际写入的数据大小。
也就是说,非阻塞 I/O 和阻塞 I/O 处理的方式是不一样的。
非阻塞 I/O 需要这样:拷贝→返回→再拷贝→再返回。
而阻塞 I/O 需要这样:拷贝→直到所有数据拷贝至发送缓冲区完成→返回。
不过在实战中,你可以不用区别阻塞和非阻塞 I/O,使用循环的方式来写入数据就好了。只不过在阻塞 I/O 的情况下,循环只执行一次就结束了。
我在前面的章节中已经介绍了类似的方案,你可以看到 writen 函数的实现。
/* 向文件描述符fd写入n字节数 */
ssize_t writen(int fd, const void * data, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = data;
nleft = n;
//如果还有数据没被拷贝完成,就一直循环
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
/* 这里EAGAIN是非阻塞non-blocking情况下,通知我们再次调用write() */
if (nwritten < 0 && errno == EAGAIN)
nwritten = 0;
else
return -1; /* 出错退出 */
}
/* 指针增大,剩下字节数变小*/
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
下面我通过一张表来总结一下 read 和 write 在阻塞模式和非阻塞模式下的不同行为特性:
关于 read 和 write 还有几个结论,你需要把握住:
read 总是在接收缓冲区有数据时就立即返回,不是等到应用程序给定的数据充满才返回。当接收缓冲区为空时,阻塞模式会等待,非阻塞模式立即返回 -1,并有 EWOULDBLOCK 或 EAGAIN 错误。
和 read 不同,阻塞模式下,write 只有在发送缓冲区足以容纳应用程序的输出字节时才返回;而非阻塞模式下,则是能写入多少就写入多少,并返回实际写入的字节数。
阻塞模式下的 write 有个特例, 就是对方主动关闭了套接字,这个时候 write 调用会立即返回,并通过返回值告诉应用程序实际写入的字节数,如果再次对这样的套接字进行 write 操作,就会返回失败。失败是通过返回值 -1 来通知到应用程序的。

accept

当 accept 和 I/O 多路复用 select、poll 等一起配合使用时,如果在监听套接字上触发事件,说明有连接建立完成,此时调用 accept 肯定可以返回已连接套接字。这样看来,似乎把监听套接字设置为非阻塞,没有任何好处。
为了说明这个问题,我们构建一个客户端程序,其中最关键的是,一旦连接建立,设置 SO_LINGER 套接字选项,把 l_onoff 标志设置为 1,把 l_linger 时间设置为 0。这样,连接被关闭时,TCP 套接字上将会发送一个 RST。
struct linger ling;
ling.l_onoff = 1;
ling.l_linger = 0;
setsockopt(socket_fd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
close(socket_fd);
服务器端使用 select I/O 多路复用,不过,监听套接字仍然是 blocking 的。如果监听套接字上有事件发生,休眠 5 秒,以便模拟高并发场景下的情形。
if (FD_ISSET(listen_fd, &readset)) {
printf("listening socket readable\n");
sleep(5);
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
这里的休眠时间非常关键,这样,在监听套接字上有可读事件发生时,并没有马上调用 accept。由于客户端发生了 RST 分节,该连接被接收端内核从自己的已完成队列中删除了,此时再调用 accept,由于没有已完成连接(假设没有其他已完成连接),accept 一直阻塞,更为严重的是,该线程再也没有机会对其他 I/O 事件进行分发,相当于该服务器无法对其他 I/O 进行服务。
如果我们将监听套接字设为非阻塞,上述的情形就不会再发生。只不过对于 accept 的返回值,需要正确地处理各种看似异常的错误,例如忽略 EWOULDBLOCK、EAGAIN 等。
这个例子给我们的启发是,一定要将监听套接字设置为非阻塞的,尽管这里休眠时间 5 秒有点夸张,但是在极端情况下处理不当的服务器程序是有可能碰到例子所阐述的情况,为了让服务器程序在极端情况下工作正常,这点工作还是非常值得的。

connect

在非阻塞 TCP 套接字上调用 connect 函数,会立即返回一个 EINPROGRESS 错误。TCP 三次握手会正常进行,应用程序可以继续做其他初始化的事情。当该连接建立成功或者失败时,通过 I/O 多路复用 select、poll 等可以进行连接的状态检测。

非阻塞 I/O + select 多路复用

我在这里给出了一个非阻塞 I/O 搭配 select 多路复用的例子。
#define MAX_LINE 1024
#define FD_INIT_SIZE 128
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
//数据缓冲区
struct Buffer {
int connect_fd; //连接字
char buffer[MAX_LINE]; //实际缓冲
size_t writeIndex; //缓冲写入位置
size_t readIndex; //缓冲读取位置
int readable; //是否可以读
};
struct Buffer *alloc_Buffer() {
struct Buffer *buffer = malloc(sizeof(struct Buffer));
if (!buffer)
return NULL;
buffer->connect_fd = 0;
buffer->writeIndex = buffer->readIndex = buffer->readable = 0;
return buffer;
}
void free_Buffer(struct Buffer *buffer) {
free(buffer);
}
int onSocketRead(int fd, struct Buffer *buffer) {
char buf[1024];
int i;
ssize_t result;
while (1) {
result = recv(fd, buf, sizeof(buf), 0);
if (result <= 0)
break;
for (i = 0; i < result; ++i) {
if (buffer->writeIndex < sizeof(buffer->buffer))
buffer->buffer[buffer->writeIndex++] = rot13_char(buf[i]);
if (buf[i] == '\n') {
buffer->readable = 1; //缓冲区可以读
}
}
}
if (result == 0) {
return 1;
} else if (result < 0) {
if (errno == EAGAIN)
return 0;
return -1;
}
return 0;
}
int onSocketWrite(int fd, struct Buffer *buffer) {
while (buffer->readIndex < buffer->writeIndex) {
ssize_t result = send(fd, buffer->buffer + buffer->readIndex, buffer->writeIndex - buffer->readIndex, 0);
if (result < 0) {
if (errno == EAGAIN)
return 0;
return -1;
}
buffer->readIndex += result;
}
if (buffer->readIndex == buffer->writeIndex)
buffer->readIndex = buffer->writeIndex = 0;
buffer->readable = 0;
return 0;
}
int main(int argc, char **argv) {
int listen_fd;
int i, maxfd;
struct Buffer *buffer[FD_INIT_SIZE];
for (i = 0; i < FD_INIT_SIZE; ++i) {
buffer[i] = alloc_Buffer();
}
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
fd_set readset, writeset, exset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&exset);
while (1) {
maxfd = listen_fd;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&exset);
// listener加入readset
FD_SET(listen_fd, &readset);
for (i = 0; i < FD_INIT_SIZE; ++i) {
if (buffer[i]->connect_fd > 0) {
if (buffer[i]->connect_fd > maxfd)
maxfd = buffer[i]->connect_fd;
FD_SET(buffer[i]->connect_fd, &readset);
if (buffer[i]->readable) {
FD_SET(buffer[i]->connect_fd, &writeset);
}
}
}
if (select(maxfd + 1, &readset, &writeset, &exset, NULL) < 0) {
error(1, errno, "select error");
}
if (FD_ISSET(listen_fd, &readset)) {
printf("listening socket readable\n");
sleep(5);
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
} else if (fd > FD_INIT_SIZE) {
error(1, 0, "too many connections");
close(fd);
} else {
make_nonblocking(fd);
if (buffer[fd]->connect_fd == 0) {
buffer[fd]->connect_fd = fd;
} else {
error(1, 0, "too many connections");
}
}
}
for (i = 0; i < maxfd + 1; ++i) {
int r = 0;
if (i == listen_fd)
continue;
if (FD_ISSET(i, &readset)) {
r = onSocketRead(i, buffer[i]);
}
if (r == 0 && FD_ISSET(i, &writeset)) {
r = onSocketWrite(i, buffer[i]);
}
if (r) {
buffer[i]->connect_fd = 0;
close(i);
}
}
}
}
第 93 行,调用 fcntl 将监听套接字设置为非阻塞。
fcntl(fd, F_SETFL, O_NONBLOCK);
第 121 行调用 select 进行 I/O 事件分发处理。
131-142 行在处理新的连接套接字,注意这里也把连接套接字设置为非阻塞的。
151-156 行在处理连接套接字上的 I/O 读写事件,这里我们抽象了一个 Buffer 对象,Buffer 对象使用了 readIndex 和 writeIndex 分别表示当前缓冲的读写位置。

实验

启动该服务器:
$./nonblockingserver
使用多个 telnet 客户端连接该服务器,可以验证交互正常。
$telnet 127.0.0.1 43211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
fasfasfasf
snfsnfsnfs

总结

非阻塞 I/O 可以使用在 read、write、accept、connect 等多种不同的场景,在非阻塞 I/O 下,使用轮询的方式引起 CPU 占用率高,所以一般将非阻塞 I/O 和 I/O 多路复用技术 select、poll 等搭配使用,在非阻塞 I/O 事件发生时,再调用对应事件的处理函数。这种方式,极大地提高了程序的健壮性和稳定性,是 Linux 下高性能网络编程的首选。

思考题

给你布置两道思考题:
第一道,程序中第 133 行这个判断说明了什么?如果要改进的话,你有什么想法?
else if (fd > FD_INIT_SIZE) {
error(1, 0, "too many connections");
close(fd);
第二道,你可以仔细阅读一下数据读写部分 Buffer 的代码,你觉得用一个 Buffer 对象,而不是两个的目的是什么?
欢迎在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 10

提建议

上一篇
21 | poll:另一种I/O多路复用
下一篇
23 | Linux利器:epoll的前世今生
unpreview
 写留言

精选留言(64)

  • HerofH
    2020-01-14
    老师您好!我看到您这在应用层设计了一个读写缓冲区,我之前看了muduo和libevent,也是设计了这样的缓冲区,并且muduo作者陈硕也提到非阻塞IO必须要设计一个应用层Buffer,我很疑惑的就是,这样的读写缓冲区的必要性是什么呢? 我大概只能理解到非阻塞IO下使用应用层写缓冲区可以让还未来得及发出的数据先保存在应用层Buffer中,然后等到可写的时候再将数据从应用层Buffer写到fd的发送缓冲区中; 那么如何理解应用层读缓冲区的必要性呢?有数据来,触发可读事件,这个时候直接调用read去读不就可以了吗,为什么一定要先读到读缓冲区呢?根据libevent中,每次读数据时都会尽量多的从fd的接收缓冲区中读取数据到应用层buffer,我的一种想法是,设置读缓冲区的作用,是否是为了减少read的调用次数呢? 还是有其它原因呢?想了解一下老师的看法,谢谢!
    展开

    作者回复: 读缓冲的作用有很多,你提到了通过设置缓冲区,减少系统调用的次数是一个方面,另外,别忘了读取的数据是需要在应用层进行报文解析的,一个应用层缓冲区显然是比较方便的,否则,需要不断的进行数据的读取,直至解析到完整的报文。我认为,应用层缓冲是"空间换时间"的一个比较好的例子。

    33
  • TinyCalf
    2020-11-08
    我在思考一个问题,select既然已经告诉我们接口可读了,为什么还要用非阻塞IO;我自己的想法是,select其实只通知了有没有内容可读,没有提供有多少数据可读,所以当我们使用阻塞IO循环read时,无法确认下一个read还是不是可读的,因此仍然可能阻塞,而非阻塞IO可以解决这个问题,不知道我想的对不对

    作者回复: 我觉得是可以这样理解的。

    共 2 条评论
    14
  • MoonGod
    2019-09-27
    感觉这篇的解释和前面的比起来太不细致了…很多地方都没说明。老师能不能多一些说明啊

    作者回复: 我在代码里多加一些注释,可以看最新的代码 https://github.com/froghui/yolanda

    共 4 条评论
    12
  • fedwing
    2020-08-13
    那么非阻塞 I/O 呢?你去了书店,问老板有没你心仪的那本书,老板查了下电脑,告诉你没有,你就悻悻离开了。一周以后,你又来这个书店,再问这个老板,老板一查,有了,于是你买了这本书。注意,这个过程中,你没有被阻塞,而是在不断轮询。但轮询的效率太低了,于是你向老板提议:“老板,到货给我打电话吧,我再来付钱取书。”这就是前面讲到的 I/O 多路复用。 对于这个我有点疑问,select和poll本质上,不都是轮询吗,为什么这里说轮询效率太低,改成select,poll
    展开

    作者回复: select和poll是"老板"这个内核自己在轮询哦,不是买书人(应用程序)在轮询,"老板"可以有多个方式优化这个过程,比如他记录了一个事件,一旦书定成了,他就发现需要告诉和他有订阅关系的"你"了。想想看,作为应用程序的"你"在这个过程中,是不是可以放开手干点别的事,比如玩个吃鸡游戏的什么。

    共 4 条评论
    9
  • 衬衫的价格是19美元
    2020-07-12
    1.select,poll,epoll是io多路复用技术,是操作系统提供的检测io事件是否就绪的方法,当然我们可以不用操作系统提供的方法而自己去写一个轮训,但是轮训会加重cpu负载。 2.当我们调用fcntl将套接字配置为非阻塞后,在该套接字上后续的accept,read,write操作都将变为非阻塞 3.非阻塞io一般都需要配合io多路复用技术使用

    作者回复: 总结到位👍

    共 3 条评论
    10
  • 扩散性百万咸面包
    2020-06-06
    老师,认真的问你一个问题。 Redis中,网上的介绍是说单线程 + 多路复用 + 非阻塞I/O。为什么不采取C10k问题中的,主从Reactor结构,多个事件分发器来充分利用CPU的多核能力呢?理论上这样更好啊。

    作者回复: 很认真的回答你,我觉得你说的 单线程 + 多路复用 + 非阻塞I/O,是和redis的设计有关的,通过这么一个设计,减少了多个线程锁的消耗,我想是redis做了一些取舍的。

    共 2 条评论
    9
  • 绿箭侠
    2020-02-28
    老师,评论区中 程序水果宝 说select非阻塞,我的理解:select(maxfd + 1, &readset, &writeset, &exset, NULL) 此处因为NULL当然阻塞,不明白为什么说select非阻塞??

    作者回复: 非阻塞和阻塞是指I/O,具体作用到的是套接字上,而select这里是I/O多路复用的一种技术,注意到这一行 : make_nonblocking(fd); 实际上是把套接字都改为非阻塞I/O,再通过I/O多路复用来接收套接字上的I/O事件。

    共 3 条评论
    5
  • javaYJL
    2019-11-05
    老师,accept()这个函数不是阻塞的吗?

    作者回复: accept和阻塞套接字一起使用就是阻塞的,和非阻塞套集字一起使用就是非阻塞的。阻塞和非阻塞是作用到套集字上的。

    5
  • 一天到晚游泳的鱼
    2019-10-05
    老师,我想请教一个问题就是, 当把一个描述符设置为非阻塞的之后,在该描述符上面的操作就会变成非阻塞的吗? 比如说把连接套接字设置为非阻塞的,send和recv就会变成非阻塞的吗?

    作者回复: 你的理解是对的。

    共 2 条评论
    5
  • yusuf
    2019-09-29
    1、133行判断是否超过了文件描述符的最大值,如果超过了,就会报错。可以考虑使用动态分配的方式,但如果超过了1024的话,使用上一节中的poll来处理会更好些 2、认为是考虑到对同一个fd的同一缓冲区进行读写操作,只用一个Buffer对象足够了
    5
  • YUAN
    2020-10-03
    select和poll也是轮训吧😄?

    作者回复: 我把它归为多路复用。轮询的意思是应用程序自己不断的从应用层发起检测。

    4
  • null
    2021-04-09
    Buffer.readable 为 1 或 0: 1-可读,说明结构体 buffer 有数据可读,也就可以往 connect_fd 写数据,因此是监听 connect_fd 的写事件。 0-不可读,说明结构体 buffer 无数据,即结构体 buffer 有空间可以写数据,也就可以从 connect_fd 读数据,因此是监听 connect_fd 的读事件。 老师,是这样不,谢谢!
    展开

    作者回复: 鼓掌👏

    3
  • skye
    2020-01-09
    read 总是在接收缓冲区有数据时就立即返回,不是等到应用程序给定的数据充满才返回。=== 老师,这个是指阻塞时吗?

    作者回复: 这句话是说,在read调用时,虽然设定需要1k个字节,实际上总是在缓冲区有数据时就立即返回了,有可能只读到了20个字节。这种行为对阻塞和非阻塞是一样的。

    3
  • 沉淀的梦想
    2019-09-30
    老师代码中进行rot13_char编码的目的是啥?

    作者回复: ROT13(回转13位,rotateby13places,有时中间加了个减号称作ROT-13)是一种简易的置换暗码。 ROT-13 编码是一种每一个字母被另一个字母代替的方法。这个代替字母是由原来的字母向前移动 13 个字母而得到的。数字和非字母字符保持不变。 它是一种在网路论坛用作隐藏八卦、妙句、谜题解答以及某些脏话的工具,目的是逃过版主或管理员的匆匆一瞥。ROT13激励了广泛的线上书信撰写与字母游戏,且它常于新闻群组对话中被提及。

    3
  • 石将从
    2019-09-28
    没有注释,看onSocketWrite和onSocketRead函数很费劲

    作者回复: 其实还是蛮简单的,稍微解释一下: onSocketRead是通过套接字读取数据,数据存放在Buffer对象里,Buffer对象通过了writeIndex记录当前数 据区可写的位置; onSocketWrite通过套接字写数据,数据来源于Buffer缓冲对象,Buffer缓冲对象的readIndex记录了当前缓冲区读的位置。

    3
  • keepgoing
    2020-08-07
    请问老师onSocketRead函数中,error != EAGAIN的情况不应该也返回1,表示读到了错误需要关闭吗。 我理解recv == 0 以及 < 0的情况都需要关闭,这里为什么只判断 == 0的情况关闭呢,另外结果返回值r == -1一直没有处理,想问下这个-1是什么用呢。看完代码比较疑惑,想请老师解答一下

    作者回复: 如果是recv == 0,则说明是EOF,正常关闭; 如果是recv <0,说明出错了,不做任何处理,继续下个fd; 如不是recv <0,但是为EAGAIN,则进入写判断; -1的作用就是为了区别出>0和==0的不同return值。

    共 3 条评论
    2
  • J.M.Liu
    2019-10-04
    老师,有一个地方不是很明白。在连接套接字设为阻塞时,当客户端发送RST后,服务端在已完成连接队列删除了连接,accept阻塞。这时候如果有新连接进来了,为什么accept还是会阻塞呀?难道新连接进来一定要先select之后,accept才能取到连接好的套接字??

    作者回复: 不是一定需要select才可以 accept的。这里的例子主要是说明如果不给监听套集字设置为非阻塞,可能会引起的问题。

    共 3 条评论
    2
  • 沉淀的梦想
    2019-09-30
    老师那个accept阻塞的实验,在我电脑(linux-4.18.0,ubuntu18.10)上的行为有点不太一样,无论是把listen_fd设置为阻塞还是非阻塞,sleep 5s还是10s或者更长,行为总是:accept成功获取到客户端连接,然后读取到客户端的RST

    作者回复: 这里举的一个例子,有可能在新的协议栈下有所改变,但是不管怎样,理论上的分析有可能导致阻塞,所以我们还是应该在平时编码中将监听套接字也设置为非阻塞的。

    共 4 条评论
    2
  • Jaime
    2021-04-16
    第一道问题,说明了这个服务器只能服务固定的连接,LINUX上是1024,改进的化,可以使用poll或者epoll突破文件描述符数量限制 第二道问题,BUFF就是一个循环缓冲区,一个读指针,一个写指针,已经可以实现读写功能,目的是节省内存

    作者回复: 👍

    1
  • null
    2021-04-09
    原文: 那么非阻塞 I/O 呢?…,这个过程中,你没有被阻塞,而是在不断轮询。 但轮询的效率太低了,于是你向老板提议:“老板,到货给我打电话吧,我再来付钱取书。”这就是前面讲到的 I/O 多路复用。 以下代码片段,摘自前面两节课,当 select/poll 的 time 参数设置为 0,也是非阻塞立刻返回,然后不断轮询调用 select/poll 函数。这不就和非阻塞 I/O 描述的是一样:立刻返回+轮询。 是不是说 select/poll 可以处理多条“链路”,而非阻塞 I/O 只处理一条链路。还是说这两个概念不是一回事,水果和动物不能混为一谈。 请问老师,是我哪里理解错了么?谢谢! select/poll 多路复用代码段: for (;;) { readmask = allreads; int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL); } for (;;) { if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) { error(1, errno, "poll failed "); } }
    展开

    作者回复: select和poll是让操作系统在有I/O实际时再从调用中返回,而且本质上select和poll都是在等待多种I/O事件,和前面每次轮询单个I/O事件是否就绪,有天壤之别。

    1