《进程分析之内存》文中,对系统/进程的内存使用情况进行分析了,本文将从cpu使用情况对进程进行分析;在这之前,先针对cpu比较相关几个概念进行介绍

CPU INFO的阅读以及对基本概念的了解;

cpu从硬件到系统层面有三个概念:物理CPU个数、物理核数、逻辑核个数;其中物理CPU的个数即硬件层面实实在在的CPU的个数;现在CPU都为多核,那么整个系统物理核心=物理CPU个数 × 每个CPU的核心个数;而逻辑核主要用在“超线程”的环境下,将原本的一个物理核心虚拟成多个核心,从而实现单物理核心并行的调度多个线程,如果开启超线程,那么总逻辑CPU数 = 物理CPU个数 × 每颗物理CPU的核数 × 超线程数;

对于一些前端机器(webserver)为了提高响应qps,会打开超线程,比如设置超线程数为2,对整体系统性能提升较为明显;根据内部”某某次”操作,超线程的打开,可以在不影响每个情况耗时的前提下,qps一样可以提升idle一倍以上;

cpu物理/逻辑信息数据都可以从/proc/cpuinfo中可以获取,下面将会具体分析;/proc/cpuinfo按照逻辑核为单位,描述相应逻辑核的相关属性,其中每个虚拟核的属性如下所示:

processor   : 0   //虚拟核编号,可用grep "processor"| wc -l获取虚拟核个数
vendor_id   : GenuineIntel //CPU厂商,这里就表示因特尔
cpu family  : 6 //厂商内部对CPU进行编号,因特尔厂商内部:“1”表示为8086和80186级芯片;“2”表示为286级芯片;“3”表示为386级芯片;“4”表示为486级芯片(SX、DX、:DX2、DX4);“5”表示为P5级芯片(经典奔腾和多能奔腾);“6”表示为P6级芯片(包括Celeron、PentiumII、PenfiumIII系列);“F”代表奔腾Ⅳ。
model       : 45 //family下面更细粒度的标识
model name  : Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz // CPU名称
stepping    : 7 //model下面更细粒度的更新版本号,那么完整cpu就可以通过vendor_id + cpu_family + model + stepping来唯一标识;
//关于CPU型号可以详细参见:http://blog.sina.com.cn/s/blog_605f5b4f010180st.html
//
cpu MHz     : 2000.084
cache size  : 15360 KB
physical id : 0  //物理CPU编号,可用grep "physical id"| sort| uniq| wc -l获取物理CPU个数
siblings    : 6  //当前物理CPU内部虚拟核心的个数,等于"超线程" × "cpu_cores"个数
core id     : 0  //当前物理CPU( physical id)内部的核编号,
cpu cores   : 6  //当前物理CPU内部核心个数
apicid      : 0
initial apicid  : 0
fpu     : yes   //是否具有浮点运算单元
fpu_exception   : yes //是否支持浮点计算异常
cpuid level : 13
wp      : yes //表明当前CPU是否在内核态支持对用户空间的写保护
//判断CPU是否64位,检查flags区段,看是否有lm标识,比如下面就为64位CPU
flags       : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm dca sse4_1 sse4_2 x2apic popcnt aes xsave avx lahf_lm arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid
bogomips    : 4000.16 //测算CPU速度
clflush size    : 64  //每次刷新缓存的大小单位
cache_alignment : 64  //缓存地址对齐单位
address sizes   : 46 bits physical, 48 bits virtual //可访问地址空间位数
power management:

相关命令有:

# 总核数 = 物理CPU个数 × 每颗物理CPU的核数
# 总逻辑CPU数 = 物理CPU个数 × 每颗物理CPU的核数 × 超线程数
# 查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq
# 查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l
# 判断每个核是否启用超核
判断每个物理核的siblings/cpu cores是否一致;如果不一致,判断虚拟核的core id是否一致;

CPU亲和力(CPU affinity)

“CPU affinity”中文唤作“CPU亲和力”,是指在CMP架构(指在一个计算机上汇集了一组处理器,各CPU之间共享内存子系统以及总线结构)下,能够将一个或多个进程绑定到一个或多个处理器上运行。而负责多CPU之间的均衡是由SMP来完成,它基于进程数来调整每个CPU使用比例:每个cpu都有一个可执行进程队列,只有当其中一个CPU的可执行队列里进程数比其他CPU队列进程数多25%时,才会将进程移动到另外空闲cpu上,这也是为什么我们经常看到某个CPU0的负载要比其他CPU高很多,但是会在25%以内;

但是基于进程数的调度存在很大的问题,因为进程会因为功能的不同,会对CPU有不同的依赖;假设四种耗费cpu的进程都在一台机器,(1)网卡中断(2)1个处理网络收发包进程(3)耗费cpu的n个worker进程(4)其他不太耗费cpu的进程;

此时(1)(2)大部分时间会出现在cpu0上,(3)的n个进程会随着调度,平均到其他多个cpu上,(4)里的进程也是随着调度分配到各个cpu上;当发生网卡中断的时候,cpu被打断了,那么分配到cpu0上的worker进程会因为中断的处理而得不到正常运行(时间片被剥夺);最恶劣恶劣的情况是:(1)(2)(3)的进程全部分配到cpu0上,其他不太耗费cpu的进程数很多,全部分配到cpu1,cpu2,cpu3上,此时如果网卡发送一次,业务就挂了!

如果对系统的自动调度能力的不信任,我们可以手动的去调度,即设置进程的cpu-affinity,将进程的运行时间片绑定在特定的CPU上;

设置CPU-affinity有多种方法,其一就是通过设置taskset命令针对当前正在运行的pid进行设置,生效后在任务的下一次调度,将会将其进程调度到指定的CPU中

//实例
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+   P COMMAND
20811 work      20   0 18.5g 1.7g 7048 S  8.0  3.6   3245:33  0 scribed-lighty
//当前P=0,表示使用的CPU-0
//taskset -cp 10,11 20811
20811 work      20   0 18.5g 1.7g 7048 S 11.3  3.6   3246:24 10 scribed-lighty
//设置pid在10,11两个cpu上进行调度,此时p=10了

方法二是在进程代码中,通过sched_setaffinity函数设置进程使用的cpu;

注意CPU-affinity的设置是可继承的,即所有的子进程都拥有父进程的设置

内核态,用户态,进程切换

在系统中,为了限制不同程序的访问能力,防止非法获取其他程序的数据或外部设备的数据,CPU将指令等级分为内核态和用户态,其中内核态的指令可以访问内存所有数据以及外围设备如网卡;而用户态的指令只能访问自己的进程空间内存,且不允许直接访问外围设备,占用CPU的资源可以被剥削;用户态和内核态的指令分别占用不同的CPU,即top命令上面的”%us和%sy”;

如果用户的程序需要访问内核的资源(写文件,访问网络等),可以通过“系统调用”来完成一次用户态与用户态之间的切换,即执行一次陷阱指令,主要的工作流程如下:

  1. 用户态程序将一些数据值放在寄存器中,或使用参数创建一个堆栈,以此表明需要系统提供的服务;
  2. 用户态程序执行陷阱指令CPU切换到内核态,并跳到位于内存指定位置的指令,其中陷阱指令是系统的一部分, 他们具有内存保护,不可被用户态程序访问,他们会读取程序放入内存的数据参数,并执行程序请求的服务;
  3. 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果

系统调用完成内核态与用户态之间的切换,相比用户态指令的执行,它是有一部的性能开销(包括:寄存器值的设置,以及指令切换);但是相比CPU进程调度过程中的进程上下文切换,这里系统调用的切换性能开销还是要小很多,下面就来分析一下进程的上下文切换;

主流的CPU在同一时间内只能运行一个线程,当一个进程用完时间片或者被更高优先级的进程抢占后,它会备份到CPU的任务队列中,同时调度其他待运行进程在CPU上运行。这里有两个概念:

  1. 任务队列:每个CPU都会维持一个任务队列,调度器会不停地根据进程的优先级从队列中调度出新进程给CPU进行运行,并将当前正在运行的进程放到任务队列中;任务队列中任务的多少反映出现目前CPU的负载情况;
  2. 进程切换:目前整个进程切换过程是通过中断技术来实现,即当CPU调度器获得了待运行进程的控制块后,立即用软中断指令来中止当前进程的运行,并保存当前进程的PC值和PSW值。其后使用压栈指令把CPU其他寄存器的值压入进程私有堆栈。然后再从待运行进程的进程控制块中取出私有堆栈指针的值并存入处理器的寄存器SP,至此SP就指向了待运行进程的私有堆栈,于是下面就自待运行进程的私有堆栈中弹出上下文进人处理器。最后,利用中断返回指令来实现自待运行进程的私有堆栈中弹出PSW值和PC值,从而完成整个切换。

线程以及轻量进程的实现

“进程是资源的管理单位,线程是计算的调度单位”这句话是常识,但是在linux中,线程这个概念却是依赖LWD(轻量进程)来实现;

线程从类型与实现来说,可以分为三种类型:

  1. 内核线程:只运行在内核态,不受用户态上下文的拖累。
  2. 用户线程:完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全部都是在用户空间完成,不需要内核的参与。这种线程是极其低消耗和高效的。但是这种多个线程在内核中只对应一个调度对象(内核线程),此时如果一个线程的堵塞在系统调度上,势必会导致整个进程的堵塞,这也是用户线程的缺点;
  3. 轻量级进程:是目前linux线程的实现方式;它建立在内核之上并由内核支持,每一个轻量级进程都与一个调度对象(内核线程)关联(因此linux创建一个线程(轻量级进程),需要经过一次系统调用,在内核中创建相对应的内核线程,相比用户线程,开销较大,同时受限内核线程数目,整个系统的线程数也是有限制的);轻量级进程由clone()系统调用创建,即与父进程是共享进程地址空间和系统资源,即它的创建只需要一个最小的执行上下文,相比创建普通进程,开销还是要小很多很多!

实践:在linux中获取指定进程的线程

方法一:ls /proc/PID/task | wc -l

//task目录下面有该PID进程下所有的线程号或轻量进程号的目录,每个linux进程默认有一个线程,所以该目录下必然有一个PID目录,每个LWP目录下面有该轻量进程的相关数据,由于多个轻量进程之间共享了进程地址空间和系统资源,所以LWD目录下面的数据大部分是一样的;

方法二:ps -efL

ps的-f参数解析:does full-format listing. When used with -L, the NLWP (number of threads) and LWP (thread ID) columns will be added.
-f参数将会完整的显示进程信息,比如PPID(父进程ID),C(CPU使用率)等,如果-L配合,将会线程每个PID的轻量进程数目和每个轻量进程号

    ps -efL |grep scribed-lighty
    UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
    work     20811     1 20811  5   86 Nov13 ?        1-01:37:21 ./scribed-lighty
    work     20811     1 21074  0   86 Nov13 ?        00:00:00 ./scribed-lighty
    work     20811     1 21075  0   86 Nov13 ?        00:00:00 ./scribed-lighty
    work     20811     1 21076  0   86 Nov13 ?        00:52:47 ./scribed-lighty

>

我们看到主进程20811,它的LWP=20811,CPU总计开销C=5%,轻量进程数目NLWP=86个;而LWP=21074,它的归属的进程PID=20811;

方法三:pstree -p PID
会将线程以进程树的方式展现出来

进程优先级PR,NICE值,nice%比例

在系统中通过top等命令可以看到每个进程的优先级PR,NICE值,nice%;它们都是和进程的优先级有关;

其中PR是进程的优先级,值越小进程的优先级越高。NICE值表示优先级的修正值,值大小位于[-20,+19]值,正值表示低优先级,负值表示高优先级,值为零则表示不调整该进程PR,即PR(new)=PR(old)+nice,系统默认PR=20,那么最终的PR位于[0,39]之间,进程切换时候根据进程之间的PR值来调度CPU的资源。但是注意:PR值只会影响用户态的CPU时间片,内核态不受PR值影响

假设一次调度中,有2个runnable的进程A和B,假设nice都为0时,内核会依次给每个进程分配1k个CPU时间片用于计算用户态指令。如果此时nice_A=0,nice_B=-10,此时CPU可能分别给A和B分配1k和1.5k的时间片用于计算用户态指令,即本次调度过程中,因为改变过优先级的进程多占用了0.5k个时间片。

此时nice%,就是来度量这种因为改变优先级的进程的占用CPU用户态的比例,即0.5k/总的调度时间;而user%为1k/总的调度时间;那么进程A在一次调度过程占用的用户态时间片比例为(0.5k+ 1k)/总的调度时间;

nice值为负数,优先级高了,可以多获取nice%的时间片;如果nice值为0或者为正值,进程被调度的优先级降低,此时nice%=0,但是在在总的调度时间不变的情况,优先级高的进程会多占时间皮,那么低优先级进程的user%就会减少;所以整个系统用于用户态的CPU比例应该是所有进程的user%+nice%

进程的nice值是可以被修改的,修改命令分别是nice和renice。

  1. nice命令:nice –n adjustment command,如果这里不指定adjustment,则默认为10。
  2. renice命令:动态修改一个pid的nice值。

系统以及进程的CPU耗时的了解

CPU是资源,进程或者系统的CPU的耗时是指CPU这个资源被占用的时间,如果拿这个时间与统计时间进行相除法,就可以得到CPU这段时间的占用比例;

CPU的使用耗时根据用处不同,分为:

  1. user:正常优先级下用户态的耗时,用于进程的用户态的指令计算
  2. nice:因为改变优先级而多获取的用户态CPU的耗时;
  3. sys:内核态的计算占用的CPU耗时
  4. iowait:内核态CPU等待IO完成的CPU耗时
  5. steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比
  6. irq:硬中断造成的CPU耗时
  7. softirq:软中断造成的CPU耗时
  8. idle:CPU空闲时间百分比。
  9. guest:忽略

在一个调度过程中,CPU总时间=user+nice+sys+iowait+idle+steal+irq+softirq+guest;对于一个进程来说只有user+nice+sys是这段时间内真正被用于CPU计算,TOP等程序也是利用这个值来计算进程占用的CPU比例;

关于IOwait,“内核态CPU等待IO完成的CPU耗时”这句话对iowait的理解还是很字面,后面分析IO会详细分析,这里简单字面进行分析;
>

磁盘作为外部设备,它是有一定的IOPS和带宽/吞吐量的限制;如果在有大量的磁盘读写的时候,IO就出现的瓶颈,IO服务时间(包括IO等待)变长,此时如果系统空闲idle资源没有其他可运行进程进行调度,那么就相当于有一部分CPU资源在“等待”这部分IO进程,换句话说(iowait+idle的时间都是CPU空闲的时间,只是idle是真的空闲,iowait是有进程在等待io而浪费的CPU空闲时间);

IO压力和IOwait其实没有必然关系,因为它有一个条件就是当前“没有其他可运行进程进行调度”,在IO压力很大的时候,IOwait可能很小,对IO的分析参考下一节,比如通过iostat进行分析;

关于Irix/Solaris mode
>

使用TOP等命令来查看每个进程的CPU耗时情况有两种模式Irix或Solaris;其中Irix on下,它统计在单个CPU的时间片(A)下,进程的user+sys的耗时比例:(use+sys)/A;一个进程同时可能有多个线程在这个时间片下占用多个CPU,那么(use+sys)/A 是可以大于100%的;而Solaris模式下(Irix off),A=A×CPU个数,从而获取进程占整个系统的CPU资源比例

TOP命令看到的%CPU默认就是Irix模式(Irix on)下的值,而ps -ef看到的%C就是Solaris模式下的值

/proc/stat的阅读

结合上面的知识,基本可以读懂/proc/stat这个文件

bash-3.00$ cat /proc/stat
//每个CPU和总CPU用户每一中计算的的时间片(Jiffies)
//Jiffies代表时间。它的单位随硬件平台的不同而不同。系统里定义了一个常数HZ,代表每秒种最小时间间隔的数目。这样jiffies的单位就是1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间间隔了。每个CPU时间片,Jiffies都要加1。CPU的利用率就是用执行用户态+系统态的Jiffies除以总的Jifffies来表示。
//CPU user      nice       sys         idel        iowait irq softirq stealstolen  guest
cpu  3579507973 923662884 1182115010 24742042424 1505324647 0 62267429 0            0
cpu0 533687044 21915510 297115205 1366731979 388976289 0 57810083 0 0
cpu1 309042325 31702797 115875683 1954446024 254394961 0 776714 0 0
cpu2 176634066 33605831 74702888 2197816335 183274070 0 223664 0 0
cpu3 112741411 30599346 43952679 2368466561 110427395 0 74039 0 0
cpu4 82092567 29779396 27776205 2459019904 67567418 0 31883 0 0
cpu5 66798645 29049156 20332706 2506608334 43466448 0 16243 0 0
cpu6 662220781 120553155 194181592 1423472844 263172456 0 2635018 0 0
cpu7 481430180 120766223 125531895 1845550767 92661358 0 289551 0 0
cpu8 369107111 121663571 86258341 2037860980 51238138 0 102709 0 0
cpu9 308882461 123089686 68038799 2139948304 26220899 0 51682 0 0
cpu10 236282379 135555206 52318720 2227790503 14142008 0 141788 0 0
cpu11 240588998 125383002 76030291 2214329883 9783202 0 114051 0 0
//一堆,每一个代表一个CPU每个中断号出现的次数
intr 55570678587 186 0 0 0 128 0 0 0 0 0 88903 0 0 0 0 0 2136397299 0 0 0 0
ctxt 1021520446354 // 进程上下文切换的次数
btime 1422384167 //系统总共的启动时间
processes 6951204073 // 系统总共运行的进程数
procs_running 14 // 当前运行队列的任务的数目。
procs_blocked 0 // 前被阻塞的任务的数目。
//每一个软中断出现次数
softirq 72347376064 0 2169065483 5827202 202608370 1870099913 0