一、top的概念
top是linux下一个用户态工具,和windows下任务管理器差不多,就是实时显示linux系统运行状态,进程线程cpu占用,内存使用情况等等。可以实时动态地查看系统的整体运行情况,是一个综合了多方信息检测系统性能和运行信息的实用工具。
二、top在哪里获取?
(1)如果是发行版的linux,top由procps组件提供,procps是内核proc utils工具集,其中不仅包含了top命令,还有我们熟知的ps,kill,free等命令都源于procps组件,CGEL6x的yocto downloads目录下也有procps,可以解开看看。
(2)如果是嵌入式linux,一般由busybox提供,相对比服务器版的top,功能会偏弱一些。a tiny top
三、top的版本
先来看下服务器上的top,命令行下敲打:top -v
-bash-4.2$ top -v |
从输出信息中,我们可以看出top的版本 其实就是procps-ng的版本,版本号为:3.3.9,后面为啥带个ng,啥意思,我们顺带随追溯下procps工具的历史。
最开始procps工具的maintainer很少有时间去处理procps,不知道为啥,反正一开始procps项目维护的并不好,在1997年的时候,Albert Cahalan为procps包写了一个新的ps程序,但随后几年里,Albert Cahalan一直悄悄的帮助debian packet maintainer修复bug,直到2001年,因为缺少维护者,Rik van Riel决定要为procps搞点事情,他在红帽公司的CVS上将旧的代码重新捡起来,并开始添加补丁,与此同时,一些开发者也以不同的形式开始添加patch。
到了2002年,Albert Cahalan把procps项目放到了http://procps.sourceforge.net,并设置procps的主版本号为3,一部分原因是他不想遗失之前的功能测试和bug fix,另一方面原因是top的源码已经被重写了,
后来procps.sourceforge.net停了,项目移动到了http://gitorious.org/procps,http://gitlab.com/procps-ng/procps,这个时候,Debian,Fedora,openSUSE开始fork这个项目,此时procps项目被重新命名为procps-ng(next-generation),版本号变更成了3.3.0,同时库的名称由libproc.so变成libprocps.so。
OK,ng就是下一代的意思,瞬间似乎明白了很多软件包的含义,libcap-ng,netsniff-ng
那top这个名字是啥意思?高高的 ? 顶部? 和任务管理器有啥关系,实际上是table of processes,主要是用来描述进程的。展示进程的自身属性及资源使用情况,如cpu占用,内存占用,运行状态。本文就是这么专!不放过一个技术细节,即使是程序命名也要追溯其来源、含义!
四、top原理?
就是借助于操作系统内核暴露出来的proc接口,top程序来进行数据统计,并以终端形式进行展示。
主要接口:/proc/pid/stat、/proc/stat、/proc/uptime、/proc/meminfo
五、Top全字段分析
好了,说了一大堆无关紧要的,直奔主题吧,看一下top的输出格式及字段详解,并以实际程序来动态调整每个字段的输出结果,直观感受下各字段的作用,眼见为实。
先来个top全景:
各部分含义:
①系统时间字段
图中字段含义:系统时间,当前时间为14:03:20
系统时间,可以通过date -s命令来修改。如:date -s 10:10:10
系统时间设置与读取分别由syscall(time)和syscall(settimeofday)系统调用来设置,并通过/etc/localtime文件进行时区转换。
修改一把:
[root@localhost top]# date -s 04:33:52 Fri Jun 8 04:33:52 CST 2018 |
来观察下top:
还原当前时间:
[root@localhost top]# date -s 14:34:26 Fri Jun 8 14:34:26 CST 2018 |
在观察下top效果:
②系统运行时间字段
图中字段含义:系统从上电运行时长为14天15小时,这期间未发生断电。
系统运行时间由/proc/uptime提供,这个时间的变动不好做实验,因为这个时间是只读的,本身就不应该让人为去设置。据说这个程序,可以通过获取内核的uptime_proc_fops结构体地址,然后进行数据篡改。更改uptime的运行时间,制造假象!这个hack行为就暂时不做实验了。
https://github.com/dkorunic/uptime_hack/blob/master/uptime_hack.c
③当前系统用户数字段
图中字段含义:当前有84个用户。
来个实验:
新建一个ssh链接到服务器,ssh yuchen@10.74.154.170,可以看到top中user会增加一个。
将ssh终端断开连接:在看下top:
实际上users用户数的统计由/var/run/utmp给出数据。
④系统负载
图中字段含义:3个字段分别表示系统最后1分钟,5分钟,15分钟,系统负载情况,数值越高说明负载越严重。
因为给出了过去时刻的3个字段,如果你看到1分钟的负载较高,但是15分钟的负载并不高,那么可能代表有一小段时间在进行cpu密集型计算,系统宏观上还是正常的,loadavg可作为系统忙时调试的一种数据参考。
来模拟下高负载:
创建256个任务同时跑,观察top情况。
先看一下空闲时,系统top情况:可以看到基本上都是0.x,基本都是空闲状态,运行良好。
程序代码:(这段代码用于创建loop程序,后面会经常用到)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>
#define MAX_PTHREAD_NUM 256
void pthread_func(void *args)
{
printf("create pthread task! pid: %d\n", syscall(__NR_gettid));
while(1);
}
int main(int argc, char **argv)
{
int i, n = 1;
pthread_t tid[MAX_PTHREAD_NUM];
if(argc == 2)
n = atoi(argv[1]);
for(i=0; i<n; i++)
pthread_create(&tid[i], NULL, (void*)pthread_func, NULL);
for(i=0; i<n; i++)
pthread_join(tid[i], NULL);
while(1);
return 0;
}
创建256个任务,启动程序,观察top:
[root@localhost top]# ./a.out 256 reate pthread task! pid: 18245 create pthread task! pid: 18106 create pthread task! pid: 18137 ……………… ……………… create pthread task! pid: 18237 create pthread task! pid: 18241
|
随着时间的推移,loadavg的值开始升高,1分钟的loadavg增长最快,5分钟loadavg增长缓慢,15分钟loadavg增长最慢。
按下大H,展开线程,可以看到系统中有大量任务在运行。
运行了2分钟的测试程序。Loadavg的值已经达到9.x, 5.x, 2.x,Top如下:
停止测试程序,loadavg开始缓慢降低。一分钟loadavg下降最快,5分钟其次,15分钟下降最慢。
一分钟过后,loadavg值已经变成了0.x
Ok,loadavg就是这么个东西,loadavg显示了1,/5/15分钟系统负载情况。
另外,在busybox中的top,loadavg的显示这样的:
Loadavg后面多出2个字段,2/135 和 1168
2表示:当前系统中运行的任务数,(R)。
135表示:系统总任务数。
1168:系统最近一次启动的任务pid
⑤Tasks行表示系统任务情况,(分别代表:任务总数,正在运行任务数,睡眠任务数,停止任务数,僵尸任务数)
图中字段依次含义:任务总数,正在运行任务数,睡眠任务数,停止任务数,僵尸任务数。可以看到总共有320个任务,1个任务运行,319个睡觉,0个停止,0个僵尸。
运行任务(R):获取cpu在执行。
睡眠任务(S/D):等待资源或信号到来,睡觉。
停止任务(T):任务处于stop状态,发送continue signal可以让其继续运行。
僵尸任务(Z):任务已经挂了,其父进程未收尸。
进程状态之--→ R
R:事实上,在内核态中,挂在cpu的运行队列上的任务都是R状态。但是处于R状态并不代表你拿到了cpu控制权。运行任务可能有多个,但是实实在在能在cpu运行的只有一个(单核系统)。所以,R任务代表你所有资源都已具备,时刻等待cpu调度。但也可能你连cpu都拿到了,那你就是真正的running!!
测试程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>
#define MAX_PTHREAD_NUM 256
void pthread_func(void *args)
{
printf("create pthread task! pid: %d\n", syscall(__NR_gettid));
while(1);
}
int main(int argc, char **argv)
{
int i, n = 1;
pthread_t tid[MAX_PTHREAD_NUM];
if(argc == 2)
n = atoi(argv[1]);
for(i=0; i<n; i++)
pthread_create(&tid[i], NULL, (void*)pthread_func, NULL);
for(i=0; i<n; i++)
pthread_join(tid[i], NULL);
while(1);
return 0;
}
创建4个loop任务,观察top:
Top -H输出如下,可以看到有5个R状态任务,4个测试程序+top程序,总cpu使用率为400%。这4个测试程序分别瓜分了4个cpu核,每个任务实实在在的处于Running状态,每个程序占用100%cpu。
再次启动4个loop任务,这次将4个任务绑定到一个cpu核上执行。看看结果:
Top输出如下,通过cpu绑核后,可以看到总cpu占用率只有100%,每个程序占用≈25%。但是我们看到这个4个任务还是处于R状态,但事实上,同一时刻只能存在一个真正运行的任务。所以说R状态有两种情况,一种是已经就绪了,随时可以运行,但尚未抢到cpu,另一种情况是真正的在cpu上执行。在操作系统眼里,这些任务都是R状态。所以,如top输出所示,CPU1核上,R状态任务有4个,但实际上真正运行只有1个。
进程状态之--→ S
S:缺少某种资源或等待事件完成,处于睡眠状态,等待被唤醒。进程也可以自己主动让住cpu,进入睡眠状态。该状态可以通过信号signal唤醒。
测试程序:
int main()
{
printf("I'm a sleep task!\n");
while(1) {
sleep(100);
}
}
睡眠任务运行:
Top输出:
可以看到长期处于睡眠态,由于是浅度睡眠可以接收信号,我们向进程发送信号kill -9 8337,可以看到进程8337被干掉,因为进程接收到sigkill信号后,被唤醒执行默认的信号处理程序,程序退出了。
进程状态之--→ D
D:disk sleep,是一种深度睡眠,通常是在等待io、磁盘。与S状态相比它不能通过信号唤醒。处于这种状态的进程必须拿到资源才能唤醒,否则一直睡觉,Kill -9也白扯。
通常系统里不会长期出现D状态任务,如果长期出现D状态任务,多半是系统已经发生了问题。所以这个实验不好做,看来又要搞破坏了。
找一块嵌入式单板,在其linux系统中mount一个nfs文件系统,mount后将网线断开,此时df -h会卡死,长时间不能返回,事实上df进程已经处于D状态,正在等待io资源完成,io源于网络,此时网络已经断开!
我们telnet登陆到单板上看下情况,可以看到df -h确实处于D状态了!额外看到D后面有个+号,是什么鬼?代表前台进程,下篇文章会解释。
此时的D状态任务像僵尸一般驻留在系统体内,不接收任何信号,ctrl+c、ctrl+z通通白废!
Kill -9 3380,kil -2 3380,向进程发信号也是如此。
我们再次接上网线,稍等片刻,系统输出nfs:server 192.168.1.168 ok字样,df -h重新复活了!串口又可以接收指令了。可见,D状态任务只有io资源满足的时候才能复活啊!
进程状态之--→ T
T:该状态表示进程处于停止状态。通常用于作业控制(job control),停止状态并不是说进程已经终止了,而是一个临时停止,举个例子:我们手机正在运行一个qq音乐进程,我们在房间内享受着优美的音乐,突然窗外传来一阵异样的噪音,这个时候我们赶紧把qq音乐暂停,爬在窗口看看是什么情况,原来是广场舞大妈们正在操练,好吧没什么大不了的,关上窗,继续播放qq音乐。当我们去看窗户的这段时间,qq进程就处理暂停状态,它并没有真的停止,也就是T状态。那么linux进程什么时候会进入T状态呢?有一种常见的方法让进程暂停执行,强制处于T状态,那就是CTRL+Z命令,该命令会让进程暂停执行。fg/bg命令让其恢复。稍后会用程序表明。另外,大家都知道gdb吧,有了T状态,我们就可以利用这个状态进行调试了,gdb在使用break设置断点的时候,实际上就是发一个stop信号给进程,让进程暂停,暂停后我们可以观察进程运行状态,变量值,等等信息,达到调试程序的功能。按下r键可以让程序继续运行,而这相当于给程序发了一个sigcont信号(继续运行)。单步调试也是同样的道理。
实验:
启动一个loop任务,让其暂停执行。
Top -p 8713看下任务状态,显示任务已经处于T状态。
Fg将任务再次唤醒,得到运行。
Tip:我们在编译yocto或者大型android类软件时,通常会耗时很久,这个时候我们想在编译的过程中,干点别的事,假如使用Ctrl+c来结束当前程序的执行。那么下次必须重新启动编译。但是如果是ctrl+z,只是先将任务暂停,爽完了,fg唤醒即可。
进程状态之--→ Z
Z:表示进程处于僵尸状态。什么?僵尸?系统里出现了这样的进程还能了得?事实上不用害怕,僵尸状态只是子进程退出时的一种临界状态,它并不可怕,处于僵尸态的进程所有占用资源已经释放(除了task_struct这个结构变量),留下task_struct结构,是为了让父进程知道子进程的死亡原因,来进行合适的处理。进程退出码保存在task_struct->exit_codel;父进程需要调用wait/waitpid来为子进程收尸,一旦wait系统调用后,子进程会从此消失。通常情况下系统中不会有Z状态进程,如果有,也不用怕,那就是父进程还没有来得及进行收尸。另外Z进程也是无法接收信号的。假如你看到系统里有个僵尸进程,你看着很不爽,你用kill -9来杀这个进程,是没有任何效果的,因为它已经死了,,只剩个躯体在那,你kill -9无非是在尸体上在多捅几刀,无任何意义。
创造一个僵尸程序:
void child_process()
{
printf("I'm child process! pid=%d\n", getpid());
sleep(1);
_exit(0);
}
int main()
{
int pid;
int status;
pid = fork();
if(pid == 0) {
child_process();
} else {
while(1);
waitpid(pid, &status, 0);
}
}
父进程创建一个子进程,子进程打印自己的pid,并退出,但父进程一直不去调用waitpid为子进程收尸,看看运行结果:
运行Top -p 9053,可以看到系统中有僵尸了!Zombie处于Z状态。
将父进程结束掉,子进程也随之退出。想想这是为什么?