核心本质:为什么要区分内核态 / 用户态?
1. 核心目标:安全与隔离
早期操作系统(如 dos)无态的区分,用户程序可直接操作硬件 / 修改内核内存,一个程序的错误(如误写内存)会直接导致整个系统崩溃。
linux 设计两种状态的核心目的:
- 权限隔离:将 “危险操作”(硬件访问、内存修改、进程调度)限定在内核态,用户程序只能在用户态执行普通逻辑;
- 故障隔离:用户态程序崩溃(如段错误)仅影响自身,不会波及内核和其他进程;
- 资源管控:内核统一管理硬件 / 内存资源,用户程序需通过 “合法接口”(系统调用)申请资源,避免资源滥用。
2. 本质定义
| 状态 | 核心描述 | 通俗比喻 |
|---|---|---|
| 用户态 | cpu 执行用户空间程序代码(如你的 c++/python 应用、shell 命令),处于低特权级,所有操作受内核限制 | 普通用户操作电脑,只能用软件,不能拆硬件 |
| 内核态 | cpu 执行内核空间代码(如系统调用、中断处理、进程调度),处于最高特权级,可直接操作所有硬件 / 内存 | 管理员操作电脑,可拆硬件、改系统配置 |
cpu 当前的执行态:内核态 / 用户态 的信息,实时存储在 cpu 的段寄存器(cs)的低 2 位(硬件层面);而进程的 “执行态快照”(比如进程上次被切换时的状态),则存储在内核的进程控制块(pcb)中。
底层支撑:cpu 特权级 & 内存地址空间
内核态 / 用户态的实现依赖两大硬件 / 系统机制:cpu 特权级 和 内存地址空间划分。
1. cpu 特权级(硬件层面的权限基础)
x86 架构的 cpu 设计了 4 个特权级(ring 0~3),linux 仅使用其中两个:
- ring 0:内核态,最高特权级,可执行所有 cpu 指令(包括特权指令,如操作内存管理单元 mmu、读写控制寄存器),访问所有硬件资源;
- ring 3:用户态,最低特权级,仅能执行普通指令(如加减运算、逻辑判断),禁止执行特权指令,禁止直接访问硬件。
为什么不用 ring1/ring2?linux 追求简洁,内核态(ring0)直接掌控所有核心资源,中间级别的特权级无实际价值,反而增加复杂度。
关键硬件寄存器(态的标识)
cpu 通过寄存器标记当前特权级:
- cs 寄存器(代码段寄存器):低 2 位表示 “请求特权级(cpl)”,0 = 内核态,3 = 用户态;
- cr0 寄存器:pe 位(保护模式位)开启后,cpu 才会启用特权级检查(linux 启动后始终开启)。
2. 内存地址空间划分(空间层面的隔离)
linux 为每个进程分配独立的 “虚拟地址空间”,该空间被划分为用户空间和内核空间两部分,且内核空间对所有进程共享。
(1)32 位 linux 地址空间(4gb 总容量)
| 区域 | 地址范围 | 所属状态 | 核心特征 |
|---|---|---|---|
| 用户空间 | 0~3gb | 用户态 | 每个进程独立(如进程 a 的 0x1000≠进程 b 的 0x1000) |
| 内核空间 | 3gb~4gb | 内核态 | 所有进程共享(映射到物理内存的同一区域) |
(2)64 位 linux 地址空间(实际可用 48 位地址,256tb 总容量)
| 区域 | 地址范围 | 所属状态 | 核心特征 |
|---|---|---|---|
| 用户空间 | 0~0x7fffffffffff(128tb) | 用户态 | 每个进程独立 |
| 内核空间 | 0xffff800000000000~...(128tb) | 内核态 | 所有进程共享 |
关键要点:
- 进程在用户态时,只能访问自身的用户空间,无法直接访问内核空间;
- 进程进入内核态后,可同时访问内核空间和当前进程的用户空间(内核需读取用户程序传递的参数时会用到);
- 内核空间是 “共享的”:所有进程的内核空间映射到物理内存的同一区域,保证内核代码 / 数据的唯一性。
内核态 vs 用户态:核心差异(全维度对比)
| 对比维度 | 用户态 | 内核态 |
|---|---|---|
| cpu 特权级 | ring 3(低特权) | ring 0(最高特权) |
| 可执行指令 | 仅普通指令(如算术运算、逻辑判断) | 所有指令(含特权指令:如操作 mmu、读写 cr 寄存器) |
| 地址访问范围 | 仅自身用户空间 | 内核空间 + 当前进程用户空间 |
| 硬件操作 | 禁止直接操作(如读写磁盘、网卡) | 可直接操作所有硬件 |
| 崩溃影响 | 仅进程自身崩溃(如段错误) | 可能导致系统宕机(如内核 panic) |
| 上下文类型 | 进程上下文(可被调度) | 进程上下文 / 中断上下文(中断上下文不可调度) |
| 内存分配 | 调用 malloc(依赖内核 brk/mmap) | 调用 kmalloc/__get_free_pages |
| 系统调用权限 | 只能发起调用,不能执行调用逻辑 | 执行系统调用的核心逻辑 |
| 信号处理 | 接收信号,在用户态执行处理函数 | 内核态完成信号的投递和触发 |
内核态 ↔ 用户态:切换机制(核心重点)
cpu 在两种状态间的切换是 “被动 / 主动触发” 的,切换过程需经过内核严格校验,且伴随 “现场保护 / 恢复”,是系统开销的重要来源。
1. 用户态 → 内核态(3 种触发方式)
这是 “用户程序请求内核服务” 或 “系统处理紧急事件” 的过程,核心是 “陷入内核(trap to kernel)”。
(1)主动触发:系统调用(最常见)
用户程序需要使用内核资源(如读写文件、创建进程)时,主动调用内核提供的 “接口”,触发态切换。
详细流程(以 64 位 linux 调用 read() 为例):
用户态准备参数:用户程序将 read 的参数(文件描述符、缓冲区地址、长度)存入寄存器(如 rdi、rsi、rdx);
触发系统调用:执行 syscall 指令(32 位是 int 0x80 中断),该指令会:
- 将当前 cpu 特权级从 ring3 改为 ring0;
- 保存用户态的现场(pc、sp、通用寄存器)到内核栈;
- 跳转到内核的系统调用入口(
sys_call_table);
内核执行逻辑:内核根据系统调用号(如 read 对应 0),从 sys_call_table 找到 sys_read 函数,执行读文件的核心逻辑(直接操作磁盘 / 缓冲区);
返回准备:内核将 read 的返回值存入寄存器,准备恢复用户态现场。
(2)被动触发:硬件中断(异步)
硬件完成工作后(如键盘按下、磁盘读写完成),触发中断控制器向 cpu 发信号,强制切换到内核态处理中断。
详细流程(以键盘输入为例):
- 硬件触发中断:键盘完成输入,向 io apic 发送中断请求(中断号 1);
- cpu 响应中断:cpu 暂停当前用户程序的执行,关中断,保护用户态现场(保存寄存器到内核栈);
- 切换到内核态:cpu 将特权级改为 ring0,根据中断号查中断向量表(idt),跳转到键盘中断处理程序;
- 内核处理中断:内核读取键盘输入数据,存入内核缓冲区,标记 “待处理”;
- 准备返回:恢复用户态现场,开中断,准备切回用户态。
(3)被动触发:异常(同步)
用户程序执行错误指令(如除 0、访问非法内存、缺页)时,cpu 触发 “异常”,强制切换到内核态处理。
详细流程(以缺页异常为例):
用户态触发异常:用户程序访问未映射到物理内存的虚拟地址,cpu 检测到后触发缺页异常(中断号 14);
切换到内核态:cpu 保护用户态现场,改为 ring0,跳转到内核的缺页异常处理函数 do_page_fault;
内核处理异常:
- 若地址合法:内核分配物理页,建立虚拟地址→物理地址映射,恢复用户程序执行;
- 若地址非法:内核向进程发送 sigsegv 信号,进程在用户态崩溃;
返回用户态:恢复现场,继续执行(或终止进程)。
2. 内核态 → 用户态(唯一核心场景)
内核完成工作后(系统调用执行完、中断 / 异常处理完),主动切回用户态,恢复用户程序的执行。
详细流程:
- 内核完成工作:系统调用 / 中断 / 异常的核心逻辑执行完毕;
- 恢复现场:从内核栈中取出之前保存的用户态现场(pc、sp、寄存器),写回 cpu 寄存器;
- 切换特权级:将 cpu 特权级从 ring0 改回 ring3;
- 执行返回指令:执行
sysret(64 位)/iret(32 位)指令,cpu 跳回用户态程序的下一条指令,继续执行。
3. 切换的核心开销
态切换的开销主要来自:
- 现场保护 / 恢复:读写 cpu 寄存器、内核栈;
- 特权级校验:cpu 检查权限、更新寄存器;
- 缓存刷新:用户态 / 内核态的地址空间切换可能导致 tlb(地址转换缓存)刷新。
优化思路:工作中减少不必要的系统调用(如批量读写文件,而非逐字节读写),可降低态切换开销,提升程序性能。
典型场景:不同操作的态分布
| 操作场景 | 状态切换流程 |
|---|---|
| 执行 printf("hello") | 用户态(拼接字符串)→ 内核态(sys_write 写标准输出)→ 用户态(继续执行) |
| 按 ctrl+c 终止进程 | 硬件中断(键盘)→ 内核态(处理中断,向进程发 sigint)→ 用户态(进程响应信号,终止) |
| 进程睡眠(sleep (1)) | 用户态(调用 sleep)→ 内核态(设置定时器,将进程置为睡眠态,调度其他进程)→ 时钟中断触发后,内核态唤醒进程 → 用户态(继续执行) |
| 访问非法内存 | 用户态(执行访问指令)→ 内核态(处理缺页异常,发 sigsegv)→ 用户态(进程崩溃) |
实战关联:工作中如何感知 / 优化态切换?
1. 查看进程的态开销
- top 命令:
%sys列表示 cpu 用于内核态的时间占比,若该值过高,说明系统调用 / 中断过多; - strace 命令:跟踪进程的系统调用(如
strace -c ls),查看调用次数 / 耗时,定位频繁的系统调用; - perf 工具:
perf trace -p <pid>实时跟踪进程的态切换和系统调用,分析开销来源。
2. 常见问题与优化
(1)% sys 过高(内核态 cpu 占比高)
原因:进程频繁调用系统调用(如逐字节 read/write)、硬件中断频繁(如高频网络包);
优化:
- 批量操作(如用缓冲区一次性读写大量数据,减少 read/write 调用次数);
- 优化中断频率(如网络网卡的中断合并);
- 用内存映射(mmap)替代 read/write,减少系统调用。
(2)用户态程序无法访问硬件
- 本质:用户态无硬件操作权限,需通过内核驱动提供的系统调用 / 设备文件间接访问;
- 解决:编写内核驱动,暴露设备文件(如
/dev/xxx),用户程序通过open/read/write访问。
(3)内核态崩溃(内核 panic)
- 现象:系统卡死,控制台打印
kernel panic - not syncing; - 原因:内核代码错误(如空指针、越界访问)、硬件故障;
- 排查:通过
dmesg、内核崩溃日志(/var/crash)定位错误代码行,修复内核模块 / 驱动。
关键补充:易混淆概念区分
1. 态切换 vs 进程切换
- 态切换:cpu 特权级的变化(ring3↔ring0),仅涉及一个进程的上下文保存 / 恢复;
- 进程切换:内核将 cpu 从一个进程切换到另一个进程,伴随态切换(通常是内核态下完成),需保存 / 恢复两个进程的上下文,开销更大。
2. 内核空间 vs 用户空间
- 内核空间:所有进程共享,仅内核态可直接访问,存储内核代码 / 数据、硬件驱动;
- 用户空间:每个进程独立,用户态仅能访问自身的用户空间,存储进程的代码 / 数据 / 栈。
3. 内核线程 vs 用户线程
- 内核线程:全程运行在内核态,无用户空间,用于执行内核任务(如 kworker、kswapd);
- 用户线程:大部分时间运行在用户态,仅需内核服务时切换到内核态。
核心总结
- 核心目的:内核态 / 用户态的区分是为了权限隔离,保护系统核心资源不被用户程序破坏;
- 底层支撑:cpu 特权级(ring0/ring3)实现权限控制,虚拟地址空间划分实现空间隔离;
- 切换触发:用户态→内核态靠 “系统调用(主动)、中断 / 异常(被动)”,内核态→用户态靠 “恢复现场 + 返回指令”;
- 实战价值:理解态切换能排查 “cpu 占比高、程序性能差、系统崩溃” 等问题,优化程序的系统调用频率可显著提升性能;
- 核心原则:linux 系统的运行本质是 “用户程序绝大部分时间在用户态执行,仅需内核资源时短暂切到内核态,用完即返回”。
简单来说:用户态是 “普通程序的安全区”,内核态是 “系统核心的管控区”,二者的切换是 “普通用户找管理员办事” 的过程,管理员(内核)会严格校验并代劳所有危险操作。
linux 用户级页表 & 内核级页表 深度讲解
简单来说:用户级页表是 “进程的私人地址簿”,每个进程一本,记录自己的虚拟地址对应哪块物理内存;内核级页表是 “系统的公共地址簿”,所有进程共享,记录内核的虚拟地址对应哪块物理内存,二者结合实现了 “进程独立运行,内核统一管控” 的目标。
- 本质区别:用户级页表是进程私有,映射用户空间,保证进程隔离;内核级页表是全局共享,映射内核空间,保证内核统一管控资源;
- 协同方式:每个进程的页表包含用户和内核两部分,进程切换时仅切换用户级页表,内核级页表共享,兼顾隔离性和效率;
- 权限控制:通过页表的 u/s 位,实现用户态只能访问用户空间,内核态可访问所有空间,保证系统安全;
- 实战价值:理解页表机制能排查段错误、内核崩溃、内存泄漏等问题,优化程序的内存访问效率,提升系统性能。
先铺垫:页表的核心作用(为什么需要页表?)
linux 采用虚拟内存机制,每个进程看到的是独立的「虚拟地址空间」(32 位 4gb/64 位 256tb),而实际数据存储在物理内存中。页表的核心作用是:
建立虚拟地址(va) 到物理地址(pa) 的映射关系,让 cpu 能通过虚拟地址找到对应的物理内存位置。
页表的实现依赖硬件(内存管理单元 mmu)和软件(内核页表管理)协同:cpu 执行指令时,mmu 自动查询页表,将虚拟地址转换为物理地址;内核负责维护页表的创建、修改和销毁。
用户级页表与内核级页表的核心定义
在 linux 中,每个进程拥有一套独立的页表,但这套页表分为两个逻辑部分:
| 页表类型 | 核心定义 | 所属范围 |
|---|---|---|
| 用户级页表 | 映射进程用户空间虚拟地址到物理地址的页表部分,每个进程独立存在,存储进程的代码、数据、栈等私有资源 | 进程私有(每个进程一套) |
| 内核级页表 | 映射内核空间虚拟地址到物理地址的页表部分,所有进程共享,存储内核代码、数据、硬件驱动等全局资源 | 系统全局(所有进程共享) |
关键澄清:不是 “每个进程有两个页表(用户 + 内核)”,而是每个进程的页表包含 “用户空间映射” 和 “内核空间映射” 两部分:
- 用户空间映射(用户级页表):每个进程不同,保证进程隔离;
- 内核空间映射(内核级页表):所有进程相同,保证内核空间全局共享。
用户级页表(进程私有)
1. 核心作用
- 映射用户空间:将进程的用户空间虚拟地址(32 位 0~3gb,64 位 0~128tb)映射到物理内存的私有区域;
- 保证进程隔离:每个进程的用户级页表独立,进程 a 的虚拟地址 0x1000 和进程 b 的 0x1000 会映射到不同的物理地址,互不干扰;
- 实现内存保护:通过页表权限位,限制用户态程序只能访问自身的用户空间,禁止访问内核空间和其他进程的用户空间。
2. 映射范围与内容
用户级页表仅映射进程的用户空间,包含以下核心内容:
| 用户空间区域 | 虚拟地址范围(64 位) | 映射内容 |
|---|---|---|
| 代码段 | 低地址区域 | 进程的可执行代码(如 c++ 编译后的二进制指令) |
| 数据段 / 堆 | 代码段上方 | 全局变量、动态分配的内存(malloc/new 分配) |
| 栈 | 高地址区域 | 函数栈帧、局部变量、函数参数 |
| 共享库 | 堆与栈之间 | 动态链接库(如 libc.so)的代码和数据 |
3. 权限控制(关键安全机制)
用户级页表的每个页表项(pte)包含权限位,核心控制用户态 / 内核态的访问权限:
- u/s 位(user/supervisor):设为 1 时,表示该页可在用户态访问;设为 0 时,仅能在内核态访问。用户级页表的所有页表项 u/s 位 = 1,保证用户态程序可访问自身用户空间;
- r/w 位(read/write):设为 1 时,该页可读写;设为 0 时,仅可读(用于代码段,防止程序误修改自身指令);
- p 位(present):设为 1 时,表示该页已映射到物理内存;设为 0 时,表示该页未映射(触发缺页异常)。
4. 生命周期管理
用户级页表与进程的生命周期绑定:
- 创建:进程创建时(
fork),内核为新进程复制父进程的用户级页表(写时复制 cow),后续进程修改内存时,内核会分配新的物理页并更新页表; - 修改:进程动态分配内存(
malloc)、加载共享库时,内核会修改用户级页表,添加新的虚拟地址→物理地址映射; - 销毁:进程退出时(
exit),内核销毁该进程的用户级页表,释放对应的物理内存。
5. 缺页异常处理
当进程访问未映射的虚拟地址(p 位 = 0)时,触发用户缺页异常,内核处理流程:
内核检查虚拟地址是否合法(是否在用户空间范围内);
若地址非法:向进程发送 sigsegv 信号,进程崩溃(段错误);
若地址合法:
- 分配物理页框;
- 更新用户级页表,建立虚拟地址→物理地址映射;
- 恢复进程执行,进程感知不到异常,继续运行。
内核级页表(全局共享)
1. 核心作用
- 映射内核空间:将内核空间虚拟地址(32 位 3gb~4gb,64 位 128tb~256tb)映射到物理内存的内核区域;
- 保证内核共享:所有进程的内核级页表完全相同,切换进程时无需修改内核空间映射,内核可快速访问全局资源;
- 支撑内核功能:映射内核代码、数据、硬件驱动、进程 pcb、页表等核心结构,保证内核正常运行。
2. 映射范围与内容
内核级页表映射内核空间,linux 内核空间分为多个功能区域(以 64 位为例):
| 内核空间区域 | 虚拟地址范围 | 映射内容 |
|---|---|---|
| 直接映射区 | 128tb~128tb + 物理内存大小 | 物理内存的直接映射(物理地址 x → 虚拟地址 = 128tb+x),用于访问物理内存的低端区域 |
| vmalloc 区 | 直接映射区上方 | 动态分配的内核内存(连续虚拟地址,物理地址可离散) |
| 永久映射区 | vmalloc 区上方 | 临时映射高端物理内存(如高端内存页) |
| 固定映射区 | 接近 128tb+2^48 | 内核固定用途的映射(如进程 pcb、页表) |
关键特性:直接映射区是内核最常用的区域,物理内存与虚拟地址一一对应,内核可直接通过虚拟地址访问物理内存,无需复杂计算。
3. 权限控制(内核专属)
内核级页表的页表项权限位与用户级相反,核心限制用户态访问:
- u/s 位 = 0:表示该页仅能在内核态访问,用户态程序无法直接访问内核空间的虚拟地址(若访问会触发页错误,导致段错误);
- r/w 位 = 1:内核空间通常可读写(代码段除外,设为只读);
- p 位 = 1:内核空间的核心代码和数据始终驻留物理内存(不会被换出到磁盘),保证内核随时可访问。
4. 生命周期管理
内核级页表是系统全局资源,生命周期与内核绑定:
- 创建:内核启动时(
start_kernel),初始化内核级页表,建立内核空间的映射; - 修改:内核加载驱动、动态分配内核内存(
kmalloc/vmalloc)时,内核会修改内核级页表,添加新的映射; - 销毁:仅当系统关机 / 重启时,内核级页表才会被销毁。
5. 缺页异常处理
内核级页表的缺页异常(内核缺页)比用户缺页更严重:
若内核访问未映射的内核空间地址,触发内核缺页异常;
内核会检查地址合法性:
- 若地址合法(如 vmalloc 分配的内存未建立映射):建立映射,恢复执行;
- 若地址非法:触发内核 panic(内核崩溃),系统宕机(内核是系统核心,无法像用户进程一样优雅退出)。
用户级页表与内核级页表的协同机制
1. 页表的物理结构(以 x86_64 为例)
linux 采用四级页表架构(页全局目录 pgd → 页上级目录 pud → 页中间目录 pmd → 页表项 pte),用户级和内核级页表共享同一套页表结构,只是映射的虚拟地址范围不同:
每个进程的 pgd(页全局目录)包含两个部分:
- 低地址条目:映射用户空间(0~128tb),即用户级页表;
- 高地址条目:映射内核空间(128tb~256tb),即内核级页表,所有进程的高地址条目完全相同,指向同一个内核页表结构。
2. 进程切换时的页表处理
进程切换是 linux 多任务的核心,页表切换的逻辑如下:
- 内核将当前进程的上下文(包括 cr3 寄存器的值)保存到 pcb;
- 加载新进程的 pcb,将新进程的 pgd 基址写入 cr3 寄存器(x86_64 中 cr3 存储当前页表的基址);
- mmu 刷新 tlb(地址转换缓存),清除旧进程的地址转换记录;
- 新进程开始执行。
关键优化:由于所有进程的内核级页表完全相同,切换进程时,仅需切换用户级页表的映射(cr3 指向新进程的 pgd),内核空间的映射无需改变 —— 内核仍可通过新进程的页表访问内核空间,保证了切换效率。
3. 态切换时的页表访问
- 用户态:cpu 只能访问用户级页表映射的用户空间(u/s=1),访问内核空间会触发页错误;
- 内核态:cpu 可同时访问用户级页表(当前进程的用户空间)和内核级页表(内核空间)—— 内核需要访问用户进程的数据时(如系统调用传递参数),可通过当前进程的用户级页表访问用户空间。
核心区别对比(用户级 vs 内核级页表)
| 对比维度 | 用户级页表 | 内核级页表 |
|---|---|---|
| 所属范围 | 进程私有(每个进程一套) | 系统全局(所有进程共享) |
| 映射空间 | 用户空间(0~128tb,64 位) | 内核空间(128tb~256tb,64 位) |
| 权限控制 | u/s=1(用户态可访问) | u/s=0(仅内核态可访问) |
| 生命周期 | 与进程绑定(创建→销毁) | 与内核绑定(启动→关机) |
| 缺页影响 | 用户缺页,进程可能崩溃(sigsegv) | 内核缺页,可能导致系统宕机(panic) |
| 内存回收 | 可被换出到磁盘(swap) | 核心代码 / 数据不会被换出 |
| 管理主体 | 进程 + 内核协同管理 | 内核独立管理 |
实战关联:工作中如何感知页表?
1. 查看进程的页表信息
/proc/<pid>/maps:查看进程的用户空间映射(用户级页表的内容),包括代码段、数据段、栈、共享库的虚拟地址范围和权限;
运行
cat /proc/1/maps # 查看init进程的用户空间映射
/proc/<pid>/pagemap:查看进程每个虚拟页的物理地址映射(需 root 权限),可分析内存使用情况;
perf record -e page-faults:跟踪进程的缺页异常,定位频繁缺页的代码路径(优化内存访问效率)。
2. 常见问题与页表的关联
(1)进程段错误(sigsegv)
- 原因之一:进程访问了未映射的用户空间地址(用户级页表 p 位 = 0),或访问了内核空间地址(用户态访问 u/s=0 的页);
- 排查:通过
gdb定位错误指令,结合/proc/<pid>/maps查看该地址是否属于用户空间的合法范围。
(2)内核 panic(内核崩溃)
- 原因之一:内核访问了未映射的内核空间地址(内核级页表 p 位 = 0),或非法修改了页表权限;
- 排查:查看内核崩溃日志(
/var/crash或dmesg),定位错误代码行,修复内核驱动或模块。
(3)内存泄漏
- 用户态内存泄漏:进程的用户级页表映射的物理页不断增加,通过
top查看res字段持续增长; - 内核态内存泄漏:内核级页表映射的物理页不断增加,通过
cat /proc/meminfo查看slab字段持续增长。
3. 性能优化与页表
- 减少缺页异常:批量分配内存(如用
mmap分配大块内存)、减少随机内存访问,降低页表的修改和缺页处理开销; - 优化 tlb 命中率:tlb 是页表的缓存,频繁切换进程会导致 tlb 刷新,降低命中率。通过绑定进程到 cpu(
taskset),减少 tlb 刷新,提升性能。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论