资讯动态

Linux进程完全指南:从fork到守护进程的实战解析

发布时间:2026/9/15 8:43:41 来源:尧图企业网站定制
1. 为什么说进程是Linux系统的灵魂不管你是在用CentOS、Ubuntu还是Debian也无论你写的是C、C还是Python只要程序跑在Linux上就绕不开进程这个话题。很多刚开始接触系统编程的朋友会觉得进程这个概念太抽象书上说“进程是程序的一次执行过程”听起来像一句正确的废话真到排查问题的时候又不知道从哪下手。我从自己的学习经历说起吧。最开始我在Windows上写代码任务管理器能看到一堆exe条目但从来不会去想这些条目背后是什么。后来转到Linux环境做服务端开发第一次用ps aux看到满屏的进程输出突然发现自己连“怎么把一个程序真正跑起来”“进程之间怎么协作”都说不清楚。这篇文章就是想把Linux进程这块基础给掰开揉碎讲清楚覆盖从理论到实操的完整链路无论你是刚接触Linux的新手还是准备面试的求职者或者已经在写业务代码但想补一补底层功底的开发者都能从中得到可以直接上手的知识。进程这块内容在系统编程里的地位相当于地基之于楼房。进程管理、进程通信、进程调度这是面试题里的常客也是排查线上问题时必须用到的基本功。本篇文章是这个系列的第三篇前两篇分别讲了文件I/O和文件系统进程天然跟文件有关系——每个进程都有打开的文件描述符表启动一个进程要加载可执行文件这些都会串联起来。这篇文章我会先把进程的底层原理讲清楚再带你完整走一遍创建、退出、回收和管理的全过程每一步都配套实际的命令和代码示例。2. 进程到底是什么从程序到进程的转变2.1 程序和进程的区别在哪里程序是一个静态的概念。你写的代码经过编译之后生成的ELF可执行文件躺在磁盘里它就是一堆二进制指令和数据不占CPU也不占内存不会自己动起来。进程则是动态的它是程序被加载到内存之后操作系统为它分配资源、安排执行的一个运行实例。用个直白的类比程序就像菜谱放在书架上不会做饭进程是厨师按照菜谱实际开火炒菜的过程。同一个菜谱可以多个厨师同时用同一个程序也可以同时跑出多个进程。比如你开两个终端分别运行./hello内存里就有两个hello进程它们的代码段可能是共享的但数据、堆、栈是独立的。进程在系统里不是裸奔的内核为了管理它维护了一个叫做进程控制块的数据结构也就是常说的PCB。Linux里这个结构叫task_struct它记录了进程的PID、状态、优先级、打开的文件、内存布局、信号处理设置等等信息。你可以把task_struct理解为进程的档案袋内核调度进程的时候先翻档案决定谁该跑、谁该等。2.2 进程的虚拟地址空间布局每个进程都以为自己独占整个内存这是虚拟内存机制给进程的幻觉。实际上进程的虚拟地址空间从低地址到高地址大致分成这么几个区域代码段Text存放机器指令只读多个进程可以共享同一份代码段。数据段Data存放已初始化的全局变量和静态变量。BSS段存放未初始化的全局变量和静态变量程序加载时被清零。堆Heap动态分配的内存区域向高地址增长对应malloc/new。内存映射区mmap共享库、mmap映射的文件放在这块通常在堆和栈之间。栈Stack存放局部变量、函数调用信息向低地址增长大小通常固定可以用ulimit -s查看。这七个区域的分布不是随便定的代码段放最底端是为了防止指针越界写到指令区栈向下增长、堆向上增长中间留出大片空间尽量减少两者碰撞的可能。顺便说一句栈和堆相向而行的设计也是很多内存耗尽问题的根源——你可能听说过栈溢出stack overflow就是递归调用太深把栈空间耗光了。文章后面讲到fork的时候你会看到父子进程的地址空间虽然内容一样但物理内存是独立的靠写时复制技术偷懒而已。3. 进程的一生从创建到消亡3.1 第一个系统调用forkLinux创建进程的方式很特别不是“新建”而是“复制”。一个进程调用fork()之后内核会创建一个几乎一模一样的子进程。子进程是父进程的副本拥有独立的地址空间、文件描述符表副本、信号处理设置等。#include stdio.h #include unistd.h int main() { pid_t pid fork(); if (pid 0) { perror(fork error); return -1; } else if (pid 0) { // 子进程走这里 printf(子进程, PID%d, 父进程PID%d\n, getpid(), getppid()); } else { // 父进程走这里, pid是子进程的PID printf(父进程, PID%d, 子进程PID%d\n, getpid(), pid); } // 整个代码块父子进程都会执行到这里 return 0; }这段代码有个非常经典的现象fork()调用一次返回两次。父进程得到的是子进程的PID子进程得到的是0。碰到返回-1就是创建失败了最常见的原因是进程数达到系统上限。程序中的两个printf都会执行。由于父进程先执行还是子进程先执行是不确定的取决于内核调度所以多次运行程序输出顺序可能不一样。我实际测试时最常见的结果是先打印父进程信息偶尔子进程会抢先这是正常现象。这里有个新手经常踩的坑fork之后printf的输出可能不是你想的顺序。因为printf是带缓冲的如果输出到终端一般是行缓冲换行符会触发刷新但如果输出重定向到文件就变成全缓冲缓冲区要在进程退出时才刷新。遇到不换行的printf加上fork子进程会复制父进程的缓冲区导致同一行内容打印两次。解决办法就是fork之前先fflush(NULL)或者每次printf都带换行并主动fflush。3.2 父子进程的隐秘差异虽然子进程是复制父进程得来的但它们不是完全相同的克隆体。有几处差异你需要牢记PID不同子进程的PPID是父进程的PID。fork的返回值不同这是代码分流的关键。子进程的文件描述符表虽然是父进程的副本但底层指向同一个文件描述双方共享文件偏移量。父进程的未决信号不会被子进程继承。子进程不会继承父进程的定时器、锁等。文件偏移量共享这一点值得细说。假设父进程用open()打开了一个文件然后fork出子进程父子进程各自调用write往文件写数据你就会发现写入的位置是接着来的不会互相覆盖。原因就是它们共享同一个file结构体偏移量字段是同一份。你如果想让文件偏移独立就得在fork之后各自open。3.3 exec系列让子进程脱胎换骨fork复制出来的子进程和老爹执行同一份代码这在实际开发中没太大用处。我们真正想要的是先fork出一个子进程然后让它执行一个完全不同的程序。这就要用到exec系列函数。exec系列做的事情本质上是用一个全新的程序镜像替换当前进程的代码段、数据段、堆和栈。它不创建新进程PID不变只是进程肚子里装的东西变了。#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { // 子进程执行ls命令 execl(/bin/ls, ls, -l, NULL); // exec成功不会走到这里, 失败才会 perror(execl error); return -1; } else { wait(NULL); printf(子进程执行完毕\n); } return 0; }execl的第一个参数是要执行的文件路径第二个参数是argv[0]也就是程序名之后是命令行参数最后必须以NULL结尾。系统会去PATH环境变量里找你指定的程序吗不会。execl需要一个绝对路径或相对路径。如果你想用PATH搜索就用execvp它会拿文件名去PATH里找。有个细节容易被忽视exec成功之后原来的进程代码不再执行所以execl后面的代码只有exec失败才会走到。实际开发里要在exec后面加个perror再返回否则exec失败了你都不知道子进程悄无声息地死了父进程还蒙在鼓里。3.4 进程退出与孤儿进程进程退出有两种方式正常退出和异常退出。正常退出是main里return或者调用exit()异常退出是收到信号比如段错误被杀掉或者调用abort()。这里要区分exit()和_exit()。exit()是C标准库函数它先执行清理工作刷新缓冲区、执行atexit注册的清理函数然后调用_exit()进入内核。_exit()是系统调用直接退出进程不做任何清理。你写了printf没换行如果调用_exit()缓冲区里的内容可能就丢了。进程退出后内核并没有立刻把它从进程表里删除。它会保留一个僵尸进程的条目记录退出状态供父进程查询。如果父进程不管它这个条目就一直占着。等父进程调用wait/waitpid收尸之后僵尸进程才真正消失。如果父进程比子进程先死呢那子进程就变成了孤儿进程。别慌孤儿进程会被1号进程收养也就是init进程现在很多系统是systemd。收养之后系统会自动帮这些孤儿进程收尸不会让他们变成没人管的僵尸。所以僵尸进程的产生本质是父进程不调用wait且一直活着。3.5 回收子进程wait和waitpid父进程等待子进程结束并获取其状态靠的是wait家族。wait会阻塞直到任意一个子进程退出waitpid更精细可以指定等待哪个子进程。#include stdio.h #include sys/wait.h #include unistd.h int main() { pid_t pid fork(); if (pid 0) { sleep(2); return 42; // 子进程退出码 } int status; pid_t ret waitpid(pid, status, 0); if (WIFEXITED(status)) { printf(子进程 %d 正常退出, 退出码%d\n, ret, WEXITSTATUS(status)); } return 0; }status是一个位图不能直接拿来当退出码用。判断逻辑是先用WIFEXITED(status)看子进程是否正常退出如果返回真再用WEXITSTATUS(status)提取退出码。如果子进程是被信号杀死的WIFSIGNALED(status)为真WTERMSIG(status)给出信号编号。waitpid还有个妙用传WNOHANG参数可以实现非阻塞等待。比如你要写一个守护进程一边处理业务一边周期性地回收子进程就可以用waitpid(-1, status, WNOHANG)轮询。返回0表示没有子进程退出返回-1表示没有子进程可等。4. 进程状态与命令行实操4.1 进程的五态模型与Linux具体实现教科书上的五态模型新建态、就绪态、运行态、阻塞态、终止态。Linux在此基础上做了细化ps命令输出里你能看到的状态字符有R、S、D、Z、T、I等各代表不同含义状态字符含义说明R运行或就绪正在运行或处于运行队列S可中断睡眠等待某个事件可以被信号唤醒D不可中断睡眠通常在等待I/O不能被杀掉Z僵尸态已退出但未被父进程回收T停止态被信号停止比如CtrlZI空闲态内核线程专用不占运行队列很多人不理解D状态觉得这进程怎么杀都杀不掉。D状态进程一般在等磁盘I/O或网络I/O内核不让信号打断它因为要保证I/O操作的原子性。解决办法通常是等I/O结束或者检查是不是存储设备出了问题。顺带一提数据库这类对磁盘要求高的服务D状态进程过多往往是存储性能瓶颈的信号。4.2 ps命令进程快照查看术ps命令是排查进程的第一入口我最常用的几个组合# 查看所有进程带完整命令信息 ps aux # 查看当前终端进程 ps -ef # 只看某个进程按名字精确匹配 ps -C nginx # 指定PID查看 ps -p 1234 -o pid,ppid,stat,commps aux输出里VSZ是虚拟内存大小RSS是实际驻留内存大小单位是KB%MEM就是RSS占物理内存的百分比。我排查内存泄漏时会重点盯RSS的变化比如每隔几秒取一次RSS值如果只增不减多半是程序内部没释放。ps -ef的PPID列非常有用。你想知道一个进程是谁拉起来的看PPID就知道了。比如你启动了一个Java服务PPID是1说明它是被systemd直接拉起来的或者经过双fork变成孤儿被收养。4.3 动态监控top和htopps是静态快照top是动态刷新。top默认按CPU使用率排序每3秒刷新一次。常用的交互快捷键P按CPU排序M按内存排序k可以杀进程r可以调整优先级。top界面上方的load average三个值分别代表1分钟、5分钟、15分钟的平均负载。很多人把它理解为CPU使用率其实不然。负载是处于R状态和D状态的进程数量总和。如果服务器的CPU核心数是8负载长期超过8说明任务排队了CPU不够用。如果负载很高但CPU使用率很低往往是有进程卡在I/O上了这时候要看wa那一列的百分比。htop是top的增强版带颜色、支持鼠标操作和树状视图按F5可以看到进程的父子关系树。排查“谁拉起了谁”这种问题树状视图比ps -ef逐行对PPID高效得多。不过htop需要额外安装CentOS是yum install htopUbuntu是apt install htop。生产环境没有htop时我一般靠ps -ef --forest来模拟树状视图效果也够用。4.4 前后台进程与作业控制终端里运行的进程可以分前后台。默认你在终端执行命令就是前台进程不能用终端做别的事。按CtrlZ可以挂起当前前台进程它会变成暂停状态T。用bg命令可以让暂停的任务在后台继续跑用fg命令把后台任务拉回前台。# 直接在后台启动程序 ./long_task.sh # 查看当前终端的作业列表 jobs # 把作业1拉回前台 fg %1 # 让作业2在后台继续运行 bg %2这里有个关键点用放到后台的进程它的标准输入仍然是这个终端。如果你退出终端这个进程可能会收到SIGHUP信号被挂断。想让它扛住终端退出继续在后台跑就得用nohup命令它会让进程忽略SIGHUP信号。更规范的做法是用setsid启动新会话彻底脱离终端控制这样进程就变成了真正的后台守护进程。我见过不少人把nohup和混着用其实nohup本身已经做了后台化不需要再加。不过加上也无妨稳妥一点。4.5 kill家族优雅与暴力kill命令听起来吓人其实它只是给进程发送一个信号进程收到信号之后怎么做取决于进程自己。最常用的信号信号值默认行为使用场景SIGTERM15终止进程kill默认信号优雅关闭SIGKILL9强制终止进程打死不退出时使用SIGHUP1终止进程终端挂断或让daemon重读配置SIGINT2终止进程CtrlC触发SIGSTOP19暂停进程不能被捕获或忽略SIGCONT18继续进程恢复被暂停的进程常规操作顺序应该是先kill PID发SIGTERM给进程一个做清理工作的机会比如关闭文件、释放资源、保存状态。等个几秒进程还在再kill -9 PID强制杀死。上来就kill -9虽然省事但可能丢失数据尤其是数据库、消息队列这类有状态的中间件强行杀死可能导致需要恢复日志或者数据不一致。实际运维中经常会遇到“PID是哪个进程占着端口”这种问题配合netstat或者ss命令就能快速定位# 查看端口8000被哪个进程占用 ss -lntp | grep 8000 # 或者用lsof lsof -i :8000输出里的PID就是你要找的目标直接kill就可以。5. 深入理解进程的亲缘关系5.1 进程树的形成PPID的妙用Linux的进程不是独立存在的它们天然形成一棵树。树根是系统启动时第一个进程旧内核是PID 1的init进程现代CentOS 7之后是systemd。这棵树的分支结构能帮你理清系统的运行逻辑。在真实的系统上排查问题时常需要用到“谁生了我”这个信息。举个例子你启动了一个Tomcat但在启动脚本里做了复杂的嵌套调用ps里看到一堆java进程分不清哪一个才是真正对外服务的。用ps -ef --forest一眼就看明白了脚本进程是根下面挂着它的子进程再往下是孙进程。还有种场景值得提一下守护进程的double-fork技巧。有些程序会fork一次让子进程再fork一次然后父进程和第一个子进程都退出第二个子进程继续运行。这么做的目的是让最终运行的进程变成孤儿被PID 1收养从而脱离会话和终端控制成为真正的后台守护进程。你如果看到某个进程的PPID是1且带着daemon的标记就可以判断它是这么来的。5.2 僵尸进程为什么必须处理僵尸进程本身不占CPU也不占内存只占一个进程表条目。但如果数量多了进程表会被塞满导致新进程创建失败表现为fork()返回ENOMEM或者EMFILE。我遇到过一个真实案例某个Java服务在多线程环境下fork子进程执行外部命令由于代码里没有正确调用waitpid回收僵尸进程越攒越多最后系统无法创建新进程服务直接瘫痪。btop或top里看到一堆Z状态的进程第一反应就是检查代码里是否有对应的wait/waitpid调用。核心思路谁fork谁负责回收。如果确实没法预测子进程什么时候退出可以用信号或者独立线程阻塞调用waitpid。如果你的程序架构比较老也可以用signal(SIGCHLD, SIG_IGN)来告诉内核子进程退出时自动回收不需要父进程主动wait。但这么做有个弊端你无法得知子进程的退出状态代码可维护性变差。我个人不太推荐这种偷懒做法。5.3 守护进程的写法写守护进程是Linux系统编程的基本功。最正统的写法有这几步#include stdio.h #include unistd.h #include stdlib.h #include sys/stat.h #include fcntl.h void daemonize() { pid_t pid fork(); if (pid 0) exit(-1); if (pid 0) exit(0); // 父进程退出 // 子进程成为会话首进程, 脱离终端 if (setsid() 0) exit(-1); // 第二次fork, 确保不是会话首进程 pid fork(); if (pid 0) exit(-1); if (pid 0) exit(0); // 切换工作目录 chdir(/); // 设置文件权限掩码 umask(0); // 标准输入输出重定向到/dev/null int fd open(/dev/null, O_RDWR); dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd 2) close(fd); }两步fork的核心原因第一次fork之后调用setsid()让子进程成为新的会话首进程彻底和终端脱离第二次fork是为了确保这个进程不是会话首进程防止它以后重新获取终端。umask(0)是清掉文件创建时的权限掩码避免继承父进程设定的某些限制。工作目录切到根目录也是为了避免占用某个挂载点导致无法卸载磁盘。实际开发中如果你用的是systemd托管服务这些步骤根本不需要自己写systemd的service文件可以指定WorkingDirectory、StandardOutput等。但了解这个过程对你理解系统层面的服务管理机制很有帮助。5.4 进程与线程的边界文章写到这里我猜你会关心线程。热搜词里出现了“进程和线程的区别”这是面试高频题也确实容易混淆。简单说线程是CPU调度的基本单位进程是资源分配的基本单位。同一个进程里的多个线程共享地址空间和文件描述符而多个进程各自独立。Linux里其实没有真正独立的线程系统调用。你调用pthread_create时内核通过clone系统调用创建一个“轻量级进程”它的地址空间和父任务共享。内核视角里线程和进程都叫任务task只是共享的资源范围不同。所以你在ps命令里能看到线程吗默认ps只显示进程但加上-L参数可以看到每个线程的轻量级进程号LWPps -eLf | grep java一个多线程的Java进程会有很多LWP行它们共享同一个PID。排查线程问题时比如CPU占用过高就要用top -H -p PID按线程级别看CPU消耗。既然进程是最小资源分配单位那进程内的多个线程出问题会互相影响吗会。比如一个线程内存越界写了坏地址触发段错误会把整个进程拖下水其他线程全挂。多少个线程就有多少条出错的可能。6. 进程管理的编排与监控组合拳6.1 用pstree看清层级遇到几十个进程相互嵌套的场景ps -ef看着费劲pstree就派上用场了。它把进程树直接画出来父子关系一眼可见pstree -ap | grep nginx如果是nginx你会看到master进程下挂着多个worker进程。这种结构很典型master负责管理、接收信号、拉起workerworker真正处理请求。理解了主从结构你给master发信号重载配置kill -HUP master_pidworker自动重新加载配置并重启这个操作就顺理成章了。另外推荐一个定时快照的思路排查疑似内存泄漏或者进程反复崩溃的问题时可以先写个脚本每5秒抓一次ps aux的输出存到文件里等个10分钟再分析。数据量不大但对定位问题很有价值。你光靠肉眼看top是不够的top的实时数据会刷过去没有对比就发现不了趋势。6.2 如何优雅地管理一组进程应用服务往往不止一个进程比如Nginx有master加workerJava服务可能有多个JVM实例数据库会有主从复制的多进程。逐个人工管理太原始常用的手段有用systemd管理服务。定义Unit文件指定ExecStart、ExecStop、Restart策略进程退出后能自动拉起崩溃了也能在系统日志里看到原因。这是目前Linux服务托管的标配。用supervisor管理Python/Node等解释型的常驻进程。配置简单、支持进程组批量启动停止适合小型团队自研服务的进程管理。用shell脚本批量操作。比如启动A、B、C三个服务写个start_all.sh里面依次执行nohup启动命令stop_all.sh里用pkill -f匹配进程名。这里有个坑pkill -f匹配的是完整命令行容易误杀比如你想杀“python server.py”结果把“python server_backup.py”也杀了。精确匹配用pkill -x或者pgrep -x或者用pid文件PID文件里存着服务启动时的PID来定向杀。6.3 进程监控工具的取舍Linux自带的工具组合虽然原始但足够强大。我在生产环境最常用的监控三板斧看负载和整体情况uptime、top看具体进程ps aux、pidstat看网络连接ss -s、ss -lntp如果你需要画趋势图可以用pidstat把采集到的数据导出成文本再用任意图表工具绘图。pidstat在sysstat包里安装sysstat后就能用# 每秒采集一次PID为1234的进程CPU和内存 pidstat -p 1234 1这里补充一个和进程管理强相关但容易踩坑的场景用户对热搜词里的“dpkg: 错误: 另外一个进程已经为 dpkg 前端锁 加锁”肯定不陌生。这就是同一个apt/dpkg操作重复执行导致的锁冲突。解决方法是找到那个占用锁的进程等它结束或者谨慎处理锁文件比如/var/lib/dpkg/lock-frontend。这里用到了所有前面讲的技能找到PID、看进程状态、确认是卡死还是正常执行、再决定是kill还是等待。7. 常见问题与排查技巧实录7.1 端口占用但找不到进程报错“端口被占用”是开发中最常见的。先看错误再执行ss -lntp | grep 8080 # 或者 lsof -i :8080如果输出为空可能是权限问题普通用户看不到其他用户的进程改用sudo执行。还有一种情况是进程监听了IPv6的地址:::8080ss输出里会出现别漏看。如果输出里出现了PID但kill之后端口还是被占用可能是进程变成了僵尸它还没被父进程回收所以端口没有释放。这时候要找到它的父进程把父进程处理掉僵尸的进程表条目才会清除。处理办法ps -o ppid -p 僵尸PID拿到PPID先合法退出父进程如果实在不行才kill -9父进程。7.2 进程杀了还在D状态和不可中断进程一个进程处于D状态不可中断睡眠时任何kill信号都无效包括SIGKILL。这会让很多运维新手崩溃总觉得是权限问题或者命令有问题。实际上卡在未完成的I/O上内核连SIGKILL都不会理。排查思路是先看它卡在哪个I/O上cat /proc/PID/stack cat /proc/PID/status # 看State行如果是NFS挂载点的I/O卡住可能是NFS服务端失联如果是本地磁盘可能是磁盘故障或者硬件排队太多。这种问题光靠kill解决不了得从底层I/O着手处理。极端情况下只能重启系统。D状态本身不是病只要数量不多、持续时间短、能自行恢复就不用管。持续出现大量D状态进程才需要警惕。7.3 为什么kill -9后端口还占用这其实和7.1是姊妹问题。kill -9杀掉进程后进程进入僵尸状态等待父进程回收。如果父进程设计不严谨一直没有wait僵尸就不会消失端口也就不释放。快速确认ps aux | grep defunct看出现defunct字样的进程。然后找到它的父进程正常情况下父进程退出后这个僵尸会被init收编并清理。如果父进程是PID 1systemd并且僵尸还在说明systemd也在处理中等几秒再看。这里有个技术细节值得分享如果你的C程序需要处理“子进程退出但父进程短时间无法wait”的情况可以捕获SIGCHLD信号在信号处理函数里调用waitpid这样子进程一退出就被回收不会产生僵尸。7.4 终端进程启动失败和WSL的诡异现象热搜词里有个“终端进程启动失败: 启动期间发生本机异常(无法启动 conpty)”这个更多是Windows上WSL或者Windows Terminal的坑和Linux本身关系不大。常见解法是检查终端的配置文件、重置或者更新组件。但有一个启发点即使是在Linux环境里如果终端启动失败也要先看是不是环境变量被改了比如PATH里少了/usr/bin导致系统找不到bash或者sh。WSL用户还会遇到“Linux删除文件后空间没释放”的问题。这其实是ext4虚拟磁盘镜像不会自动缩小的特性导致的不是删除失败。你删了文件文件系统内部释放了空间但挂载VHDX镜像仍然占用宿主机空间。解决方法是定期做一次磁盘压缩在Windows管理员的PowerShell里执行Optimize-VHD命令或者干脆重建磁盘镜像。这个问题的本质是理解虚拟磁盘和文件系统的层级关系不是进程管理的问题但排查时很容易让人误以为是某个进程还在占用文件。7.5 快速定位CPU占用高的线程Java服务CPU飙高时排查路径如下# 1. 找到Java进程PID top -b -n 1 | grep java # 2. 查看该进程下线程占用CPU情况 top -H -p PID # 3. 把线程十进制PID转十六进制 printf %x\n 线程PID # 4. 用jstack导出线程快照, 搜索十六进制线程号 jstack PID | grep -A 30 nid0x十六进制这套组合操作能直接定位到具体是哪个业务线程在空转或者死循环非常实用。核心思路就是进程级高CPU只能定位到进程必须下探到线程级别才能对症下药。8. 我的几条实践感悟进程这块内容表面上不难但真正用好需要不少实战积累。这里分享几条我自己的心得体会。第一写多进程程序时一定要把“谁负责回收子进程”想清楚。程序写得快不叫本事跑上一个星期不出僵尸才是本事。父子进程的职责边界从一开始就要明确不然出了问题你连从哪里查起都不知道。第二kill进程前先想一想它有没有子进程需要处理。比如你kill掉一个master进程它的worker进程怎么办有些框架会有父进程退出时自动带掉子进程的机制有些则不会留下孤儿进程在后台空跑。规范做法是逐层停止先停入口流量再优雅停止worker最后停master。第三多看/proc目录。你装个ps、top更好用但它本质上是读取/proc下的信息。想知道某个进程的详细信息就直接用cat /proc/PID/status查看。这个方法不受工具限制在任何Linux环境下都能用很多精简版容器里没有ps命令但/proc一定存在。第四进程管理和端口、文件描述符是联动的。一个进程卡住往往不只是进程本身的问题可能是它持有的文件、网络连接、锁没释放。排查时不要只看进程状态要看它关联的资源。lsof -p PID可以把这个进程打开的所有文件列出来经常能发现意想不到的线索。希望这篇文章能帮你把Linux进程这块底子打牢。进程是操作系统最核心的资源抽象之一弄懂它后续学进程通信、网络编程、并发模型都会顺畅得多。下一篇我会写进程间通信的常用手段包括管道、消息队列、共享内存和信号量到时候见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价