开发高并发应用时, 通常是基于 “降低 用户空间
& IO等待
&上下文切换
对CPU资源的消耗 ” 进行思考设计。
对于JVM应用来说, 常见的耗CPU的有:
- 频繁GC,访问量高时,有可能造成频繁的GC,甚至FGC。当调用量大时,内存分配过快,就会造成GC线程不停的执行,进行大量分析计算,导致CPU飙高 。
- 计算密集: 序列化与反序列化 、 加解密 、 正则表达式 等 大量计算。( Java 正则表达式使用的引擎实现是 NFA 自动机,这种引擎在进行字符匹配会发生回溯(backtracking) )
- 线程上下文切换过频: 大量线程都处于不断的阻塞状态(锁等待、IO等待等)、锁竞争激烈等执行状态的变化过程中。 线程数高的应用 Runable 和 Running 状态的线程不多,这时 CPU 使用率不一定会高 ; BLOCKED 和 WAITING 状态的,这种线程是不会占用 CPU 的 , 这时候又得考虑是否有死锁现象。
- 某些线程在做无阻塞的运算,eg. while(true)中不停的做运算,没有任何阻塞 。 死循环会调用 CPU 寄存器进行计数,这个操作就会占用 CPU。其次,如果线程一直处于死循环状态, 不会让出 CPU,除非操作系统时间片到期,但死循环会不断向系统申请时间片,直到系统没有空闲时间做别的事情。
用户态 & 内核态
操作系统都是采用虚拟存储器, 对32位Linux操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
- 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF,也就是 3G-4G部分 ) 给大家共享使用,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。
- 而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)称为用户空间, 用来运行用户程序。
操作系统的核心是内核(kernel
),可以访问受保护的内存空间,也有访问底层硬件设备( 硬盘、网卡等)的所有权限 ; 处在内核空间称为内核态。
用户进程只能受限的访问内存,且不允许访问外设,占用cpu的能力被剥夺,cpu资源可以被其他程序获取 ; 也就是说不能直接操作内核,保证了内核的安全 。处在用户空间称为用户态。通过内存页表 ( page cache
) 操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程的地址空间中的数据。
intel cpu提供Ring0
- Ring3
三种级别的运行模式,Ring0
级别最高,Ring3
最低。Linux使用了Ring3
级别运行用户态,Ring0
作为内核态,没有使用Ring1和Ring2。Ring3
状态不能访问Ring0
的地址空间,包括代码和数据。
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block
),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行的进程(获得CPU),才可能将其转为阻塞状态, 此时是不占用CPU资源。
在 Linux
系统中,线程就是能并行运行且与fork
创建它们的父进程)共享同一地址空间(内存区域)和其他资源的 轻量级的进程, 每个进程都有自己的内核栈 。
模式切换
系统调用: 在类 Unix 系统中是指活跃(正在运行在 CPU 上)的进程对于内核所提供的服务的请求。eg.(I/O)和新进程创建, I/O 可以被定义为任何信息流入或流出 CPU 与主内存(RAM)。
当用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作、网络数据发送等操作,必须通过write
,send
等系统调用命令来执行内核中的代码来完成操作;这时,必须切换到Ring0
,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后再切换回Ring3
回到用户态。
- 当进程运行在内核态时: CPU堆栈指针寄存器指向的是内核堆栈地址,使用的是内核堆栈。 内核栈是属于操作系统空间的一块固定区域,可以用于保存中断现场、保存操作系统子程序间相互调用的参数、返回值等。
- 当进程运行在用户态时:CPU堆栈指针寄存器指向的是用户堆栈地址,使用的是用户堆栈。 用户栈是属于用户进程空间的一块区域,用户保存用户进程子程序间的相互调用的参数、返回值等。
当进程由于中断进入内核态时,系统会把一些用户态的堆栈地址保存到内核中, 然后设置CPU堆栈指针寄存器的地址为内核栈地址 ; 这样就完成了用户栈向内核栈的切换 。当返回用户态时,取出内核栈中得信息( 用户栈地址 )恢复到CPU栈指针寄存器 ,这样就返回到程序原来执行的地方。
用户态和内核态 在类 Unix 系统中共存意味着当系统调用发生时 CPU 切换到内核态是必要的。这应该叫做模式切换而不是上下文切换,因为没有改变当前的进程。
上下文切换
上下文指的是: 某一时间点 CPU 寄存器和程序计数器的内容。
寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存), 寄存器通过对常用值(运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在(或下一个将要)执行的指令的位置,具体依赖于特定的系统。
上下文切换在多任务操作系统中是一个必须的特性。多任务操作系统是指多个进程运行在一个 CPU 中互不打扰,看起来像同时运行一样。这个并行的错觉是由于上下文在高速的切换(每秒几十上百次),当某一进程自愿放弃它的 CPU 时间或者系统分配的时间片用完时,就会发生上下文切换。
上下文切换有时也因硬件中断而触发,硬件中断是指硬件设备(如键盘、鼠标、调试解调器、系统时钟)给内核发送的一个信号,该信号表示一个事件(如按键、鼠标移动、从网络连接接收到数据)发生了。
上下文切换只能发生在内核态中,分为 进程(线程)上下文切换、中断上下文切换。可以认为是:内核在 CPU 上 挂起 正在执行进程(含线程)的 运行,然后继续执行之前挂起的众多进程中的某一个。
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内核中的某处;
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复;
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
上下文切换通常是计算密集型的,也就是说,它需要相当可观的处理器时间;在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
查看系统的上下文切换
如果当上下文切换次数超过一万次,或者切换次数出现数量级增长时,很可能已经出现了性能问题。
vmstat
主要用来分析系统内存使用情况,也常用来分析 CPU
上下文切换和中断的次数。
# 每隔 5 秒输出 1 组数据
$ vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 190952 156956 2456464 0 0 9 40 1 1 11 2 87 0 0
0 0 0 190640 156960 2456532 0 0 0 33 4880 9497 4 3 93 0 0
-
cs(context switch) 是每秒上下文切换的次数。
-
in(interrupt) 是每秒中断的次数。
-
r(Running or Runnable) 是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
-
b(Blocked) 是处于不可中断睡眠状态的进程数。
查看每个进程的详细情况,使用 pidstat
# 每隔 5 秒输出 1 组数据
# -w 参数表示输出进程切换指标,而 -u 参数则表示输出 CPU 使用指标
$ pidstat -w -u 5
Linux 3.10.0-1062.9.1.el7.x86_64 (iZwz9hzc7pd8k5wat4x4wiZ) 12/10/2020 _x86_64_ (2 CPU)
03:52:00 PM UID PID %usr %system %guest %CPU CPU Command
03:52:01 PM 1000 1496 0.00 0.99 0.00 0.99 0 pidstat
03:52:01 PM 1000 6515 0.00 0.99 0.00 0.99 1 java
03:52:01 PM 0 12805 0.99 0.99 0.00 1.98 0 AliYunDun
03:52:01 PM 1000 21522 0.00 0.99 0.00 0.99 0 java
03:52:01 PM 1000 27459 0.99 0.00 0.00 0.99 0 java
03:52:00 PM UID PID cswch/s nvcswch/s Command
03:52:01 PM 0 1 0.99 0.00 systemd
03:52:01 PM 0 6 3.96 0.00 ksoftirqd/0
03:52:01 PM 0 9 97.03 0.00 rcu_sched
03:52:01 PM 0 14 1.98 0.00 ksoftirqd/1
03:52:01 PM 0 308 0.99 0.00 crond
03:52:01 PM 1000 391 0.99 0.00 sshd
- cswch 表示每秒自愿上下文切换的次数。 指进程无法获取所需资源,导致的上下文切换。比如,IO、内存等系统资源不足时,就会发生自愿上下文切换。
- nvcswch 表示每秒非自愿上下文切换的次数。 指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在抢占 CPU 时,就容易发生非自愿上下文切换。
默认 pidstat
显示进程的指标数据,加上 -t
参数后,才会输出线程的指标 。
通过 /proc/interrupts
来读取中断的使用情况,通过运行下面的命令:
# -d 参数表示高亮显示变化的区域
$ watch -d cat /proc/interrupts
... ...
CPU0 CPU1
0: 82 0 IO-APIC-edge timer
1: 10 0 IO-APIC-edge i8042
4: 742 0 IO-APIC-edge serial
6: 3 0 IO-APIC-edge floppy
8: 0 0 IO-APIC-edge rtc0
9: 0 0 IO-APIC-fasteoi acpi
10: 0 0 IO-APIC-fasteoi virtio4
11: 34 0 IO-APIC-fasteoi uhci_hcd:usb1
12: 15 0 IO-APIC-edge i8042
14: 0 0 IO-APIC-edge ata_piix
15: 28445440 0 IO-APIC-edge ata_piix
24: 0 0 PCI-MSI-edge virtio2-config
25: 17640933 53622308 PCI-MSI-edge virtio2-req.0