Linux(13)(上)

一.认识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的缺点
  1. 支持的文件描述符数量有限

    • 默认最多 FD_SETSIZE = 1024 个(fd 范围 0~1023),太少了。
  2. 输入输出参数设计低效

    • readfds/writefds/exceptfds 是输入输出型参数,每次调用后内容被内核修改;

    • 因此每次调用前都必须重新初始化关心的事件集合。

  3. 频繁的数据拷贝

    • 每次调用 select 都需将整个 fd_set 从用户空间拷贝到内核空间,返回时再拷贝回来。
  4. 线性遍历开销大

    • 用户层:需遍历所有 fd 判断是否就绪(即使只有少数活跃);

    • 内核层:也需遍历全部被监视的 fd 检查状态,时间复杂度 O(n)。

select的缺点那么多,那我们是如何对它进行改进的呢,关于后面的内容我应该会先带来select的重要代码,然后讲解poll函数!

相关推荐
aixingkong9212 小时前
Nvidia为例 XPU的Multi-Host、Socket Direct 、GPUDirect、RDMA 技术点分析
服务器·网络·硬件架构·硬件工程
雯宝4 小时前
mnt_init 函数
linux
律宏阔10 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔10 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴10 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千10 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3710 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3710 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3710 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW10 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux