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 描述符不是打开的文件
相关推荐
乘云数字DATABUFF4 天前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
荣--6 天前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森6 天前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜7 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB8 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
zzzzzz3109 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode9 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户03284722207010 天前
如何搭建本地yum源(上)
运维
大树8813 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠13 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql