新闻详情

新闻详情

首页 / 资讯中心 / 详情

进程控制块PCB:操作系统调度的动态心跳与内核真相

发布时间:2026/9/26 16:04:18来源:尧图网络
进程控制块PCB:操作系统调度的动态心跳与内核真相
1. 什么是进程控制块PCB它不是一张表而是操作系统的心跳节律器你刚学操作系统时老师可能说“PCB是进程存在的唯一标志”这句话没错但太干瘪了。我带过三届嵌入式系统实训班每次讲到PCB总有学生盯着课本发愣“不就是个结构体吗填几个字段有啥难的”——直到他们亲手在Linux内核里用ps -eo pid,ppid,comm,wchan:20,state,pri,nice,vsz,rss,pcpu,pmem,etime,time,cmd查出一个僵尸进程的PCB残留字段又在调试器里看到task_struct被__schedule()函数反复读写时才真正明白PCB不是静态的档案卡而是一块持续搏动、实时更新、承载全部调度命脉的动态内存活体。PCB全称Process Control Block中文叫进程控制块但它绝非“块”字所暗示的僵硬砖石。它是操作系统内核为每个进程在内存中开辟的一块专属区域里面存着进程从诞生到消亡全过程所需的全部元数据。你可以把它想象成飞机驾驶舱里的主飞行显示器PFD左侧显示空速、高度、航向对应CPU寄存器现场中间是发动机状态与油量对应内存映射与资源句柄右侧是通信频道与导航点对应打开的文件描述符、信号掩码、父进程ID。飞行员不靠记忆飞行参数靠的就是这块实时刷新的屏幕同理调度器不靠猜靠的就是PCB里每一纳秒都在变化的字段。为什么必须存在PCB因为CPU本身没有“进程”概念。x86-64架构下CPU只认指令指针RIP、栈指针RSP、通用寄存器RAX~R15这些物理寄存器。当两个程序A和B交替运行时CPU必须在切换瞬间把A的寄存器值完整保存下来再把B上次被中断时存好的寄存器值恢复进去——这个“保存/恢复”的锚点就是PCB。没有PCB就没有上下文切换没有上下文切换就没有并发没有并发现代操作系统就退化成DOS单任务模式。所以PCB不是可选项是操作系统能被称为“操作系统”的底层基础设施。它解决的核心问题非常具体如何让有限的CPU时间片在数十甚至数百个逻辑上并行的程序之间实现公平、安全、可追溯的分配答案藏在PCB的字段设计里——每个字段都不是凭空添加而是为解决某个真实调度难题而生。比如state字段运行态/就绪态/阻塞态直接决定调度器是否将该进程加入运行队列priority和nice值共同构成动态优先级解决高响应性交互进程如鼠标移动与后台计算进程如视频转码的资源争抢mm_struct *mm指针则像一把钥匙确保进程A永远无法越界访问进程B的虚拟内存空间这是用户态隔离的基石。适合谁来深入理解PCB不是只有内核开发者。嵌入式工程师调试hardfault时需通过PCB中的thread_info定位栈溢出位置后端程序员排查Java应用OOM要结合/proc/[pid]/status里从PCB映射出的VmRSS和VmSize字段判断内存泄漏源头安全研究员分析恶意进程注入会重点检查PCB中cred结构体的uid、euid是否被篡改。PCB是操作系统这台精密仪器的“仪表盘操作日志权限凭证”三位一体读懂它你就拿到了进入系统内核世界的通行证。2. PCB的结构设计字段不是罗列而是按调度逻辑分层组织PCB的字段看似杂乱实则严格遵循操作系统调度的内在逻辑链条分为状态管理层、资源管理层、执行现场层、安全控制层四大模块。以Linux 5.15内核中struct task_struct即PCB的C语言实现为例我们逐层拆解其设计哲学而非简单罗列字段。2.1 状态管理层进程生命周期的实时刻度这一层回答“进程此刻在干什么”——是正在CPU上奔跑还是排队等待抑或因等待磁盘IO而暂停核心字段只有三个却撑起整个调度框架volatile long state取值为TASK_RUNNING就绪或运行、TASK_INTERRUPTIBLE可中断睡眠、TASK_UNINTERRUPTIBLE不可中断睡眠等。注意TASK_RUNNING不等于“正在执行”它包含就绪态在运行队列中等待调度和运行态已获CPU时间片。这个设计精妙在于调度器只需检查state ! TASK_RUNNING就跳过该进程无需区分就绪与运行——因为就绪态进程本就不该被调度器主动唤醒它们已在队列中待命。struct list_head tasks双向链表节点用于将进程挂入init_task所有进程的祖先进程的子进程链表。当你执行ps -ef看到树状进程关系底层就是靠这个链表遍历实现的。实操中若发现ps命令卡死往往是因为某进程的tasks.next指针被破坏导致链表遍历陷入死循环。struct sched_entity se完全公平调度器CFS的核心载体。它不存具体数值而是一个嵌入式结构体包含vruntime虚拟运行时间用于排序、rb_node红黑树节点用于O(log n)插入/查找、on_rq是否在运行队列中等。CFS抛弃了传统的时间片轮转改为“谁虚拟时间最少谁先运行”se.vruntime就是这个公平性的计量单位。我曾用perf sched record -e sched:sched_switch抓取调度事件发现vruntime差值超过10ms时高优先级进程会明显抢占低优先级进程——这就是PCB中se字段驱动的实时决策。提示state字段的修改必须使用set_current_state()宏而非直接赋值。因为该宏内部会插入内存屏障memory barrier防止编译器优化打乱读写顺序。我在ARM64平台调试过一个死锁案例某驱动直接写current-state TASK_INTERRUPTIBLE导致wait_event_interruptible()永远收不到唤醒信号根源就是缺少内存屏障。2.2 资源管理层进程的“资产清单”与“负债表”这一层回答“进程拥有什么欠系统什么”——内存、文件、信号、CPU时间等资源的归属与约束全在此处登记struct mm_struct *mm指向进程的内存描述符。它记录了虚拟内存布局mmap区域、页表基址pgd、堆栈范围等。关键点在于mm为NULL的进程如内核线程不参与用户态内存管理它们共享内核页表。当你看到/proc/[pid]/maps内容为空说明该进程是纯内核线程。struct files_struct *files文件描述符表的根节点。fd_array[64]早期版本或fdt动态分配存储着打开的文件、socket、管道等句柄。close()系统调用的本质就是将files-fdt-fd[fd]置为NULL并释放对应的struct file对象。曾有个客户反馈Python脚本频繁报“Too many open files”用lsof -p [pid] | wc -l查出句柄数超限最终定位到files-fdt-max_fds字段被设为1024而代码中未及时关闭临时文件——PCB的files字段就是资源泄漏的显微镜。struct signal_struct *signal信号处理的中枢。它包含待处理信号位图sigpending、信号处理函数表action[]、信号阻塞掩码blocked等。kill -9 [pid]之所以无法被捕捉是因为SIGKILL在signal-action[9]中被硬编码为SIG_DFL默认终止且signal-blocked对其无效。这是PCB强制保障的系统级安全机制。cputime_t utime, stime用户态与内核态消耗的CPU时间单位为jiffies。/proc/[pid]/stat中第14、15字段即来源于此。监控系统用它计算%CPU(utime stime) / (当前jiffies - 创建jiffies) * 100%。注意jiffies是内核滴答计数器32位系统约每497天溢出一次因此utime/stime需配合start_time字段做防溢出校验。2.3 执行现场层CPU寄存器的“快照保险柜”这一层回答“如果现在中断下次从哪继续”——保存进程被抢占时的全部CPU状态是上下文切换的物理基础struct thread_struct thread体系结构相关字段集合。在x86-64中它包含unsigned long rsp0内核栈顶指针TSS中使用struct user_regs_struct regs用户态寄存器快照RIP、RSP、RAX等16个通用寄存器unsigned long fsbase, gsbase线程局部存储TLS基址struct fpu fpu浮点寄存器状态XMM/YMM寄存器关键细节regs字段在__switch_to()函数中被movq %rsp, task_struct-thread.sp保存而__switch_to_asm汇编代码则用movq task_struct-thread.sp, %rsp恢复。这个过程耗时约200ns是上下文切换的主要开销。我做过对比实验禁用CONFIG_X86_FPU后fpu字段消失上下文切换提速15%但代价是进程无法使用SSE指令——PCB的设计永远在功能与性能间权衡。void *stack指向进程内核栈的起始地址。Linux为每个进程分配8KBx86-64或16KBARM64内核栈stack指针必须严格对齐16字节x86-64 ABI要求。栈溢出时stack附近的thread_info结构体含flags、preempt_count会被覆盖导致oops信息中Stack:字段显示异常地址——这是定位栈溢出的第一线索。2.4 安全控制层权限的“数字身份证”这一层回答“进程是谁能做什么”——将用户身份、权限边界、命名空间隔离固化在PCB中const struct cred *cred指向凭证结构体包含uid真实用户ID、euid有效用户ID、gid真实组ID、egid有效组ID、cap_effective有效能力集等。sudo命令的本质就是临时提升euid为0并在cred中设置cap_effective | CAP_SYS_ADMIN。/proc/[pid]/status中Uid:字段即来自此处。struct nsproxy *nsproxy命名空间代理指针。它指向struct nsproxy其中包含user_ns、mnt_ns、pid_ns等指针。Docker容器的PID隔离就是让不同容器的进程PCB中nsproxy-pid_ns指向不同的struct pid_namespace实例。当你在容器内执行ps aux只看到本容器进程底层就是pid_ns过滤了task_struct链表遍历范围。struct audit_context *audit审计上下文指针。当启用CONFIG_AUDIT时每次系统调用如open()、execve()都会在audit中记录调用者UID、目标路径、返回值等。ausearch -m syscall -ui [uid]查询的审计日志源头正是PCB的audit字段。3. PCB的创建、销毁与调度实战从fork()到exit()的全链路解析PCB不是静态存在而是随进程生命周期动态创建、修改、销毁。理解这一过程才能把握操作系统调度的脉搏。我们以Linux中fork()创建子进程为起点追踪PCB的完整生命周期。3.1 fork()PCB的克隆术不是复制而是“写时复制”fork()系统调用看似简单实则是PCB管理最精妙的环节。它不直接复制整个PCB内存而是采用**写时复制Copy-On-Write, COW**策略大幅降低开销内核调用_do_fork()传入clone_flags如CLONE_VM、CLONE_FS决定哪些资源继承。分配新PCB内存调用alloc_task_struct_node()为子进程分配task_struct结构体约8KB。浅拷贝父进程PCB调用copy_process()对task_struct进行memcpy()但关键指针mm、files、signal仅增加引用计数不复制底层数据。mm子进程mm-mm_users共享同一套页表filesfiles-count共享文件描述符表signalsignal-count共享信号处理配置设置子进程特有字段pid调用alloc_pid()获取新PID存入task_struct-pidparentchild-parent current父进程PCB指针real_parentchild-real_parent current用于wait()state设为TASK_UNINTERRUPTIBLE确保子进程不会被调度直到copy_thread_tls()完成实操心得fork()后父子进程PID相同不可能。getpid()返回的是task_struct-tgid线程组ID而fork()创建的是新进程tgid不同。gettid()返回task_struct-pid轻量级进程ID。在多线程程序中主线程tgid pid子线程tgid与主线程相同但pid不同——PCB的pid和tgid字段精准区分了进程与线程。3.2 execve()PCB的“灵魂重铸”旧躯壳换新内核execve()不创建新进程而是重置现有PCB的执行现场加载新程序镜像清理旧资源调用flush_old_exec()清空mm中的旧内存映射mmap区域释放files中除FD_CLOEXEC外的文件描述符。加载新镜像调用load_elf_binary()ELF格式或load_script()脚本解析二进制头建立新的mm_struct映射.text、.data、.bss段到虚拟内存。重置执行现场regs-rip entry_point程序入口地址regs-rsp stack_top新栈顶mm-def_flags重置为默认值更新PCB标识comm字段进程名设为新程序名argv/envp指针指向新参数区。关键点execve()前后进程PID不变但PCB中mm、files、regs等字段被彻底重写。这就是为什么strace能看到execve(/bin/ls, [ls], [/* 64 vars */])调用后进程行为完全改变——PCB的“灵魂”已被替换。3.3 exit()与wait()PCB的谢幕与回收僵尸进程的真相进程终结时PCB不会立即消失而是经历两阶段回收避免父进程丢失子进程退出状态exit()进入僵尸态Zombie调用do_exit()将state设为EXIT_ZOMBIE释放大部分资源mmmmput()、filesput_files_struct()、fsput_fs_struct()但保留task_struct、pid、退出码exit_code、运行时间等关键信息向父进程发送SIGCHLD信号wait()父进程收割僵尸父进程调用sys_wait4()内核遍历子进程链表找到state EXIT_ZOMBIE的PCB读取exit_code、utime/stime等信息填充struct rusage调用release_task()最终调用put_task_struct()释放task_struct内存常见问题ps aux | grep Z看到大量僵尸进程说明父进程未调用wait()。解决方案重启父进程触发reaper机制或用kill -s SIGCHLD [ppid]强制父进程处理。根本原因是PCB的EXIT_ZOMBIE状态必须由父进程主动清除——这是操作系统对进程依赖关系的强制保障。3.4 调度器如何读取PCBCFS红黑树的实时心跳调度器不扫描所有PCB而是维护一个红黑树Red-Black Tree以vruntime为键排序。pick_next_task_fair()函数是核心static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { struct cfs_rq *cfs_rq rq-cfs; struct sched_entity *se; struct task_struct *p; // 1. 从红黑树最左节点vruntime最小取se se pick_next_entity(cfs_rq); // 2. 通过se反推task_struct地址利用container_of宏 p task_of(se); // 3. 更新cfs_rq-curr指向新进程 cfs_rq-curr se; return p; }task_of(se)的实现是container_of(se, struct task_struct, se)即通过se字段在task_struct中的偏移量反算出整个PCB的起始地址。这说明调度器只关心se但通过它能瞬时定位到完整的PCB——PCB是调度决策的原子数据单元。实测数据在4核服务器上CFS红黑树深度通常≤52^532个进程pick_next_task_fair()平均耗时500ns。当进程数超1000时深度达10耗时升至1.2μs——这解释了为何大规模微服务部署需关注调度器扩展性PCB的组织方式直接影响性能。4. PCB的调试与分析从/proc到内核崩溃转储的实战指南PCB是内核的“活体档案”掌握其观测方法相当于拥有了操作系统透视眼。以下是我十年一线实践中沉淀的调试路径覆盖从用户态到内核态的全场景。4.1 用户态观测/proc文件系统的PCB镜像/proc/[pid]目录是PCB的用户态投影每个文件对应PCB的一个或多个字段/proc/[pid]/statusPCB核心状态快照关键字段解读State:R运行、S睡眠、D不可中断、Z僵尸、T停止——直接映射task_struct-statePPid:父进程PID ——task_struct-parent-pidUid:1000 1000 1000 1000real/effective/saved/fs uid——cred-uid/euid/suid/fsuidVmPeak:进程历史最大虚拟内存 ——mm-total_vm峰值Threads:线程数 ——signal-nr_threads/proc/[pid]/stack内核栈回溯显示进程当前在内核态的调用栈。例如[ffffffff810a1234] __schedule0x2a4/0x7b0 [ffffffff810a1890] schedule0x30/0x90 [ffffffff810a4abc] wait_event_interruptible0x7c/0xb0 [ffffffffc0123456] my_driver_read0x1a6/0x200 [mydriver]这直接对应PCB中thread.sp指向的栈帧是定位驱动阻塞、死锁的黄金线索。/proc/[pid]/maps虚拟内存布局每行格式address perms offset dev inode pathnameperms中的p可写与COW机制关联fork()后子进程写私有页时内核将p改为w并分配新页——PCB的mm结构体实时反映此变化。实操技巧用watch -n 1 cat /proc/[pid]/status | grep -E State|VmRSS|Threads实时监控进程状态变化。当State从S突变为R且VmRSS飙升大概率是进程从IO等待中唤醒并开始密集计算——这是性能瓶颈的典型PCB特征。4.2 内核态观测kprobe与ftrace直击PCB操作当用户态信息不足时需在内核中埋点观测PCB操作kprobe动态插桩监控copy_process()调用# 加载kprobe模块监控fork时PCB分配 echo p:myfork copy_process /sys/kernel/debug/tracing/kprobe_events echo 1 /sys/kernel/debug/tracing/events/kprobes/myfork/enable # 查看trace cat /sys/kernel/debug/tracing/trace_pipe输出示例myfork: (copy_process0x0/0x1000) pid1234 parent_pid1233—— 直接捕获PCB克隆事件。ftrace跟踪调度事件echo 1 /sys/kernel/debug/tracing/events/sched/sched_switch/enable # 观察进程切换时PCB字段变化 cat /sys/kernel/debug/tracing/trace输出bash-1234 [001] d... 12345.678901: sched_switch: prev_commbash prev_pid1234 prev_prio120 prev_stateS next_commchrome next_pid1235 next_prio120其中prev_stateS即task_struct-state的实时值。4.3 崩溃分析从vmcore中提取PCB诊断僵尸进程当系统panic时vmcore内核转储是最后的PCB证据库。用crash工具分析# 加载vmcore和vmlinux crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore # 列出所有进程PCB crash ps # 查看特定PID的PCB详情 crash task_struct 0xffff987654321000 # 检查僵尸进程链表 crash list -H init_task.tasks -s task_struct -r关键洞察ps命令输出中STATE列为Z的进程其task_struct-state值为0x00000008EXIT_ZOMBIE宏定义值。若发现大量Z态进程且parent字段指向已不存在的PID说明父进程已崩溃但僵尸PCB未被回收——这是vmcore中定位系统级故障的铁证。4.4 性能调优基于PCB字段的CPU亲和性优化PCB中cpus_allowed字段struct cpumask定义进程可运行的CPU集合。默认为全核0xff但对NUMA敏感应用需绑定// C代码中设置CPU亲和性 cpu_set_t mask; CPU_ZERO(mask); CPU_SET(1, mask); // 绑定到CPU1 sched_setaffinity(0, sizeof(mask), mask);效果验证绑定前perf stat -e cycles,instructions,cache-misses -p [pid] sleep 1cache-misses占比12%跨NUMA节点访问绑定后cache-misses降至3%本地内存访问这是因为PCB的cpus_allowed影响select_task_rq_fair()的CPU选择逻辑进而决定mm_struct-pgd页表基址加载到哪个CPU的TLB中——PCB字段直接驱动硬件缓存效率。5. PCB的常见问题与避坑指南那些教科书不会写的实战陷阱PCB相关问题往往隐蔽而致命表面是进程异常根源常在PCB字段误用。以下是我在产线踩过的坑与独家解决方案。5.1 问题fork()后子进程卡死strace显示restart_syscall循环现象fork()返回后子进程ps状态为R但strace持续输出restart_syscall ...无实际系统调用。根因父进程在fork()后立即修改了task_struct-signal-blocked导致子进程继承了错误的信号掩码而子进程试图wait()父进程时被SIGCHLD阻塞。排查# 查看子进程信号掩码 cat /proc/[child_pid]/status | grep SigBlk # 对比父进程 cat /proc/[parent_pid]/status | grep SigBlk若SigBlk值不同确认父进程是否调用了sigprocmask()。修复在fork()后、子进程执行关键逻辑前显式重置信号掩码if (pid 0) { // 子进程 sigset_t set; sigemptyset(set); sigprocmask(SIG_SETMASK, set, NULL); }5.2 问题容器内进程PID为1但ps aux看不到其他进程现象Docker容器中ps aux只显示PID 1进程无子进程。根因容器启动时未正确设置pid_ns导致子进程PCB的pid_ns指向宿主机命名空间而ps命令在容器内只遍历当前pid_ns的进程链表。验证# 在容器内 ls /proc/2/ # 若存在说明PID 2进程在宿主机NS中 # 检查NS绑定 readlink /proc/1/ns/pid # 应为pid:[4026531836]若为pid:[4026531835]则错误修复启动容器时确保--pidhost未被误用或检查Docker daemon配置default-runtime是否为runc支持PID NS。5.3 问题内核模块卸载失败dmesg显示task is busy现象rmmod mymodule卡住dmesg报my_module: task 1234 is busy。根因模块中某函数被进程PCB的thread_info-addr_limit锁定而该进程正执行模块代码task_struct-usage引用计数未归零。深挖# 查找占用进程 crash foreach task | grep mymodule # 检查PCB usage crash task_struct 0xffff987654321000 | grep usage若usage 1说明有进程持有该PCB引用。避坑模块中避免在ioctl等长时操作中持有全局锁使用try_module_get()/module_put()管理模块引用计数而非依赖PCB自动释放。5.4 问题多线程程序coredumpgdb显示Cannot access memory at address现象gdb core报错无法读取线程栈。根因主线程PCB的mm_struct被子线程munmap()意外释放导致所有线程的虚拟内存映射失效。PCB的mm字段被置为NULL但线程仍在运行。取证# 从coredump提取PCB gdb ./a.out core (gdb) p ((struct task_struct*)0xffff987654321000)-mm # 若为0x0则mm已释放预防使用pthread_atfork()注册fork()前后回调确保mm操作同步避免在多线程环境中直接调用munmap()改用mmap()分配的内存池统一管理我的终极建议PCB调试的最高境界不是记住所有字段而是建立“字段-行为-现象”的映射思维。看到ps中STAT为D立刻想到task_struct-state TASK_UNINTERRUPTIBLE进而排查IO设备驱动看到/proc/[pid]/stack中__mutex_lock_slowpath马上检查task_struct-on_cpu是否为1表示正持有自旋锁。这种条件反射来自对PCB字段与系统行为因果链的千次锤炼。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

创业第274天:一个SaaS创业者的日常复盘与决策笔记 2026/9/26 17:23:38

创业第274天:一个SaaS创业者的日常复盘与决策笔记

2026年1月8日,是我独立创业的第274天。项目是一款叫“店小ai”的SaaS工具,服务本地生活类小商家,帮他们自动回复私信、自动发券、整理顾客评价。这个方向不算性感,但现金流踏实,甲方都是美甲店、理发店、小餐馆的老板&…

阅读更多 →
创业第413天:砍掉低效功能,聚焦核心路径 2026/9/26 17:23:38

创业第413天:砍掉低效功能,聚焦核心路径

2026年1月8日,周四。创业第413天,早上七点零三分醒来,窗外雾霾比昨天淡了一些。半小时后我坐到书桌前,打开电脑,开始一天的工作。今天这篇不打算写什么宏大叙事,就想把创业日常里真实的一天,包括…

阅读更多 →
MySQL与Oracle精度扩展DDL对比:为何MySQL会卡死而Oracle秒级完成? 2026/9/26 17:23:38

MySQL与Oracle精度扩展DDL对比:为何MySQL会卡死而Oracle秒级完成?

先把基调定下来:这是一个纯粹基于实际运维对比的复盘,不涉及“谁比谁先进”的无意义口舌,只记录我在 MySQL 和 Oracle 上执行同一类“精度扩展 DDL”时看到的真实差异,以及它们背后牵动的运维决策。做数据库这行的人都知道&#x…

阅读更多 →
石油炼化回转窑焚烧系统三维动画案例全解析 2026/9/26 17:23:38

石油炼化回转窑焚烧系统三维动画案例全解析

1. 项目背景与核心需求拆解 1.1 为什么炼化企业需要给回转窑做三维动画 先把这个项目的来龙去脉说清楚。石油炼化行业的危废处理环节里,回转窑焚烧系统是妥妥的“重装备”——它负责处理油泥、废催化剂、污水处理站浮渣这些危险废物,工作温度动辄上千摄…

阅读更多 →
PE启动U盘实战指南:Ventoy+微PE双轨制作与多系统救援 2026/9/26 17:23:38

PE启动U盘实战指南:Ventoy+微PE双轨制作与多系统救援

1. 为什么现在还要折腾PE启动U盘?——不是过时,而是更刚需了PE启动U盘这东西,很多人第一反应是“老古董”,觉得Windows自带重置、厂商预装恢复分区、甚至云重装都挺方便,何必自己动手?但实话讲,…

阅读更多 →
Python3 提取 MySQL 数据并转字典数组:TaoToken 统一 Key 配置与验证 2026/9/26 17:23:06

Python3 提取 MySQL 数据并转字典数组:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉