尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

进程创建原理与实战:fork/exec与CreateProcess对比

发布时间:2026/9/29 2:39:04

资讯中心
01
ARTICLE

进程创建原理与实战:fork/exec与CreateProcess对比

进程创建原理与实战:fork/exec与CreateProcess对比
1. 先弄清楚这道练习到底要你交付什么“课堂练习3.2进程的创建”这种题目第一眼看上去平平无奇就是让程序里多出一个进程而已。可真坐到机器前面敲代码你会发现它牵出来的东西特别多操作系统怎么描述进程、内核用什么系统调用把进程造出来、父进程和子进程谁先跑、跑完之后谁负责收尸、创建失败时错误码怎么读。这些恰好是后面进程池、进程通信、并发调优、乃至排查“后台有一堆 java 进程却看不到窗口”这类现场问题的基础。我给这道练习的定位是它不是让你背 API而是让你亲手体会“创建”这个动作在两套完全不同的操作系统上被设计成了两种思路。Linux 走的是复制再替换的两步走Windows 走的是创建即完成的一步到位。理解了这层差别你在写 Python 的 multiprocessing、Java 的 ProcessBuilder、甚至看任务管理器里那一长串同名进程时脑子里就会自动浮现出父子关系树而不是一堆看不懂的名字。适合谁看正在做操作系统实验的同学、刚转后端需要补并发基础的开发者、以及被“进程占着文件删不掉”“程序启动了但窗口不出现”折腾过的运维和普通用户。下面我把这道练习从原理到实现、从观测到排错完整走一遍代码都是可以直接复制运行的版本坑我也提前给你标出来。2. 进程创建的本质内核到底做了哪几件事2.1 从“程序”到“进程”中间隔了什么我们写在硬盘上的那份可执行文件只是一堆指令和数据的静态集合它占着磁盘但不占 CPU也不占调度队列。进程是它被加载进内存、拿到独立地址空间、有了自己的栈和堆、被内核登记到调度器里之后的样子。所以“创建进程”这个词严格说分两步先造出一个内核能调度的实体再把可执行文件的内容塞进它的地址空间。进程在内核里有个名字叫任务控制块Linux 下是task_structWindows 下是 EPROCESS 结构。这个名字本身就说明了重点——它是一个“控制块”记录着这个进程的 PID、父进程、优先级、状态、打开的文件描述符表、内存映射、信号处理方式等等。创建进程本质就是让内核分配并初始化这样一个结构再把它挂到调度队列上。很多人第一次做练习时会以为创建进程很“重”其实内核在这一步相当克制。Linux 的fork并不会立刻把父进程的内存整块复制一份它用的是写时复制父子进程一开始共享同一批物理页页表项都指向同一块内存只是标记成只读。谁要写入谁就触发一次缺页中断内核这时候才真正复制那一页。所以fork的开销主要在复制页表和task_struct这类元数据上真正的内存搬运被推迟到了写入那一刻。这个设计的意义很直接进程创建后大概率马上要执行exec换成新程序提前复制一遍纯属浪费。2.2 三种创建方式的返回值决定了你代码怎么写做 Linux 部分的练习fork的返回值是必须烂熟于心的。它一次调用、两处返回这不是笔误是设计。父进程拿到子进程的 PID子进程拿到 0出错则拿到负数并设置errno。你的代码分支判断就是靠这个区分的写错一个等号逻辑就全乱了。返回值返回在哪个进程含义与后续动作pid 0父进程拿到子进程 PID通常继续做自己的事或调用 wait 回收pid 0子进程只可能是子进程通常接着调用 execve 替换镜像pid 0父进程创建失败查看 errno 定位原因这里有个所有人都会踩的坑fork之后子进程不是从main开始执行的它从fork这一行之后继续往下跑。所以fork之前打开的文件、申请的缓冲区、打印了一半的输出子进程都能看见。如果fork之前用了带缓冲的printf父进程缓冲区里没刷出去的内容会被子进程继承最后打印两遍。解决方法是在fork前手动fflush(stdout)或者干脆用不带缓冲的write。2.3 exec 不是创建是把当前进程“换掉”execve这一族函数经常和fork一起被提到但它本身不创建新进程。执行execve后当前进程的地址空间被完全替换成新程序的代码段、数据段和栈PID 不变打开的文件描述符默认保留。正因为“PID 不变”fork加exec才能组合成一个看起来像“启动了一个新程序”的完整流程父进程复制出子进程子进程把自己替换成目标程序父进程拿着 PID 继续管。有六个 exec 变体名字的区别就两点l表示参数以列表形式逐个传入v表示参数打包成数组一次传入p表示去 PATH 环境变量里找程序e表示可以自定义环境变量。日常用得最多的是execlp和execvp因为它们会自动搜路径写起来省事。注意exec系列函数成功调用后不会返回一旦返回就说明失败了。新手常见的写法是exec(...)后面跟一句“如果失败就退出”这是对的但如果写成“exec 成功后就退出”逻辑就反了。3. 两套操作系统两种创建哲学3.1 Linux 的“复制再替换”和 Windows 的“一步到位”Linux 的fork加exec是分离式的好处是灵活你可以在复制之后、替换之前这段时间里做很多事情比如修改文件描述符、切换工作目录、设置用户身份、关闭多余句柄这些操作都在子进程里做不影响父进程。守护进程的三次 fork、容器启动时的命名空间配置都依赖这个中间窗口。Windows 没有等价的fork它的CreateProcess是创建和加载一条龙。你给一个可执行文件路径或命令行内核直接建进程、建主线程、加载镜像、返回句柄和 PID中间不给用户态留操作空间。想在这种模型里“先改点东西再执行”只能靠命令行参数或者环境变量传递这也是 Windows 上脚本风格明显不同的原因。维度Linuxfork execWindowsCreateProcess是否分两步分两步中间有操作窗口一步完成内存复制写时复制按页触发直接加载新镜像创建后返回父进程拿 PID子进程返回 0返回 PROCESS_INFORMATION 结构句柄管理文件描述符默认继承是否继承由参数显式决定典型用途服务端、脚本、容器桌面程序、服务、批处理3.2 Windows 上那几个必须记住的参数CreateProcess参数一长串实际写起来真正影响结果的没几个。第一个参数是应用程序名可以为空此时内核从命令行里解析出可执行文件第二个参数是命令行注意它是可写的字符串指针很多封装库会在内部复制一份再传。第五个参数控制句柄继承桌面程序一般传 FALSE避免子进程莫名其妙拿到一堆不该拿的句柄。创建标志里CREATE_NEW_CONSOLE会给子进程单独开一个控制台窗口CREATE_NO_WINDOW表示不要窗口CREATE_SUSPENDED让主线程挂起不跑等你配置完再唤醒。这几个标志直接决定你看到的现象传了CREATE_NO_WINDOW程序跑起来了但界面上什么都没有这就是很多人抱怨“只有进程没有窗口”的一个典型来源。还有一点返回的PROCESS_INFORMATION里有进程句柄和线程句柄用完必须显式关闭否则句柄计数一直不降时间长了会拖垮程序。3.3 高级语言帮你封装了多少藏了多少用 C 写练习是为了看清底层但真实工作里更多是调语言的封装。Python 的multiprocessing在 Linux 上默认用fork在 Windows 和 macOS 上默认用spawn。spawn会启动一个全新的解释器重新导入你的主模块再把函数参数序列化过去。这就带来一个致命细节主模块必须有if __name__ __main__:保护否则新解释器一导入模块又去创建进程无限套娃直到系统撑爆。Java 这边Runtime.exec是早期接口它会把整个字符串按空白拆分路径里有空格就废了。ProcessBuilder改掉了这个毛病参数是一个列表每个元素是独立的配合redirectErrorStream(true)把标准错误合并到标准输出能省掉一个读取线程。但无论哪个接口子进程的输出流都必须有人读否则缓冲区写满后子进程就卡在那里等父进程还傻乎乎地waitFor双方僵住。4. 动手实操把三种实现完整跑一遍4.1 C 语言版本最能看清 fork 的一版先看最小可运行版本重点是返回值判断和回收动作#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main(void) { fflush(stdout); pid_t pid fork(); if (pid 0) { perror(fork failed); return 1; } if (pid 0) { printf(child : pid%d ppid%d\n, getpid(), getppid()); execlp(echo, echo, child is replacing itself, (char *)NULL); perror(execlp failed); _exit(127); } printf(parent: pid%d child%d\n, getpid(), pid); int status 0; pid_t done waitpid(pid, status, 0); if (done 0) { perror(waitpid failed); return 1; } if (WIFEXITED(status)) { printf(child exited with code %d\n, WEXITSTATUS(status)); } return 0; }编译运行gcc fork_demo.c -o fork_demo ./fork_demo你能同时看到父进程和子进程的打印。三个细节值得留意fork前我加了fflush避免输出重复子进程替换失败时用的是_exit而不是exit因为exit会刷新并跑一遍父进程继承过来的清理逻辑可能在缓冲区里留下重复内容父进程用waitpid阻塞等待拿到退出状态后再解析。如果你想知道fork到底有多快可以在循环里跑一千次并计时注意每次都要wait回收不然会积累一堆僵尸进程。实测在普通笔记本上纯fork加立即exit的单次开销在几十微秒量级这个数字在你后面决定要不要上进程池时很有参考价值。4.2 Python 版本注意启动方式的差异Python 版能明显提升开发效率但跨平台行为差异要学会主动控制import multiprocessing as mp import os import time def worker(task_id): print(f[child {os.getpid()}] task{task_id} ppid{os.getppid()}) time.sleep(0.1) return task_id * task_id if __name__ __main__: print(f[parent {os.getpid()}]) with mp.Pool(processes4) as pool: results pool.map(worker, range(8)) print(results:, results)在 Linux 上跑你会发现所有子进程的 ppid 都是主进程换成spawn启动方式再跑一次if __name__ __main__: mp.set_start_method(spawn)行为会变子进程是全新解释器模块被重新导入函数和参数都得经过序列化。启动慢一些但隔离性更好也避免了fork在有多线程场景下可能带来的锁状态不一致问题。提示Windows 上只有spawn这一种选择。如果你在 Windows 上写多进程代码却忘了加if __name__ __main__:程序会不断创建新进程任务管理器里的 python 进程数量会爆炸式增长这就是最常见的“进程数异常”现场。4.3 Java 版本流处理是绕不过去的一关Java 里推荐的写法是ProcessBuilder下面这个版本把输出、超时、退出码都照顾到了import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.concurrent.TimeUnit; public class ProcDemo { public static void main(String[] args) throws Exception { ProcessBuilder pb new ProcessBuilder(ls, -l, /tmp); pb.redirectErrorStream(true); Process p pb.start(); try (BufferedReader r new BufferedReader(new InputStreamReader(p.getInputStream()))) { String line; while ((line r.readLine()) ! null) { System.out.println([child] line); } } boolean done p.waitFor(10, TimeUnit.SECONDS); if (!done) { p.destroy(); System.out.println(timeout, process killed); } else { System.out.println(exit code p.exitValue()); } } }这段代码里有三个关键点。第一redirectErrorStream(true)让标准错误合并进标准输出一个流就够了。第二必须把流读空不然子进程输出多的时候会阻塞。第三waitFor带超时避免程序永远挂在等待上。Java 9 以后还可以用ProcessHandle查进程信息ProcessHandle ph p.toHandle(); System.out.println(pid ph.pid()); ph.parent().ifPresent(parent - System.out.println(ppid parent.pid())); ph.children().forEach(c - System.out.println(child pid c.pid()));ProcessHandle.allProcesses()能把整机进程列出来配合过滤条件可以写一个简单版的“怎么查找电脑后台进程”工具按命令行关键字筛比手动翻列表高效得多。4.4 进程池批量任务下这笔账怎么算如果你的任务是“处理一万个文件”给每个文件 fork 一次就不划算了。每次创建都要复制页表、分配内核结构一万次下来光开销就够呛。进程池的思路是提前建好固定数量的进程任务通过队列分发进程反复复用。池子开多大有讲究不能拍脑袋。下面这张表是我自己压测总结的起点具体还要看任务特征任务类型建议池大小判断依据纯计算密集物理核数或核数 1CPU 一直是满的多了只会互相抢IO 密集文件、网络核数的 2 到 4 倍大量时间在等待进程可以多切几次混合型核数 2 起步先按经验值跑再看 P95 延迟调有外部资源限制按资源上限倒推比如数据库连接数只有 20池子别超过它判断方法很朴素把池子大小当变量从核数开始往上加观察吞吐量和单任务平均耗时。当平均耗时开始明显上升、吞吐量不再涨说明已经过了拐点再往里塞进程只会增加上下文切换。注意进程池里的进程一旦创建它继承的是创建那一刻的父进程状态。如果你在创建池之前打开了数据库连接或文件句柄这些资源会被所有子进程共享引用交叉读写很容易出问题。稳妥做法是池创建放在前面连接在子进程内部自己建。5. 进程创建之后要盯住什么5.1 父子关系、状态和观测命令创建只是开始进程跑起来之后你得能看见它。Linux 上最常用的组合是ps -ef看全量、ps -o pid,ppid,stat,cmd -p pid看指定进程、top或htop看实时资源、pstree -p pid看家族树。pstree那个命令特别直观一眼就能看出谁是爹谁是儿子做练习时把结果截图放进实验报告比文字描述有说服力。状态字段里的字母要认识R是运行或就绪S是可中断睡眠最常见D是不可中断睡眠通常在等 IOZ是僵尸T是被停止。看到Z就要警觉说明子进程已经退出但父进程还没回收。看到一堆D大概率是磁盘或网络出了状况这时候连kill -9都杀不掉因为信号要等到系统调用返回才处理。Windows 上对应的是任务管理器的详细信息页可以加“命令行”“父进程 ID”两列这两个列默认是隐藏的加上之后排查效率提升明显。命令行视图里wmic process get processid,parentprocessid,name也能用不过新系统更推荐 PowerShell 的Get-Process和Get-CimInstance Win32_Process。5.2 僵尸进程和孤儿进程处理时机完全不同僵尸进程是子进程退出后留下的一条记录包含退出状态等着父进程调用wait或waitpid来取。父进程不取这条记录就一直挂着占一个进程表项。偶尔几个无所谓量大了会耗光 PID 资源。解决方式有三种父进程老老实实回收父进程忽略SIGCHLD信号部分系统上会让内核自动回收或者干脆双 fork让子进程变成孤儿后被 init 收养由它负责收尸。孤儿进程是另一回事父进程先退出了子进程还活着它会被 PID 为 1 的进程接管。这本身不是错误守护进程就是这么做的。两者的区别记住一句话僵尸是死了没人埋孤儿是活着没了爹。提示写练习的时候如果不加wait程序结束时终端可能会短暂出现一些异常提示或者残留进程。养成创建后必回收的习惯代码里fork和wait尽量成对出现。5.3 进程通信的衔接点从这里开始单进程跑通之后下一步自然要传数据。父子进程之间最直接的是管道pipe()创建一对读写文件描述符fork之后父子各持一端写入读出即可。它的限制是只能单向传字节流而且缓冲区满了会阻塞。要做双向通信就建两根管道或者干脆用socketpair拿一对全双工描述符。进程通信还不止这些。信号适合做通知比如父进程用SIGTERM让子进程优雅退出共享内存适合大数据量的高频交换速度最快但要自己处理同步消息队列介于两者之间有边界、有类型用起来省心。实际项目里也经常用外部手段通信比如通过一个本地文件、通过标准输入输出、通过本机的套接字这些本质上都还是操作系统提供的机制在支撑。学习顺序我的建议是先把管道吃透理解字节流、阻塞、EOF 这几个概念再上共享内存和信号量理解同步为什么必须存在最后看套接字因为它能平滑过渡到跨机器通信。6. 报错和异常现场的排查实录6.1 创建阶段直接失败先看错误码fork失败基本都是资源问题CreateProcess失败更多是路径和权限问题。把常见错误码整理成一张表遇到时按图索骥能省不少时间错误码出现场景常见原因处理方向EAGAINLinux fork进程数或线程数达到上限查ulimit -u、cgroup 的 pids 限制ENOMEMLinux fork内核内存不足以分配结构查内存和 overcommit 设置ENOENTexec / CreateProcess可执行文件路径不存在核对绝对路径、PATHEACCESexec / CreateProcess没有执行权限或目录无权限检查 x 位和目录权限E2BIGexec参数或环境变量太长精简参数拆分传递ERROR_FILE_NOT_FOUNDWindows路径写错或没写扩展名用完整路径注意转义ERROR_ACCESS_DENIEDWindows权限不足或被安全软件拦截换有权限的账户执行排查顺序建议是先确认程序路径能不能单独跑起来再确认当前账号有没有权限最后才怀疑系统资源限制。很多人一上来就怀疑内核参数其实九成问题是路径写错了。6.2 进程活着但界面不出来怎么查这个现象很常见桌面程序启动后任务管理器里能看到进程但窗口就是不出现。能想到的原因至少有四类。第一类是启动参数带了“不显示窗口”的选项比如 Windows 的CREATE_NO_WINDOW或者程序自身的静默模式参数。第二类是程序启动后卡在初始化比如在等一个不可用的网络资源、等一把被占用的文件锁、或者等一个已经退出的父进程。第三类是进程被启动在了不同的会话里图形界面属于当前登录会话如果程序是从服务或计划任务里拉起来的它落在另一个会话中窗口自然不会出现在你面前。第四类是它本来就是个后台进程压根没有界面和同名或相似名字的前台程序不是一回事。排查办法是逐层排除先用命令行直接启动看有没有报错输出再用进程的父进程 ID 往上找看是谁把它拉起来的然后看它的命令行参数确认是不是带了静默开关最后看它的工作目录和依赖很多程序因为找不到配置文件而在初始化阶段卡住。注意同一款软件出现多个同名进程通常不是故障。桌面软件普遍采用主进程加若干辅助进程的结构辅助进程负责渲染、网络、插件隔离它们由主进程创建关掉主窗口时会一起退出。判断是否异常要看资源占用和持续时间而不是数量本身。6.3 资源占用异常和“进程拒绝访问”CPU 和内存突然飙升时第一步是定位到具体进程第二步才是判断它是否正常。定位用top按 CPU 排序或者ps aux --sort-%cpu | headWindows 上直接任务管理器按列排序。找到之后看它的命令行、启动时间、父进程一般就能判断是业务程序、系统组件还是可疑的东西。“进程无法中断拒绝访问”这类问题多数是权限层级造成的。你以普通用户身份运行的任务管理器看不到也动不了以更高权限运行的进程。解决办法是以相同或更高权限去操作。还有一部分是进程正处于不可中断的内核态等待这时候任何信号都要等它从系统调用返回才行硬等或者重启是唯一选择。和文件相关的“进程占用导致无法删除”在 Linux 上可以用lsof | grep 文件名或fuser -v 文件名找到持有者Windows 上资源监视器的“关联的句柄”搜索框也能直接搜文件名。虚拟机软件、数据库这类程序会创建锁文件并长时间持有异常退出后锁文件残留下次启动就报“另一个程序已锁定文件的一部分”。处理方式很明确确认原进程确实不在了删除残留的锁文件再启动。这一步之前一定要确认不然可能损坏数据文件。7. 从这道练习往外延伸的两条线7.1 进程和线程到底怎么取舍做完进程创建几乎所有人都会问同一个问题既然创建进程有开销那用线程是不是更好。答案取决于你要保护什么。进程有独立的地址空间一个崩了不影响另一个隔离性强适合跑不可信代码或者容易出内存问题的任务线程共享地址空间通信成本低切换快但一处越界或一个未捕获异常就可能把整个进程带走。经验上的分界线是这样任务之间需要频繁交换大量数据优先线程任务之间需要强隔离、或者要跑第三方不信任的代码优先进程需要压榨多核又怕全局解释器锁Python 里就得多进程任务本身是网络等待为主异步加线程往往比多进程划算。没有银弹先测量再选型。7.2 把监控和资源分析串成一条线练习里学的观测命令放大到整机就是一套监控体系。查看单个进程用ps、top、ProcessHandle找进程持有的文件用lsof、资源监视器看进程树用pstree、进程资源管理器想看历史趋势就得上采集工具把 CPU、内存、句柄数、线程数按时间存下来。监控里有两个容易被忽略的指标。一个是句柄数或文件描述符数程序长时间运行后只涨不降基本可以确定有资源泄漏。另一个是进程数量短时间内大量新增进程往往是启动脚本出了问题比如循环里反复拉起子进程没有退出条件前面提到的 Python 忘记加主模块保护就是典型例子。把这两个指标加入日常观察清单很多问题能在爆发前被发现。我个人在做这类练习时最大的体会是不要满足于“程序跑起来了”。能跑通只说明语法没错真正把进程创建搞明白是你能预测它的行为——知道它会复制什么、什么时候回收、失败时报什么错、在系统里长什么样。把fork的返回值判断写对三次、把僵尸进程亲眼看到一次、把子进程的输出流读到 EOF 一次这三件事做完后面再遇到进程池、进程通信、后台进程异常心里就有底了。最后分享一个我常用的偷懒技巧写多进程调试代码时先在worker开头把os.getpid()和os.getppid()打出来一眼就能看清父子关系比单步调试快得多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。