1. 先说清楚:进程到底是个什么东西
很多刚接触Linux的朋友,第一个绕不过去的概念就是“进程”。我当年学的时候也很懵:老师讲“进程是程序的一次执行”,听完感觉懂了,真到命令行里一敲ps,满屏的PID、TTY、TIME,立刻又不知道自己在看什么了。
不妨换个角度来理解:程序是静态的,它就是硬盘上一个文件,比如/usr/bin/python3,你双击或者敲命令之前,它什么都不干。进程是动态的,是你把程序加载到内存里、让CPU去跑它的那一整套运行状态。打个比方,程序是菜谱,进程是照着菜谱做菜的过程。菜谱可以复印无数份,同一个程序也可以同时启动多个进程,彼此互不干扰。
Linux系统里,进程是资源分配的基本单位。CPU时间、内存空间、打开的文件、网络连接,全都挂在进程名下。所以学会查看进程、理解进程的状态、知道怎么创建和管理进程,基本上就是掌握了“看透这台机器在干什么”的能力。这篇博文从零开始,带你把Linux进程的“查看”和“创建”这两件事彻底搞明白,所有命令我都在Ubuntu 22.04上实测过,CentOS 7/8、Debian等主流发行版也通用。
适合谁看?刚装好Linux虚拟机、想在命令行里找感觉的新手,学操作系统课但对着理论概念一脸茫然的学生,以及日常工作需要登录服务器排查问题的运维和开发。只要你能打开终端,这篇文章就能跟着走完。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 查看进程:先学会看清系统里正在发生什么
2.1 ps命令:最基础也最常用的进程快照
ps是Process Status的缩写,作用是把当前系统里的进程状态拍一张“快照”给你看。要注意,它默认只显示当前终端会话里的进程,想看到全系统的进程,必须加参数。
我日常用得最多的组合是ps -ef和ps aux,两个都能看全部进程,只是输出格式略有差异:
bash复制# 方式一:System V风格
ps -ef
# 方式二:BSD风格
ps aux
ps -ef的输出大概长这样:
bash复制UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:14 ? 00:00:03 /sbin/init splash
root 214 1 0 09:14 ? 00:00:00 /lib/systemd/systemd-journald
user 1024 987 0 09:20 pts/0 00:00:00 bash
user 1876 1024 0 10:02 pts/0 00:00:00 ps -ef
每一列的含义如下:
- UID:启动这个进程的用户。看到root就要多留个心眼,确认是不是自己启动的。
- PID:进程ID,整个系统里唯一标识一个进程的数字。
- PPID:父进程ID,也就是“谁创建了我”。这个字段排查问题非常有用,后面创建进程的小节会细说。
- C:CPU占用率的粗略估算值。
- STIME:进程启动时间。
- TTY:进程关联的终端。
?表示没有关联终端,通常是后台服务或守护进程。 - TIME:进程累计消耗的CPU时间,不是运行时长。
- CMD:启动这个进程的命令行。
ps aux多了%CPU、%MEM、VSZ、RSS这些列,偏向于看资源占用。想找某个具体进程,直接配合grep过滤:
bash复制ps -ef | grep nginx
但这样会把grep自己那一行也带出来,强迫症看着难受。稳妥一点的做法:
bash复制ps -ef | grep nginx | grep -v grep
或者干脆用pgrep,直接只输出PID:
bash复制pgrep -a nginx
-a参数会同时显示命令名,实战里比grep -v grep更干净。
2.2 进程状态STAT:那一堆字母到底在说什么
ps aux输出里有一个STAT列,新手经常被它吓到,一看到R、S、D、Z就开始查文档。其实这些状态一点都不神秘,它们是进程当前“活”到什么阶段的标记:
| 状态码 | 含义 | 说明 |
|---|---|---|
| R | Running/Runnable | 正在运行或排在运行队列里,不一定真的占着CPU |
| S | Sleeping | 可中断睡眠,在等某个事件,比如等用户输入、等网络数据 |
| D | Uninterruptible Sleep | 不可中断睡眠,通常在等磁盘I/O,这种状态很难被杀掉 |
| T | Stopped | 被暂停了,比如按了Ctrl+Z |
| Z | Zombie | 僵尸进程,进程已结束但父进程还没回收它的退出状态 |
| I | Idle | 内核线程专用的一种空闲状态,可以忽略 |
状态后面偶尔还会跟一些附加字母,比如S+里的+表示这个进程在前台进程组里,Ss里的s表示它是会话领导者。新手阶段不需要全部记住,先认识两个关键状态就够用了:
Z(僵尸)一定要警惕。出现僵尸进程说明父进程没有调用wait()来回收子进程的退出码,虽然僵尸进程本身不占CPU和内存,但它会占着PID和进程表项。PID是有限资源,僵尸多了系统可能会卡。后面讲到清理进程时,我会说说怎么处理它。D(不可中断睡眠)也不能忽视。进程卡在磁盘I/O上时会出现这个状态,kill -9都杀不掉,因为内核不允许在I/O操作中途打断它。遇到D状态进程多的机器,先检查磁盘是不是出问题了,而不是急着杀进程。
2.3 top/htop:动态刷新,像“任务管理器”一样看系统
ps是快照,适合抓瞬时状态。但如果你想持续观察CPU、内存的变化,或者想找“到底是哪个进程在疯狂吃CPU”,就得用top。
top是Linux自带的动态进程监视器,默认每3秒刷新一次。进入界面后,上半部分是系统概况:负载均衡、CPU使用率、内存和交换分区使用情况;下半部分是进程列表,默认按CPU占用率排序。
刚开始用top的人最容易问的问题是:CPU那行显示的一堆百分比是什么意思?
bash复制%Cpu(s): 5.2 us, 1.3 sy, 0.0 ni, 93.2 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st
- us:用户空间进程占用的CPU
- sy:内核空间占用的CPU
- ni:被调整过优先级的进程占用的CPU
- id:空闲CPU
- wa:等待I/O完成的CPU时间,这个值高说明磁盘或网络是瓶颈
- hi/si:处理硬件/软件中断消耗的CPU
- st:被虚拟机管理程序“偷走”的CPU时间,云服务器上这个值高说明宿主机资源紧张
top启动后还有一些常用交互快捷键:
P:按CPU使用率排序M:按内存使用率排序k:输入PID杀掉进程(会提示你输入信号编号,默认15是正常终止)q:退出
htop是top的增强版,界面彩色、支持鼠标操作、可以树状显示进程关系。Ubuntu/Debian安装很简单:
bash复制sudo apt install htop
CentOS系用yum install htop或dnf install htop。htop的F5键可以切换到树形视图,父子进程关系一眼就能看清,排查问题比top直观不少。我的个人习惯是:服务器上快速看一眼用top,本机或者有图形界面的环境用htop,两个结合着来。
3. 创建进程:从命令行到系统内核的完整链路
3.1 前台进程与后台进程:&符号和Ctrl+Z的用法
在终端里敲一条命令,不加任何特殊处理,它就在前台运行。前台进程会霸占当前终端,你什么命令都敲不了,只能等它跑完。
bash复制sleep 100
这条命令会“睡”100秒,期间终端完全卡住。想恢复操作,要么等它结束,要么按Ctrl+C直接终止。这就是前台进程最直观的体验。
有的程序运行时间长,我们又想在等待期间继续干别的,这时就需要把进程放到后台去:
bash复制sleep 100 &
命令末尾加一个&符号,shell会立刻返回一个提示:
bash复制[1] 2560
[1]是shell给这个后台任务的编号,2560是进程PID。输入jobs可以查看当前shell的所有后台任务:
bash复制jobs -l
-l参数会同时显示PID。
想把一个正在前台运行的进程变成后台运行,先按Ctrl+Z暂停它,然后输入bg让它到后台继续跑:
bash复制# 1. 运行一个长时间命令
sleep 300
# 2. 按 Ctrl+Z,终端显示已停止
# [1]+ Stopped sleep 300
# 3. 让它去后台继续运行
bg
反过来,想把后台任务调回前台,用fg命令:
bash复制fg %1
%1是任务编号。需要说明的是,&和Ctrl+Z创建的后台任务,跟终端绑定在一起。终端一关,这些进程通常也会收到挂断信号(SIGHUP)而退出。想突破这个限制,就得用下面要介绍的nohup。
3.2 fork与exec:进程诞生的底层原理
在命令行里敲命令创建进程很容易,但很多人不清楚系统底层到底发生了什么。这里简单讲一下Linux创建进程的核心机制,懂了它你才能理解为什么进程会有父子关系,也才能理解后面讲的僵尸进程。
Linux创建新进程大体分两步走:
fork():内核把当前进程复制出一份几乎完全一样的副本,复制出来的叫子进程,原来的叫父进程。子进程拥有独立的PID,但继承了父进程的内存内容、文件描述符、环境变量等。exec():子进程把自己“改头换面”,用新的程序替换掉从父进程继承来的内存映像,从此变成一个全新的程序。
举个例子,你在bash里输入ls,bash先fork()出一个子进程,这个子进程里跑的代码还全是bash的,紧接着执行exec(),把/usr/bin/ls这个程序加载进来,子进程就变成了ls。
这就是为什么ps -ef里总是能看到进程有PPID——每个进程背后都有一个“父”在创建它。系统的第一个进程是PID 1,通常是systemd或init,它是所有用户进程的祖先,直接或间接地“繁衍”了整棵进程树。
需要补充的是,fork()复制子进程用的是“写时复制”(Copy-On-Write)技术,并不是把父进程内存完整复制一遍。刚fork完时,父子进程共享同一块物理内存,只有当某一方要修改数据时,内核才真正复制。所以创建进程的开销远比你想象的小,这就是为什么Linux能轻松跑出几百上千个进程。
3.3 nohup与setsid:让进程摆脱终端的束缚
实际工作中,我们经常通过SSH登录服务器执行任务,跑完就退出。如果任务还没结束怎么办?最常用的方案就是nohup。
bash复制nohup ./my_script.sh > output.log 2>&1 &
拆开看:
nohup:让进程忽略SIGHUP信号。终端断开时,系统会给这个终端相关的进程发送SIGHUP,nohup让进程不理会它,也就不会误退出。> output.log:把标准输出重定向到文件。没有这步的话,nohup默认会写入当前目录的nohup.out文件,临时跑没问题,长期任务最好自己指定日志文件。2>&1:把标准错误也重定向到同一个地方。不加上这一步,程序报错信息可能输出到别处,排查问题很痛苦。&:放到后台运行。
还有一类场景,进程不仅要摆脱终端,还要彻底脱离当前会话(Session)和进程组的束缚,这时用setsid更彻底:
bash复制setsid ./my_script.sh
setsid会让新进程开启一个新的会话,它跟当前终端、当前会话完全没关系了,哪怕整个SSH连接断开,它也会继续运行。对比一下:
nohup只是忽略挂断信号,进程仍然属于原来的会话。setsid是让进程“另立门户”,成为新会话的领导进程。
顺带提一句,如果你希望把nohup启动的进程做成开机自动运行的系统服务,正确做法是写systemd service单元,而不是继续用nohup。systemd能管理进程的启停、崩溃自动重启、日志收集,比裸跑后台进程靠谱得多。
4. 实操演练:用一个真实场景串联所有知识点
4.1 场景设定:写一个“不听话”的测试脚本来练手
光讲理论记不住,下面用一个小练习把前面的知识全部串起来。假设我们在自己的用户目录下写一个Python脚本,模拟一个耗时的工作任务:
python复制#!/usr/bin/env python3
import time
print("任务已启动,PID:", end="")
import os
print(os.getpid())
for i in range(30):
print(f"第 {i+1} 秒,工作中...")
time.sleep(1)
print("任务全部完成")
给它加上执行权限并运行:
bash复制chmod +x work.py
./work.py
这时进程在前台运行,每秒打印一行。我们拿这个简简单单的场景,依次验证前面讲的查看和创建进程的操作。
4.2 在另一个终端里“抓住”这个进程
脚本在前台跑着,我们打开一个新的SSH连接或新的终端窗口,尝试把这个进程找出来:
bash复制ps -ef | grep work.py | grep -v grep
输出类似:
bash复制user 3120 2560 0 10:30 pts/0 00:00:00 /usr/bin/python3 ./work.py
关键信息:
- PID是3120
- PPID是2560,也就是bash的PID
- TTY是pts/0,说明它确实关联在第一个终端上
再执行:
bash复制ps -o pid,ppid,stat,cmd -p 3120
看看STATE列。运行期间的Python脚本状态通常是S(睡眠)或R(运行),因为它在time.sleep()和打印输出之间来回切换。
这边继续用top观察,按P按CPU排序。sleep为主的脚本CPU占用很低,这是正常的,别以为脚本卡住了。如果脚本里有密集计算,CPU才会明显高起来。
4.3 彻底跑一把:同时体验前台、后台和脱离终端
在这个流程里,你可以按顺序做下面几个操作,每一步都回头用ps -ef验证进程状态的变化:
- 先正常在前台运行
./work.py,另一个终端里确认PID和状态。 - 回到运行脚本的终端,按
Ctrl+Z暂停它,用ps查看STAT变为T。 - 输入
bg让它转入后台运行,STAT从T切回S。 - 输入
jobs -l,能看到任务编号和PID。 - 然后模拟断开的情况:直接关掉这个终端窗口,再在新终端里
ps -ef | grep work.py,你会发现进程已经没了——这就是SIGHUP的威力。
最后,再用nohup跑一次,体验区别:
bash复制nohup ./work.py > work.log 2>&1 &
记录输出的PID,然后直接把终端关掉。重新打开一个终端,执行:
bash复制ps -ef | grep work.py | grep -v grep
tail -f work.log
你会看到脚本依然在运行,日志还在持续写入。这就是nohup的作用:让进程忽略挂断信号,笃定地离开终端也能存活。
4.4 用pstree观察进程树结构
如果安装了pstree(Ubuntu/Debian一般自带,没有就sudo apt install pstree),还能以树形图看进程关系:
bash复制pstree -p | grep -A 2 work.py
输出会展示bash作为父进程,下面是python3子进程。这个命令能帮你直观建立“父子进程”的心理模型,理解为什么PID、PPID这两个字段这么重要。
5. 清理进程与应对异常:实操中避不开的坑
5.1 kill、kill -9和pkill的正确使用姿势
查到PID之后,想要终止进程,最常用的是kill命令:
bash复制kill 3120
不带参数时,kill发送的是SIGTERM(信号15),这是“请你正常退出”的信号。程序收到SIGTERM后可以做清理工作,比如保存文件、关闭连接,然后自己结束。这是最温和、最推荐的方式。
如果进程不理会SIGTERM(可能是程序写得不规范,也可能是卡死在不可中断状态),那就需要动用更暴力的手段:
bash复制kill -9 3120
-9对应SIGKILL信号,内核直接强制终止进程,程序完全没有机会做任何清理。能用SIGTERM就不用SIGKILL,这是我的经验之谈。直接kill -9数据库、redis这类有持久化需求的进程,最坏情况会损坏数据文件。
按名字批量杀进程用pkill:
bash复制pkill -9 work.py
按完整命令行过滤可以用pkill -f:
bash复制pkill -9 -f "python3 ./work.py"
不过pkill -f容易误伤,因为它是子串匹配。比如pkill -f work会干掉所有命令行里含“work”字样的进程,生产环境要格外小心,先pgrep -a -f work确认再动手。
5.2 僵尸进程和孤儿进程到底是怎么回事
僵尸进程(Zombie)和孤儿进程(Orphan)是面试高频考点,也是实战中让人头疼的问题。两者容易混淆,这里一次性讲清楚。
僵尸进程:子进程先结束,父进程还在运行,但父进程没有调用wait()系统调用来“收尸”,子进程的退出状态就一直残留在内核的进程表里。用ps看,它的STAT是Z,命令行后面还会跟一个<defunct>标记。僵尸进程不消耗CPU和内存,但占据着PID。
危害在于,PID是有上限的,如果父进程一直不回收,僵尸进程会越积越多,最终可能导致系统无法创建新进程。
有一个常见的坑:父进程明明只是个普通脚本,怎么也会产生僵尸?因为很多脚本语言调用外部命令时,如果语言运行时没处理好wait,子进程的回收就会滞后。遇到一堆僵尸进程,先找它的PPID是谁,再决定怎么办:
- 如果PPID是PID 1(systemd),那说明父进程已经退出,systemd会负责回收,僵尸一般不会残留太久。
- 如果PPID是某个一直运行的进程,那多半是那个父进程写得不合格。临时解法是杀掉父进程,让僵尸进程变成孤儿进程,由PID 1收养并清理。根治方法是修复父进程代码,在fork之后正确调用wait或使用信号处理机制。
孤儿进程:父进程先退出,子进程还在运行。Linux不会让这些子进程变成“没妈的孩子”,内核会把它们重新“过继”给PID 1(systemd/init),由它接管。所以孤儿进程本身不会造成资源泄漏,你甚至经常能见到孤儿进程一直跑着正常工作。
5.3 进程资源占用异常的排查思路
排查“系统卡顿”是我被问得最多的场景之一。分享一个标准排查流程,按顺序执行,基本能定位90%的问题:
- 执行
top,按P看CPU占用最高的进程,按M看内存占用最高的进程。 - 确认异常进程后,如果是自己启动的程序,先确认它是不是本身就该这么吃资源;如果不是,用
ps -ef | grep PID查看它的启动命令、工作目录、环境变量。 - 用
lsof -p 进程PID查看这个进程打开了哪些文件,高CPU进程通常伴随日志文件无限写入或临时文件泄漏。 - 执行
free -h看内存是否吃紧,df -h看磁盘是否写满,这两个都是系统卡顿的常见元凶。 - 如果进程状态大量是
D,优先检查磁盘I/O,iostat -x 1(需sysstat包)看await是否异常。 - 最后才决定是否终止进程,不要一上来就
kill -9。
这几步走下来,既能把异常的进程清理掉,也能避免误杀正常服务。
6. 进程管理的进阶延伸:优先级、监控与日志
6.1 nice值:让进程“谦让”一些
Linux进程的CPU调度不是完全平等的,每个进程有一个“优先级”的属性,通过nice值来体现,范围是-20到19。nice值越低,优先级越高,能分到的CPU时间越多;反之则是“友好地让出CPU”。
普通用户启动进程时,可以用nice指定nice值:
bash复制nice -n 10 ./work.py
把nice值设为10,相当于告诉内核:这个进程不太重要,CPU资源先让给别的进程。
进程已经在运行了想调整,用renice:
bash复制renice -n 5 -p 3120
注意,普通用户只能调高nice值(让进程更谦让),调低(提高优先级)通常需要root权限,防止有人恶意抢占CPU。
查看所有进程的nice值,ps -l能看到NI列,top里也能看到。这个功能在有限资源环境下很实用,比如服务器上同时跑着业务服务和一些不怎么重要的批处理任务,把批处理任务的nice值调高,能减少对核心服务的影响。
6.2 快速定位进程的工作目录和启动命令
排查问题时经常遇到一种情况:知道PID,但不清楚这个进程是从哪个目录启动的。这时用/proc文件系统最方便:
bash复制# 查看进程的启动命令
cat /proc/3120/cmdline
# 查看进程的工作目录
ls -l /proc/3120/cwd
# 查看进程的可执行文件链接
ls -l /proc/3120/exe
输出示例:
bash复制# cmdline
/usr/bin/python3./work.py
# cwd
/home/user/test -> /home/user/test
# exe
/usr/bin/python3.10 -> /usr/bin/python3.10
/proc是Linux内核暴露给用户空间的一个虚拟文件系统,每个运行中的进程都有一个以PID命名的目录。在Linux上排查问题,/proc迟早要学,它是理解进程内部状态的“万能钥匙”。
命令格式方面需要留意:/proc/PID/cmdline里参数之间用\0分隔而不是空格,直接用cat看会挤在一起。想看得清楚可以用:
bash复制tr '\0' ' ' < /proc/3120/cmdline
echo
配合/proc还能直接查看进程的环境变量:
bash复制cat /proc/3120/environ | tr '\0' '\n'
这招在排查“为什么这个进程配置不对”时特别好用,可以直接看到它继承了哪些环境变量。
6.3 systemd服务:现代Linux管理进程的“正规军”
聊到进程管理,绕不开systemd。目前主流Linux发行版(Ubuntu 16.04+、CentOS 7+、Debian 8+)都用systemd作为初始化系统,它不仅是开机启动的第一个进程(PID 1),还统一管理着系统上绝大多数后台服务。
几个最常用的systemd命令:
bash复制# 查看服务状态
systemctl status nginx
# 启动/停止/重启服务
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
# 设置开机自启
systemctl enable nginx
# 查看所有正在运行的服务
systemctl list-units --type=service --state=running
systemd管理的服务进程,本质上也是用前面说的fork/exec机制创建的,只是多了很多进程生命周期管理的能力。比如Restart=always能让服务崩溃后自动重启,LimitNOFILE能调高文件描述符上限,Environment=可以给服务注入配置。
我自己写定时任务或者常驻脚本时,第一选择就是写systemd service,而不是nohup。原因有三个:
- 崩溃自动重启,不用人工盯
- 开机自启,断电重启后服务能自己恢复
- 日志统一交给journald,用
journalctl -u 服务名就能查,不用自己管理日志文件
写个最简单的service单元:
ini复制[Unit]
Description=My Work Script
[Service]
ExecStart=/home/user/work.py
Restart=always
User=user
[Install]
WantedBy=multi-user.target
保存到/etc/systemd/system/work.service,然后:
bash复制sudo systemctl daemon-reload
sudo systemctl start work
sudo systemctl enable work
systemctl status work
就能看到一个由systemd托管的常驻进程,比nohup优雅得多。
7. 最后再分享几个实战中的小技巧
学完前面这些,你已经能应对日常90%的进程管理场景了。最后分享几个我实际工作中踩过坑总结出来的小技巧。
第一,kill -9前先思考十秒。很多新手遇到进程不听话,上来就是kill -9。但生产环境里,这可能导致数据丢失、服务状态不一致、集群脑裂。正确的优先级是:先试kill(SIGTERM),10秒后看看进程退没退,实在不行再kill -9。数据库类的进程,更是要慎重。
第二,日志文件特别大时,别用tail -f直接看。先用ls -lh 日志文件看看文件大小,超过几百MB就别直接用tail -f了,用tail -n 100看最后100行,或配合grep过滤关键字,否则整台服务器都可能被拖卡。
第三,排查CPU飙高时,别只盯着top的进程列表。如果一个java或python进程CPU很高,用top -H -p PID可以查看这个进程内部的线程占用情况,再配合jstack或py-spy dump出线程栈,才能定位到具体是哪一行代码在死循环。光杀进程是治标不治本。
第四,善用timeout命令给命令设置“死限”:
bash复制timeout 30 ./work.py
这条命令如果30秒内没跑完,就会被自动终止。写脚本调接口、跑批量任务时非常实用,可以有效防止命令因为等待外部服务而无限挂起。
进程管理这件事,表面上是一堆命令的堆砌,实际上考验的是你对“计算机里正在发生什么”的理解。希望这篇内容能帮你把ps输出的那一串数字,变成一台台看得见、摸得着的运行实体。
