写在之前:此文翻译自:https://peteris.rocks/blog/htop,做了少许改动,感谢原作者。上篇见《Linux性能利器Htop:完胜top、strace》。
长久以来,我只知Linux有个神器htop,却不知道htop的各项指标的内涵。
比如,2核的服务器 CPU利用率为 50%,那为啥load average 却显示 1.0?那接下来开始捋捋。。。
  • Process state
接下来看下htop中进程状态列,其用字母 S 表示。
下面是进程列的可能取值:
R运行状态(running)或者运行队列中的就绪状态(runnable)
S中断睡眠(等待事件完成)
D非中断睡眠(常为IO)
Z僵尸进程,无效进程但是未被父进程回收
T被控制信号停止
t跟踪时被调试者停止
X死亡状态
注意,当你运行ps时,它将也会显示子状态,比如Ss,R+,Ss+,等等.
$ ps x  PID TTY      STAT   TIME COMMAND 1688 ?        Ss     0:00 /lib/systemd/systemd --user 1689 ?        S      0:00 (sd-pam) 1724 ?        S      0:01 sshd: vagrant@pts/0 1725 pts/0    Ss     0:00 -bash 2628 pts/0    R+     0:00 ps x
R - 运行状态或者运行队列中的就绪状态
在这种状态下,进程正在运行或者在运行队列中等待运行。
那运行的都是啥呢?
当你编译所写的源代码,生成的机器码是CPU指令集,并保存为可执行文件。当你启动程序时,该程序被加载进内存,然后CPU执行这些指令集。
从根本上来说,CPU是在执行指令,换句话说,处理数字。
S - 中断睡眠
这意味着该进程的指令不能在CPU上立即执行。相反地,该进程等待某个事件或者条件发生。当事件发生,系统内核设置进程状态为运行状态。
本例是GNU的coreutils软件包中的sleep工具。它将睡眠指定秒数。
$ sleep 1000 & [1] 10089 $ ps f  PID TTY      STAT   TIME COMMAND 3514 pts/1    Ss     0:00 -bash 10089 pts/1    S      0:00  \_ sleep 1000 10094 pts/1    R+     0:00  \_ ps f
所以这是一个中断睡眠。那如何中断该进程?通过发送控制信号。
你能在 htop 中点击 F9 ,然后在左侧菜单中选择一个信号发送。
发送的信号中最有名的是kill。因为kill是一个系统调用,其能发送信号给进程。程序/bin/kill能从用户空间做系统调用,默认的信号是使用TERM,该信号要求进程中止或者杀死。
信号其实只是一个数字,但是数字太难记住,所以我们常说对应的名字。信号名字用大写表示,并用SIG前缀。
常用的信号有INTKILLSTOPCONTHUP
让我们发送INT(也称作,SIGINT或者2或者 Terminal interrupt )中断睡眠。
$ kill -INT 10089[1]+  Interrupt               sleep 1000
当你在键盘上敲击CTRL+C 也会产生上面同样的效果。 bash 将发送前台进程 SIGINT 信号。
顺便提一下,在 bash中,虽然大部分操作系统都有 /bin/kill ,但是 kill 是一个内建命令。这是为什么呢?如果你创建的进程达到限制条件,它允许进程被kill。
实现相同功能的命令:
  • kill -INT 10089
  • kill -2 10089
  • /bin/kill -2 10089
另外一个有用的信号是 SIGKILL (也被称作 9)。你可以使用该信号kill掉不响应的进程,省的你发狂的按 CTRL+C 键盘。
当编写程序时,你能设置信号handler函数,该函数将在进程收到信号时被调用。换句话说,你能捕获信号,然后做点什么事。例如,清理或者优雅的关闭进程。所以发送 SIGINT 信号(用户想中断一个进程)和SIGTERM (用户想中止一个进程)并不意味着进程被中止。
当你运行Python脚本,你会发现一个意外:
$ python -c 'import sys; sys.stdin.read()'^C Traceback (most recent call last):  File "<string>", line 1, in <module> KeyboardInterrupt
你可以告诉内核强制中止一个进程,使用发送 KILL信号:
$ sleep 1000 & [1] 2658$ kill -92658[1]+  Killed                  sleep 1000
D - 非中断睡眠
不像中止睡眠进程那么简单,你不能用信号唤醒该进程。这就是为什么许多人喊怕看到这个状态。你不能kill该进程,因为kill意味着通过发送SIGKILL 信号给该进程。
如果进程必须等待不中断或者事件被期望快速发生,那这个状态被使用,比如读写磁盘。但是这仅仅发生一秒分之一。
引用自StackOverflow
不中断进程经常等到I/O出现页缺失(page fault)。进程/任务不能在这种状态下中断,因为它不能处理任何信号;如果中断了,另外一个页缺失将会发生,会返回到原始位置。
换句话说,如果你在使用NFS(网络文件系统)时出现中断,那得好久才恢复。
或者,以我的经验看,意味着进程正在交换许多小内存。
让我们试着一个进程进入不中断睡眠。
8.8.8.8 是Google提供的公共DNS服务。他们没有一个开放的NFS,但是也不能阻止试验。
$
sudo
mount
8.8
.
8.8
:/tmp /tmp & [
1
]
12646


$
sudo
ps x | grep mount.nfs

12648
pts/
1
   D      
0
:
00
/sbin/mount.nfs
8.8
.
8.8
:/tmp /tmp -o rw
如何找出刚才发生了什么? strace!
strace上面ps的输出命令:
$ sudo strace /sbin/mount.nfs 8.8.8.8:/tmp /tmp -o rw ... mount("8.8.8.8:/tmp", "/tmp", "nfs", 0, ...
所以 mount 系统调用正在阻塞进程。
如果想看看发生了什么,你能运行带intr 选项的 mount 命令来中断: sudo mount 8.8.8.8:/tmp /tmp -o intr
Z - 僵尸进程,无效进程但是未被父进程回收
当一个进程以 exit退出时,它还有子进程,此时子进程变成了僵尸进程。
  • 如果僵尸进程存在一小会,那相当正常;
  • 僵尸进程存在很长时间可能导致程序bug;
  • 僵尸进程不消耗内存,仅仅是一个进程ID;
  • 僵尸进程不能被kill
  • 发生SIGCHLD 信号能让父进程回收僵尸进程;
  • kill 僵尸进程的父进程能摆脱父进程和其僵尸进程
下面写个C程序的例子展示下:
#
include
<stdio.h>

#
include
<stdlib.h>

#
include
<unistd.h>


int
main() {

printf
(
"Running\n"
);


int
pid = fork();


if
(pid ==
0
) {

printf
(
"I am the child process\n"
);

printf
(
"The child process is exiting now\n"
);  

exit
(
0
);  }
else
{  

printf
(
"I am the parent process\n"
);

printf
(
"The parent process is sleeping now\n"
);    sleep(
20
);

printf
(
"The parent process is finished\n"
);  }


return0
; }
安装GNU C编译器(GCC):
sudo apt install -y gcc
编译代码并运行:
gcc zombie.c -o zombie ./zombie
查看进程树:
$ ps f  PID TTY      STAT   TIME COMMAND

3514
pts/
1
   Ss    
0
:
00
-bash

7911
pts/
1
   S+    
0
:
00
 \_ ./zombie

7912
pts/
1
   Z+    
0
:
00
     \_ [zombie] <defunct>

1317
pts/
0
   Ss    
0
:
00
-bash

7913
pts/
0
   R+    
0
:
00
 \_ ps f
我们得到了僵尸进程。当父进程退出,僵尸进程也退出。
$ ps f  PID TTY      STAT   TIME COMMAND

3514
pts/
1
   Ss+    
0
:
00
-bash

1317
pts/
0
   Ss    
0
:
00
-bash

7914
pts/
0
   R+    
0
:
00
 \_ ps f
如果用while (true) ; 代替 sleep(20) ,僵尸进程将正常退出。
使用exit时,该进程所有的内存和资源被释放,其它的进程可以继续使用。
为什么要保留僵尸进程存在呢?
父进程使用 wait系统调用找出其子进程退出码(信号 handler)。如果一个进程睡眠,它需要等待唤醒。
为什么不简单的强制进程唤醒和kill掉?当你厌倦小孩时,你不会把他扔垃圾桶。这里的原因相同。坏事情总会发生的。
T - 被控制信号停止
打开两个终端窗口,使用 ps u能查看到用户的进程:
$ ps u USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ubuntu    13170.00.9214204992 pts/0    Ss+  Jun07   0:00 -bash ubuntu    35141.51.0214205196 pts/1    Ss   07:280:00 -bash ubuntu    35280.00.6360843316 pts/1    R+   07:280:00 ps u
忽略 -bashps u进程。
现在在其中一个终端窗口运行cat /dev/urandom > /dev/null 。其进程状态为 R+ ,意味着正在运行。
$ ps u USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ubuntu    35401030.16168688 pts/1    R+   07:290:04 cat /dev/urandom
CTRL+Z 停止该进程:
$ # CTRL+Z[1]+  Stopped                 cat /dev/urandom > /dev/null $ ps aux USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ubuntu    354086.80.16168688 pts/1    T    07:290:15 cat /dev/urandom
该进程的状态现在为 T
在第一个终端运行 fg 可以重新恢复该进程。
另外一种停止进程的方法是用 kill 发送 STOP 信号给进程。然后使用 CONT 信号可让进程恢复执行。
t - 跟踪时被调试者停止
首先,安装GNU调试器(gdb):
sudo apt install -y gdb
监听网络端口1234的入网连接:
$ nc -l1234 & [1] 3905
状态显示睡眠状态,那意味着该进程在等待网络传入数据。
$ ps u USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ubuntu    39050.00.19184896 pts/0    S    07:410:00 nc -l1234
运行调试器,并与进程ID为3905的进程关联:
sudo gdb -p 3905
你会发现这个进程的状态变为t,这意味着调试器正在跟踪该进程。
$ ps u USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ubuntu    39050.00.19184896 pts/0    t    07:410:00 nc -l1234
  • Process time
Linux是一个多任务的操作系统。这意味着,即使机器只有一个PCU,也能在同一个时间点运行多个进程。你可以通过SSH连接服务器查看 htop 输出,同时你的web服务也在通过网络传输博客内容给读者。
那系统是如何做到在单个CPU上一个时间点只执行一个指令呢?答案是时间共享。
一个进程运行“一点时间”,然后挂起;这时另外一个等待的进程运行“一点时间”。进程运行的这“一点时间”称为时间片(time slice)。
时间片通常是几毫秒。所以只要服务器系统的负载不高,你是注意不到的。
这也就可以解释为什么平均负载(load average)是运行进程的平均数了。如果你的服务器只有一个核,平均负载是1.0,那CPU的利用率达到100%。如果平均负载高于1.0,那意味着等待运行的进程数大于CPU能承载运行的进程数。所以会发现服务器宕机或者延迟。如果负载小雨1.0,那意味着CPU有时会处于空闲状态,不做任何工作。
这也给你一个提示:为什么一个运行了10秒的进程的运行时间有时会高于或者低于准确的10秒。
  • Process niceness and priority
当运行的task数比可用的CPU核数要多时,你必须找个方法决定接下来哪个task运行哪个task保持等待。这其实是 task scheduler的职责。
Linux内核的scheduler负责选择运行进程队列中哪个进程接下来运行,依赖于内核使用的scheduler算法。
一般你不能影响scheduler,但是让它知道哪个进程更重要。
Nice值(NI) 是表示用户空间进程优先级的数值,其代表静态优先级。Nice值的范围是-20~+19,拥有Nice值越大的进程的实际优先级越小(即Nice值为+19的进程优先级最小,为-20的进程优先级最大),默认的Nice值是0。Nice值增加1,降低进程10%的CPU时间。
priority(优先级,PRI)是Linux内核级的优先级,其代表动态优先级。该优先级范围从0到139,0到99表示实时进程,100到139表示用户空间进程优先级。
你可以改变Nice值让内核考虑该进程优先级,但是不能改变priority。
Nice值和priority之间的关系如下:
PR = 20 + NI
所以 PR = 20 + (-20 to +19) 的值是0到39,映射为100到139。
在启动进程前设置该进程的Nice值:
nice -n niceness program
当程序已经正在运行,可用 renice改变其Nice值:
renice -n niceness -p PID
下面是CPU使用颜色代表的意义:
蓝色:低优先级线程(nice > 0)
绿色:常规优先级线程
红色:内核线程
  • 内存使用 - VIRT/RES/SHR/MEM
进程给人的假象是只有一个进程使用内存,其实这是通过虚拟内存实现的。
进程没有直接访问物理内存,而是拥有虚拟地址空间,Linux内核将虚拟内存地址转换成物理内存或者映射到磁盘。这就是为什么看起来进程能够使用的内存比电脑真实的内存要多。
这里说明的是,想准确计算一个进程占用多少内存并不是那么直观。你也想计算共享内存或者磁盘映射内存吗?htop 显示的一些信息能帮助你估计内存使用量。
内存使用颜色代表的意义:
绿色:Used memory
蓝色:Buffers
橘黄色:Cache
VIRT/VSZ - 虚拟内存
task使用的虚拟内存总量。它包含代码、数据和共享内存(加上调出内存到磁盘的分页和已映射但未使用的分页)。
VIRT 是虚拟内存使用量。它包括所有的内存,含内存映射文件。
如果应用请求1GB内存,但是内存只有1MB,那 VIRT显示1GB。如果 mmap映射的是一个1GB 文件, VIRT也显示1GB。
大部分情况下, VIRT并不是一个太有用的数字。
RES/RSS - 常驻内存大小
task使用的非交换的物理内存。
RES是常驻内存使用量。
RES相比于 VIRT,能更好的表征进程的内存使用量:
不包含交换出的内存;
不包含共享内存
如果一个进程使用1GB内存,并调用fork()函数,fork的结果是两个进程的 RES 都是1GB,但是实际上只使用了1GB内存。因为Linux采用的是copy-on-write机制。
SHR - 共享内存大
task使用的共享内存总量。
简单的反应进程间共享的内存。
#
include
<stdio.h>

#
include
<stdlib.h>

#
include
<unistd.h>


int
main() {

printf
(
"Started\n"
);  sleep(
10
);  size_t memory =
10
*
1024
*
1024
;
// 10 MBchar
* buffer =
malloc
(memory);

printf
(
"Allocated 10M\n"
);  sleep(
10
);


for
(size_t i =
0
; i < memory/
2
; i++)    buffer[i] =
42
;

printf
(
"Used 5M\n"
);  sleep(
10
);


int
pid = fork();

printf
(
"Forked\n"
);  sleep(
10
);


if
(pid !=
0
) {

for
(size_t i = memory/
2
; i < memory/
2
+ memory/
5
; i++)      buffer[i] =
42
;

printf
(
"Child used extra 2M\n"
);  }  sleep(
10
);


return0
; }
fallocate -l10G gcc -std=c99 mem.c -o mem ./mem
Process  Message               VIRT  RES SHR main     Started               4200680
604

main     Allocated 10M        14444680
604

main     Used 5M              144446168
1116

main     Forked               144446168
1116

child    Forked               144445216
0

main     Child used extra 2M        8252
1116

child    Child used extra 2M        52160
MEM% - 内存使用量占比
task当前使用的内存占比。
该值为 RES 除以RAM总量。
如果 RES 是400M,你有8GB的RAM,MEM%400/8192*100 = 4.88%
未完待续。。。
Enjoy!

侠天,专注于大数据、机器学习和数学相关的内容,并有个人公众号:bigdata_ny分享相关技术文章。
若发现以上文章有任何不妥,请联系我。
继续阅读
阅读原文