后端开发中,网络请求、文件读写都离不开 IO。传统阻塞 IO 只能单线程处理一个请求,无法支撑高并发;而 IO 多路复用、异步 IO 是 Redis、Nginx、Netty 等高性能组件的核心。不懂 IO 模型,就无法理解高并发服务器的设计思想。
1:IO本质
所有IO(网络/文件)都只有两个核心步骤,这是理解IO模型的根基:
1:等待数据就绪:内核等待数据从网卡/硬盘写入内核缓冲区
2:数据拷贝:内核将数据从内核空间拷贝到用户空间
IO优化的核心:尽可能减少等待时间
2:五种IO模型
用钓鱼帮你秒懂,对应专业定义 + 流程:
1. 阻塞 IO(最基础)
- 通俗:坐在河边死等鱼上钩,啥也不干,直到鱼上钩才拉杆
- 专业:内核数据没准备好,进程一直阻塞,直到数据拷贝完成才返回
- 特点:最简单、默认模式、CPU 利用率低、单线程只能处理一个 IO
2. 非阻塞 IO
- 通俗:每隔几秒看一眼鱼竿,没鱼立刻返回,去干别的,轮询检查
- 专业:数据没准备好直接返回
EWOULDBLOCK,进程循环轮询 - 特点:不阻塞、CPU 浪费严重、仅特定场景用
3. 信号驱动 IO
- 通俗:鱼竿装铃铛,鱼上钩内核发信号,收到信号再去拉杆
- 专业:内核数据就绪,发送
SIGIO信号通知进程,进程再发起 IO - 特点:不用轮询、通知后仍需主动拷贝数据
4. IO 多路复用(高并发核心)(后面重点)
- 通俗:雇人看100 根鱼竿,哪个有鱼喊你,你只负责拉杆
- 专业:用
select/poll/epoll同时监听多个文件描述符,任一就绪就处理 - 特点:单线程处理百万连接、网络服务器标配
5. 异步 IO(最理想)
- 通俗:雇人直接把鱼钓好送到你家,你全程不用管
- 专业:内核完成等待 + 拷贝全流程,完成后通知进程直接用数据
- 特点:全程无阻塞、实现复杂、Linux 支持有限
3:知识层级
1:什么层的知识
操作系统内核层 + 网络应用层 / 传输层
- 不属于硬件层、语言层;是OS 内核提供的 IO 调度机制
- 网络 IO 基于Socket 套接字 (传输层 TCP/UDP),是网络编程的底层基石
2:在OS/网络中扮演的角色
- 操作系统:管理进程 IO 状态、内核缓冲区、用户 / 内核空间数据拷贝、调度 IO 流程
- 网络编程 :决定服务器并发能力,是高并发 Web 服务器 / 网关 / 中间件的核心底层
3:和语言层的关系
和编程语言无关!语言只做「封装」,不创造 IO 模型
- C++/Java/Python/Go:底层都是调用Linux 系统调用 (
recvfrom/select/fcntl) - 语言差异:只是封装了 OS 的 IO 能力(Java NIO、Python asyncio、Go net 包)
- 跨平台:Windows 用 IOCP,Linux 用 epoll,API 不同但 IO 模型思想一致
4:三种IO实战编程(C++和Linux环境)
1:阻塞IO
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
char buf[1024]={0};
cout<<"阻塞IO:请输入内容(不输入就一直等待):"<<endl;
//阻塞:无输入时进程挂起
ssize_t n = read(0,buf,sizeof(buf));
if(n>0) cout<<"读取内容"<<buf<<endl;
return 0;
}
2:非阻塞IO(fcntl设置)
cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
using namespace std;
// 设置文件描述符为非阻塞
void setNonBlock(int fd) {
int flag = fcntl(fd, F_GETFL); // 获取当前属性
fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 添加非阻塞标记
}
int main() {
setNonBlock(0); // 标准输入0号描述符设为非阻塞
char buf[1024] = {0};
cout << "非阻塞IO:轮询检查输入..." << endl;
while(true) {
ssize_t n = read(0, buf, sizeof(buf)-1);
if(n < 0) {
// 无数据:返回EAGAIN/EWOULDBLOCK,继续轮询
if(errno == EAGAIN || errno == EWOULDBLOCK) {
cout << "暂无数据,1秒后重试..." << endl;
sleep(1);
continue;
}
}
if(n > 0) {
cout << "读取内容:" << buf << endl;
break;
}
}
return 0;
}
3:IO多路复用(select实现)
cpp
//select入门
#include <iostream>
#include <sys/select.h>
#include <unistd.h>
using namespace std;
int main()
{
//1.定义监听集合
fd_set read_set;
while(true)
{
//步骤1:清空集合+加入要监听的fd(0=标准输入)
FD_ZERO(&read_set);
FD_SET(0, &read_set);//监听标准输入
//步骤2:调用select函数,阻塞等待fd就绪
//参数:最大fd+1,读集合,写集合,异常集合,超时时间(nullptr=永久等待)
//返回值:就绪fd的数量,-1表示出错,0表示超时
int ret=select(1,&read_set,nullptr,nullptr,nullptr);
if(ret<0)
{
cerr<<"select error!"<<endl;
break;
}
if(FD_ISSET(0,&read_set))//步骤3:判断哪个fd就绪了+处理数据
{
char buf[1024]={0};
ssize_t n=read(0,buf,sizeof(buf)-1);
if(n>0)//成功读取数据
{
buf[n]='\0';
cout<<"read from stdin:"<<buf<<endl;
}
}
}
return 0;
}