缺页中断不是“出错”,是内核最忙的一条正常路径

缺页中断不是"出错",是内核最忙的一条正常路径

前两篇我们把地址翻译这条路走完了:地址是假的、按页翻译、查多级页表、TLB 兜底。中间反复出现一个词------缺页中断(page fault):CPU 拿着虚拟地址查页表,翻不出来,就停下当前指令、陷入内核。

但前两篇都把它当黑箱:要么段错误崩掉,要么内核"现场补一页"。补一页这三个字,藏了一整套内核里最高频的逻辑。这篇就把这个黑箱打开------内核接手之后,一步步判断了什么、按什么顺序、最后怎么补上那一页,或者怎么决定杀掉你。

先纠一个名字带来的误会。fault 听着像"出错",但你程序里绝大多数缺页是完全正常 的:malloc 完第一次写、程序第一次执行到某段代码、读一个 mmap 进来的文件------全靠缺页才把内存真正落地。缺页是内核最忙的正常路径之一,不是异常。真正"出错"的那种(段错误)只是这棵决策树最末端的一个分支。

本文所有数字真跑。环境是一台 Linux(aarch64,内核 6.12,4KB 页)。每段都给了命令,复制能复现。函数名都是 Linux 内核里真实的符号,你可以拿去 grep 源码。

一、内核接手的第一件事:这地址你到底有没有资格碰

CPU 翻页表失败、陷入内核,最先跑起来的是体系结构相关的入口函数------在 arm64 上叫 do_page_fault(x86 上是 do_user_addr_fault,干的事一样)。它手里有两样东西:出错的那个虚拟地址 ,和当时是想读、想写、还是想执行

内核的第一个问题不是"去哪儿弄这页内存",而是:这个地址,这个进程到底有没有资格碰?

判断的依据,就是上一篇看过的那张 /proc/self/maps。进程合法拥有的每一段地址区间,内核内部用一个叫 VMA (virtual memory area,虚拟内存区域)的东西记着------maps 里的每一行就是一个 VMA,记着这段区间的起止地址、权限(读/写/执行)、以及这段内存是什么 (匿名内存?还是映射了磁盘上哪个文件?)。do_page_fault 拿着出错地址,调用 find_vma 去这些区间里查:这个地址落在哪个 VMA 里?

为什么要有 VMA,页表不够吗? 不够,因为这俩记的不是一回事。回想上一篇:你 malloc 了 1GB,页表只长了 8KB------多级页表只为你真碰过的页建条目 。可那 1GB 里大部分页还没碰、页表里压根没有,它们却是合法的 :你随时去写,内核就该补页,而不是报错。内核凭什么知道"这片还没碰的地址是合法的"?光查页表查不到(没建条目),靠的就是 VMA------malloc 那一刻内核记了一个 VMA"这段归你、可读写",但一个物理页都没分。所以页表记的是"已经兑现的翻译",VMA 记的是"承诺过的资格"。缺页处理第一步的安检,查的正是这份"承诺清单",它才能区分"合法但还没碰"和"非法瞎访问"------这两种在页表里长得一模一样(都查不到)。

css 复制代码
   出错地址 ──► find_vma 在进程所有 VMA 里查它落在哪个区间

   /proc/self/maps(每行 = 一个 VMA,末尾那几位是权限)
   aaaaca4d0000-aaaaca4d1000 r-xp   ← 代码段:可读可执行,不可写
   aaaaca4f0000-aaaaca4f1000 rw-p   ← 数据段:可读可写
   aaaaf432c000-aaaaf434d000 rw-p   [heap]   ← 堆
   ffffb6760000-ffffb68fc000 r-xp   libc     ← 库代码
   ...
   ffff...           rw-p   [stack]  ← 栈

   查的结果分两种:根本没有哪个 VMA 罩住它  /  有,但权限不符

这一步直接岔出这棵树最容易理解的两个"失败"分支。

分支①:没有任何 VMA 罩住这个地址 → 这地址你从没申请过 → 段错误。

随手编个地址去读,最干脆:

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

// 信号处理器里不能用 printf(它不是 async-signal-safe),用 write 直接写
void handler(int sig, siginfo_t *si, void *ctx){
    char buf[128];
    int n = snprintf(buf, sizeof buf,
        "收到 SIGSEGV: 内核查遍所有 VMA 都没找到地址 %p, 判定非法访问\n", si->si_addr);
    write(1, buf, n);
    _exit(139);
}

int main(void){
    setvbuf(stdout, NULL, _IONBF, 0);          // 关缓冲,保证崩前的打印能出来
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);

    int *wild = (int*)0x1234;                  // 一个从没映射过的地址
    printf("准备读 %p ...\n", (void*)wild);
    volatile int x = *wild;                    // 缺页 -> find_vma 查无此 VMA -> SIGSEGV
    printf("不该到这 %d\n", x);
    return 0;
}
bash 复制代码
gcc -O0 segv.c -o segv && ./segv; echo "退出码=$?"

跑起来:

yaml 复制代码
准备读 0x1234 ...
收到 SIGSEGV: 内核查遍所有 VMA 都没找到地址 0x1234, 判定非法访问
退出码=139

find_vma 查遍所有 VMA,没有一个区间罩得住 0x1234,内核的结论是"你从没申请过这块地方",给进程发 SIGSEGV------就是我们熟悉的段错误(退出码 139 = 128 + 信号 11)。注意 si_addr:内核把到底是哪个地址出的事精确告诉了你,这正是它第一步查的东西。

分支②:有 VMA,但你干的事它不允许 → 保护错误。

这个更微妙:地址是合法的,但你的操作越权了。比如对一段只读内存动手写:

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

void handler(int sig, siginfo_t *si, void *ctx){
    char b[160];
    int n = snprintf(b, sizeof b,
        "收到 SIGSEGV: 地址 %p 在 VMA 里(合法),但那段是只读的,写它=越权 -> 保护错误\n",
        si->si_addr);
    write(1, b, n);
    _exit(139);
}

int main(void){
    setvbuf(stdout, NULL, _IONBF, 0);
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);

    long page = sysconf(_SC_PAGESIZE);
    char *p = mmap(NULL, page, PROT_READ,                  // 只读映射
                   MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    printf("读它没问题: 第一个字节=%d\n", p[0]);            // 读 -> OK
    printf("现在去写它 %p ...\n", (void*)p);
    p[0] = 1;                                              // 写 -> 越权 -> 保护错误
    return 0;
}
bash 复制代码
gcc -O0 prot.c -o prot && ./prot; echo "退出码=$?"
makefile 复制代码
读它没问题: 第一个字节=0
现在去写它 0xffffabd19000 ...
收到 SIGSEGV: 地址 0xffffabd19000 在 VMA 里(合法),但那段是只读的,写它=越权 -> 保护错误
退出码=139

这次地址有 VMA 罩着(读它毫无问题),但那个 VMA 的权限是只读。内核拿"你想干什么"(写)去对 VMA 的权限(只读)一比,不符,照样 SIGSEGV。这就是为什么改一个字符串常量、往代码段写东西会崩------不是地址不存在,是权限这一关没过。

到这里能看出内核的做事顺序了:它先安检,再办事 。第一步跟"去哪儿弄这页内存"毫无关系,纯粹在问"你有没有资格碰这里、能不能这么碰"。两道关------有没有 VMA、权限符不符------任何一道没过,直接 SIGSEGV,连后面都不用走。我们平时眼里"一种"的段错误,在内核这儿其实是两个不同的失败分支。

安检过了,才进入真正有意思的部分。

二、安检通过:那这页内存,到底该从哪儿来

地址合法、操作也合规,内核才开始办正事。这一步进入和体系结构无关的核心函数 handle_mm_fault,它再往下走到 handle_pte_fault------名字里的 pte 就是上一篇那个第 4 级页表里、真正记录页框号的表项。

handle_pte_fault 要决定的是:这一页内容该从哪儿来? 答案不止一种,它根据"这个 VMA 是什么类型、页表项当前是什么状态"分出几个分支,每个分支由一个专门的函数处理:

scss 复制代码
   handle_pte_fault 看 VMA 类型 + 页表项状态,分派给不同处理函数:

   ┌─ 匿名内存(malloc/栈/堆),页表项是空的
   │     → do_anonymous_page:要么给共享零页(只读),要么清零一张新物理页
   │
   ├─ 文件映射(mmap了文件 / 程序代码段),页不在内存
   │     → do_fault / filemap_fault:去文件(磁盘或page cache)把这页读进来
   │
   ├─ 页被换出去过(swap),页表项记着它在交换区的位置
   │     → do_swap_page:从磁盘交换区把它换回内存
   │
   └─ 页在内存、但只读,而你要写(典型=fork后的共享页)
         → do_wp_page:写时复制,复制一份新页给你写

这四个分支,正好对应你程序里几乎所有"正常缺页"的来源。下面逐个用真实数据砸实------重点看一个贯穿全篇的区分:minor fault (次缺页,没碰磁盘)和 major fault(主缺页,真去磁盘转了一趟)。这两个数 Linux 一直在替你数,下面先说清楚怎么自己把它们看出来------后面每一节的数字都是这么读出来的,不是我空口说的。

这些缺页数,你怎么自己看到

正文后面每一节都会甩出一个缺页数(16384、32、4096......),它们都来自同一个地方:Linux 为每个进程实时累计着 minor / major fault 的次数。看它有两条路,由浅入深:

最省事------不用改代码:/usr/bin/time -v 在任何程序前面挂上它跑,结束后它把整个进程的缺页数打出来:

bash 复制代码
/usr/bin/time -v ./prog
yaml 复制代码
	Minor (reclaiming a frame) page faults: 16468
	Major (requiring I/O) page faults: 0
	Maximum resident set size (kbytes): 66728

(注意是 /usr/bin/time -v,不是 shell 内置的 time------得写全路径。装 perf 的话,perf stat -e minor-faults,major-faults ./prog 也行。)

最精确------程序里读 getrusage 上面那个法子数的是"整个进程从头到尾"的缺页,掺了启动、库加载的开销(所以是 16468 不是齐整的 16384)。想精确测"某一段代码 触发了多少缺页",就在那段代码前后各读一次 getrusage,相减:

c 复制代码
#include <sys/resource.h>

struct rusage r0, r1;
getrusage(RUSAGE_SELF, &r0);          // 测量区间------前

/* ... 想测的那段代码 ... */

getrusage(RUSAGE_SELF, &r1);          // 测量区间------后
printf("这段代码触发: minor=%ld  major=%ld\n",
       r1.ru_minflt - r0.ru_minflt,   // 没碰磁盘的缺页
       r1.ru_majflt - r0.ru_majflt);  // 真去磁盘的缺页

正文后面所有数字,用的都是这个"前后相减取增量"的办法------所以它们干净,正好等于那段循环碰过的页数。(还有第三条路,连 C 都不用写:/proc/<pid>/stat 这个文件的第 10、12 个字段就是该进程的 minflt / majflt,cat 出来就能看。)

下面进入四个分支,每个都用这套办法把数读给你看。

三、匿名页:内核就地造一页,不碰磁盘

malloc 来的内存、栈、堆,都是匿名内存 ------它不对应磁盘上任何文件,内容凭空产生。第一次访问它触发缺页,走 do_anonymous_page

这里内核分读和写两种情况,真正的区别不是缺页次数 ,而是缺页后内核有没有分配真实物理内存。下面的代码把两种都测,用 RSS(进程真正占用的物理内存)来揭示差别:

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <unistd.h>

int main(void){
    long page = sysconf(_SC_PAGESIZE);
    size_t N = 64L*1024*1024;              // 64MB = 16384 个 4KB 页
    struct rusage r0, r1;

    // ---- 第一次只"读" ----
    char *z = mmap(NULL, N, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    madvise(z, N, MADV_NOHUGEPAGE);
    long rss_before = r0.ru_maxrss;
    getrusage(RUSAGE_SELF, &r0); rss_before = r0.ru_maxrss;
    long sum = 0;
    for (size_t i = 0; i < N; i += page) sum += z[i];
    getrusage(RUSAGE_SELF, &r1);
    printf("匿名页只读: minor=%ld major=%ld  RSS增长=%ld kB  内容和=%ld\n",
           r1.ru_minflt-r0.ru_minflt, r1.ru_majflt-r0.ru_majflt,
           r1.ru_maxrss - rss_before, sum);

    // ---- 另开一块,第一次"写" ----
    char *p = mmap(NULL, N, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    madvise(p, N, MADV_NOHUGEPAGE);
    getrusage(RUSAGE_SELF, &r0); rss_before = r0.ru_maxrss;
    for (size_t i = 0; i < N; i += page) p[i] = 1;
    getrusage(RUSAGE_SELF, &r1);
    printf("匿名页写  : minor=%ld major=%ld  RSS增长=%ld kB\n",
           r1.ru_minflt-r0.ru_minflt, r1.ru_majflt-r0.ru_majflt,
           r1.ru_maxrss - rss_before);
    printf("(64MB = %ld 页 = %ld kB)\n", (long)(N/page), (long)(N/1024));
    return 0;
}
bash 复制代码
gcc -O0 anon.c -o anon && ./anon
ini 复制代码
匿名页只读: minor=16384 major=0  RSS增长=0 kB      内容和=0
匿名页写  : minor=16384 major=0  RSS增长=65400 kB
(64MB = 16384 页 = 65536 kB)

读和写的缺页次数 都是 16384------每个页第一次碰都要陷内核一趟,逃不掉。真正的差别在 RSS:只读 RSS 增长 0 ,内核没分配任何物理页,而是让所有只读的页表项都指向一张全局共享的只读"零页"(内容保证全 0,所以 sum=0)。写 RSS 增长 65400kB ≈ 64MB,每一页都分了真物理页。

内核的逻辑是:你只是读、而且匿名内存保证是 0,那就让所有人共享同一张零页,不浪费物理内存;等你哪天真写,再给你一张独属于你的新页。

四、文件页:这页在磁盘上,得真去取------major fault 登场

如果你 mmap 的是一个文件(或者程序代码段------它本质就是可执行文件被映射进来的),那这页的内容在磁盘上 。第一次访问、内容还没进内存,走 do_fault / filemap_fault:内核得真去磁盘(或者文件在 page cache 里的副本)把这页读进来。

读磁盘是缺页里最贵的操作,所以它单独算一类------major fault。下面的代码造一个 64MB 文件、清掉内存缓存逼内核真读盘,并关掉预读(否则内核顺手读一大片,把大多数缺页变成 minor):

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <unistd.h>

int main(void){
    long page = sysconf(_SC_PAGESIZE);
    size_t N = 64L*1024*1024;
    struct rusage r0, r1;

    // 造一个 64MB 文件(每字节填 7,方便校验)
    int fd = open("/tmp/big.bin", O_RDWR|O_CREAT|O_TRUNC, 0644);
    char *tmp = malloc(N); memset(tmp, 7, N);
    write(fd, tmp, N); free(tmp); fsync(fd);

    // 把文件内容从内存缓存里清掉,逼内核真去磁盘读
    sync();
    int dc = open("/proc/sys/vm/drop_caches", O_WRONLY);
    if (dc >= 0) { write(dc, "3", 1); close(dc); }      // 需要 root 权限

    char *p = mmap(NULL, N, PROT_READ, MAP_PRIVATE, fd, 0);
    madvise(p, N, MADV_RANDOM);                          // 关预读,一页一页来
    long sum = 0;
    getrusage(RUSAGE_SELF, &r0);
    for (size_t i = 0; i < N; i += page) sum += p[i];
    getrusage(RUSAGE_SELF, &r1);
    printf("文件页 64MB 逐页读: minor=%ld major=%ld  (共 %ld 页, 每页值=%ld)\n",
           r1.ru_minflt-r0.ru_minflt, r1.ru_majflt-r0.ru_majflt,
           (long)(N/page), sum/(long)(N/page));
    return 0;
}
bash 复制代码
gcc -O0 filepage.c -o filepage && ./filepage
ini 复制代码
文件页 64MB 逐页读: minor=0 major=16384  (共 16384 页, 每页值=7)

把它和第三节那行并排放,是这篇最干净的一组对照:

ini 复制代码
   同样 16384 个页,第一次访问:

   匿名页(malloc)写:   minor=16384   major=0       ← 内核就地造,不碰磁盘
   文件页(mmap文件)读: minor=0       major=16384   ← 每页都得去磁盘取

   差别全在 handle_pte_fault 那一步分派给了谁:
   do_anonymous_page(凭空造)  vs  filemap_fault(去磁盘取)

同样是"第一次碰 16384 个页、触发 16384 次缺页",一边 major=0、一边 major=16384,差了一整个数量级的代价。你的代码完全一样(都是循环逐页读),区别全在内核第二步------handle_pte_fault 判断"这页从哪来"时,匿名内存走了"凭空造一页",文件页走了"去磁盘取一页"。

这就是为什么 minor 和 major 要分开数:minor fault 是微秒级(内核内存里捣鼓一下),major fault 是毫秒级(等磁盘)。差三个数量级。你平时感觉"程序卡了一下",很多时候就是一串 major fault------内核在替你等磁盘 I/O。调性能时如果看到 major fault 居高不下,基本就是内存不够、数据反复从磁盘进出的信号。

五、写时复制:fork 之后那个谜,到这儿彻底闭环

还剩第四个分支 do_wp_page,wp = write protect。它处理的情况是:页在内存里、好好的,但被标成了只读,而你要写它。

最典型的来源就是 fork。上上篇我们见过 fork 的"同址两值"之谜,当时只说了结论(写时复制),现在能从缺页这一侧把它讲透了。

fork 出子进程时,内核没有真复制父进程的内存,而是让父子的页表指向同一批物理页、并把这些页全标成只读 。于是父子读这些页都正常(读不违反只读);可一旦有谁去 ,写只读页 → 触发缺页 → 走 do_wp_page:内核这才复制一份新物理页给写的那一方,把它的页表项改指到新页、权限恢复成可写,再让那条写指令重来。

每一个被写到的页,触发一次 COW 缺页。父进程先写满 16MB(4096 页),fork 后让子进程逐页改写:

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void){
    setvbuf(stdout, NULL, _IONBF, 0);
    long page = sysconf(_SC_PAGESIZE);
    size_t N = 16L*1024*1024;              // 16MB = 4096 页
    struct rusage r0, r1;

    char *p = malloc(N);
    for (size_t i = 0; i < N; i += page) p[i] = 1;   // 父:先写满,页已落地

    pid_t pid = fork();                    // fork:页被标只读、父子共享
    if (pid == 0) {
        getrusage(RUSAGE_SELF, &r0);
        for (size_t i = 0; i < N; i += page) p[i] = 2; // 子:逐页写 -> 每页一次 COW 缺页
        getrusage(RUSAGE_SELF, &r1);
        printf("[子] 写时复制触发的缺页 = %ld  (共 %ld 页)\n",
               r1.ru_minflt-r0.ru_minflt, (long)(N/page));
        _exit(0);
    }
    wait(NULL);
    return 0;
}
bash 复制代码
gcc -O0 cow.c -o cow && ./cow
scss 复制代码
[子] 写时复制触发的缺页 = 4096  (共 4096 页)

4096 个页,子进程写一遍,正好 4096 次缺页------一页一次,每一次就是 do_wp_page 现场复制了一张新页。

css 复制代码
   fork 后(共享,只读)              子写第 k 页 → do_wp_page
   父 ─┐                              父 ─► 物理页A (原值)
       ├─► 物理页A (只读,共享)
   子 ─┘                              子 ─► 物理页B (复制出来的新页,可写)
                                            ↑ 这一次 COW 缺页干的事
   读都不缺页;谁写谁触发一次缺页,分一张私有页给它

上上篇那个"同址两值"到这里彻底闭环:父子虚拟地址一直没变,变的是子进程那次写触发了 do_wp_page,把它的页表项从共享只读页,掉包成了一张私有的新页。

还有第五种没细讲:页被换出到磁盘交换区 (swap)过,页表项里记着它在交换区的位置,再次访问触发缺页走 do_swap_page,把它从磁盘换回内存------这也是一次 major fault。swap 本身够再写一篇,这里只在决策树上给它留个位置。

六、补完那一页,最后一步:回到出错的指令重来

不管走的是哪个分支------清零造页、从磁盘取、复制新页------内核做完之后都干同一件事:把这一页的映射填进页表,然后让 CPU 回到当初出错的那条指令,重新执行一次。

这次再查页表,映射有了,翻译通过,指令顺利跑完。整个缺页处理对你的程序是透明 的:你那行 p[i] = 1 不知道自己中途陷进过内核、内核忙活了一通、又被放回来重跑了一遍。它只是"执行了一次赋值"。

把整条路径连起来:

scss 复制代码
   p[i] = 1   (CPU 查页表失败,陷入内核)
        │
        ▼
   do_page_fault ── find_vma ──┬─ 没有 VMA ───────────► SIGSEGV(段错误)
        (第一步:安检)        └─ 有 VMA,但权限不符 ──► SIGSEGV(保护错误)
        │ 合法
        ▼
   handle_mm_fault → handle_pte_fault ──┬─ do_anonymous_page  匿名页:清零造一页 (minor)
        (第二步:这页从哪来)           ├─ filemap_fault      文件页:去磁盘取   (major)
        │                               ├─ do_swap_page       换出过:从swap换回 (major)
        │                               └─ do_wp_page         只读被写:复制一页 (COW)
        ▼
   填好页表项 ──► 回到 p[i]=1 这条指令重新执行 ──► 这次翻译通过,跑完

把这篇收一下,其实就是一棵两层的决策树:

  • 第一层是安检do_page_fault 拿出错地址调 find_vma,问"这地址你有没有资格碰"。没有 VMA(地址不存在)或权限不符(越权操作),就是你熟悉的两种段错误。这一步跟"弄内存"毫无关系,纯粹在确认资格。
  • 第二层是"这页从哪来"handle_pte_fault 按 VMA 类型和页表项状态分派------匿名页凭空清零造(do_anonymous_page,minor)、文件页去磁盘取(filemap_fault,major)、换出过的从交换区换回(do_swap_page,major)、只读被写的复制一份(do_wp_page,COW)。你程序里几乎所有"正常"的内存落地,都是这一层在干活。
  • 最后填好页表、回到出错指令重跑,整个过程对你的代码完全透明。

这也把前面几篇连成了一个完整的故事:你打印出来的地址是假的(虚拟地址)→ 访问它要查多级页表、TLB 兜底 → 翻不出来就缺页、陷入内核 → 内核走这棵决策树,安检 + 补页 → 回到原指令重跑、这次翻译通过。从 &x 那一串数字,到内存条上真实的一格,中间这条路,到这篇算是走完了。

还欠一篇:物理内存真的不够用时,内核怎么挑"最不常用"的页踢到磁盘交换区、腾地方给新的缺页(swap 与页面回收)------也就是本篇 do_swap_page 那个分支的反面。那是另一棵树,下次再走。


相关推荐
小宇子2B2 小时前
内存不够时,内核怎么把"冷"页踢出去——swap 与页面回收
操作系统
磊 子3 小时前
二.内核讲解
开发语言·操作系统·系统
下午写HelloWorld17 小时前
Linux系统及Ubuntu常用指令
linux·ubuntu·操作系统
Surest18 小时前
AI时代操作系统过时了么?
操作系统
小宇子2B19 小时前
页表凭什么不撑爆内存,CPU 凭什么查得不嫌慢
操作系统
Surest1 天前
OpenHarmony 技术拆解(一):多内核兼容与硬件能力发布机制
操作系统
我命由我123451 天前
Windows 操作系统 - Windows 查看防火墙是否开启、Windows 查看防火墙放行端口
java·运维·开发语言·windows·java-ee·操作系统·运维开发
老王熬夜敲代码1 天前
CPU缓存的访问机制
操作系统·cpu
茶马古道的搬运工2 天前
Linux-Ubantu-贴士-建立Docker 沙盒(三)
操作系统