11、基础 | C++的IO框架
I/O 复用
概述
当TCP客户端同时处理两个输入:标准输入和TCP套接字时,会遇到以下问题:
- 问题描述:在客户端阻塞于标准输入上的
fgets
调用时,如果服务器进程突然终止,服务器TCP虽然正确地给客户端发送了一个FIN,但是客户端正阻塞于从标准输入读入的过程,因此无法立即看到这个EOF(文件结束符)。直到从标准输入读到数据后,客户端才能看到EOF,但此时可能已经错过了及时处理服务器关闭的时机。 - 解决方案需求:这样的进程就需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(如TCP连接关闭、标准输入有数据可读等),就通知进程。这种能力就被称为I/O复用(multiplexing)。
I/O 模型
存在5种主要的I/O模型:
- 阻塞式I/O
- 非阻塞式I/O
- I/O 复用
- 信号驱动式I/O
- 异步I/O
一个输入操作的两个阶段
一个输入操作通常包括两个不同的阶段:
- 等待数据准备好:在这个阶段,进程需要等待,直到所需的数据到达内核的缓冲区。
- 从内核向进程复制数据:一旦数据准备好,内核就会将数据从内核空间复制到用户空间,供进程使用。
这些阶段和模型的不同之处在于它们处理等待数据准备好这一阶段的方式。
select 函数
概述
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它。主旨思想在于不再由应用程序自己监视客户端连接,而是由内核替应用程序监视文件描述符的状态。
详细解析
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset,
fd_set *exceptset, struct timeval *timeout);
maxfdpl: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readset: 监控有读数据到达文件描述符集合,传入传出参数
writeset: 监控写数据到达文件描述符集合,传入传出参数
exceptset: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,这称为轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
函数内容详解
- 注意事项:
select
能监听的文件描述符个数受限于FD_SETSIZE
,一般为1024。单纯改变进程打开的文件描述符个数并不能改变select
监听的文件个数。
- 描述符就绪条件:
- 当某个套接字上发生错误时,该套接字将被
select
标记为既可读又可写。
- 当某个套接字上发生错误时,该套接字将被
- 接收低水位标记和发生低水位标记的目的:
- 允许应用进程控制
select
返回可读或可写条件之前有多少数据可读或有多大空间可用于写。例如,可以设置至少存在64字节的数据准备好读时,select
才唤醒应用进程,从而避免频繁打断应用进程处理其他工作。
- 允许应用进程控制
select
总结
-
优点:
select
遵循POSIX标准,跨平台移植性较好。select
监控的超时等待时间可以精细到微秒。
-
缺点:
select
所能监控的描述符数量有最大上限,取决于宏_FD_SETSIZE
,默认大小为1024。select
的监控原理是在内核中进行轮询遍历,这种遍历会随着描述符的增多而性能下降。select
返回时会移除所有未就绪的描述符,只给用户返回就绪的描述符集合,但没有直接告诉用户哪个描述符就绪,需要用户自己遍历才能获知。select
每次监控都需要重新将集合拷贝到内核中才能进行监控。select
每次返回都会移除所有未就绪的描述符,因此每次监控都要重新向集合中添加描述符。
poll 函数
概述
该函数提供的功能与select
类似,不过在处理流设备时,它能够提供额外的信息。select
函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它。
详细解析
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
events标志以及测试revents标志的一些常量:
—————————————处理输入—————————————————————
POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
—————————————处理输出—————————————————————
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
—————————————处理错误————————————————————
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
nfds 监控数组中有多少文件描述符需要被监控
timeout 毫秒级等待
-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
函数内容详解
- 注意事项:
- 如果不再监控某个文件描述符时,可以把
pollfd
结构体中的fd
成员设置为-1,这样poll
将不再监控此pollfd
,下次poll
返回时,会把对应的revents
成员设置为0。 pollfd
结构包含了要监视的事件(通过events
成员)和发生的事件(通过revents
成员),不再使用select
的"值-结果"传递方式。- 同时,
poll
函数对pollfd
数组的大小并没有最大数量限制(但监控的文件描述符数量过大后,性能也会有所下降)。 - 和
select
函数一样,poll
返回后,需要轮询pollfd
数组来获取就绪的描述符。
- 如果不再监控某个文件描述符时,可以把
epoll 函数
概述
epoll是Linux下多路复用I/O接口(select/poll)的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。获取事件时,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合即可。目前,epoll是Linux大规模并发网络程序中的热门首选模型。epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait
的调用,提高应用程序效率。
基础API
- 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,与内存大小有关。
#include <sys/epoll.h>
int epoll_create(int size) size: 监听数目
- 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event: 告诉内核需要监听的事件
struct epoll_event {
__uint32_t eve
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。