文章目录
基于I/O复用的服务器端
多进程服务器缺点:
- 创建进程时需要大量的运算和内存空间, 由于每个进程都具有独立的内存空间,所以相互间的数据交换也要求采用相对复杂的方法 (IPC (管道)属于相对复杂的通信方法)
理解复用

人不是同时说话----时分复用
双方说话频率不一样----频分复用
复用技术在服务器端的应用

无论连接多少客户端,提供服务的进程只有一个!
IO模型
[1]blockingIO - 同步阻塞IO
[2]nonblockingIO - 非阻塞IO
[3]signaldrivenIO - 信号驱动IO
[4]asynchronousIO - 异步IO
[5]IOmultiplexing - IO多路复用
同步阻塞 IO (Blocking IO)
原理:
- 进程发起 IO 操作后会被阻塞,直到内核完成数据准备并将其复制到用户空间,进程才会继续执行。
- 主要特点是进程阻塞挂起不消耗CPU资源,能及时响应每个操作;实现难度低,
应用场景
- 连接数少且 IO 操作耗时短的场景,像文件下载这种大资源的操作就不适合;
- 适用并发量小的网络应用开发,不适用并发量大的应用,因为每一个请求IO会阻塞进程,所以每请求分配一个处理进程(线程)去响应,系统开销大。
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定和监听...
while (true) {
int client_fd = accept(server_fd, nullptr, nullptr); // 阻塞等待连接
char buffer[1024] = {
0};
int valread = read(client_fd, buffer, 1024); // 阻塞等待数据
std::cout << "Read: " << buffer << std::endl;
// 处理数据...
}
}
非阻塞式I/O模型:
原理:
- 进程发起 IO 操作后立即返回,通过主动轮询(Polling)方式不断检查 IO 状态,直到数据就绪
应用场景: - 连接数较多,但每个连接 IO 操作频繁且耗时短的场景。
- 适合实现轻量级轮询机制。
缺点 - 轮询消耗大量 CPU 资源,效率低。
int main() {
int fd = open("file.txt", O_RDONLY | O_NONBLOCK); // 设置非阻塞
char buffer[1024];
while (true) {
int n = read(fd, buffer, 1024);
//内核缓冲没数据时,read返回-1,errno此时为EAGAIN表示是轮训发现没数据直接返回了
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 数据未就绪,继续轮询
std::cout << "Polling..." << std::endl;
usleep(1000);
} else {
// 数据就绪,处理数据
std::cout << "Read: " << buffer << std::endl;
break;
}
}
}
信号驱动IO
原理:
- 进程通过
sigaction注册信号处理函数,内核在数据就绪时发送 SIGIO 信号通知进程。
流程: - 进程注册信号处理函数 → 继续执行其他任务 → 当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据`
应用场景 - 需要异步通知但不要求立即处理的场景。
- 适合处理少量但重要的 IO 事件。
特点:回调机制,实现、开发应用难度大;
void signal_handler(int signum) {
if (signum == SIGIO) {
// 数据就绪,读取数据
char buffer[1024];
read(fd, buffer, 1024);
std::cout << "Read: " << buffer << std::endl;
}
}
int main() {
signal(SIGIO, signal_handler);// 进程注册信号处理函数
fcntl(fd, F_SETOWN, getpid()); // 设置进程为信号接收者
fcntl(fd, F_SETFL, O_ASYNC); // 启用异步 IO
// 继续执行其他任务...
}
异步IO
原理:
- 当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据
特点:
- 不阻塞,数据一步到位;Proactor模式;
- 需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性;
- 实现、开发应用难度大;
- 非常适合高性能高并发应用;
IO复用模型
https://blog.youkuaiyun.com/weixin_45905650/article/details/123181040IO多路复用、同步、异步、阻塞和非阻塞的区别
并发服务器的第二种实现方法一一基于I/O复用( Multi-plexing) 的服务器端构建
IO 复用(I/O Multiplexing)是一种在一个线程中并发处理多个 I/O 操作的机制,允许一个进程或线程监视多个文件描述符(sockets、files、pipes等),并在其中任意一个文件描述符就绪(可读或可写)时进行操作。这种机制主要用于提高系统的性能和效率,特别是在处理大量并发连接或事件时非常有用。.
IO分两阶段:
- 数据准备阶段
- 内核空间复制回用户进程缓冲区阶段。如下图:
I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
- select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,
- 异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间
select
函数介绍
slelect函数

使用流程
- 先思考
- 是否存在套接字接收数据?—读文件描述符集
- 无需阻塞传输数据的套接字有哪些?—写文件描述符集
- 哪些套接字发生了异常?—异常文件描述符集

-
设置文件描述符
- 不超过1024,因为监视的机制是轮询机制,监视太多效率低,此时首先需要将要监视的文件描述符按照监视项(接收、传输、异常) 进行区分,即按照上述 3种监视项分。
- 使用fd_set数组(存有0和1的位数组):

- 在fd-set变量中注册或更改值的操作都由下列宏完:


-
指定监视范围
- select 函数要求通过 第一个参数传 监视对象文件描述符的数量 ,因此,需要得到注册在fd_set变量中的文件描述符 ,但每次新建文件描述符时,其值都会增1 ,故只需将最大的文件描述符值加1再传递到select 函数即可,加1 是因为文件描述符的值从 0开始
-
超时时间
- select是阻塞操作 ,当然要设置超时事件
- timeval结构体定义如:

- 超时后, select 函数返回0。 因此,可以通过返回值了解返回原因
-
调用 select 函数后查看结果
- select函数调用完成后,向其传递的fd_set中将发生变化, 原来为1的所有位均变为0 ,但发生变化的文件描述符对应位除外。 因此,可以认为值仍为 1的位置上的文件描述符发生了变化。

- select函数调用完成后,向其传递的fd_set中将发生变化, 原来为1的所有位均变为0 ,但发生变化的文件描述符对应位除外。 因此,可以认为值仍为 1的位置上的文件描述符发生了变化。
原理
原理流程
- fd_set拷贝到内核->内核轮询->有事件/超时时,再次轮询->标记fd_set发生事件的fd->内核态拷贝到用户态->用户态轮询哪个fd有事件;
当用户process调用select的时候
-
用户态到内核态的数据拷贝:
select每次调用时,会将readfds、writefds、exceptfds从用户空间拷贝到内核空间(用户态拷贝到内核态)。这是一个 O(n) 的操作,当监控的文件描述符(FD)数量很大时,开销显著。
-
轮询检查可读事件:
- 内核遍历所有监控的 FD,调用对应的
poll函数检查是否有数据可读 / 可写。如果没有事件,进程进入睡眠。
- 当有事件发生或超时,内核再次遍历所有 FD,将就绪的 FD 标记在原 fd_set 中返回给用户(无变化的fd置为0,有变化的仍保持为1)(内核态拷贝到用户态)

- 内核遍历所有监控的 FD,调用对应的
-
用户态再次轮询:
- 用户程序需要遍历所有 FD,通过
FD_ISSET判断哪些 FD 就绪。这是第二个 O(n) 的操作。
- 用户程序需要遍历所有 FD,通过
select问题
- 重复拷贝开销:每次调用select,都需要把被监控的fds集合从用户态空间拷贝到内核态空间,即使大部分 FD 未发生变化。高并发场景下这样的拷贝会使得消耗的资源是很大的。

最低0.47元/天 解锁文章
474

被折叠的 条评论
为什么被折叠?



