fork 创建进程,exec 替换其中的程序
fork 后接 exec,是 Unix 常见的进程启动模型之一。fork 创建子进程,成功的 execve 则替换调用进程中的程序映像。它们解决不同问题,也具有不同继承规则。在一个程序为另一个命令安排输入、输出、环境和生命周期管理时,必须分清这两个动作,否则很容易把创建新身份与替换运行内容混为一谈。 坏掉的样子 fork 不会让之后所有状态都相互独立。普通私有内存通常通过写时复制逐渐分离,但共享映射仍然共享。继承描述符也继续引用父进程对应的同一份打开文件描述,因此任一侧读取普通文件,都可能改变另一侧观察到的偏移。复制描述符表,不等于为每个文件重新独立执行 open;进程不同,也不能证明资源状态完全不同。 exec 同样不会简单保留全部启动设置。带 close-on-exec 的描述符在成功执行时关闭,其他符合条件的描述符可以继续存在。execve 接收显式环境数组,不是自动复制调用者当前环境。被捕获信号的处理方式会重置,旧内存映射也会消失,即使进程身份和若干其他属性仍然保留。需要逐项理解哪些状态跨越了替换边界。 多线程进程执行 fork 时尤其需要注意。子进程只包含发起调用的线程,却可能继承由其他、已经不在子进程中的线程持有的锁。在 exec 之前任意调用库函数,就可能死锁,因此这段间隔里的可用操作受到约束。方便理解的单线程演示,不能直接变成在线多线程服务器 fork 后随意处理业务状态的方案。 快速验证 把脚本保存为 forkexec.py,在一次性 Linux 环境中运行 python3 forkexec.py。实测使用 Linux 6.12.76 和 Python 3.11.2。样本刻意保持单线程,包含一个私有 Python 值、一份显式共享匿名映射、临时数据文件,以及传递少量结果的管道: from pathlib import Path from tempfile import TemporaryDirectory import errno,json,mmap,os,sys program=r''' import errno,json,os,sys original_pid,kept,dropped,report=map(int,sys.argv[1:]) assert os.getpid()==original_pid try:os.fstat(dropped) except OSError as error:assert error.errno==errno.EBADF else:raise AssertionError('close-on-exec descriptor survived') assert os.read(kept,2)==b'ef' assert os.environ['XB_FORK_VALUE']=='child-value' os.write(report,json.dumps({'pid_unchanged':True,'kept_descriptor':'ef','close_on_exec':'closed','explicit_environment':'child-value'}).encode()) ''' with TemporaryDirectory(prefix='xbcatm-fork-') as directory: data=Path(directory)/'data';data.write_bytes(b'abcdef') descriptor=os.open(data,os.O_RDONLY);dropped=os.dup(descriptor) read_end,write_end=os.pipe();os.set_inheritable(descriptor,True);os.set_inheritable(write_end,True) assert not os.get_inheritable(dropped) private=['parent'];shared=mmap.mmap(-1,8,flags=mmap.MAP_SHARED);shared[:]=b'parent!!' assert os.read(descriptor,2)==b'ab' sys.stdout.flush();pid=os.fork() if pid==0: try: os.close(read_end);private[0]='child';shared[:]=b'child!!!' assert os.read(descriptor,2)==b'cd' try:os.execve(str(Path(directory)/'missing-program'),['missing-program'],{}) except FileNotFoundError:pass else:raise AssertionError('missing exec unexpectedly succeeded') os.execve(sys.executable,[sys.executable,'-c',program,str(os.getpid()),str(descriptor),str(dropped),str(write_end)],{'XB_FORK_VALUE':'child-value'}) except BaseException as error: os.write(write_end,('ERROR '+repr(error)).encode());os._exit(1) os.close(write_end) try: waited,status=os.waitpid(pid,0);report=os.read(read_end,4096) assert waited==pid and os.WIFEXITED(status) and os.WEXITSTATUS(status)==0,report result=json.loads(report);assert result['pid_unchanged'] assert private==['parent'] and shared[:]==b'child!!!' assert os.lseek(descriptor,0,os.SEEK_CUR)==6 print('fork: private Python state diverges, MAP_SHARED changes and open-description offsets remain shared') print('missing exec returns to the child; successful exec keeps its PID, honors close-on-exec, and receives an explicit environment') print('parent reaped a successful exit with waitpid and retained its original private value') finally: shared.close() for fd in [descriptor,dropped,read_end]:os.close(fd) fork 之前,父进程从 abcdef 读取 ab。子进程修改自己的 Python 列表和共享映射,再通过继承描述符读取 cd。子进程结束后,父进程列表仍为原值,共享映射却包含子进程写入的字节。这两个结果需要共同解释,才能准确描述内存继承:只展示普通变量不同,无法证明不存在共享映射。 ...