CPU性能篇:进程状态;进程组和会话;案例
倪朋飞 《Linux 性能优化实战》 学习笔记

进程状态说明 ===================================================================================================== 进程状态 top命令下进程状态 R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。 D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。 不可中断状态,表示进程正在跟硬件交互,为了保护进程数据和硬件的一致性,系统不允许其他进程或中断打断这个进程。进程长时间处于不可中断状态,通常表示系统有 I/O 性能问题。 Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。 僵尸进程,这是多进程应用很容易碰到的问题。 僵尸进程表示进程已经退出,但它的父进程还没有回收子进程占用的资源。短暂的僵尸状态我们通常不必理会,但进程长时间处于僵尸状态,就应该注意了,可能有应用程序没有正常处理子进程的退出。 正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源; 而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。 如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。 通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。 一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。 大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。 S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。 当进程等待的事件发生时,它会被唤醒并进入 R 状态。 I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。 硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。 要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。 T 或者 t,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。 向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped); 再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。 而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。 X,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。 -------------------------------------------------------------------------- ps命令中,进程状态更多 s 表示这个进程是一个会话的领导进程 + 表示前台进程组。

进程组和会话 ===================================================================================================== 进程组和会话。它们用来管理一组相互关联的进程 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员; 而会话是指共享同一个控制终端的一个或多个进程组。 比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

案例 docker run --privileged --name=app -itd feisky/app:iowait #我的虚拟机IO性能远超教程环境的虚拟机,所以想要复现需要修改容器内应用的C源码。。。 #我的虚拟机IO性能直接读取时,可以达到1660M;而教程环境只能达到34M。。。 top #观察平均负载、僵尸进程的数量、iowait、处于Z状态的具体进程 top -o S #按R sort后,可以发现僵尸进程不断增加的 dstat 1 10 #观察IO情况 pidstat -d -p 4344 1 3 #观察指定PID的IO数据 pidstat -d 1 20 #观察所有活跃进程的IO数据 strace -p 6082 #strace跟踪进程 ps aux | grep 6082 perf record -g #记录进程调用的信息到文件 perf report #根据文件进行分析,找到了iowait高的原因;接下去进一步分析容器中的C源码,修复源码。(根因在于C函数的调用,使该IO是直接对磁盘进行读写的,绕过了系统缓存) pstree -aps 3084 #使用pstree命令查看指定PID的PPID。(僵尸进程PPID也是容器,查看源码,根因在于源码中的wait()函数编写错误!导致了僵尸进程没有被及时回收。) ----------------------------------------------------------------------------------------------------------- [root@yefeng ~]# ps aux | grep /app root 2442 0.0 0.0 4500 588 pts/2 Ss+ 21:42 0:00 /app root 2621 0.0 0.8 70040 65624 pts/2 R+ 21:45 0:00 /app root 2622 0.0 0.8 70040 65624 pts/2 R+ 21:45 0:00 /app root 2624 0.0 0.0 112812 960 pts/1 R+ 21:45 0:00 grep --color=auto /app # 按下数字 1 切换到所有 CPU 的使用情况,观察一会儿按 Ctrl+C 结束 $ top top - 05:56:23 up 17 days, 16:45, 2 users, load average: 2.00, 1.68, 1.39 #平均负载( Load Average),过去 1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高; #而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。 Tasks: 247 total, 1 running, 79 sleeping, 0 stopped, 115 zombie #Tasks,有 1 个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理。 %Cpu0 : 0.0 us, 0.7 sy, 0.0 ni, 38.9 id, 60.5 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 0.0 us, 0.7 sy, 0.0 ni, 4.7 id, 94.6 wa, 0.0 hi, 0.0 si, 0.0 st #两个 CPU 的使用率情况,用户 CPU 和系统 CPU 都不高,但 iowait 分别是 60.5% 和 94.6%,好像有点儿不正常。 ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top 4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app #每个进程的情况, CPU 使用率最高的进程只有 0.3%,看起来并不高;但有两个进程处于 D 状态,它们可能在等待 I/O,但光凭这里并不能确定是它们导致了 iowait 升高。 4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app #iowait升高,处于D状态的进程就很可疑;D状态一般表示进程正在跟硬件交互 1 root 20 0 160072 9416 6752 S 0.0 0.1 0:38.59 systemd ... $ dstat 1 10 # 间隔1秒输出10组数据 You did not select any stats, using -cdngy by default. --total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai stl| read writ| recv send| in out | int csw 0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885 0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138 #dstat 的输出,我们可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135 0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177 0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144 0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147 0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131 0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168 0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134 [root@yefeng ~]# dstat 1 10 You did not select any stats, using -cdngy by default. ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai hiq siq| read writ| recv send| in out | int csw 0 15 83 0 0 2| 496M 67k| 0 0 | 0 0 |1237 348 0 0 100 0 0 0| 0 0 | 120B 134B| 0 0 | 90 157 0 0 100 0 0 0| 0 0 | 312B 118B| 0 0 | 93 158 0 51 37 0 0 13|1660M 0 |2915B 118B| 0 0 |4113 342 #案例才34M;而我的设备达到1660M,设备性能差太多了,无法复现iowait 0 22 74 0 0 4| 900M 0 |5950B 150B| 0 0 |2473 340 0 0 100 0 0 0| 0 0 |5726B 134B| 0 0 | 84 154 0 1 100 0 0 0| 0 0 |4507B 254B| 0 0 | 91 159 0 0 100 0 0 0| 0 0 | 180B 134B| 0 0 | 58 123 1 50 37 0 0 14|1641M 0 | 60B 118B| 0 0 |3867 236 0 23 74 0 0 3| 919M 0 | 245B 134B| 0 0 |2425 377 0 0 100 0 0 0| 0 0 |5749B 134B| 0 0 | 87 152 $ top # 观察一会儿按 Ctrl+C 结束 ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top #再次使用top进行观察 4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app #两个 D 状态的进程,PID 分别是 4344 和 4345 4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app ... $ pidstat -d -p 4344 1 3 # -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据 06:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:38:51 0 4344 0.00 0.00 0.00 0 app #kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(单位是时钟周期)。 06:38:52 0 4344 0.00 0.00 0.00 0 app #它们都是 0,那就表示此时没有任何的读写,说明问题不是 4344 进程导致的。 06:38:53 0 4344 0.00 0.00 0.00 0 app #pidstat -d -p 4344 1 3命令没有发现IO问题 #那么干脆观察所有进程的 I/O 使用情况 $ pidstat -d 1 20 # 间隔 1 秒输出多组数据 (这里是 20 组) ... 06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1 06:48:47 0 6080 32768.00 0.00 0.00 170 app #观察一会儿可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 32 MB,看来就是 app 的问题。 06:48:47 0 6081 32768.00 0.00 0.00 184 app 06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:48 0 6080 0.00 0.00 0.00 110 app 06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:49 0 6081 0.00 0.00 0.00 191 app 06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:51 0 6082 32768.00 0.00 0.00 0 app 06:48:51 0 6083 32768.00 0.00 0.00 0 app 06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:52 0 6082 32768.00 0.00 0.00 184 app 06:48:52 0 6083 32768.00 0.00 0.00 175 app 06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:53 0 6083 0.00 0.00 0.00 105 app $ strace -p 6082 #strace 正是最常用的跟踪进程系统调用的工具。 strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted #strace 命令居然失败了,并且命令报出的错误是没有权限。(因为是僵尸进程,strace失败是合理的) $ ps aux | grep 6082 root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct> #进程 6082 已经变成了 Z 状态,也就是僵尸进程。 $ perf record -g $ perf report 找到我们关注的 app 进程,按回车键展开调用栈, 可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。 iowait高问题修复:在C源码文件中,删除open函数的 O_DIRECT 这个选项 -------------------------------------------------------------------------------------------- 僵尸问题修复: 既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,也就是找出父进程,然后在父进程里解决。 $ pstree -aps 3084 # -a 表示输出命令行选项 # p表PID # s表示指定进程的父进程 systemd,1 └─dockerd,15006 -H fd:// └─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml └─docker-containe,3991 -namespace moby -workdir... └─app,4009 #运行完,你会发现 3084 号进程的父进程是 4009,也就是 app 应用。 └─(app,3084) #####所以,我们接着查看 app 应用程序的代码,看看子进程结束的处理是否正确,比如有没有调用 wait() 或 waitpid() ,抑或是,有没有注册 SIGCHLD 信号的处理函数。 [root@yefeng ~]# pstree -asp 4650 systemd,1 --switched-root --system --deserialize 22 └─dockerd-current,2231 --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd--userland-proxy-path=/usr/libex └─docker-containe,2237 -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd- └─docker-containe,3326 c8ce606ec915524021c53599bb6b08096e5a114432647c2d06e2901b0859f503/var/run/docker/libcontainerd/c8ce606ec915524021c53599bb6b08096e5a114432647c2d06e2901b0 └─app,3343 └─(app,4650) 僵尸问题的根因在于C源文件中虽然写了 wait() 函数,但是并不是在合适的地方,导致wait函数实际一直没有生效,即缺乏了子进程的回收机制 所以修复过程也就是修复源码

iowait升高、僵尸进程问题的小结: ======================================================================================================================================== iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度。 碰到 iowait 升高时: 需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。 等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态(即不可中断状态)的进程,多为可疑进程。但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。 这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。 僵尸进程的问题: 相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查 wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战