欢迎光临
我们一直在努力

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

// 操作文件描述符属性(如设置非阻塞、获取/设置状态标志等)
// 通用性强 可用于 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函数介绍

// 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结构

// 表示时间值(秒 + 微秒) 用于 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函数!

    赞(0)
    未经允许不得转载:171主机测评 » Linux(13)(上)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址