阻塞和非阻塞

"阻塞"和"非阻塞"主要是在说:当一个操作暂时无法完成时,调用者是停在那里等,还是立刻返回。

它经常出现在:

复制代码
read()
write()
recv()
send()
accept()
connect()

尤其是网络编程里。

一、阻塞是什么意思

阻塞就是:函数暂时做不了,就一直等,直到能做为止。

比如:

复制代码
char buf[1024];
recv(sockfd, buf, sizeof(buf), 0);

如果 sockfd 是阻塞 socket,并且现在没有数据可读,那么 recv 会卡住。

程序会停在这里:

复制代码
recv(sockfd, buf, sizeof(buf), 0);
// 没数据就不往下走

直到发生下面某种情况:

对端发来了数据

对端关闭连接

出错

被信号打断

设置了超时并超时

所以阻塞 I/O 的感觉是:

复制代码
你要读数据?
现在没数据。
那你在这里等着。

二、 非阻塞是什么意思

非阻塞就是:函数暂时做不了,也不会等,而是马上返回。

同样是:

复制代码
recv(sockfd, buf, sizeof(buf), 0);

如果 sockfd 是非阻塞 socket,并且现在没有数据可读,那么 recv 不会卡住,而是返回:

复制代码
-1

并设置:

复制代码
errno == EAGAIN || errno == EWOULDBLOCK

意思是:

复制代码
现在没数据,你之后再试。

所以非阻塞 I/O 的感觉是:

复制代码
你要读数据?
现在没数据。
我不等,直接告诉你暂时不能读。

三、 一个直观比喻

阻塞:

复制代码
你去奶茶店买奶茶。
店员说还没做好。
你站在柜台前一直等,什么都不干。
等做好后,拿到奶茶再走。

非阻塞:

复制代码
你去奶茶店问好了没。
店员说还没好。
你马上离开,去做别的事。
过一会儿再来问。

在程序里,"站在那里等"就是线程被阻塞。

四、阻塞 read/recv 示例

复制代码
char buf[1024];

ssize_t n = recv(fd, buf, sizeof(buf), 0);

std::cout << "recv returned\n";

如果 fd 是阻塞的,并且没有数据,那么:

复制代码
std::cout << "recv returned\n";

这一行不会执行,直到 recv 返回。

也就是说,整个线程卡在 recv 上。

五、 非阻塞 recv 示例

先把 socket 设置成非阻塞:

复制代码
#include <fcntl.h>
#include <unistd.h>

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

然后:

复制代码
char buf[1024];

ssize_t n = recv(fd, buf, sizeof(buf), 0);

if (n > 0) {
    // 读到了 n 字节
} else if (n == 0) {
    // 对端关闭连接
} else {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 现在没数据,不是错误,之后再读
    } else {
        // 真正的错误
    }
}

非阻塞 I/O 的核心就是要处理:

复制代码
EAGAIN
EWOULDBLOCK

它们表示"现在做不了,但不是致命错误"。

六、accept 的阻塞和非阻塞

accept 用来接受新连接。

阻塞模式下:

复制代码
int client_fd = accept(listen_fd, nullptr, nullptr);

如果没有客户端连接进来,程序就卡住。

非阻塞模式下:

复制代码
int client_fd = accept(listen_fd, nullptr, nullptr);

如果暂时没有新连接,返回 -1,并且:

复制代码
errno == EAGAIN || errno == EWOULDBLOCK

所以非阻塞服务器里经常这样写:

复制代码
while (true) {
    int client_fd = accept(listen_fd, nullptr, nullptr);

    if (client_fd >= 0) {
        set_nonblocking(client_fd);
        // 加入 epoll
    } else {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;
        }
        // 其他错误
        break;
    }
}

这里的 while 很常见,尤其是 epoll 的边缘触发模式下。

七、 send/write 的阻塞和非阻塞

很多人以为 send 一定能立刻把数据发出去,其实不是。

阻塞 socket:

复制代码
send(fd, data, len, 0);

如果内核发送缓冲区满了,send 会卡住,直到缓冲区有空间。

非阻塞 socket:

复制代码
ssize_t n = send(fd, data, len, 0);

如果发送缓冲区满了,它返回:

复制代码
-1

并设置:

复制代码
errno == EAGAIN || errno == EWOULDBLOCK

还有一个重要点:send 可能只发送一部分。

比如你想发 10000 字节:

复制代码
ssize_t n = send(fd, data, 10000, 0);

返回值可能是:

复制代码
3000

意思是只发出去了 3000 字节,剩下 7000 字节要你之后继续发。

所以非阻塞网络程序里一般要有发送缓冲区:

复制代码
struct Connection {
    int fd;
    std::string write_buffer;
};

没发完的数据放进 write_buffer,等下次 EPOLLOUT 再发。

八、connect 的阻塞和非阻塞

connect 也有区别。

阻塞模式:

复制代码
connect(fd, addr, len);

会一直等到连接成功或失败。

非阻塞模式下:

复制代码
int ret = connect(fd, addr, len);

如果连接还在进行中,通常返回:

复制代码
-1

并且:

复制代码
errno == EINPROGRESS

意思是:

复制代码
连接正在建立,还没完成。

这时你可以用 epoll 监听这个 fd 的可写事件:

复制代码
EPOLLOUT

等它可写时,再用 getsockopt 检查是否连接成功。

九、阻塞的问题

阻塞本身不是坏事。

简单程序里,阻塞很好用:

复制代码
while (true) {
    int client = accept(listen_fd, nullptr, nullptr);
    recv(client, buf, sizeof(buf), 0);
    send(client, response, response_len, 0);
}

代码直观。

但高并发服务器里,阻塞会带来问题。

假设一个线程处理一个连接:

复制代码
10000 个连接 = 10000 个线程

这会有:

线程创建成本高

上下文切换成本高

栈内存占用大

调度开销大

慢连接会占住线程

如果一个客户端一直不发数据,线程就一直卡在 recv 上。

十、非阻塞的问题

非阻塞也不是万能的。

如果你这么写:

复制代码
while (true) {
    ssize_t n = recv(fd, buf, sizeof(buf), 0);

    if (n < 0 && errno == EAGAIN) {
        continue;
    }
}

这会疯狂空转,CPU 直接飙高。

因为没有数据时,你一直问:

复制代码
有数据吗?
没有。
有数据吗?
没有。
有数据吗?
没有。

所以非阻塞 I/O 通常要配合:

复制代码
select / poll / epoll

也就是:

复制代码
没有事件时睡眠;
有事件时再尝试读写。

这就是 epoll 的作用。

十一、 epoll 和非阻塞的关系

epoll 通常配合非阻塞 fd 使用。

典型流程:

复制代码
把 socket 设为非阻塞
注册到 epoll
epoll_wait 等事件
有 EPOLLIN 再 recv
读到 EAGAIN 停止
继续 epoll_wait

代码结构像这样:

复制代码
while (true) {
    int n = epoll_wait(epfd, events, 1024, -1);

    for (int i = 0; i < n; ++i) {
        int fd = events[i].data.fd;

        if (events[i].events & EPOLLIN) {
            while (true) {
                ssize_t ret = recv(fd, buf, sizeof(buf), 0);

                if (ret > 0) {
                    // 处理数据
                } else if (ret == 0) {
                    // 对端关闭
                    close(fd);
                    break;
                } else {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        break;
                    }
                    close(fd);
                    break;
                }
            }
        }
    }
}

这就是高性能网络编程里最经典的模式之一。

相关推荐
zhang133830890751 小时前
CG-85D 水工大坝渗压监测振弦式传感器
运维·服务器·网络·人工智能·自动化
pt10431 小时前
网络自动化Python课程:Git版本控制基础入门与实验演示
网络·python·自动化
esabby1 小时前
253个原生IP + 40M独享回国带宽:香港站群服务器的硬核拆解
服务器·网络协议·tcp/ip
treesforest1 小时前
随意装软件也会被恶意IP入侵电脑?
网络·网络协议·tcp/ip·网络安全·ip属地·查ip归属地
zcmodeltech1 小时前
智慧城市沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的园区-城市-数字孪生联动方案
服务器·数据库·人工智能·stm32·嵌入式硬件·信息可视化·智慧城市
程序猿小玉兒1 小时前
Quartz定时任务偶尔不执行
服务器·windows·microsoft
酷可达拉斯2 小时前
Linux操作系统-tcpdump抓包定位网络问题实战
linux·运维·服务器·网络·tcpdump
可爱系程序猿10 小时前
网络打印机地址正常却无法出纸:从 TCP/IP 端口到 Print Spooler 的排错步骤
网络·网络协议·tcp/ip
Dontla11 小时前
预置known_hosts如何防止中间人攻击(MITM)?(服务器公钥、服务器私钥)Runner、DNS污染、BGP劫持、同网络ARP欺骗、SSH握手
服务器·网络·ssh