Linux虚拟内存与物理内存:从页表到OOM的完整排障指南

发布时间:2026/9/30 11:49:05
Linux虚拟内存与物理内存:从页表到OOM的完整排障指南 在Linux下面和内存打交道多了你会遇到一个特别迷惑的现象程序里打印出来的指针值动辄是0x7ffc...这种一长串十六进制数看着像物理内存条上的某个位置实际上它跟物理内存完全是两个世界——这是虚拟地址空间给每个进程造的“幻觉”。虚拟地址空间、物理内存、Linux 这三者的关系是我觉得每个做后端、做运维、做嵌入式开发的人都该彻底吃透的东西。这篇就围绕它们之间的映射、分配、回收和排障展开把你平时在top、free、/proc里看到的那些数字和内核背后做的事串起来讲一遍。适合被线上内存问题折磨过、又想真正弄懂底层原理的人读。1. 为什么程序拿到的地址是“假”的虚拟地址空间的由来与意义1.1 直接面对物理内存的日子在虚拟地址空间这套机制普及之前程序访问内存的方式非常原始地址总线是多少位CPU 就去访问对应编号的物理内存单元程序里写的是什么地址指令里访问的就是什么地址。这种模式下不存在什么“翻译”指针的值就是真实的内存条坐标。这种设计在单任务、单进程的简单系统里勉强能用但一旦系统里同时跑多个程序问题立刻暴露。最简单的场景程序 A 分配了一块内存地址是0x10000程序 B 可能也恰好想用0x10000。两个进程同时认为自己独占这块地址结果就是互相踩踏数据被改得面目全非。更要命的是如果程序里有个野指针错误地写向了0x0那一下可能直接把操作系统核心区域清空整个机器瞬间崩溃。早期的个人电脑上一个普通用户程序能把整个系统搞死这是真实发生过的情况。所以虚拟地址空间的第一个使命就是充当“隔离墙”。每个进程看到的地址都是从 0 开始、看起来连续的一大片空间互不干扰。CPU 和操作系统通过硬件 MMU 配合进程页表把每个进程的虚拟地址翻译成不同的物理地址。进程 A 虚拟地址0x10000对应的物理页和进程 B 虚拟地址0x10000对应的物理页可以完全是两码事。1.2 三个无法回避的问题隔离问题。没有虚拟地址空间一个进程就可以合法地读取和修改另一个进程的内存。现代系统里用户程序崩溃不该连累内核和其他进程这是基本的可用性要求。虚拟地址空间让每个进程只能访问自己的页表所映射的页面再加上特权级保护用户态代码根本没法触碰内核态内存。连续性问题。物理内存的分配是动态的系统跑得越久内存碎片化越严重。一个进程可能需要几百 MB 甚至几 GB 的连续内存但物理内存中根本找不到这么大块的连续空闲区域。虚拟地址空间巧妙地绕开了这个问题进程看到的虚拟地址是连续的但背后的物理页可以散布在内存条的任意角落。如果物理页 1、5、9、12 都空闲着那就把这四个页框分别映射到进程虚拟地址空间的四个连续页上逻辑上就是一段连续的 16KB 内存。对进程来说它完全不知道自己用的物理内存在物理上有多“分散”。扩容问题。没有虚拟地址空间时进程总内存不能超过物理内存的大小你不可能跑一个需要 1GB 内存的程序同时物理内存只有 512MB。虚拟地址空间让“超额分配”成为可能程序启动时可以先申请大量虚拟内存真正用多少再按需映射物理页。配合 swap 机制物理内存紧张时还能把暂时不用的数据换到磁盘上让程序看起来拥有远超物理内存的可用空间。1.3 MMU 和操作系统如何配合实现“隔离与抽象”整个机制依赖两个角色一个是硬件 MMU内存管理单元另一个是操作系统内核。MMU 负责在 CPU 每次访问内存时查找当前进程的页表把虚拟地址翻译成物理地址操作系统负责维护这些页表决定哪个虚拟页映射到哪个物理页框。进程切换时操作系统会切换对应的页表让 CPU 的 MMU 认到新的地址空间。也就是说同一个 CPU 上0 到 10 毫秒 A 进程把虚拟地址0x400000当成它的代码段10 到 20 毫秒 B 进程把虚拟地址0x400000当成它的数据段MMU 每次都通过不同的页表翻译两边完全不会冲突。有了这层抽象操作系统的调度、加载、动态库共享都变得简单。比如共享库文件可以在物理内存中只保留一份然后同时映射到几十个进程的虚拟地址空间里大家共享同一份代码页而不是每个进程都拷贝一份。这块映射关系就是后面我们常说的 page cache 和文件映射的基础。理解了这个大前提再看页表翻译就顺理成章了。2. 页表翻译虚拟地址怎么一步步找到物理页2.1 页与页框4KB 粒度带来的灵活性虚拟地址空间和物理内存都按固定大小的“页”来管理。Linux 默认使用 4KB 的页虚拟内存这一侧叫“虚拟页”物理内存这一侧叫“物理页框”。页表项PTE记录的就是虚拟页到物理页框的映射关系同时包含权限位是否可读、是否可写、是否可执行、当前是否驻留在物理内存中等。为什么选 4KB 作为默认粒度这是历史权衡的结果。页越小内存分配越精细内部碎片越少但页表项数量会急剧膨胀页越大页表更紧凑、TLB 能覆盖更多内存但分配和回收的颗粒度太粗容易浪费。Linux 后面也支持 2MB 和 1GB 大页。大页主要用于数据库、虚拟机这类内存占用大且行为可预测的场景但分配大页需要连续的物理内存有时候还得靠内存 compaction 才能腾出来不是想用就能用。2.2 算一笔账一级页表为什么不可行有人会想既然要维护虚拟地址到物理地址的映射那把整个虚拟地址空间的所有映射放在一张大表里不就行了算一下就知道不行。以 64 位系统常见的 48 位用户虚拟地址空间为例假设页大小 4KB那么虚拟地址空间有 2^48 / 2^12 2^36 个虚拟页。每个页表项在 64 位系统上通常占 8 字节一张覆盖全部地址空间的一级页表需要 2^36 × 8 字节 512GB。一个进程就要 512GB 的页表物理内存根本没这个可能。多级页表的思路是把地址空间切成目录层级每层只建立实际用到的部分。绝大多进程的虚拟地址空间是稀疏的堆、栈、代码段、共享库各自占一小块中间大片空洞根本不需要页表项。通过多级目录只有被访问到的“路径”才会分配目录页整体内存开销就能压到非常低。2.3 四级页表PGD 到 PTE 的完整翻译过程常见的 x86-64 架构使用四级页表PGD顶级目录、PUD、PMD、PTE页表项。如果开启了 57 位地址空间的 LA57 特性中间还会多一层 P4D这里我们先讲最常见的四级结构。一个 48 位虚拟地址被拆成这样地址段位宽作用bit 47:399 bitPGD 索引bit 38:309 bitPUD 索引bit 29:219 bitPMD 索引bit 20:129 bitPTE 索引bit 11:012 bit页内偏移翻译时先从 CPU 控制寄存器 CR3 拿到当前进程 PGD 页的物理基址用虚拟地址的 PGD 索引找到下一级目录页的地址再到下一级用 PUD 索引找到再下一级依次穿过四级最后 PTE 里存着物理页框号物理页框号加上页内偏移就是最终的物理地址。以虚拟地址0x00007f4a02b3c010为例页大小 4KB 时低 12 位0x010是页内偏移中间三段 9 位分别是 PMD、PUD 索引高 9 位是 PGD 索引。具体数值你可以用十六进制计算器逐段截取验证。一次普通内存访问在没有缓存的情况下硬件要走完这四级页表查询才能拿到物理地址。这代价相当高所以必须有 TLB 来救场。2.4 TLB 与页表缓存的性能担当TLB 是 MMU 内部的一块高速缓存专门缓存最近用过的“虚拟页号 → 物理页框号”翻译结果。命中 TLB 时一次内存访问不需要走四级页表代价极小没命中硬件才去内存里逐级查页表。TLB 最大的敌人是进程切换。切换进程后旧进程的 TLB 条目对应的是旧地址空间不能再用否则会发生严重的地址串扰。所以切换进程时传统实现要刷掉整个 TLB这也意味着新进程刚切换过来时TLB 基本都是冷的翻译命中率低运行速度会有可感知的下降。现代 CPU 提供了 PCID进程上下文标识符这类特性允许 TLB 同时保留多个进程的条目减少冲刷开销。高并发服务里每次请求都伴随进程/线程切换TLB 命中率对整体性能影响非常大。这里我多说一句调整线程数、接入层模型时如果你发现 CPU 占用高但业务量没涨可以关注一下系统层面的上下文切换和 TLB 开销。把线程数调到远超 CPU 核数有时候反而会因频繁切换而性能下降这不一定是业务代码的问题。3. Linux 的内存“魔法”懒分配、写时复制与换页3.1 申请内存不等于拿到物理内存很多 C/C 程序员第一次看到malloc(1GB)只花了几毫秒会觉得很神奇。其实malloc只是把一个虚拟地址区间标记为“这个进程可以使用”并没有真的去物理内存里划一块 1GB 的区域。真实的物理内存分配往往要等到进程真正去读写这块虚拟地址时才发生。这种机制叫“懒分配”lazy allocation。进程首次写入某个虚拟页时CPU 找不到对应的页表项触发缺页异常内核这才分配一个物理页框填好页表项然后让进程继续执行。好处显而易见程序启动、加载配置、申请大块缓冲区的速度都极快而且那些申请了但至始至终没被访问的内存永远不会消耗物理内存。判断进程到底触发过多少次缺页可以用/usr/bin/time -v ./your_program输出里会明确列出 Minor page faults 和 Major page faults。Minor fault 表示页已经在物理内存中比如共享库的页面只是当前进程还没建立映射Major fault 表示数据在磁盘上需要真正读盘代价要高得多。3.2 fork 的写时复制进程创建为何如此轻量用fork()创建子进程如果立刻复制父进程的全部物理内存那进程创建的成本会非常高。Linux 采用的方案是写时复制Copy-on-WriteCOWfork 时并不复制物理页只是让父子进程的页表都指向同一批物理页框然后把页表项标记为只读。之后只要父子进程都只读地访问这些页大家相安无事共享同一份物理内存。一旦某一方尝试写入CPU 触发写保护异常内核才临时复制一个物理页框把要写的那一页单独分配给触发写入的进程并恢复页表的写权限。这样就做到了“用多少复制多少不用不复制”。COW 也让虚拟内存和物理内存的统计变得有意思。比如 fork 之后ps里看到父进程和子进程的 RSS 都很大但这可能只是同一个物理页被同时算进了两个进程的 RSS实际物理内存消耗并没有翻倍。这些共享页包括代码段、动态库、fork 后没被修改的内存等。3.3 物理内存不够时换页与 swap物理内存紧张时内核需要把一部分页面腾出来给更需要的进程。可回收的页面有两类一类是 page cache也就是磁盘文件内容的缓存。如果是干净页直接丢弃就行下次要读再从磁盘读回来如果是脏页需要先写回磁盘。另一类是匿名页也就是进程通过malloc等分配的内存它们没有磁盘文件做后盾必须写到 swap 分区或 swapfile 里才能把物理页让出来。内核里有个kswapd内核线程负责这件事。当空闲物理页低于低水位线它就开始异步回收如果低于最低水位线内核会转为同步回收此时进程申请内存可能直接阻塞表现为系统突然“卡住”。这就是为什么内存压力大时你有时会看到 load 不高但机器响应缓慢。swappiness参数控制内核回收时对匿名页的倾向程度范围在 0 到 200 之间新内核默认 60。但它不是“开了 swap 就一定会用”更不是“内存还剩很多时就玩命换页”。默认配置下内核优先回收 page cache 来满足内存需求只有 cache 回收力度不够时才会去动匿名页。3.4 mmap 的另一个世界除了malloc/brk这类堆分配Linux 还有一种非常核心的映射方式mmap。它可以把一个文件区域映射进进程的虚拟地址空间进程读写这块虚拟内存就相当于读写文件对应区域。可执行文件加载、动态库加载、共享内存底层全都依赖mmap。mmap的典型优势是省内存。多个进程可以同时映射同一个大文件物理内存里只保留一份文件页大家共享。缺点是映射区域中的缺页会直接关联到磁盘 I/O如果程序随机访问一个很大的映射文件会产生大量缺页性能可能比预读式read还差。工程上做内存缓存、配置热加载经常用mmap但一定要清楚它的 I/O 特性和共享语义不能只看到“用起来像内存”的便利。4. 怎么观测虚拟内存与物理内存的真相4.1 /proc/meminfo系统级内存体检Linux 上第一眼看系统内存状态free -h是很多人的习惯但它只是展开了/proc/meminfo的一部分。要深入理解直接看下面几个字段字段含义排障时怎么用MemTotal物理内存总量确认机器规格MemAvailable估计可分配给新程序的量比 MemFree 更真实因为包含可回收的 page cacheMemFree完全空闲的物理页很小不一定代表内存不够CommitLimit基于 overcommit 设置计算出的虚拟内存承诺上限overcommit2 时严格限制Committed_AS系统全部进程已请求的虚拟内存总量和 CommitLimit 对比看是否逼近上限MemAvailable是我日常最看重的指标。它估算的是“在不触发明显 swap 的情况下还能给进程多少内存”内核会把可回收的 page cache 也算进可分配量。如果只看 MemFree你会在 cache 吃紧的机器上误判内存不足只看 MemTotal 减去已用内存又容易忽略 cache 在压力下可以被迅速回收的事实。4.2 VIRT / RSS / PSS / USS进程内存的四个视角top和ps里最容易被误解的两列是 VIRT 和 RES。VIRT 是进程虚拟地址空间总大小包括代码段、数据段、堆、栈以及所有mmap的共享库和文件。一个进程可能统计出几 GB 的 VIRT但真实物理内存占用没那么大。RES也就是 RSS驻留内存集表示进程当前实际在物理内存中的页的总和注意这里包含了共享的页面。光看 RSS 还不够因为它不会告诉你共享页有多少。更精细的视角有三个指标含义特点RSS进程驻留的物理页总数共享页重复计数PSS按共享比例分摊后的驻留内存更接近真实归属USS仅进程独占的物理内存不含任何共享页看私有个体最优看每个进程的真实内存占用我一般用cat /proc/PID/smaps | grep -E ^Pss:|^Size:|^Rss:把每个虚拟内存区域的 Pss 加一块才是这个进程实际应该背负的内存成本。共享库占的物理内存如果系统里有 50 个进程在用它PSS 会把这部分按比例分摊不会冤枉某一个进程。4.3 smaps 与 mincore把每一页都看清楚/proc/PID/smaps会按虚拟内存区域给出 Size、Rss、Pss、Shared_Clean、Private_Dirty 等明细。Private_Dirty 高通常意味着进程自己写脏了不可回写的匿名内存是评估内存成本的硬指标之一。Shared_Clean 高则说明大量共享库页一般不用太担心。如果想确认某个虚拟地址范围到底有多少页真的在物理内存中可以用mincore系统调用。它对给定地址区间返回每一页是否驻留内存。下面这段 C 代码可以实测#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/mman.h #define PAGE_SIZE 4096 int main() { size_t len 8 * 1024 * 1024; // 8MB unsigned char *p mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p MAP_FAILED) return 1; size_t pages len / PAGE_SIZE; unsigned char *vec calloc(pages, 1); size_t resident 0; if (mincore(p, len, vec) 0) { for (size_t i 0; i pages; i) if (vec[i] 0x01) resident; printf(after mmap, resident: %zu/%zu pages\n, resident, pages); } memset(p, 0xAA, len); memset(vec, 0, pages); if (mincore(p, len, vec) 0) { resident 0; for (size_t i 0; i pages; i) if (vec[i] 0x01) resident; printf(after memset, resident: %zu/%zu pages\n, resident, pages); } free(vec); munmap(p, len); return 0; }在内存不足的机器上这段程序可能触发 OOM小内存机器慎跑。但输出会非常直观mmap刚返回时驻留页极少memset之后几乎全部驻留。这就是虚拟地址空间和物理内存之间“按需分配”的直接证据。4.4 一个观察懒分配的小实验RSS 从 0 到 1GB再举一个更贴近日常的操作。写一个简单的 C 程序malloc(1GB)后先sleep一会儿然后写满这 1GB再sleep。分别在这两个阶段运行ps -o pid,vsz,rss,comm -p PID第一阶段VSZ 会显示大约 1GB但 RSS 可能只有几 MB第二阶段因为写满RSS 直接冲上去。很多人第一次看到这个对比会觉得“内存变了”其实物理内存从没提前分配过只是触发缺页后才真正被占用。这种机制对大型稀疏数据结构特别友好。如果你写程序时担心开一个大数组会浪费内存只要初始化为零再按需访问物理内存成本是可控的。5. 实战排障内存不足、OOM 和 swap 抖动5.1 缺页是入口minor fault 和 major fault 的差别所有虚拟内存转物理内存的过程都从缺页异常开始。一个进程运行期间会产生大量缺页关键在于它们是哪一类。Minor fault 的页面已经在物理内存中缺的只是映射处理极快Major fault 需要从磁盘读数据一次可能就是几十上百微秒的代价。判断进程是否在频繁打磁盘看两处。一是/usr/bin/time -v里的 Major page faults。二是动态观测perf stat -e page-faults,minor-faults,major-faults -p PID sleep 10如果 Major faults 一直不停增长说明程序的工作集远大于物理内存数据反复换入换出也就是俗称的抖动。此时加内存、优化访问局部性、调整数据结构布局比盲目调大缓存更有意义。5.2 OOM Killer 在杀掉谁当物理内存和 swap 全部耗尽内核会启动 OOM Killer选择一个或几个进程杀掉来释放内存。它依据每个进程的 oom_score 来排序进程使用物理内存RSS越多分数越高同时会考虑 oom_score_adj这个值可以由管理员或系统服务调整默认是 0范围从 -1000 到 1000。打分逻辑很朴素占用内存多的进程先死因为杀掉它释放最多。所以数据库、Java 应用这类吃内存大户往往是 OOM 时的第一候选。线上为了防止重要服务被误杀常见的做法是把关键进程的 oom_score_adj 调低比如echo -800 /proc/PID/oom_score_adj但这只是调整优先级不是免除。真要免除需要-1000此时进程基本不会进入 OOM Killer 的候选名单。排查 OOM 时第一件事是看内核日志dmesg | grep -i oom日志里会写明被选中进程的 PID、名字、消耗了多少内存、当时系统内存状态以及每个进程的 oom_score 排序。这比到处猜“为什么偏偏杀了我的进程”靠谱得多。5.3 swap 抖动的识别与处理swap 本身不是坏事它是内存压力的缓冲垫。坏的是 swap 抖动内存不够内核把进程的页换出去进程紧接着又要访问这些页于是再换进来反复打磁盘。快速识别抖动的方法vmstat 1观察 siswap in和 soswap out两列。如果这两列长时间不为 0且数值很大说明系统在持续换页。此时再看磁盘 I/O 的 await 和 CPU 的 wa 列通常也会同步飙升。处理思路分几步。第一步确认是不是真的有进程在快速增长内存用top -o %MEM找到元凶第二步如果是业务膨胀优先考虑通过 cgroup 限制把它隔离在一个受控的内存额度里第三步调整 swappiness。延迟敏感、内存模型固定的服务通常可以调低 swappiness 让内核尽量少换页但这只是缓解不是根治。最稳妥的方案还是让物理内存覆盖工作集或者把进程拆开、限流、削峰。5.4 cgroup 限制下的 OOM 盲区容器技术普及后很多人遇到过“容器明明没用多少内存却被 OOM 杀掉”的情况。这往往是因为 cgroup v2 的memory.max限制不仅统计进程的匿名页也统计 page cache。容器内读取文件、写日志都会把文件内容留在 page cache 里这部分物理内存会计入容器的内存用量。当 page cache 加上进程实际使用的内存超过限制时触发的是 cgroup 级 OOM通常表现为容器直接被杀死进程收不到任何失败提示日志里可能只有“Killed”或者直接重建。排查时必须看 cgroup 自己的统计cat /sys/fs/cgroup/container-id/memory.current cat /sys/fs/cgroup/container-id/memory.statmemory.stat里有anon、file、unevictable等分类。如果file很大基本可以判断是 page cache 吃掉了限制额度。处理方式包括限制容器内缓存类操作、定期清理无效缓存或者如果业务确实需要更多内存调整限制。始终记住一件事容器里的free显示的是宿主机内存不是 cgroup 给你的额度不能拿它来判断容器内存是否吃紧。6. 我踩过的几个坑和现在保持的习惯6.1 乱改 overcommit_memory 引发的灵异问题有一段时间我很担心系统内存超卖觉得应该严格限制把/proc/sys/vm/overcommit_memory设成了 2。改完没几天陆续有服务启动失败报错千奇百怪。有 Java 进程直接说无法分配内存有 Python 进程 import 库时报 MemoryError还有应用加载配置文件到一半崩溃。后来查下来根因都一样overcommit_memory2 时内核会按照 CommitLimit swap MemTotal × overcommit_ratio 来限制虚拟内存承诺总量。所有进程加起来的虚拟地址空间一旦超过这个上限malloc或者mmap就会失败。很多程序启动时会一次性预留大量虚拟地址比如 JVM 的堆、线程栈、JIT 代码区域这些地址通常并不会立即使用但已经消耗 Committed_AS。在严格模式下一碰上限程序根本起不来。从此我明白了虚拟地址空间超标不等于物理内存超标。限制超卖应该按业务场景和 cgroup 去做而不是粗暴地禁用 overcommit。后来我把它改回 0启发式系统立刻恢复正常。6.2 VIRT 涨不等于内存泄漏做过一次持续数周的稳定性观察发现有个服务的 VIRT 从 3GB 慢慢涨到 30GB监控告警直接打到“疑似内存泄漏”。我花了两天时间用 valgrind、ASAN 排查一无所获。最后把/proc/PID/smaps打开才发现绝大部分虚拟内存属于线程栈、JIT 代码缓存和若干带MAP_NORESERVE的预分配区。这些区域占的是虚拟地址空间物理内存 RSS 一直非常平稳。从那以后我看内存问题先分两层虚拟内存层面看 VSZ 和 Commit 相关指标物理内存层面看 RSS、PSS、cache 和 swap。如果 RSS 稳定、PSS 稳定业务也没有变慢那 VIRT 涨大多只是“虚拟地址分配策略”的问题不是真正的泄漏。6.3 我的内存排障命令组合被真实问题反复捶过之后我形成了一套固定的排查顺序。先看整体free -h vmstat 1 5再看个体ps -eo pid,comm,vsz,rss,cmd --sort-rss | head -20锁定了可疑进程看它的地址空间明细cat /proc/PID/smaps | awk /^Pss:/{sum$2} END{print sum}如果怀疑某个具体虚拟内存区域异常配合工具做熔断分析perf record -e page-faults -p PID perf report这套组合能覆盖从系统级到进程级、再到指令级的绝大部分内存问题定位路径。比起上来就猜按这条线走下来大多数问题都能在半小时内圈定范围。6.4 最后分享一个小技巧举个平时很容易踩的测试坑你想做内存占用实验但在一个长期运行的机器上page cache 可能虚高导致你测出来的“空闲内存”很不准确。实验前可以先落盘缓存再清理sync echo 1 /proc/sys/vm/drop_caches注意drop_caches清理的是 page cache不影响进程的匿名内存所以对业务进程没影响但不要在生产业务高峰期随手执行它会让磁盘读缓存失效瞬时拖慢 I/O 敏感服务。真要在生产做测试更推荐用 cgroup 建一个隔离环境让所有内存行为都限定在可控的范围内。跑 Linux 越久越觉得“虚拟地址空间”这四个字是所有内存问题的基础。你看到的地址不是物理地址你申请的内存不一定真的存在你统计的内存大小也不代表物理消耗。把这层逻辑想通了再看 top 里的 VIRT、free 里的 available、OOM 日志里的 score每一个数字都有它的意义。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询