一.认识IO和五种IO模型
引入:
应用层
read和write的本质是把数据从用户空间拷贝到内核空间(或反之)------本质上就是拷贝函数。但当缓冲区满(
write)或空(read)时,操作无法立即完成,进程就会等待。因此,I/O = 等 + 拷贝:
等:判断设备或缓冲区是否就绪;
拷贝:在用户空间与内核空间之间搬运数据。
高效 I/O 的核心是:单位时间内,"等"的比重越小,I/O 效率越高。
1.1 五种IO模型:
1.阻塞式 IO:发起 I/O 后一直等待,直到完成。
2.非阻塞 IO :立即返回,未就绪则反复轮询。(非阻塞要搭配循环从而达到轮询的效果)
3.信号驱动 IO :I/O 就绪时内核发信号通知进程。
4.多路复用 :单线程监听多个 fd,就绪才操作。
5.异步 IO :提交请求后不等,完成后由内核通知。
1.2 IO模型讲解:
信号驱动 IO :先注册信号处理函数,内核就绪时发信号通知,进程再执行拷贝。
多路复用 :由
select/epoll等系统调用统一等待多个 fd 就绪 ,进程负责后续拷贝;与信号驱动类似,但用轮询/事件机制代替信号。异步 IO :进程既不等也不拷贝 ------提交请求后做其他事,内核完成"等+拷贝"后通知结果。
同步IO/异步IO:
同步 :进程必须亲自完成数据拷贝 ;"等"可由自己阻塞,或由
select/信号等机制辅助。异步 :进程不等待、不拷贝------提交请求后即返回,由内核完成全部 I/O 并通知结果。
同步执行/异步执行:
同步执行 :发起操作后,必须等它做完才能继续。
异步执行 :发起操作后,不用等,可以马上干别的事,结果 later 通知。
进程间同步:
同步 :协调多个进程的执行顺序,解决 "谁先谁后" 的问题。
异步 :各进程独立运行,不强制等待彼此 ,通过事件、消息等 later 交互。
本章重点是2和4,之所以有2是因为4设计到非阻塞的相关知识,顺便把2讲了
二.fcntl
文件描述符,默认都是阻塞IO,我们讲这个主要是用它修改成非阻塞IO
cpp// 操作文件描述符属性(如设置非阻塞、获取/设置状态标志等) // 通用性强 可用于 socket、pipe、普通文件等所有文件描述符 #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ ); // 参数: // fd - 文件描述符(如 socket 返回的 sockfd) // cmd - 操作命令(如 F_GETFL、F_SETFL) // arg - 可选参数(依 cmd 而定 通常为 int 或 struct flock*) // 返回值: // 成功:返回值依 cmd 而定(如 F_GETFL 返回标志,F_SETFL 返回 0) // 失败:返回 -1 并设置 errno // 常见用法: // 获取当前文件状态标志 int flags = fcntl(sockfd, F_GETFL); if (flags == -1) { /* error */ } // 设置为非阻塞模式 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); // 设置为阻塞模式 fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);注意:
设置为非阻塞后,若底层 fd 数据未就绪,
recv/read/write/send会立即返回 -1;此时无法仅凭返回值区分是"资源未就绪 "还是"发生真实错误";
必须检查
errno:若
errno == EAGAIN或EWOULDBLOCK,表示数据未就绪,属正常情况 ;(这也是上面讲IO的第二种形式内核会返回EWOULDBLOCK)其他
errno(如ECONNRESET,EBADF等)表示真正的 I/O 错误。
三.select
1.select函数介绍
cpp// I/O 多路复用:监视多个文件描述符是否可读、可写或发生异常 // 阻塞直到至少一个 fd 就绪、超时或被信号中断(常用于单线程高并发服务器) #include <sys/select.h> int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout ); // 参数: // nfds - [in] 待监视的文件描述符范围上限,应为所有 fd 中最大值 + 1 // readfds - [in/out] 指向"可读"文件描述符集合: // 输入:指定要监视哪些 fd 可读 // 输出:返回后仅保留就绪的 fd // writefds - [in/out] 指向"可写"文件描述符集合(语义同 readfds,可为 NULL) // exceptfds - [in/out] 指向"异常"文件描述符集合(通常设为 NULL) // timeout - [in/out] 超时控制: // 输入:指定最大等待时间(NULL = 永久阻塞;{0,0} = 立即返回) // 输出:某些系统会更新为剩余时间(Linux 不修改) // 返回值: // > 0:就绪的文件描述符总数 // = 0:超时,无 fd 就绪 // -1:出错(如被信号中断、无效 fd 等),设置 errno // 常用宏(操作 fd_set): // FD_ZERO(&set) - 清空集合 // FD_SET(fd, &set) - 将 fd 加入集合 // FD_CLR(fd, &set) - 从集合中移除 fd // FD_ISSET(fd, &set) - 检查 fd 是否在集合中(调用 select 后用于判断是否就绪) // 注意: // • fd_set 是值-结果参数(传入关心的 fd,返回就绪的 fd) // • nfds 必须正确设置(不是 fd 总数,而是 max_fd + 1) // • select 修改传入的 fd_set 和 timeout(部分系统),若需复用应重新初始化 // • 文件描述符数量受限(通常 <= 1024)2.timeval结构
cpp// 表示时间值(秒 + 微秒) 用于 select、gettimeofday 等函数 // 常用于设置超时或获取高精度时间 #include <sys/time.h> struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // 微秒(0 ~ 999999) }; // 说明: // - 用于 select 时 若 tv_sec=0 且 tv_usec=0 表示非阻塞 // 典型用法: // 设置 2.5 秒超时 struct timeval timeout; timeout.tv_sec = 2; timeout.tv_usec = 500000; select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
3.1 select的要点
注意:
**1.**select除第一个函数是输入函数,其余全是输入/输出函数(你既要输入进去,内核还会给你输出回来)
**2.**2、3、4这三个函数,它们是采用位图,那位图是如何表示的呢(我们用读、8位比特举例)
输入时,用户告诉内核,它想监听那些fd:
这个图片就是,我想监听fd2和fd0
输出时,内核告诉用户,那些fd就绪了:
这个图片就是,fd2就绪了可以读
3. 因此select必然涉及大量位图操作,因此内核给予宏让我们进行操作
4. 当
timeout != NULL时,select是限时阻塞(最多等 timeout 指定的时间)。
输入:指定
select最多阻塞等待的时间,例如[5, 1]表示 5 秒 + 1 微秒。输出:
主动退出 :若 2、3、4 中任意 fd 就绪,
select立即返回,timeout的值是总时间-就绪时间。被动退出 :若超时到期仍无事件,
select返回 0,timeout的值是0。此时timeout就要重新输入,不然它的值就是上一次的返回值,因此每次select调用结束后都必须要给其重新输入,其实2、3、4也是要的因为它们都是输出函数
5. 如何知道2、3、4它们总共可以表示多少个文件描述符呢, 首先它们的类型是
fd_set,因此可以使用sizeof取大小(单位字节),然后因为一个字节等于8位,而2、3、4它们是用1位代表一个文件描述符,因此sizeof取完要*8,这样就能知道有多少个文件描述符。6. 因为
select的 2、3、4 参数(readfds/writefds/exceptfds)是输入输出型参数:
输入时:你设置关心的 fd 集合(比如 5 个);
输出时:内核只保留就绪的 fd(比如只剩 1 个),其余位被清零。
如果不重新设置,下次调用
select就只会监听上次就绪的那 1 个 fd,漏掉原本也要关心的其他 4 个 。因此,每次调用select前都必须重新初始化这些 fd_set。7.
select等的不只是 listen 套接字,还包括其他套接字的可读、可写或异常事件 。因此我们还需要对fd进行判断是准备要accept还是已经accept成功现在需要读
3.2 select的缺点
支持的文件描述符数量有限
- 默认最多
FD_SETSIZE = 1024个(fd 范围 0~1023),太少了。输入输出参数设计低效
readfds/writefds/exceptfds是输入输出型参数,每次调用后内容被内核修改;因此每次调用前都必须重新初始化关心的事件集合。
频繁的数据拷贝
- 每次调用
select都需将整个fd_set从用户空间拷贝到内核空间,返回时再拷贝回来。线性遍历开销大
用户层:需遍历所有 fd 判断是否就绪(即使只有少数活跃);
内核层:也需遍历全部被监视的 fd 检查状态,时间复杂度 O(n)。
select的缺点那么多,那我们是如何对它进行改进的呢,关于后面的内容我应该会先带来select的重要代码,然后讲解poll函数!






