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 信号处理函数的注册就行了。
iowait升高、僵尸进程问题的小结
复制代码

 

posted @   雲淡風輕333  阅读(97)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示