欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 创投人物 > Linux错误(5)程序fork子进程后访问内存触发缺页中断(COW)

Linux错误(5)程序fork子进程后访问内存触发缺页中断(COW)

2025/3/13 7:45:48 来源:https://blog.csdn.net/Once_day/article/details/146215085  浏览:    关键词:Linux错误(5)程序fork子进程后访问内存触发缺页中断(COW)

Linux错误(5)程序fork子进程后访问内存触发缺页中断(COW)


Author: Once Day Date: 2025年3月12日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: Linux实践记录_Once_day的博客-CSDN博客

参考文章:

  • 监测 Linux 内存缺页中断事件 | 陈谭军的博客 | tanjunchen Blog

文章目录

  • Linux错误(5)程序fork子进程后访问内存触发缺页中断(COW)
        • 1. 问题分析
          • 1.1 现象介绍
          • 1.2 分析原因
          • 1.3 解决思路
          • 1.4 解决方法
          • 1.5 posix_spawn和vfork介绍
        • 2. 实例验证
          • 2.1 复现故障
          • 2.2 使用vfork替代fork
          • 2.3 使用posix_spawn
        • 3. 总结

1. 问题分析
1.1 现象介绍

在一个多线程程序中,使用 popen() 创建子进程后,系统出现了大量缺页中断(Page Fault),导致瞬间突发耗时(约 50ms)。由于 popen() 本质上调用了 fork(),而 fork() 在多线程环境下可能会触发写时拷贝(Copy-On-Write, COW),进而导致内存页复制,引发性能抖动。

在这里插入图片描述

1.2 分析原因

在多线程程序中调用 popen(),其内部会执行 fork() 创建子进程,而 fork() 后的子进程会继承父进程的地址空间(COW 机制)。如果在 fork() 之后,父进程或子进程修改了共享内存页,则会触发 COW,导致大规模页复制,从而引发缺页中断和性能下降。

(1)COW 触发大量页复制

  • fork() 之后,父子进程共享相同的页面,并标记为写时拷贝(COW)。
  • 如果父进程或子进程修改这些页面,就会触发COW 机制,导致内核分配新页面并复制数据,进而引发缺页异常和额外的 CPU/内存开销。

(2)多线程环境导致 fork() 继承大量页

  • 多线程程序的堆(heap)、栈(stack)等数据结构较为复杂,fork() 复制的页表较多,增加了COW 触发的概率。
  • malloc() 可能在 fork() 之前分配了大量内存,而 fork() 之后,glibcmalloc 可能会在子进程执行 exec() 之前触发COW。

(3)TLB(Translation Lookaside Buffer)失效

  • fork() 之后,子进程对共享的内存进行写操作,导致TLB 失效,进而影响性能。

(4)popen() 内部实现使用了 fork()

  • popen() 本质上是 fork() + exec() + pipe(),导致fork() 继承了父进程的所有地址空间,增加了COW 触发可能性。
1.3 解决思路

要减少 fork() 触发的COW 及缺页中断,可以从以下四个角度进行优化:

  • 避免 fork() 之后的内存写入,减少 COW 触发。
  • 使用 vfork() 代替 fork(),减少页表复制。
  • 使用 posix_spawn() 代替 fork()+exec(),避免 COW。
  • 优化 mallocmmap 行为,减少 fork() 继承的页面。
1.4 解决方法

(1)预防 COW 触发,减少 fork() 继承的内存

  • fork() 之前调用 madvise(MADV_DONTNEED)madvise() 可释放不必要的内存,减少 fork() 继承的页面,降低 COW 触发概率。

    void *ptr = malloc(1024 * 1024); // 分配 1MB 内存
    madvise(ptr, 1024 * 1024, MADV_DONTNEED);  // 释放物理页
    
  • fork() 之前调用 malloc_trim()malloc_trim()glibc 释放未使用的堆,减少 fork() 继承的页。

    #include <malloc.h>
    malloc_trim(0);  // 释放空闲堆内存
    
  • 避免 fork() 之后修改全局变量,fork() 之后,尽量不要修改共享内存(如全局变量、堆变量),防止触发 COW。

(2)使用 vfork() 代替 fork()

  • vfork()不会复制地址空间,子进程直接共享父进程的内存,避免 COW 触发。

  • 适用于子进程立即执行 exec()的场景。

    pid_t pid = vfork();
    if (pid == 0) {execlp("ls", "ls", NULL);  // 立即 exec(),避免修改内存_exit(1);  // 失败退出
    }
    
  • 注意: vfork() 会阻塞父进程,适用于 exec() 立即替换进程的场景。

(3)使用 posix_spawn() 代替 fork()+exec()

  • posix_spawn()底层可避免 fork() 继承大量页面,减少 COW 触发。

  • 适用于创建子进程并执行新程序的场景。

    #include <spawn.h>
    extern char **environ;
    pid_t pid;
    posix_spawn(&pid, "/bin/ls", NULL, NULL, (char *const[]){"ls", NULL}, environ);
    
  • fork()+exec() 更快,避免 fork() 复制页面。减少缺页中断,提高创建子进程的效率。

(4)避免 mallocmmap 影响 fork()

  • 使用 pthread_atfork() 保护 malloc()pthread_atfork() 可在 fork() 之前锁定 malloc,防止 COW 影响。

    pthread_atfork(lock_malloc, unlock_malloc, unlock_malloc);
    
  • 避免在 fork() 之后调用 malloc()malloc() 可能修改 glibcheap 结构,导致COW触发,建议在 fork() 之前预分配内存。

  • 使用 mmap() 代替 malloc()mmap() 分配的匿名映射页可以避免 COW,适用于大块内存分配。

    void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
优化点优化方法
减少 fork() 继承的内存madvise(MADV_DONTNEED), malloc_trim(0)
避免 fork() 后 COW 触发避免修改全局变量,避免 malloc()
改用 vfork() 避免 COWvfork() 适用于 exec() 场景
使用 posix_spawn() 代替 fork()更高效,减少缺页中断
优化 mallocmmap 行为pthread_atfork(), mmap()
1.5 posix_spawn和vfork介绍

posix_spawn() 是创建子进程并执行新程序的高效方法,通常用于替代 fork()+exec() 组合。

  • 避免 fork() 继承大量地址空间,减少写时拷贝(COW)和缺页中断。
  • 实现方式因系统不同,在Linux,posix_spawn() 可能使用 vfork() 进行优化。在macOS,posix_spawn() 是系统调用,比 fork()+exec() 更高效。
  • 适用于创建子进程并立即执行新程序的场景。

vfork()fork() 的优化版本,子进程直接共享父进程地址空间,不会复制页表。

  • 子进程与父进程共享地址空间,避免 fork() 的COW 机制和TLB 失效。
  • 子进程执行exec()之前,不能修改内存,否则可能影响父进程。
  • 父进程会被阻塞,直到子进程exec()_exit() 退出。
特性posix_spawn()vfork()
避免 fork() 页表复制✅ 是✅ 是
子进程共享父进程地址空间❌ 否✅ 是
父进程是否被阻塞❌ 否✅ 是
适用于 exec() 之后的场景✅ 是✅ 是
适用于复杂子进程启动✅ 是❌ 否
实现方式fork()vfork()(平台相关)直接共享地址空间

posix_spawn() 适用于一般 fork()+exec() 替代方案,避免 fork() 继承大量内存。

vfork() 适用于 exec() 立即执行的情况,但不适合复杂子进程逻辑。

2. 实例验证
2.1 复现故障

使用下述的代码可以复现fork和exec之间父进程修改堆内存触发缺页中断的情况。

#define _GNU_SOURCE
#define __USE_GNU#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sched.h>
#include <sys/mman.h>volatile int thread_stop  = 0;
int          enable_write = 0;void writeaddr(char *addr)
{for (int i = 0; i < 4096; i++) {addr[i] = i;}
}void *thread_func(void *arg)
{// 绑定线程 3 号 CPUcpu_set_t mask;CPU_ZERO(&mask);CPU_SET(3, &mask);pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask);printf("thread_func\n");// char *p2 = malloc(4096 * 100);// memset(p2, 0x0, 4096 * 100);// sleep();while (!thread_stop) {for (int i = 0; i < 100; i++) {if (enable_write) {writeaddr((char *)arg + i * 4096);}}}return NULL;
}int main(int argc, char *argv[])
{// argv[1] 为 1 时,测试缺页中断(COW)int enable_fork = 0;if (argc > 1) {enable_fork = 1;}if (argc > 2) {enable_write = 1;}char *p = malloc(4096 * 100);memset(p, 0x0, 4096 * 100);// 绑定 2 号 CPUcpu_set_t mask;CPU_ZERO(&mask);CPU_SET(2, &mask);sched_setaffinity(0, sizeof(mask), &mask);// 创建线程执行 writeaddrpthread_t tid;pthread_create(&tid, NULL, thread_func, p);// 等待同步sleep(1);// fork 一个子进程if (enable_fork) {int pid = fork();if (pid == 0) {execl("/bin/echo", "echo", NULL);exit(0);}}if (enable_fork) {// 等待子进程结束wait(NULL);}thread_stop = 1;// 等待线程结束pthread_join(tid, NULL);return 0;
}

从perf stat计数可以明显看出,fork子进程后,父进程的工作线程读写堆内存,会触发缺页中断,大概刚好100+(一个页面4KB)。

在这里插入图片描述

2.2 使用vfork替代fork

这里使用vfork替代fork,从下图可以看到,缺页中断不再增加,因为父进程被堵塞了。

在这里插入图片描述

从堵塞的时间来看,时间较触发缺页中断还要短一些:

max_time: 1556610 ns => 触发缺页中断 fork
max_time: 1121580 ns => 不触发缺页中断 vfork

并且,只有调用vfork的线程会被堵塞,其他线程并未被堵塞。

2.3 使用posix_spawn

使用posix_spawn的效果与vfork类似,如下:

在这里插入图片描述

3. 总结

在Linux环境下,如果一个程序需要创建子进程,如果这个程序自身是一个复杂的多线程程序,最好不要通过popen等接口运行脚本,因为这可能造成父进程中其他线程触发缺页中断,造成服务波动。

如果需要创建子进程,最好通过vfork或者posix_spawn接口,指定子进程的属性,比如避免复制页表,直接共享内存空间,然后子进程快速执行exec切换内存空间。

对于时延敏感性应用,更合适的做法是通过一个代理进程来执行shell或者创建子进程,然后通过RPC进行通信。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词