写在前面
- 博文内容为 Linux 性能指标 CPU 上下文切换认知
- 内容涉及:
- 上下文认知,发生上下文切换的场景有哪些
- 上下文指标信息查看,内核上下文切换事件跟踪,系统上下文切换统计
- 上下文异常场景分析,CPU亲和性配置优化上下文
- 理解不足小伙伴帮忙指正 :),生活加油
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
上下文认知
什么是CPU上下文切换?
通俗的话讲,给定的CPU在某个时间点仅可以运行一个进程,为了制造出单处理器同时运行多个任务的假象(实际受限系统调度器:调度策略 调度优先级), 每个进程完成他们的任务一般都需要停止和启动很多次。 Linux内核就要不断地在不同的进程间切换
。这种不同进程间
的切换称为上下文切换
上下文切换时, CPU要保存旧进程的所有上下文信息,并取出新进程的所有上下文信息。上下文中包含了 Linux 跟踪新进程的大量信息,其中包括: 进程正在执行的指令,分配给进程的内存,进程打开的文件
等
所以实际上上下文切换涉及大量信息的移动,上下文切换的开销可以是相当大的
上下文切换可以是内核调度的结果。简单来讲,为了保证公平地给每个进程分配处理器时间,内核会周期性地中断正在运行的进程,在适当的情况下,内核调度器会决定开始另一个进程,而不是让当前进程继续执行。
每次这种周期性中断或定时发生时
,系统都可能进行上下文切换。每秒定时中断的次数与架构和内核版本有关
。
一个检查中断频率的简单方法是用 /proc/interrupts
文件,它可以确定已知时长内发生的中断次数
。
通过这个命令,可以观察到5秒钟内定时器中断次数的变化
代码语言:javascript复制┌──[root@vms81.liruilongs.github.io]-[~]
└─$cat /proc/interrupts | grep time; sleep 5 ;cat /proc/interrupts | grep time
0: 337 0 IO-APIC-edge timer
LOC: 9896498 9871317 Local timer interrupts
0: 337 0 IO-APIC-edge timer
LOC: 9901529 9876213 Local timer interrupts
┌──[root@vms81.liruilongs.github.io]-[~]
└─$
LOC 即为本地定时器中断
上面定时器的启动频率为 (9896498-9901529)/5 =1000
,即每秒要中断 1000次,同时也可以理解为内核在 sleep 进程执行中,每秒发生 1000 次 CPU 定时中断
代码语言:javascript复制如果
上下文切换
明显多于定时器中断
,那么这些切换极有可能是由I/O请求或其他长时间运行的系统调用(如休眠)造成的。当应用请求的操作不能立即完成时,内核启动该操作,保存请求进程,并尝试切换到另一个已就绪进程。这能让处理器尽量保持忙状态。
#上下文切换数量
cs=$(vmstat 1 1 | awk 'NR==3{print $12}')
实际中调度策略
不同,定时器中断的意义也不一样:
实时调度策略 :如FIFO
(先进先出)和时间片轮转(RR
),这些策略依赖于定时中断来确保实时进程的及时执行,但同时也需要考虑非实时进程的调度以避免饥饿
普通调度策略 :如CFS
,定时中断用于动态调整时间片,以实现公平性和效率的平衡
什么是上下文
当多个进程进行切换时,内核会包含前一个和后一个进程的相关信息。每次一个进程让出CPU时,内核都会存储
进程当前的操作状态,当以后该进程再次被调度回CPU时,可以从相同的位置恢复操作。
这些操作状态数据
又被称为上下文
,包含CPU的寄存器数据以及程序的计数器数据
。
CPU 寄存器
,是 CPU 内置的容量小、但速度极快的内存
。程序计数器
,则是用来存储 CPU 正在执行的指令
位置、或者即将执行的下一条指令位置。
给进程切换CPU时间片,就是所谓的上下文切换
。CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器
所指的新位置,运行新任务
保存下来的上下文,会存储在系统内核中,并在任务重新被分配到时间片时再次被加载,这样看起来系统实际中同时运行多个任务,具体和对应的CPU 调度策略有关系,不同调度策略分配时间片策略不同。
只有进程会发生上下文切换么?
实际上不仅进程
会发生CPU上下文切换,线程
,协程
和中断
也会发生CPU上下文切换。CPU上下文切换包括:
进程
上下文切换线程
上下文切换协程
上下文切换中断
上下文切换
进程上下文切换涉及到虚拟内存、栈、全局变量
等用户空间资源
,以及内核堆栈、寄存器
等内核空间的状态。这种切换发生在进程调度
时,例如:
CPU时间片用完
、系统资源不足
、进程通过 sleep 函数主动挂起
、高优先级进程抢占时间片
、硬件中断时CPU上的进程被挂起转而执行内核中的中断服务
进程上下文切换
Linux 按照特权等级
,把进程的运行空间分为内核空间
和用户空间
,分别对应着下图中, CPU 特权等级
的 Ring 0
和 Ring 3
。
在这里插入图片描述
内核空间(Ring 0)
具有最高权限,可以直接访问所有资源;用户空间(Ring 3)
只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
进程既可以在用户空间运行,又可以在内核空间中运行。
进程在用户空间运行时,被称为进程的用户态
,而陷入内核空间的时候,被称为进程的内核态
。
从用户态到内核态的转变
,需要通过系统调用
来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open()
打开文件,然后调用 read()
读取文件内容.
这里可以通过 bcc 或者 perf 工具来跟踪系统调用
采集数据
代码语言:javascript复制┌──[root@liruilongs.github.io]-[~]
└─$perf record -g $(which cat) test.log
Holler
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.012 MB perf.data (2 samples) ]
┌──[root@liruilongs.github.io]-[~]
└─$
输出数据
代码语言:javascript复制┌──[root@liruilongs.github.io]-[~]
└─$perf script > perf_script.txt
┌──[root@liruilongs.github.io]-[~]
└─$cat perf_script.txt
cat 3070 403.637613: 250000 cpu-clock:pppH:
ffffffffa2afb616 vma_interval_tree_remove 0x156 ([kernel.kallsyms])
ffffffffa2b12068 unlink_file_vma 0x48 ([kernel.kallsyms])
ffffffffa2b05da1 free_pgtables 0x71 ([kernel.kallsyms])
ffffffffa2b1176a unmap_region 0x10a ([kernel.kallsyms])
ffffffffa2b13bcd __do_munmap 0x20d ([kernel.kallsyms])
ffffffffa2b156f6 mmap_region 0x2f6 ([kernel.kallsyms])
ffffffffa2b15de0 do_mmap 0x380 ([kernel.kallsyms])
ffffffffa2ae69b8 vm_mmap_pgoff 0xd8 ([kernel.kallsyms])
ffffffffa2b131b8 ksys_mmap_pgoff 0x58 ([kernel.kallsyms])
ffffffffa284b2a3 __x64_sys_mmap 0x33 ([kernel.kallsyms])
ffffffffa2805089 x64_sys_call 0x3b9 ([kernel.kallsyms])
ffffffffa35c2f36 do_syscall_64 0x56 ([kernel.kallsyms])
ffffffffa36000df entry_SYSCALL_64_after_hwframe 0x67 ([kernel.kallsyms])
7ff93a740cb7 mmap64 0x17 (/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)
7ff93a724601 _dl_map_object 0x1f1 (/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)
cat 3070 403.637863: 250000 cpu-clock:pppH:
ffffffffa28a5a6f do_user_addr_fault 0x2ff ([kernel.kallsyms])
ffffffffa35c6ed7 exc_page_fault 0x77 ([kernel.kallsyms])
ffffffffa3600bb7 asm_exc_page_fault 0x27 ([kernel.kallsyms])
ffffffffa2eb6130 copy_user_generic_unrolled 0xa0 ([kernel.kallsyms])
ffffffffa2ac3705 filemap_read 0x165 ([kernel.kallsyms])
ffffffffa2ac3a62 generic_file_read_iter 0xe2 ([kernel.kallsyms])
ffffffffa2c6ecfb ext4_file_read_iter 0x5b ([kernel.kallsyms])
ffffffffa2b9b65a new_sync_read 0x10a ([kernel.kallsyms])
ffffffffa2b9bff3 vfs_read 0x103 ([kernel.kallsyms])
ffffffffa2b9eac7 ksys_read 0x67 ([kernel.kallsyms])
ffffffffa2b9eb69 __x64_sys_read 0x19 ([kernel.kallsyms])
ffffffffa2806a8a x64_sys_call 0x1dba ([kernel.kallsyms])
ffffffffa35c2f36 do_syscall_64 0x56 ([kernel.kallsyms])
ffffffffa36000df entry_SYSCALL_64_after_hwframe 0x67 ([kernel.kallsyms])
7ff93a5f37e2 read 0x12 (/usr/lib/x86_64-linux-gnu/libc.so.6)
┌──[root@liruilongs.github.io]-[~]
└─$
也可以使用 strace
命令
┌──[root@liruilongs.github.io]-[~]
└─$strace cat test.log
execve("/usr/bin/cat", ["cat", "test.log"], 0x7ffcc8683ce8 /* 35 vars */) = 0
brk(NULL) = 0x55f45d320000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffbabbec00) = -1 EINVAL (无效的参数)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f05f1400000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=62779, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 62779, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f05f13f0000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >