找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1887

积分

0

好友

248

主题
发表于 4 天前 | 查看: 14| 回复: 0

内核启动的基本流程

内核启动的核心链路可概括为:引导加载程序(Bootloader)→ 加载内核映像 → 内核解压 → 内核启动 → 调用 start_kernel 函数 → 启动初始进程(Init Process)。

Linux内核启动过程流程图

1.1、启动加载程序 (Bootloader)

引导加载程序(常见的有 GRUB、LILO、syslinux 等)是连接硬件与内核的关键桥梁,核心作用是完成内核启动前的准备工作,为内核运行搭建基础环境。

  • 加载内核映像:引导加载程序会从硬盘、U盘等存储设备中,将经过压缩的内核映像(如 vmlinuz)读取并加载至内存。这类内核映像通常采用 gzip 等压缩格式封装,以二进制文件形式存在。
  • 加载 initrd/initramfs:若系统配置了初始 RAM 盘(initrd)或初始 RAM 文件系统(initramfs),引导加载程序会同步将这些文件加载到内存中。它们将为内核启动初期提供必要的临时文件系统支持。

1.2、内核解压阶段

在压缩内核映像的头部,嵌入了一段小型解压缩程序,其唯一职责是完成内核主体部分的解压操作。

  • 执行内核解压:当内核映像被加载到内存后,这段内置的解压缩程序会自动运行,将压缩的内核主体解压到内存中预先分配的合适地址。
  • 移交控制权至解压后内核:解压操作完成后,系统控制权会立即转移到解压后内核代码的入口地址,后续将由完整的内核代码主导启动流程。

1.3、内核启动(Kernel Startup)

解压后的内核代码会从一个固定入口点开始执行,该入口点与具体的硬件架构强相关。例如在 x86 架构下,通常为 startup_32(32位系统)或 startup_64(64位系统)函数。

  • 架构相关初始化:内核会根据当前硬件架构,执行一系列针对性的初始化操作,包括设置 CPU 运行模式、初始化内存分页机制、构建基础内存映射表等,确保硬件环境适配内核运行。
  • 初始化内核堆栈:堆栈是函数调用与数据临时存储的基础,内核会在此阶段完成自身堆栈的创建与配置,为后续的函数调用和数据操作提供支撑。
  • 调用 start_kernel 函数:完成基础硬件环境初始化后,内核会触发 start_kernel 函数的调用,这一函数是内核整体初始化流程的核心枢纽。

1.4、start_kernel 函数

start_kernel 函数定义于 init/main.c 文件中,是内核初始化的核心函数,几乎所有关键的内核子系统初始化都在此完成。

  • 初始化控制台:配置内核的打印输出机制,确保后续内核运行状态、错误信息等能够正常输出展示。
  • 初始化内存管理子系统:构建初始的内存管理结构,初始化内存分配器,为内核及后续进程的内存使用提供管理能力。
  • 硬件设备检测与初始化:遍历系统中的硬件设备,加载对应的驱动程序,完成硬件设备的激活与配置。
  • 启动中断处理机制:配置中断控制器,注册中断处理函数,使内核能够响应并处理硬件设备发出的中断请求。
  • 初始化内核调度器:初始化进程调度相关的数据结构与算法,为后续进程的调度执行做好准备。
  • 加载初始进程:创建并启动系统中第一个用户空间进程(通常为 /sbin/init),完成从内核态到用户态的过渡。

1.5、启动初始进程

init 进程是用户空间的首个进程,被誉为“用户空间进程之祖”,核心职责是完成系统后续的初始化工作,构建完整的用户空间运行环境。

  • init 进程自身初始化:执行系统预设的初始化脚本,配置系统环境变量、设置系统时区、初始化网络参数等基础配置。
  • 启动用户空间服务:按照系统配置,启动各类系统服务与守护进程(如网络服务、日志服务、图形界面服务等),最终完成整个系统的启动流程。

内核文件加载及解压缩

2.1、为什么是压缩文件

Linux 内核映像默认采用压缩格式封装,这一设计是基于存储、加载效率及实用性等多方面的考量,具体原因如下:

  • 节省存储资源:压缩后的内核映像体积大幅减小,能有效降低在存储设备上的占用空间。这对于存储资源有限的嵌入式设备、轻量型系统,以及需要频繁分发、更新内核的场景尤为重要。
  • 提升加载效率:尽管压缩文件需要额外的解压步骤,但较小的文件体积意味着引导加载程序从存储设备读取数据到内存的时间显著缩短。现代处理器的解压速度极快,解压耗时远小于读取更大体积未压缩文件的耗时,整体上能加快启动流程。
  • 优化传输效率:在通过网络传输内核映像(如远程 OTA 更新)时,压缩格式能大幅减少带宽占用,提升传输速度,降低更新过程中的网络开销。
  • 便于管理分发:体积更小的内核映像更易于在光盘、U盘等移动介质上存储和分发,同时也简化了备份、迁移等管理操作。
  • 适配标准流程:采用压缩格式已成为 Linux 内核的标准实现,主流引导加载程序(如 GRUB)均已原生支持压缩内核映像的识别与处理,确保了启动流程的兼容性与可靠性。

2.2、文件类型 vmlinuxz 和 bzImage

了解压缩内核映像之前,我们首先需要明确未经压缩的内核编译产物——vmlinux 文件的基础概念。

2.2.1、什么是 vmlinux?

vmlinux 是内核编译链接阶段生成的完整内核映像文件,包含了内核的全部代码、数据结构及未加载的模块,是未经任何压缩和特殊处理的原始内核文件,通常位于内核源码根目录下,其核心特性如下:

  • 未压缩特性:作为原始编译产物,vmlinux 保留了内核的完整体积,未经过任何压缩算法处理。
  • ELF 格式封装:vmlinux 默认采用 ELF(可执行与可链接格式),这是一种通用的二进制文件格式,支持存储可执行代码、目标文件、共享库等,便于编译链接与调试。
  • 包含调试符号:文件中嵌入了完整的调试符号表与符号信息,这些信息是内核调试、性能分析及问题定位的关键依据。
  • 无固定后缀:vmlinux 通常不添加文件后缀,但可通过文件头信息直接识别其 ELF 格式属性。

2.2.2、vmlinux 的生成过程

vmlinux 是内核编译流程中链接阶段的产物,其简化生成步骤如下:

  • 编译源码文件:内核源码中的所有 .c 文件(C语言代码)和 .S 文件(汇编代码)会被分别编译为目标文件(.o 文件)。
  • 链接目标文件:链接器(如 ld)会将所有生成的目标文件,结合内核链接脚本,最终链接生成一个完整的内核映像,即 vmlinux。

链接命令举例:
ld -o vmlinux [object files] [linker scripts]

2.2.3、vmlinuxz 和 bzImage 的生成过程

得到原始的 vmlinux 文件后,内核会通过后续处理生成可用于启动的压缩内核映像,主要包括 vmlinuz 和 bzImage 两种格式,生成流程如下:

  • 生成 vmlinuz:通过 gzip 等压缩工具对 vmlinux 进行压缩,生成压缩后的内核映像 vmlinuz。 压缩命令示例:gzip -c vmlinux > vmlinuz
  • 生成 bzImage:针对 x86 架构,内核提供了 bzImage(大内核映像)格式,通过 make bzImage 命令可将 vmlinux 压缩并封装为 bzImage 格式,解决了早期 zImage 格式的内存地址限制问题。

2.2.4、其他压缩格式

除了 vmlinuz 和 bzImage,Linux 内核还有多种适配不同场景的压缩映像格式,各自具备特定的应用场景:

  • zImage:早期的内核映像格式,适用于体积较小的内核,受限于传统 x86 架构的低内存地址空间,现已逐渐被 bzImage 替代。
  • uImage:专为 U-Boot 引导加载程序设计的内核映像格式,广泛应用于嵌入式系统。其包含 U-Boot 专属的头部信息,支持多种压缩算法,可被 U-Boot 直接识别并加载。

2.2.5、Android 系统文件

在 Android 系统中,内核映像格式通常为 zImage 或 Image.gz,具体取决于设备所采用的引导加载程序及硬件配置:

  • zImage:在早期 Android 设备中较为常见,引导加载程序可直接加载 zImage 格式的内核映像,完成解压后启动内核。
  • Image.gz:本质上是经过 gzip 压缩的内核映像,与通用 Linux 系统中的 vmlinuz 功能类似,仅命名上有所差异。引导加载程序加载后需先解压,再启动内核。

2.3、内核加载过程

内核从加载到完成解压的核心流程可概括为:引导加载程序(Bootloader)→ 跳转到内核入口点 → 执行解压缩代码 → 调用解压函数 → 解压内核映像 → 返回解压结果 → 跳转到内核启动流程(Kernel Startup)。

内核解压缩流程图

2.3.1、内核映像加载到内存中

引导加载程序的核心任务之一是将内核映像及相关文件加载到内存,并完成控制权的移交,具体步骤如下:

  • 加载核心文件:引导加载程序(如 GRUB)会读取配置文件(通常为 grub.cfg),根据配置信息加载压缩内核映像(vmlinuz 等)及可选的 initrd/initramfs 文件至内存。
  • 设置内核参数:通过命令行方式向内核传递启动参数(如根文件系统路径、调试模式开关等),为内核启动提供定制化配置。
  • 移交控制权:完成加载与参数设置后,引导加载程序将系统控制权转移到内核映像的入口点,结束自身工作。
1)、引导加载程序(Bootloader)的启动过程(以 BIOS 为例)
  • CPU 重置后,会自动执行内存地址 0xfffffff0 处的指令,该指令为跳转指令,直接指向 BIOS 的入口地址。
  • BIOS 启动后,会执行硬件自检(POST)和基础硬件初始化,随后根据预设的启动顺序(如硬盘、U盘、光盘)选择启动设备。
  • BIOS 将控制权移交至启动设备的主引导扇区(MBR,占用512字节),由主引导扇区代码继续引导流程。
2)、内核文件的具体加载流程
  • 主引导扇区代码执行后,由于其体积有限(仅512字节),会进一步跳转到更复杂的引导加载程序核心映像(如 GRUB 的 core image)。
  • 核心映像启动后,会加载引导加载程序的扩展模块和配置文件(grub.cfg),解析其中的内核加载配置。
  • 根据 grub.cfg 的配置,引导加载程序将压缩内核映像(vmlinuz)和 initrd/initramfs 文件加载到指定内存地址。
  • 加载完成后,引导加载程序将控制权移交至内核映像的入口点,完成从 BIOS 到内核的控制权转移。

3.2、内核解压过程(以 x86 架构为例)

3.2.1、关键文件和代码路径

  • arch/x86/boot/header.S:内核启动的汇编代码入口,定义了内核映像的入口点地址及基础启动环境配置。
  • arch/x86/boot/compressed/head_64.S、arch/x86/boot/compressed/misc.c:内核解压相关的核心代码,分别负责解压前的环境准备和具体解压逻辑实现。
  • arch/x86/kernel/head_64.S、arch/x86/kernel/head.c:解压后内核的启动代码,负责完成内核启动的后续初始化。

3.2.2、解压核心步骤

3.2.2.1、程序跳转到内核入口点:

引导加载程序根据配置加载内核映像(vmlinuz)至内存后,会直接跳转到内核映像的入口点地址,启动内核代码的执行流程。

3.2.2.2、解压缩程序的初始化:

内核入口点代码(定义于 header.S)会首先完成 CPU 状态初始化(如设置运行模式)、内存环境配置等基础工作,随后跳转到解压代码的入口函数。其中,32位系统对应 startup_32 函数,64位系统对应 startup_64 函数。

ENTRY(startup_32)
// 设置 CPU 状态和内存环境
jmp decompress_kernel // 跳转到解压缩代码
3.2.2.3、解压缩代码执行:

解压代码的入口点定义于 arch/x86/boot/compressed/head_64.S,该代码会完成段寄存器配置、临时堆栈创建等解压环境准备工作,随后调用具体的解压函数入口。

ENTRY(decompress_kernel)
// 设置硬件环境
// 调用解压缩函数入口方法
jmp decompress_kernel_method
3.2.2.4、调用解压缩函数:

在 arch/x86/boot/compressed/misc.c 文件中,decompress_kernel 函数会根据内核映像的压缩格式,选择对应的解压算法(如 gzip 的 inflate 函数),并执行内核映像的解压操作。

void decompress_kernel(...) {
    // 选择解压算法
    // 调用相应的解压函数
    decompress_method(); // 调用特定的解压算法,如 inflate()
}
3.2.2.5、解压缩完成后跳转到内核入口:

内核映像解压完成后,解压代码会将解压后内核的入口地址(通常为 start_kernel 函数地址)加载到寄存器(如 x86 架构的 %eax 寄存器),通过跳转指令将控制权移交至解压后的内核代码,启动后续的内核初始化流程。

jmp *%eax // 跳转到解压后内核的入口地址

3.2.3、关键名词解释

3.2.3.1、跳转入口点和控制权转移

从技术实现角度,跳转入口点与控制权转移的核心逻辑一致,均通过修改程序计数器(PC,32位架构)或指令指针(IP,64位架构)的寄存器值实现。

程序计数器/指令指针是 CPU 的专用寄存器,用于存储当前正在执行指令的内存地址。CPU 执行完当前指令后,会自动将该寄存器值更新为下一条指令的地址,从而维持程序的连续执行。当需要跳转到新的入口点时,只需将新入口地址写入该寄存器,CPU 就会从新地址开始执行指令,实现执行流程的转移,即控制权的移交。

两者的表述侧重不同:“跳转到入口点”更强调执行流程的起始位置切换,“控制权转移”更侧重执行权限从一个代码上下文(如引导加载程序)到另一个上下文(如内核)的移交。在内核启动流程中,两者通常可互换使用。

3.2.3.2、initrd/initramfs 文件

加载压缩内核映像(vmlinuz)时,引导加载程序通常会同步加载 initrd(初始 RAM 盘)或 initramfs(初始 RAM 文件系统)。它们的核心作用是为内核启动初期提供临时根文件系统,协助内核完成最终根文件系统的挂载与初始化。

核心特性

  • 驱动支持:内核启动初期可能需要加载特定硬件驱动(如 SCSI 磁盘驱动、RAID 控制器驱动)才能访问根文件系统,而这些驱动可能未内置在内核中。initrd/initramfs 提供了临时文件系统,内核可从中加载所需驱动。
  • 复杂根文件系统适配:对于 LVM(逻辑卷管理)、RAID、加密文件系统等复杂存储配置,内核需在挂载实际根文件系统前执行额外初始化(如解密、逻辑卷激活),这些操作可通过 initrd/initramfs 中的脚本完成。
  • 通用内核适配:Linux 发行版通常提供通用内核以适配多种硬件配置,通过 initrd/initramfs 可在启动时动态加载对应硬件的驱动模块,无需为每种硬件单独编译内核。

加载与使用流程

  • 引导加载程序将内核映像与 initrd/initramfs 文件一同加载到内存,并将控制权移交内核。
  • 内核启动后,识别并挂载 initrd/initramfs 作为临时根文件系统。
  • 内核从临时根文件系统加载必要的驱动模块,执行初始化脚本。
  • 初始化脚本完成硬件配置后,挂载实际的根文件系统(如 /dev/sda1)。
  • 切换至实际根文件系统,释放 initrd/initramfs 占用的内存资源。

内核启动(start_kernel)

start_kernel 是 Linux 内核初始化流程的核心函数,被誉为“内核的起点”。它负责统筹内核各子系统、驱动程序及关键组件的初始化,最终完成从内核态到用户态的过渡,将控制权移交至用户空间进程。

3.1、start_kernel 方法介绍

3.1.1、第一个 C 函数的位置

start_kernel 函数的定义位于内核源码的 init/main.c 文件中,同时它也是 Linux 内核启动过程中执行的第一个 C 语言函数(此前执行的均为汇编代码)。

3.1.2、主要功能

  • 初始化内核基础环境:包括内存管理、进程调度等核心子系统的基础配置。
  • 初始化各功能子系统:涵盖文件系统、网络子系统、设备驱动框架等关键组件的初始化。
  • 启动首个用户进程:创建并启动系统中第一个用户空间进程(init 进程),完成内核态到用户态的切换。

3.2、start_kernel 源码解析

asmlinkage __visible void __init start_kernel(void)  
{  
    char *command_line;  
    extern const struct kernel_param __start___param[], __stop___param[];  

    /* ... 其他初始化代码 ... */  

    /* 设置页表和内存管理 */  
    paging_init();  
    mem_init();  
    kmem_cache_init();  

    /* 设备和驱动程序初始化 */  
    driver_init();  
    init_irq_proc();  
    softirq_init();  
    time_init();  
    console_init();  

    /* 文件系统初始化 */  
    vfs_caches_init_early();  
    mnt_init();  
    init_rootfs();  
    init_mount_tree();  

    /* 初始化进程 */  
    pid_cache_init();  
    proc_caches_init();  
    /* 启动 init 进程 */  
    rest_init();  

    /* ... 其他初始化代码 ... */  

    /* 调用内核参数解析函数 */  
    kernel_param_init(karg_strings, num_args);  

    /* ... 其他初始化代码 ... */  

    /* 永远不会返回 */  
    cpu_idle();  
}

启动初始进程(init process)

4.1、进程概念介绍

4.1.1、内核进程(Kernel Thread)和用户进程(User Process)

4.1.1.1、内核进程(Kernel Thread)

内核进程是由内核直接创建和调度的线程,运行于内核态(特权态),主要用于处理内核内部的后台任务。与用户进程不同,内核进程不直接与用户空间交互,其生命周期完全由内核管理。

  • 运行空间:位于内核地址空间,可直接访问内核所有数据结构与硬件资源。
  • 权限等级:拥有最高特权级,无需通过系统调用即可操作内核资源。
  • 核心作用:执行内核内部任务,如磁盘 I/O 调度、网络数据包处理、中断后续处理等。
  • 创建方式:通过内核提供的 kernel_thread 函数创建。

注意:内核进程是独立的任务实体,与后续提到的 0 号、1 号、2 号进程无直接衍生关系。

4.1.1.2、用户进程(User Process)

用户进程是在用户空间中执行的进程,用户通过编写和执行应用程序来创建用户进程。用户进程通过系统调用与内核交互,进行资源分配、文件操作、网络通信等。

  • 运行空间:用户进程运行在用户态,受限于用户空间的权限,不能直接访问硬件和内核数据结构。
  • 权限:内核线程运行在内核态,具有更高的权限,能够直接操作内核资源。

4.1.2、0号进程、1号进程、2号进程

Linux 系统中有三个核心进程,它们构成了整个系统进程树的根基,各自承担不同的核心职责:

  • 0号进程:内核进程,又称 swapper 进程、idle 进程(空闲进程),是系统中第一个进程,运行于内核态,负责在系统无其他可运行进程时占用 CPU,避免 CPU 闲置。
  • 1号进程:用户进程,又称 init 进程,是系统中第一个用户空间进程,由 0 号进程衍生而来,负责系统初始化及管理用户空间所有进程。
  • 2号进程:内核进程,又称 kthreadd 进程,由 0 号进程衍生而来,负责创建和管理其他内核线程,是内核线程的“父进程”。
4.1.2.1、0号进程(swapper/idle/空闲进程)

0 号进程是 Linux 系统启动过程中由内核初始化代码直接创建的首个进程,其 task_struct 结构体的 comm 字段为“swapper”。它的核心职责是维护系统空闲时的 CPU 运行状态,当系统中无其他可调度进程时,调度器会选择 0 号进程执行,其本质是一个无限循环的空闲任务。

0 号进程的创建过程发生在内核引导的汇编阶段(如 x86 架构的 arch/x86/kernel/head.S 文件),该阶段完成 CPU 与内存的基础配置后,跳转到 start_kernel 函数,后续通过 rest_init 函数衍生出 1 号和 2 号进程。

4.1.2.2、1号进程(init进程)和2号进程(kthreadd进程)
  • 1号进程和2号进程都是在 rest_init 函数中创建的
  • 1号进程通过 kernel_init 创建
  • 2号进程通过 kthreadd 创建

4.2、rest_init 函数-初始化入口

rest_init 函数是 start_kernel 函数执行过程中的关键函数,核心职责是创建 1 号和 2 号进程,完成内核态到用户态的过渡,其核心代码实现如下:

static noinline void __ref rest_init(void)
{
    // 通知RCU(Read-Copy Update)子系统,调度器即将开始。这是确保RCU在调度器开始运行前正确初始化的关键步骤。
    rcu_scheduler_starting();
    // 创建pid=1的1号进程
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    /** 处理1号进程相关代码 **/

    // 创建pid=2的2号进程
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    /** 处理2号进程相关代码 **/
    /** 其他初始化代码 **/
}

4.2.1、kernel_thread 函数

int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    return do_fork(flags | CLONE_VM | CLONE_UNTRACED, (unsigned long)fn,
                   (unsigned long)arg, NULL, NULL, 0);
}

核心参数说明:do_fork 函数通过复制当前进程(父进程)的上下文(如进程控制块、地址空间)创建新进程,其中 flags 参数控制进程创建的特性,fn 参数指定新进程的入口函数,arg 为入口函数的参数。

4.2.2、kernel_init 函数 - 初始化1号进程(init)

kernel_init 是 1 号进程的入口函数,核心职责是完成用户空间的初始化,启动系统首个用户空间进程(/sbin/init 等),代码实现如下:

static int __ref kernel_init(void *unused)
{
    /** 其他初始化代码 **/
    // 启动用户空间进程
    if (ramdisk_execute_command) {
        run_init_process(ramdisk_execute_command);
    } else if (execute_command) {
        run_init_process(execute_command);
    } else {
        run_init_process("/sbin/init");
    }

    return 0;
}

4.3、系统启动

1 号进程(init 进程)启动后,会通过一系列初始化操作,完成整个操作系统的启动,最终为用户提供可交互的系统环境。

4.3.1、系统初始化脚本

init 进程会读取系统预设的初始化配置文件,执行对应的初始化脚本:传统系统通常读取 /etc/inittab 配置文件及 /etc/init.d/ 目录下的启动脚本;采用 systemd 的现代系统则读取 /lib/systemd/system/、/etc/systemd/system/ 目录下的单元文件(unit files)。

这些初始化操作包括:设置系统环境变量、挂载各类文件系统(如 /tmp、/proc、/sys)、配置网络参数、启动日志服务等基础系统服务。

4.3.2、启动用户界面

完成基础系统服务启动后,init 进程会根据系统配置,启动用户交互界面相关服务,具体流程如下:

  • 启动图形登录管理器:若系统配置为图形界面模式,init 进程会启动图形登录管理器(如 GDM、LightDM、SDDM)。登录管理器负责提供图形化登录界面,接收用户输入的用户名和密码,完成身份验证。
  • 启动桌面环境:用户身份验证通过后,登录管理器会启动用户配置的桌面环境(如 GNOME、KDE、Xfce)。桌面环境提供图形化的用户交互界面,包括桌面图标、任务栏、文件管理器等组件,用户可通过桌面环境运行应用程序、管理系统资源。

4.3.3、图形界面启动流程(systemd 示例)

采用 systemd 作为 init 进程的现代 Linux 系统,图形界面启动流程如下:

  • systemd 启动后,读取自身配置文件,初始化核心系统服务(如系统时钟、内存管理、设备管理)。
  • 根据系统默认目标(target)配置,启动 graphical.target(图形界面目标),该目标包含了启动图形界面所需的所有依赖服务。
  • systemd 通过单元文件启动图形登录管理器服务(如 gdm.service)。
  • 登录管理器启动后,在显示器上展示图形登录界面,等待用户登录。
  • 用户输入用户名和密码并通过验证后,登录管理器启动用户会话,加载用户配置的桌面环境。
  • 桌面环境启动完成后,用户进入图形化交互界面,系统启动流程正式结束。

流程图总结

为清晰梳理内核启动的完整链路,以下用简化流程图概括核心流程:

电脑通电 → 加载 BIOS(硬件自检与初始化) → 引导加载程序(Bootloader)启动 → 加载压缩内核映像(vmlinuz)与 initrd/initramfs → 解压 vmlinuz → 执行 start_kernel 函数(内核初始化) → 启动 init 进程(1 号进程) → 执行系统初始化脚本 → 启动用户服务与图形界面 → 进入登录界面 → 系统启动完成

系统启动完整流程图




上一篇:Python接口设计思想:面向对象中协议与Duck Typing的哲学
下一篇:基于XIAO ESP32-S3的TinyML宠物表情图像识别项目全流程解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 08:51 , Processed in 0.208326 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表