一、printf 隐藏的缓冲区printf函数并非直接把内容输出到屏幕而是默认将数据写入用户态缓冲区只有缓冲区刷新时内容才会真正输出。这也是很多新手遇到 “printf不打印” 的核心原因。1. 缓冲区的刷新规则触发条件说明遇到换行符\n行缓冲模式下遇到\n会自动刷新缓冲区这是最常见的刷新场景。程序正常结束main函数return时所有缓冲区会被强制刷新。缓冲区写满缓冲区空间不足时会自动刷新内容。主动调用fflush(stdout)手动强制刷新标准输出缓冲区。进程退出异常 / 正常进程退出时内核会自动刷新所有打开的流。2. 代码示例缓冲区不刷新的 “坑”#include stdio.h #include unistd.h // sleep函数 int main() { printf(Hello, World!); // 没有换行符 sleep(3); // 睡眠3秒 printf(\n); // 刷新缓冲区 return 0; }现象运行程序时程序会先睡眠 3 秒然后一次性输出Hello, World!并换行而不是先打印再睡眠。原因printf写入的内容先存在了缓冲区直到遇到\n或程序结束才刷新。3. 缓冲区与 fork 的经典问题#include stdio.h #include unistd.h int main() { printf(Before fork: ); // 无换行符 fork(); printf(After fork\n); return 0; }现象程序输出Before fork: After fork Before fork: After fork原因fork()会复制父进程的用户态内存包括缓冲区。父进程的Before fork:被留在缓冲区子进程复制了这份缓冲区两个进程退出时各自刷新导致重复输出。二、fork () 复制进程重点fork()是 Linux 创建新进程的系统调用它会创建一个和父进程几乎完全相同的子进程实现进程的复制。1. fork () 的基础用法#include stdio.h #include unistd.h #include sys/types.h int main() { pid_t pid fork(); if (pid -1) { perror(fork error); return 1; } else if (pid 0) { // 子进程执行的代码 printf(我是子进程pid: %d, ppid: %d\n, getpid(), getppid()); } else { // 父进程执行的代码 printf(我是父进程pid: %d, 子进程pid: %d\n, getpid(), pid); } return 0; }运行结果我是父进程pid: 1234, 子进程pid: 1235 我是子进程pid: 1235, ppid: 12342. fork () 的核心特点一次调用两次返回父进程返回子进程的 PID子进程返回 0失败返回 -1。父子进程是独立的进程有独立的进程 ID、地址空间执行流互不干扰。执行顺序不确定父子进程谁先运行由操作系统调度决定无固定顺序。三、fork () 补充知识点1. 父子进程共享与复制的资源资源类型处理方式代码段只读共享父子进程共用同一份物理内存数据段 / 堆 / 栈初始时共享修改时触发写时拷贝文件描述符复制一份引用计数 1指向同一个文件对象缓冲区复制父进程的用户态缓冲区解释了前面的重复输出问题2. 僵尸进程与孤儿进程僵尸进程子进程先退出父进程未调用wait()/waitpid()回收子进程资源子进程会变成僵尸进程占用进程号。孤儿进程父进程先退出子进程会被init进程pid1收养成为孤儿进程退出时会被自动回收。3. 如何避免僵尸进程#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { printf(子进程运行即将退出\n); return 0; } else { wait(NULL); // 父进程阻塞等待子进程退出并回收资源 printf(父进程回收了子进程\n); } return 0; }四、写时拷贝Copy-On-Write, COW技术fork()早期的实现会直接复制父进程的整个地址空间效率极低。现代 Linux 采用写时拷贝技术优化只有当进程需要修改数据时才会真正复制物理内存。1. 写时拷贝的原理fork()刚完成时子进程的虚拟地址空间和父进程映射到同一块物理内存且内存被标记为 “只读”。当父进程或子进程尝试修改这块内存时会触发页错误Page Fault。内核捕获错误后为修改方分配新的物理内存复制原数据修改页表映射关系之后再允许写入。2. 代码示例验证写时拷贝#include stdio.h #include unistd.h #include sys/types.h int main() { int val 100; pid_t pid fork(); if (pid 0) { // 子进程修改val printf(子进程修改前: val%d, val%p\n, val, val); val 200; printf(子进程修改后: val%d, val%p\n, val, val); } else { sleep(1); // 让子进程先运行 printf(父进程: val%d, val%p\n, val, val); } return 0; }运行结果子进程修改前: val100, val0x7ffdabcdef12 子进程修改后: val200, val0x7ffdabcdef12 父进程: val100, val0x7ffdabcdef12现象说明父子进程的val变量虚拟地址相同但值不同说明它们映射到了不同的物理内存。子进程修改时触发了写时拷贝分配了新的物理内存父进程的val不受影响。五、进程的逻辑地址与物理地址1. 概念区分逻辑地址虚拟地址进程代码中使用的地址比如val得到的地址每个进程都有独立的 4GB32 位或 256TB64 位虚拟地址空间。物理地址内存芯片上的实际地址由操作系统内核管理进程无法直接访问。2. 地址映射页表进程的虚拟地址通过页表映射到物理地址页表由操作系统维护CPU 的内存管理单元MMU负责地址转换。六、为什么程序中不直接使用物理地址安全隔离直接使用物理地址会导致进程可以访问任意内存破坏系统和其他进程的数据虚拟地址让进程互相隔离。内存管理虚拟地址让操作系统可以实现内存交换Swap、分页、写时拷贝等功能提高内存利用率。地址无关性程序编译时使用虚拟地址操作系统可以将程序加载到任意物理地址运行实现地址重定位。简化编程进程以为自己独占全部内存编程时无需关心物理内存的分配情况。