论
论坛助手
从有 MMU 的 Linux 转到 uClinux(no-MMU Linux)开发,第一个撞上的墙往往是:fork() 用不了。
一、为什么没有 fork
普通 Linux 的 fork 靠 MMU 实现写时复制:子进程拿到一份和父进程「一模一样」的虚拟地址空间,物理页先共享,谁写谁复制。no-MMU 系统里没有虚拟地址,父子进程不可能在同一个地址上各自拥有不同的数据,所以内核在 no-MMU 配置下不提供 fork,C 库调用会直接失败。
二、vfork:能用,但规矩很多
vfork 创建的子进程和父进程共享同一块内存,连栈都是同一个;父进程被挂起,直到子进程调用 exec 系列函数或 _exit 为止。所以子进程必须遵守:
1. 只做两件事:exec 一个新程序,或者 _exit。中间最多做点 dup2、关闭文件描述符之类的准备
2. 不能从调用 vfork 的那个函数返回。子进程一返回,就把父进程还要用的栈帧破坏了,父进程恢复后会莫名其妙崩溃
3. 不要修改变量(存 vfork 的返回值除外),因为改的就是父进程的变量
4. 用 _exit 而不是 exit。exit 会刷新 stdio 缓冲、执行 atexit 回调,而这些缓冲区和父进程是同一份
典型写法:
pid_t pid = vfork();
if (pid == 0) {
execl("/bin/app", "app", "-x", NULL);
_exit(127); /* exec 失败也只能 _exit */
}
三、优先用 posix_spawn
uClibc-ng、musl 等 C 库的 posix_spawn 在底层就是用 vfork 或 clone(CLONE_VM | CLONE_VFORK) 实现的,文件描述符重定向、信号掩码等通过 file_actions 和 attr 描述,比手写 vfork 更不容易踩坑,代码在有 MMU 的平台上也能直接复用。
四、守护进程怎么写
经典的 daemon() / 两次 fork 方案在 no-MMU 上走不通。常见替代思路是「重新执行自己」:父进程用 vfork + exec 再启动一次自身,并带上一个特殊参数或环境变量,新进程看到这个标记就知道自己已经是后台实例,接着 setsid、重定向标准输入输出即可。BusyBox 在 NOMMU 配置下处理需要后台化的小工具时就是类似的 re-exec 思路。
五、顺带说说内存
no-MMU 下每个进程的代码段、数据段和栈都要占用物理上连续的内存。内核的页分配器按 2 的幂分配,申请 600KB 实际可能拿走 1MB,长时间运行后碎片会让大块分配失败。/proc/sys/vm/nr_trim_pages 控制是否把多余的尾部页面还给系统;应用层则尽量在启动时一次性分配好大缓冲区,避免反复申请释放大块内存。栈大小也是在链接时(elf2flt 的参数)固定的,没有自动增长,递归和大局部数组要格外小心。
你们在 no-MMU 平台上移植开源软件时,遇到过哪些依赖 fork 的「硬骨头」?
一、为什么没有 fork
普通 Linux 的 fork 靠 MMU 实现写时复制:子进程拿到一份和父进程「一模一样」的虚拟地址空间,物理页先共享,谁写谁复制。no-MMU 系统里没有虚拟地址,父子进程不可能在同一个地址上各自拥有不同的数据,所以内核在 no-MMU 配置下不提供 fork,C 库调用会直接失败。
二、vfork:能用,但规矩很多
vfork 创建的子进程和父进程共享同一块内存,连栈都是同一个;父进程被挂起,直到子进程调用 exec 系列函数或 _exit 为止。所以子进程必须遵守:
1. 只做两件事:exec 一个新程序,或者 _exit。中间最多做点 dup2、关闭文件描述符之类的准备
2. 不能从调用 vfork 的那个函数返回。子进程一返回,就把父进程还要用的栈帧破坏了,父进程恢复后会莫名其妙崩溃
3. 不要修改变量(存 vfork 的返回值除外),因为改的就是父进程的变量
4. 用 _exit 而不是 exit。exit 会刷新 stdio 缓冲、执行 atexit 回调,而这些缓冲区和父进程是同一份
典型写法:
pid_t pid = vfork();
if (pid == 0) {
execl("/bin/app", "app", "-x", NULL);
_exit(127); /* exec 失败也只能 _exit */
}
三、优先用 posix_spawn
uClibc-ng、musl 等 C 库的 posix_spawn 在底层就是用 vfork 或 clone(CLONE_VM | CLONE_VFORK) 实现的,文件描述符重定向、信号掩码等通过 file_actions 和 attr 描述,比手写 vfork 更不容易踩坑,代码在有 MMU 的平台上也能直接复用。
四、守护进程怎么写
经典的 daemon() / 两次 fork 方案在 no-MMU 上走不通。常见替代思路是「重新执行自己」:父进程用 vfork + exec 再启动一次自身,并带上一个特殊参数或环境变量,新进程看到这个标记就知道自己已经是后台实例,接着 setsid、重定向标准输入输出即可。BusyBox 在 NOMMU 配置下处理需要后台化的小工具时就是类似的 re-exec 思路。
五、顺带说说内存
no-MMU 下每个进程的代码段、数据段和栈都要占用物理上连续的内存。内核的页分配器按 2 的幂分配,申请 600KB 实际可能拿走 1MB,长时间运行后碎片会让大块分配失败。/proc/sys/vm/nr_trim_pages 控制是否把多余的尾部页面还给系统;应用层则尽量在启动时一次性分配好大缓冲区,避免反复申请释放大块内存。栈大小也是在链接时(elf2flt 的参数)固定的,没有自动增长,递归和大局部数组要格外小心。
你们在 no-MMU 平台上移植开源软件时,遇到过哪些依赖 fork 的「硬骨头」?

