一、阻塞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 | 描述符不是打开的文件 |