IO模型与并发服务器设计

一、阻塞IO与非阻塞IO

1.1 阻塞IO

层面 步骤 说明
用户层 调用recv 执行 recv(fd, buf, sizeof(buf), 0),程序卡在这里等待数据
内核第1步 检查缓冲区 检查socket接收缓冲区是否有数据
内核第2步 无数据时睡眠 将进程状态设为 TASK_INTERRUPTIBLE(可中断睡眠),移出CPU运行队列,调度其他进程运行
内核第3步 数据到达并唤醒 网卡触发硬件中断 → 内核将数据拷贝到接收缓冲区 → 唤醒进程(状态改为 TASK_RUNNING),重新加入运行队列

1.2 非阻塞IO

层面 步骤 说明
用户层 设置非阻塞 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞标志
用户层 循环轮询 while循环中调用 recv(),若返回值 ≥0 则数据就绪退出循环
用户层 错误处理 若返回 -1 且 errno != EAGAIN,则退出程序
用户层 避免占满CPU usleep(1000) 短暂休眠,防止忙等待占用CPU
内核第1步 检查缓冲区 检查接收缓冲区,无论是否有数据都立即返回结果
内核第2步 无数据时 返回 EAGAIN 错误码,进程继续执行(不进入睡眠)

1.3 fcntl函数

项目 内容
头文件 #include <fcntl.h> #include <unistd.h>
函数原型 int fcntl(int fd, int cmd, ... /* arg */);
作用 对文件描述符进行各种控制操作
fd 要操作的文件描述符
cmd 控制命令(如 F_SETFL 设置状态标志、F_GETFL 获取状态标志)
arg 可选参数,取决于 cmd 的具体命令
成功返回值 取决于 cmd 命令(如 F_GETFL 返回文件状态标志)
失败返回值 返回 -1(并重置错误码)

二、IO多路复用

2.1 IO多路复用

项目 内容
定义 单线程或单进程管理多个文件描述符(如套接字)的技术
核心机制 通过系统调用(如select、poll、epoll)监视多个IO操作的状态
通知方式 当某个IO操作就绪(可读、可写或发生异常)时,通知应用程序进行处理
提高并发性能 单线程可处理大量连接,无需为每个连接创建线程/进程
高效管理大量连接 避免资源浪费,降低系统开销
与非阻塞IO协作 结合使用可实现更高效的异步处理模式

2.2 select函数

项目 内容
头文件 #include <sys/socket.h> #include <sys/types.h> #include <sys/select.h>
函数原型 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds 要监视的最大文件描述符 + 1
readfds 要监视的读文件描述符集合,不关心则传 NULL
writefds 要监视的写文件描述符集合,不关心则传 NULL
exceptfds 要监视的异常文件描述符集合,不关心则传 NULL
timeout 超时时间,NULL 表示永久阻塞
成功返回值 返回就绪的文件描述符个数
失败返回值 返回 -1(并重置错误码)

2.3 poll函数

项目 内容
头文件 #include <poll.h>
函数原型 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds 指向 pollfd 结构体数组的指针,描述要监视的文件描述符及其事件
nfds fds 数组中有效描述符元素的数量
timeout 等待时间,单位为毫秒
成功返回值 返回结构体中 revents 域不为 0 的文件描述符个数
超时返回值 超时前没有任何事件发生,返回 0
失败返回值 返回 -1(并重置错误码)

三、并发服务器

3.1 服务器模型

项目 循环服务器 并发服务器
特点 同一时刻只能响应一个客户端的请求 同一时刻可以响应多个客户端的请求
TCP服务器 默认是循环服务器(因为 accept 和 recv 两个阻塞函数相互影响) 可通过多线程/多进程实现
UDP服务器 - 默认是并发服务器(只有一个阻塞函数 recvfrom)

3.2 多进程并发服务器

3.2.1 多线程并发服务器实现原理

角色 步骤 说明
主线程 创建监听Socket socket() → bind() → listen()
主线程 循环接收连接 while(1) { accept() 接收新连接 }
主线程 创建子线程 接收到新连接后,创建子线程处理该连接
子线程 处理请求 recv()/send() 处理客户端请求
子线程 关闭连接 close() 关闭连接

3.2.2 fork多进程并发服务器实现原理

模式 角色 步骤 说明
fork模型 主进程 创建监听Socket → bind() → listen() 主进程负责监听连接
fork模型 主进程 while(1) { accept() → fork() } 接收新连接后创建子进程处理
fork模型 子进程 close(监听Socket) 关闭继承自父进程的监听Socket
fork模型 子进程 recv()/send() → exit() 处理客户端请求后退出
fork模型 特点 父子进程完全独立,崩溃互不影响 -
预fork优化 启动时 预先创建多个子进程 类似Apache架构
预fork优化 负载均衡 通过共享监听socket(SO_REUSEPORT)实现 -

3.3 IO多路复用并发服务器

角色 步骤 说明
主线程 第1步 创建监听Socket → bind() → listen()
主线程 第2步 初始化 fd_set 集合,将监听Socket加入集合
主线程 第3步 while(1) 循环调用 select() 监听所有fd
主线程 第4步 select() 返回就绪的fd数量
主线程 第5步 遍历每个就绪的fd
主线程 判断分支 如果是监听Socket → accept() 新连接并加入 fd_set
主线程 判断分支 如果是普通Socket → recv()/send() 处理数据

3.4 pollfd结构体

cs 复制代码
struct pollfd{
    int fd;//文件描述符
    short events;//等待的事件
    short revents;//实际发生的事件
};
事件类别 常值 说明
读事件 POLLIN 普通或优先带数据可读
读事件 POLLPRI 高优先级数据可读
写事件 POLLOUT 普通或优先带数据可写
写事件 POLLWRNORM 普通数据可写
错误事件 POLLERR 发生错误
错误事件 POLLHUP 发生挂起
错误事件 POLLNVAL 描述符不是打开的文件
相关推荐
MXsoft6181 小时前
运维的尽头,是把“救火”变成“算命”
运维
莎士比亚的文学花园2 小时前
Linux驱动开发(1)——系统移植
linux·运维·服务器
IpdataCloud2 小时前
IPv6商用数据的IP离线库能解决哪些业务问题?适用场景与接入指南
网络·网络协议·tcp/ip
PH = 72 小时前
OverlayFS联合文件系统使用示例
java·linux·服务器
志栋智能2 小时前
超自动化巡检:解锁运维数据的深层价值
运维·服务器·数据库·自动化
坚持就完事了3 小时前
Linux中的mv命令
linux·运维·服务器
SongYuLong的博客3 小时前
Claude Code安装配置(Linux)
linux·运维·服务器
S1998_1997111609•X3 小时前
MacOS/ˉsh(so.))os.apkair/AI
开发语言·网络·人工智能
linux修理工4 小时前
禁用 Windows 跳转列表和最近文档跟踪的注册表修改
运维