我们知道网络本身就是IO,但是如果按照系统IO和语言的IO,对于网络而言很慢。本期我们就来学习网络IO。
而我们的前辈总结了五种IO模型,本期我们就来学习什么是IO。
相关代码:LinuxNet/IO · 楼田莉子/Linux学习 - 码云 - 开源中国
目录
[函数表达式(C 语法)](#函数表达式(C 语法))
[非阻塞 IO 常见误区](#非阻塞 IO 常见误区)
前言
什么是IO
之前我们学习的系统IO主要是外设与内存的通信。而网络通信本质上依然是IO的一种。
IO分为两个部分------等待数据就绪+数据拷贝。
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
如何设计高效的IO
根据上述内容,我们很容易就发现设计出高级IO------少调用系统IO,且使用零拷贝技术优化
五种IO模型
| IO模型 | 核心机制 | 数据从内核到用户态的流程 | 是否阻塞在系统调用上 | 是否需轮询 | 执行特点 | 典型函数/技术 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|---|
| 阻塞IO | 用户进程发起read/recv后,一直等待内核数据准备完成并复制到用户空间,期间进程挂起 | 等待数据 → 拷贝数据,两次阶段都阻塞 | 是 | 否 | 进程主动放弃CPU,进入睡眠状态,直到IO完成被内核唤醒;适合单任务顺序处理 | read, recv, accept | 简单直观,适合单连接或低并发 | 并发能力差,连接阻塞会浪费CPU |
| 非阻塞IO | 调用立即返回,若数据未就绪返回错误(如EWOULDBLOCK),用户需循环调用直到成功 | 等待阶段不阻塞(立即返回),拷贝阶段阻塞 | 仅在拷贝阶段阻塞 | 是(用户主动轮询) | 进程反复执行系统调用,忙等待(busy waiting)消耗大量CPU时间;适合延迟不敏感但需尝试多个fd的场景 | fcntl(O_NONBLOCK) + read/recv | 一个线程可处理多个连接 | 轮询浪费CPU,延迟较高 |
| IO多路复用 | 使用select/poll/epoll等同时监听多个fd,任一fd就绪后通知进程,进程再调用read/recv | 等待阶段阻塞在select/epoll上(而非每个fd),就绪后拷贝数据时阻塞 | 阻塞在select/epoll上,数据拷贝阶段阻塞 | 否(由内核通知) | 单线程或少量线程通过事件循环同时监听数百上千个fd;epoll采用回调机制,无线性扫描 | select, poll, epoll | 支持高并发,单线程可管理成千上万连接 | 两次系统调用(epoll_wait + recv),编程相对复杂 |
| 信号驱动IO | 注册信号处理函数,内核在数据就绪时发送SIGIO信号,进程在信号处理中调用read/recv | 等待阶段不阻塞(立即返回),数据就绪后信号通知,拷贝时阻塞 | 仅在拷贝阶段阻塞 | 否 | 异步通知机制,但信号处理函数中只能执行异步安全函数;TCP下SIGIO无法区分read/write/accept事件,实际使用极少 | sigaction, fcntl(F_SETFL, O_ASYNC) | 无轮询,不阻塞等待,通知及时 | 信号队列有限,TCP场景下SIGIO不区分多种事件,实际使用较少 |
| 异步IO | 发起aio_read后立即返回,内核完成数据准备及拷贝后,通过信号或回调通知进程 | 等待和拷贝阶段都由内核完成,完全不阻塞进程 | 全程无阻塞 | 否 | 用户态只需提交请求并绑定回调,内核完成全部工作后主动通知;真正的异步非阻塞,可大幅度提高IO密集型任务效率 | aio_read, aio_write, io_uring | 真正的非阻塞,系统调用次数少,性能最高 | 实现复杂,需要内核原生支持(io_uring在Linux 5.1+成熟) |
阻塞IO
在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
非阻塞IO
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
信号驱动IO
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

IO多路复用
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

异步IO
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

fcntl接口
作用
-
改变已打开文件描述符的状态标志 (如
O_NONBLOCK、O_ASYNC、O_DIRECT等)。 -
对于 socket,非阻塞模式下调用
read/recv若无数据立即返回-1并设errno为EAGAIN或EWOULDBLOCK;write/send若写缓冲区满同样立即返回错误。 -
常用于配合
select/epoll实现高并发网络服务。
函数表达式(C 语法)
cpp
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
参数(针对非阻塞IO)
-
fd:要操作的文件描述符(如 socket、普通文件、管道等)。 -
cmd:操作命令。设置非阻塞时常用的两个命令:-
F_GETFL:获取文件描述符的当前状态标志。 -
F_SETFL:设置文件描述符的状态标志(可组合多个标志)。
-
-
arg:根据cmd的可选参数。-
当
cmd = F_GETFL时,arg忽略,返回值是当前标志。 -
当
cmd = F_SETFL时,arg是要设置的标志值(通常是flags | O_NONBLOCK或flags & ~O_NONBLOCK)。
-
常用标志:
O_RDONLY、O_WRONLY、O_RDWR、O_NONBLOCK、O_ASYNC、O_CLOEXEC等。
返回值
-
成功:
-
对于
F_GETFL:返回当前文件描述符的标志(int 类型)。 -
对于
F_SETFL:返回 0。
-
-
失败 :返回 -1,并设置
errno指示错误类型(如EBADF:fd 无效;EINVAL:cmd 无效等)
示例代码
cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
// 设置 fd 为非阻塞模式,成功返回 true,失败返回 false
bool setNonBlocking(int fd)
{
// 获取当前标志
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
{
perror("fcntl F_GETFL failed");
return false;
}
// 设置 O_NONBLOCK 标志
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
perror("fcntl F_SETFL O_NONBLOCK failed");
return false;
}
return true;
}
int main()
{
// 1. 创建 socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
// 2. 设置为非阻塞
if (!setNonBlocking(sock)) {
close(sock);
return 1;
}
// 3. 准备连接地址(例如本机 8080 端口)
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
// 4. 非阻塞 connect:立即返回,正常会返回 -1 且 errno == EINPROGRESS
int ret = connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (ret < 0 && errno != EINPROGRESS) {
perror("connect");
close(sock);
return 1;
}
std::cout << "Non-blocking connect issued, waiting for connection establishment..." << std::endl;
// 生产环境这里应该使用 select/epoll 等待可写事件,此处简单 sleep 模拟
sleep(1);
// 5. 尝试非阻塞 recv(假设连接已建立且服务端发送了数据)
char buffer[1024];
while (true) {
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 没有数据可读,这是非阻塞模式的正常情况
std::cout << "No data available now (EAGAIN/EWOULDBLOCK), would retry later." << std::endl;
// 实际应加入 epoll 等待下次可读事件,这里简单跳出循环演示
break;
} else {
perror("recv error");
break;
}
} else if (n == 0) {
std::cout << "Connection closed by peer." << std::endl;
break;
} else {
buffer[n] = '\0';
std::cout << "Received: " << buffer << std::endl;
}
}
close(sock);
return 0;
}
结果为:

非阻塞 IO 常见误区
| 误区 | 正确理解 |
|---|---|
fcntl 设置了非阻塞后,所有操作都立即返回 |
只有那些可能阻塞的操作(如 read、write、connect、accept)才会立即返回错误;close 等仍可能阻塞(但通常忽略)。 |
非阻塞模式下 connect 返回 EINPROGRESS 就是失败 |
这是正常现象,后续必须通过 select/epoll 检查是否可写,并调用 getsockopt(SO_ERROR) 确认连接成功。 |
非阻塞 send 返回 EAGAIN 就是缓冲区满,可以稍等一会再发 |
正确,但等待应基于 epoll 的可写事件,而不是固定延时。 |
以轮询的方式读取标准输入
代码为:
cpp
#include <iostream>
#include <poll.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
int main()
{
struct pollfd fds[1];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
std::cout << "Polling stdin (type something and press Enter, or wait 5s for timeout)..." << std::endl;
while (true) {
int ret = poll(fds, 1, 5000); // 5秒超时,每轮重新计时
if (ret == -1) {
perror("poll");
return 1;
}
if (ret == 0) {
std::cout << "Timeout: no data within 5s, polling again..." << std::endl;
continue;
}
if (fds[0].revents & POLLIN) {
char buf[1024];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n == -1) {
perror("read");
return 1;
}
if (n == 0) {
std::cout << "EOF reached, exiting." << std::endl;
break;
}
buf[n] = '\0';
std::cout << "Read: " << buf;
}
if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
std::cerr << "poll error/hangup on stdin" << std::endl;
break;
}
}
return 0;
}
结果为:

本期内容就到这里了,喜欢请点个赞谢谢
封面图自取:
