Linux(13)(上)

一.认识IO和五种IO模型

引入:

  • 应用层 readwrite 的本质是把数据从用户空间拷贝到内核空间(或反之)------本质上就是拷贝函数。

  • 但当缓冲区满(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 == EAGAINEWOULDBLOCK,表示数据未就绪,属正常情况(这也是上面讲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函数!

相关推荐
忧郁的橙子.2 小时前
26期_01_Pyhton linux基本命令
linux·运维·服务器
郝学胜-神的一滴2 小时前
深入解析Linux网络编程之bind函数:从基础到实践的艺术
linux·服务器·网络·c++·websocket·程序人生
西京刀客2 小时前
macOS 打出来的 tar 包,Linux 常见告警(tar 包里带了 macOS 的扩展属性(xattr))
linux·运维·macos
mango_mangojuice2 小时前
Linux学习笔记(角色,权限管理)1.21
linux·笔记·学习
pythonchashaoyou2 小时前
静态住宅ip是什么,静态住宅IP选型全解
网络·网络协议·tcp/ip
遇见火星2 小时前
Linux综合性能监控工具dstat命令详解
linux·服务器·php·dstat
相思难忘成疾2 小时前
通向HCIP之路:第三步:动态路由协议OSPF(全)
服务器·网络·智能路由器·hcip
REDcker2 小时前
HTTP请求数据包流转详解:localhost、127.0.0.1、公网 IP、公网域名访问时的数据流转
网络·tcp/ip·http
咕噜咕噜啦啦3 小时前
ROS入门
linux·vscode·python