第四章 进程管理稳定性:进程生命周期、进程优先级与调度策略、OOM Killer机制、进程组与cgroup
各位同学,今天我们聊聊进程管理。说实话,Android系统稳定性出问题,十有八九跟进程管理脱不了干系。我这些年排查的稳定性问题里,进程相关的至少占了一半。你想想看,一个系统里几百个进程在跑,谁先谁后,谁生谁死,这要是没个规矩,不乱套才怪。
4.1 进程生命周期——从生到死的完整旅程
进程的生命周期,说白了就是五个状态:创建、就绪、运行、阻塞、终止。嗯,这里要注意,Android里还有个特殊的"已停止"状态,这个我们后面再说。
核心要点:进程状态转换的稳定性风险,往往出现在状态切换的边界条件上。
我在项目中遇到过一个问题:某个后台服务进程频繁地在"运行"和"阻塞"之间切换,每次切换都要做上下文保存和恢复。你猜怎么着?这个进程一天之内切换了上百万次,直接把CPU时间片都耗在了切换上,系统响应变得奇慢无比。
进程创建时,fork()系统调用会复制父进程的地址空间。我个人习惯在创建进程后立即检查返回值,因为fork失败的情况虽然少见,但一旦发生,后果很严重。
// 进程创建的标准写法
pid_t pid = fork();
if (pid == -1) {
// 创建失败,需要处理
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
// 记得关闭不必要的文件描述符
close_unused_fds();
// 执行实际工作
do_work();
} else {
// 父进程
// 等待子进程结束,避免僵尸进程
waitpid(pid, &status, 0);
}
避坑指南:我曾经见过一个案例,父进程fork后没有调用wait,子进程结束后变成了僵尸进程。系统里积累了上千个僵尸进程,最终导致进程表溢出,新进程无法创建。记住:每个fork都要对应一个wait。
4.2 进程优先级与调度策略——谁先谁后,谁多谁少
Android的进程优先级,从高到低大致是:前台进程、可见进程、服务进程、后台进程、空进程。但实际调度时,Linux内核用的是完全公平调度器(CFS),它不看优先级数字,而是看"虚拟运行时间"。
为什么会这样?因为优先级数字是静态的,但进程的行为是动态的。CFS的思路很简单:让每个进程都能公平地获得CPU时间。你想想看,如果一个高优先级进程一直在运行,低优先级进程永远得不到CPU,那系统就卡死了。
| 进程类型 | 优先级范围 | 典型场景 | 稳定性风险 |
|---|---|---|---|
| 前台进程 | -20 ~ -5 | 当前交互的App | 优先级反转 |
| 可见进程 | -4 ~ 0 | 可见但非前台 | 资源饥饿 |
| 服务进程 | 1 ~ 5 | 后台Service | 被误杀 |
| 后台进程 | 6 ~ 10 | 不可见Activity | 频繁创建销毁 |
| 空进程 | 11 ~ 19 | 无组件的进程 | 容易被回收 |
我记得有一次,一个视频播放App在播放时突然卡顿。排查后发现,是另一个后台进程在疯狂做垃圾回收,占用了大量CPU。解决方案很简单:给播放线程设置实时优先级,确保它不会被其他线程抢占。
// 设置线程优先级
struct sched_param param;
param.sched_priority = 99; // 实时优先级
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
// 需要CAP_SYS_NICE权限
perror("sched_setscheduler failed");
}
个人经验:设置实时优先级要谨慎。我曾经在一个项目中给所有音频处理线程都设了实时优先级,结果导致系统UI线程得不到CPU,界面直接卡死。记住:实时优先级是双刃剑,用好了是神器,用不好是灾难。
4.3 OOM Killer机制——谁该为内存不足买单
OOM Killer,说白了就是系统内存不够时,选一个进程杀掉。但选谁呢?Linux内核有一套复杂的评分机制,综合考虑进程的内存占用、运行时间、优先级等因素。
Android对OOM Killer做了定制,引入了lmk(Low Memory Killer)。它比标准OOM Killer更智能,会根据内存压力等级,选择不同优先级的进程来杀掉。
// 查看OOM评分
// 每个进程的oom_score可以在/proc/[pid]/oom_score中查看
adb shell cat /proc/1234/oom_score
// 调整OOM评分,值越大越容易被杀
adb shell echo -1000 > /proc/1234/oom_score_adj
嗯,这里要注意,oom_score_adj的取值范围是-1000到1000。负值表示不容易被杀,正值表示容易被杀。系统核心服务通常设为-1000,确保它们不会被OOM Killer误杀。
我曾经踩过的坑:有一次,系统总是莫名其妙地重启。排查了三天,最后发现是某个系统服务被OOM Killer杀了。为什么?因为这个服务在启动时申请了大量内存,oom_score_adj设得不够低。从那以后,我养成了一个习惯:所有关键服务启动后,第一件事就是检查并调整oom_score_adj。
4.4 进程组与cgroup——资源隔离的艺术
进程组,就是把多个进程放在一个组里统一管理。cgroup(Control Groups)则是Linux内核提供的资源隔离机制。Android从4.3开始引入cgroup,用来限制App的资源使用。
cgroup可以限制的资源包括:CPU使用率、内存使用量、I/O带宽、网络带宽等。每个App启动时,系统会为它创建一个cgroup,设置好资源上限。
// cgroup的目录结构示例
/sys/fs/cgroup/
├── cpu/
│ ├── app1/ # App1的CPU限制
│ ├── app2/ # App2的CPU限制
│ └── ...
├── memory/
│ ├── app1/ # App1的内存限制
│ ├── app2/ # App2的内存限制
│ └── ...
└── blkio/
├── app1/ # App1的I/O限制
├── app2/ # App2的I/O限制
└── ...
我个人习惯在排查性能问题时,先检查cgroup配置。因为很多时候,App卡顿不是因为代码写得不好,而是cgroup限制了它的资源。
核心要点:cgroup的稳定性价值在于:当一个App出现内存泄漏或CPU异常时,cgroup可以把它隔离起来,不让它影响其他App。这就是"故障隔离"的思想。
我记得有一次,一个游戏App因为内存泄漏,占用了2GB内存。如果没有cgroup限制,它会把整个系统的内存都吃光,导致所有App都被OOM Killer杀掉。但因为cgroup限制了它的内存上限,系统只是杀掉了这个游戏App,其他App正常运行。
最后说一句:进程管理稳定性的核心,就是"隔离"和"优先级"这两个词。隔离保证了一个进程的故障不会扩散,优先级保证了关键进程能获得足够的资源。理解了这两点,你就掌握了进程管理稳定性的精髓。