Lazy loaded image
考研
Linux 网络编程学习笔记——九、I_O 复用
字数 11158阅读时长≈ 28 分钟
2026-10-16
2026-10-16

目录

I/O 复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常网络程序在下列情况下需要使用 I/O 复用技术:
  • 客户端程序要同时处理多个 socket ,比如非阻塞 connect 技术;
  • 客户端程序要同时处理用户输入和网络连接,比如聊天室程序;
  • TCP 服务器要同时处理监听 socket 和连接 socket ,这是 I/O 复用使用最多的场合;
  • 服务器要同时处理 TCP 请求和 UDP 请求,比如回射服务器;
  • 服务器要同时监听多个端口,或者处理多种服务,比如 xinetd 服务器。
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。Linux 下实现 I/O 复用的系统调用主要有 select 、poll 和 epoll 。

一、select 系统调用

select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。

1. select API

  • nfds:指定被监听的文件描述符的总数。它通常被设置为 select 监听的所有文件描述符中的最大值加 1 ,因为文件描述符是从 0 开始计数的;
  • readfds 、writefds 和 exceptfds:分别指向可读、可写和异常等事件对应的文件描述符集合,应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件描述符。select 调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这 3 个参数是 fd_set 结构指针类型;
  • fd_set 结构体:仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符,fd_set 能容纳的文件描述符数量由 FD_SETSIZE 指定,这就限制了 select 能同时处理的文件描述符的总量;
  • timeout:用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。不过我们不能完全信任 select 调用返回后的 timeout 值,比如调用失败时 timeout 值是不确定的,由以上定义可见,select 提供了一个微秒级的定时方式。如果给 timeout 变量的 tv_sec 成员和 tv_usec 成员都传递 0 ,则 select 将立即返回。如果给 timeout 传递 NULL ,则 select 将一直阻塞,直到某个文件描述符就绪;
  • select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select 将返回 0 。select 失败时返回 -1 并设置 errno 。如果在 select 等待期间,程序接收到信号,则 select 立即返回 -1 ,并设置 errno 为 EINTR 。
由于位操作过于烦琐,应该使用下面的一系列宏来访问 fd_set 结构体中的位:

2. 文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于 select 的使用非常关键。在网络编程中,下列情况下 socket 可读:
  • socket 内核接收缓存区中的字节数大于或等于其低水位标记 SO_RCVLOWAT 。此时我们可以无阻塞地读该 socket ,并且读操作返回的字节数大于 0 ;
  • socket 通信的对方关闭连接。此时对该 socket 的读操作将返回 0 。
  • 监听 socket 上有新的连接请求。
  • socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。
下列情况下 socket 可写:
  • socket 内核发送缓存区中的可用字节数大于或等于其低水位标记 SO_SNDLOWAT 。此时我们可以无阻塞地写该 socket ,并且写操作返回的字节数大于 0 ;
  • socket 的写操作被关闭。对写操作被关闭的 socket 执行写操作将触发一个 SIGPIPE 信号;
  • socket 使用非阻塞 connect 连接成功或者失败(超时)之后;
  • socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。
网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据。

3. 处理带外数据

socket 上接收到普通数据和带外数据都将使 select 返回,但 socket 处于不同的就绪状态:前者处于可读状态,后者处于异常状态:

二、poll 系统调用

poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者:
  • fd:指定文件描述符;
  • events:告诉 poll 监听 fd 上的哪些事件,它是一系列事件的按位或:
    • 事件
      描述
      是否可作为输入
      是否可作为输出
      POLLIN
      数据(包括普通数据和优先数据)可读
      是
      是
      POLLRDNORM
      普通数据可读
      是
      是
      POLLRDBAND
      优先级带数据可读(Linux 不支持)
      是
      是
      POLLPRI
      高优先级数据可读,比如 TCP 带外数据
      是
      是
      POLLOUT
      数据(包括普通数据和优先数据)可写
      是
      是
      POLLWRNORM
      普通数据可写
      是
      是
      POLLWRBAND
      优先级带数据可写
      是
      是
      POLLRDHUP
      TCP 连接被对方关闭,或者对方关闭了写操作。它由 GNU 引入
      是
      是
      POLLERR
      错误
      否
      是
      POLLHUP
      挂起,比如管道的写端被关闭后,读描述符上将收到 POLLHUP 事件
      否
      是
      POLLNVAL
      文件描述符没有打开
      否
      是
事件
描述
是否可作为输入
是否可作为输出
POLLIN
数据(包括普通数据和优先数据)可读
是
是
POLLRDNORM
普通数据可读
是
是
POLLRDBAND
优先级带数据可读(Linux 不支持)
是
是
POLLPRI
高优先级数据可读,比如 TCP 带外数据
是
是
POLLOUT
数据(包括普通数据和优先数据)可写
是
是
POLLWRNORM
普通数据可写
是
是
POLLWRBAND
优先级带数据可写
是
是
POLLRDHUP
TCP 连接被对方关闭,或者对方关闭了写操作。它由 GNU 引入
是
是
POLLERR
错误
否
是
POLLHUP
挂起,比如管道的写端被关闭后,读描述符上将收到 POLLHUP 事件
否
是
POLLNVAL
文件描述符没有打开
否
是
  • revents:由内核修改,以通知应用程序 fd 上实际发生了哪些事件;
  • nfds:指定被监听事件集合 fds 的大小。其类型 nfds_t 的定义如下: cpp typedef unsigned long int nfds_t;
  • timeout:指定 poll 的超时值,单位是毫秒。当 timeout 为 -1 时,poll 调用将永远阻塞,直到某个事件发生;当 timeout 为 0 时,poll 调用将立即返回;
  • poll 系统调用的返回值的含义与 select 相同。
上表中,POLLRDNORM 、POLLRDBAND 、POLLWRNORM 、POLLWRBAND 由 XOPEN 规范定义。它们实际上是将 POLLIN 事件和 POLLOUT 事件分得更细致,以区别对待普通数据和优先数据。但 Linux 并不完全支持它们。通常,应用程序需要根据 recv 调用的返回值来区分 socket 上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。不过,自 Linux 内核 2.6.17 开始,GNU 为 poll 系统调用增加了一个 POLLRDHUP 事件,它在 socket 上接收到对方关闭连接的请求之后触发。这为我们区分上述两种情况提供了一种更简单的方式。但使用 POLLRDHUP 事件时,我们需要在代码最开始处定义 _GNU_SOURCE 。

三、epoll 系统调用

1. 内核事件表

epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select 、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像 select 和 poll 那样每次调用都要重复传入文件描述符集或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下 epoll_create 函数来创建:
size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有 epoll 系统调用的第一个参数,以指定要访问的内核事件表。
下面的函数用来操作 epoll 的内核事件表:
  • fd:要操作的文件描述符;
  • op:指定操作类型:
    • EPOLL_CTL_ADD ,往事件表中注册 fd 上的事件;
    • EPOLL_CTL_MOD ,修改 fd 上的注册事件;
    • EPOLL_CTL_DEL ,删除 fd 上的注册事件。
  • EPOLL_CTL_ADD ,往事件表中注册 fd 上的事件;
  • EPOLL_CTL_MOD ,修改 fd 上的注册事件;
  • EPOLL_CTL_DEL ,删除 fd 上的注册事件。
  • event:指定事件,它是 epoll_event 结构指针类型:
    • events:描述事件类型。epoll 支持的事件类型和 poll 基本相 同。表 示epoll 事件类型的宏是在 poll 对应的宏前加上“E”,比如 epoll 的数据可读事件是 EPOLLIN 。但 epoll 有两个额外的事件类型 EPOLLET 和 EPOLLONESHOT 。它们对于 epoll 的高效运作非常关键。
    • data:用于存储用户数据:
      • epoll_data_t:是一个联合体,其 4 个成员中使用最多的是 fd ,它指定事件所从属的目标文件描述符;
      • ptr:可用来指定与 fd 相关的用户数据。但由于 epoll_data_t 是一个联合体,我们不能同时使用其 ptr 成员和 fd 成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用 epoll_data_t 的 fd 成员,而在 ptr 指向的用户数据中包含 fd 。
  • events:描述事件类型。epoll 支持的事件类型和 poll 基本相 同。表 示epoll 事件类型的宏是在 poll 对应的宏前加上“E”,比如 epoll 的数据可读事件是 EPOLLIN 。但 epoll 有两个额外的事件类型 EPOLLET 和 EPOLLONESHOT 。它们对于 epoll 的高效运作非常关键。
  • data:用于存储用户数据:
    • epoll_data_t:是一个联合体,其 4 个成员中使用最多的是 fd ,它指定事件所从属的目标文件描述符;
    • ptr:可用来指定与 fd 相关的用户数据。但由于 epoll_data_t 是一个联合体,我们不能同时使用其 ptr 成员和 fd 成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用 epoll_data_t 的 fd 成员,而在 ptr 指向的用户数据中包含 fd 。
  • epoll_data_t:是一个联合体,其 4 个成员中使用最多的是 fd ,它指定事件所从属的目标文件描述符;
  • ptr:可用来指定与 fd 相关的用户数据。但由于 epoll_data_t 是一个联合体,我们不能同时使用其 ptr 成员和 fd 成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用 epoll_data_t 的 fd 成员,而在 ptr 指向的用户数据中包含 fd 。
  • epoll_ctl 成功时返回 0 ,失败则返回 -1 并设置 errno 。

2. epoll_wait 函数

epoll 系列系统调用的主要接口是 epoll_wait 函数。它在一段超时时间内等待一组文件描述符上的事件,其原型如下:
  • timeout:含义与 poll 接口的 timeout 参数相同;
  • maxevents:指定最多监听多少个事件,它必须大于 0 ;
  • 该函数成功时返回就绪的文件描述符的个数,失败时返回 -1 并设置 errno 。
epoll_wait 函数如果检测到事件,就将所有就绪的事件从内核事件表(由 epfd 参数指定)中复制到它的第二个参数 events 指向的数组中。这个数组只用于输出 epoll_wait 检测到的就绪事件,而不像 select 和 poll 的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率:

3. LT 和 ET 模式

epoll 对文件描述符的操作有两种模式:LT(Level Trigger,电平 触发)模式和 ET(Edge Trigger,边沿触发)模式:
  • LT 模式是默认的工作模式,这种模式下 epoll 相当于一个效率较高的 poll 。当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以 ET 模式来操作该文件描述符。对于采用 LT 工作模式的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时,epoll_wait 还会再次向应用程序通告此事件,直到该事件被处理;
  • ET 模式是 epoll 的高效工作模式,对于采用 ET 工作模式的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。可见,ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;
  • 注意:每个使用 ET 模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。

4. EPOLLONESHOT 事件

即使我们使用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程)在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该 socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个 socket 的局面。这当然不是我们期望的。我们期望的是一个 socket 连接在任一时刻都只被一个线程处理。这一点可以使用 epoll 的 EPOLLONESHOT 事件实现。
对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕,该线程就应该立即重置这个 socket上 的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket 。
尽管一个 socket 在不同时间可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务。这就保证了连接的完整性,从而避免了很多可能的竞态条件。

四、三组 I/O 复用函数的比较

前面我们讨论了 select 、poll 和 epoll 三组 I/O 复用系统调用,这 3 组系统调用都能同时监听多个文件描述符。它们将等待由 timeout 参数指定的超时时间,直到一个或者多个文件描述符上有事件发生时返回,返回值是就绪的文件描述符的数量。返回 0 表示没有事件发生。现在我们从事件集、最大支持文件描述符数、工作模式和具体实现等四个方面进一步比较它们的异同,以明确在实际应用中应该选择使用哪个。
这 3 组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果:
  • select 的参数类型 fd_set 没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此 select 需要提供 3 个这种类型的参数来分别传入和输出可读、可写及异常等事件。这一方面使得 select 不能处理更多类型的事件,另一方面由于内核对 fd_set 集合的在线修改,应用程序下次调用 select 前不得不重置这 3 个 fd_set 集合。
  • poll 的参数类型 pollfd 则多少“聪明”一些。它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁得多。并且内核每次修改的是 pollfd 结构体的 revents 成员,而 events 成员保持不变,因此下次调用 poll 时应用程序无须重置 pollfd 类型的事件集参数。由于每次 select 和 poll 调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为 。
  • epoll 则采用与 select 和 poll 完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用 epoll_ctl 来控制往其中添加、删除、修改事件。这样,每次 epoll_wait 调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。epoll_wait 系统调用的 events 参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到 。
poll 和 epoll_wait 分别用 nfds 和 maxevents 参数指定最多监听多少个文件描述符和事件。这两个数值都能达到系统允许打开的最大文件描述符数目,即 65535(cat/proc/sys/fs/file-max)。而 select 允许监听的最大文件描述符数量通常有限制。虽然用户可以修改这个限制,但这可能导致不可预期的后果。select 和 poll 都只能工作在相对低效的 LT 模式,而 epoll 则可以工作在 ET 高效模式。并且 epoll 还支持 EPOLLONESHOT 事件。该事件能进一步减少可读、可写和异常等事件被触发的次数。
从实现原理上来说,select 和 poll 采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件的算法的时间复杂度是 。epoll_wait 则不同,它采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。因此 epoll_wait 无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是 。但是,当活动连接比较多的时候,epoll_wait 的效率未必比 select 和 poll 高,因为此时回调函数被触发得过于频繁。所以 epoll_wait 适用于连接数量多,但活动连接较少的情况。
系统调用
select
poll
epoll
事件集合
用户通过 3 个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用 select 都要重置这 3 个参数
统一处理所有事件类型,因此只需一个事件集参数。用户通过 pollfd.events 传入感兴趣的事件,内核通过修改 pollfd.revents 反馈其中就绪的事件
内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用 epoll_wait 时,无需反复传入用户感兴趣的事件。epoll_wait 系统调用的参数 events 仅用来反馈就绪的事件
应用程序索引就绪文件描述符的时间复杂度
最大支持文件描述符数
一般有最大值限制
65535
65535
工作模式
LT
LT
支持 ET 高校模式
内核实现和工作效率
采用轮询方式来检测就绪事件,算法时间复杂度为
采用轮询方式来检测就绪事件,算法时间复杂度为
采用回调方式来检测就绪事件,算法时间复杂度为

五、I/O 复用的高级应用——聊天室

像 ssh 这样的登录服务通常要同时处理网络连接和用户输入,这也可以使用 I/O 复用来实现。本节以 poll 为例实现一个简单的聊天室程序,以阐述如何使用 I/O 复用技术来同时处理网络连接和用户输入。该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两个部分。其中客户端程序有两个功能:
  • 一是从标准输入终端读入用户数据,并将用户数据发送至服务器;
  • 二是往标准输出终端打印服务器发送给它的数据。
服务器的功能是接收客户数据,并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外)。

1. 客户端

客户端程序使用 poll 同时监听用户输入和网络连接,并利用 splice 函数将用户输入内容直接定向到网络连接上以发送之,从而实现数据零拷贝,提高了程序执行效率:

2. 服务器

服务器程序使用 poll 同时管理监听 socket 和连接 socket ,并且使用牺牲空间换取时间的策略来提高服务器性能:

六、I/O 复用的高级应用——同时处理 TCP 和 UDP 服务

至此,我们讨论过的服务器程序都只监听一个端口。在实际应用中,有不少服务器程序能同时监听多个端口,比如超级服务 inetd 和 android 的调试服务 adbd 。
从 bind 系统调用的参数来看,一个 socket 只能与一个 socket 地址绑定,即一个 socket 只能用来监听一个端口。因此,服务器如果要同时监听多个端口,就必须创建多个 socket ,并将它们分别绑定到各个端口上。这样一来,服务器程序就需要同时管理多个监听 socket ,I/O 复用技术就有了用武之地。另外,即使是同一个端口,如果服务器要同时处理该端口上的 TCP 和 UDP 请求,则也需要创建两个不同的 socket :一个是流 socket ,另一个是数据报 socket ,并将它们都绑定到该端口上。下面所示的回射服务器就能同时处理一个端口上的 TCP 和 UDP 请求:

七、超级服务 xinetd

Linux 因特网服务 inetd 是超级服务。它同时管理着多个子服务,即监听多个端口。现在 Linux 系统上使用的 inetd 服务程序通常是其升级版本 xinetd 。xinetd 程序的原理与 inetd 相同,但增加了一些控制选项,并提高了安全性。

1. xinetd 配置文件

xinetd 采用 /etc/xinetd.conf 主配置文件和 /etc/xinetd.d 目录下的子配置文件来管理所有服务。主配置文件包含的是通用选项,这些选项将被所有子配置文件继承。不过子配置文件可以覆盖这些选项。每一个子配置文件用于设置一个子服务的参数。比如,telnet 子服务的配置文件 /etc/xinetd.d/telnet 的典型内容如下:
项目
含义
service
服务名
flags
设置连接的标志。REUSE 表示复用 telnet 连接的 socket 。该标志已经过时,每个连接都默认启用 REUSE 标志
socket_type
服务类型
wait
服务采用单线程方式(wait = yes)还是多线程方式(wait = no)。单线程方式表示 xinetd 只 accept 第一次连接,此后将由子服务进程来 accept 新连接。多线程方式表示 xinetd 一直负责 accept 连接,而子服务进程仅处理连接 socket 上的数据读写
user
子服务进程将以 user 指定的用户身份运行
server
子服务程序的完整路径
log_on_failure
定义当服务不能启动时输出日志的参数
disable
是否启动该子服务

2. xinetd 工作流程

xinetd 管理的子服务中有的是标准服务,比如时间日期服务 daytime 、回射服务 echo 和丢弃服务 discard 。xinetd 服务器在内部直接处理这些服务。还有的子服务则需要调用外部的服务器程序来处理。xinetd 通过调用 fork 和 exec 函数来加载运行这些服务器程序。比如 telnet 、ftp 服务都是这种类型的子服务。下面仍以 telnet 服务为例来探讨 xinetd 的工作流程:
  • 首先,查看 xinetd 守护进程的 PID :
    • 然后开启两个终端并分别使用如下命令 telnet 到本机:
      • 接下来使用 ps 命令查看与进程 9543 相关的进程:
        • 由此可见,每次使用 telnet 登录到 xinetd 服务,它都创建一个子进程来为该 telnet 客户服务。子进程运行 in.telnetd 程序,这是在 /etc/xinetd.d/telnet 配置文件中定义的。每个子进程都处于自己独立的进程组和会话中。可以使用 lsof 命令进一步查看子进程都打开了哪些文件描述符:
          • 这里省略了一些无关的输出。通过 lsof 的输出知道,子进程 9810 关闭了其标准输入、标准输出和标准错误,而将 socket 文件描述符 dup 到它们上面。因此,telnet 服务器程序将网络连接上的输入当作标准输入,并把标准输出定向到同一个网络连接上。
        • 再进一步,对 xinetd 进程使用 lsof 命令:
          • 这一条输出说明 xinetd 将一直监听 telnet 连接请求,因此 in.telnetd 子进程只处理连接 socket ,而不处理监听 socket 。这是子配置文件中的 wait 参数所定义的行为。对于内部标准服务,xinetd 的处理流程也可以用上述方法来分析:
            1
        原创存证 · 本地哈希 · f83597b2d9af...
        算法:SHA-256
        时间:2026-10-16
        哈希:f83597b2d9af4134090fd763b4e406f1c1f0ee5630c29239635ad6cbd65bfb09

        该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。


        评论
        Loading...