性能分析利器火焰图(1)- CPU上下文切换

2,736 阅读5分钟

相关系列文章

性能分析火焰图(2)- 火焰图生成原理

性能分析火焰图(3)- 性能优化实战

早先跟张师傅(挖坑的张师傅)有学习过一些火焰图的东西,在后来的工作和实践中帮助很大,故将这个火焰图的系列梳理,总结并在公司内部分享,现在将文档再加以总结分享输出出来,里面有一些资料是之前网上很多前辈有的,如果相同,纯属借鉴

本系列主要介绍火焰图是什么东东,介绍systemmap原理,以及火焰图原理,如何生成火焰图,以及如何用火焰图定位问题。

我们可以回想下自己现在服务中是如何定位性能问题的,假如我们的线上出现了大部分的接口请求耗时增加,服务性能下降,但是依赖的相关方服务都反馈他们的服务一切正常,这个时候大家会怎么做?

如果有经验的同事这个时刻,可能会从GC,日志,和请求链路上寻找线索,比如dump下内存快照,线程堆栈,和查询多个请求接口之前的共同异常点来判断,但是如果没有经验的同学就有可能抓瞎了,没有思路(就比如我刚入职的时候)

俗话说 “工欲善其事,必先利其器”,我们作为服务的开发者,代码的编写者,如果没有两把刷子,没有金刚钻,也很难把这个活干好。linux中有很多性能检测和调优的工具,火焰图就是基于其中的perf 工具将其采集的数据更加直观的体现出来。

因为我们主要分析的是CPU的火焰图,所以在开篇我们先回顾一下CPU的一些相关知识

CPU的上下文切换和oncpuoffcpuCPU 的上下文切换和on-cpu、off-cpu

    我们都知道我们CPU在运行的时候因为多任务"并行"的关系,会进行很多的CPU时间片的切换,也就是说CPU可能执行一会这个任务,然后再执行一会另外一个任务,因为每个任务都是不一样的,所以在切换任务的时候势必会进行线程上下文的切换

1. CPU上下文:CPU执行任务前必须依赖的环境

  • CPU寄存器
  • 程序计数器

2. 上下文切换的类型

  • 进程上下文切换(每次进程上下文的切换需要几十纳秒到数微妙的CPU时间)

    • 保存当前进程的内核堆栈和CPU寄存器
    • 保存当前进程的虚拟内存和栈
    • 加载新进程的内核堆栈和寄存器
    • 刷新进程的虚拟内存和用户栈
  • 线程上下文切换

  • 中断上下文切换(响应硬件的时间,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件)

    系统调用的过程中是有上下文切换的,并且发生了两次上下文切换(保存用户的指令位置,保存寄存器的值),一次用户态切换到内核态,一次内核态切回用户态

3. 分析进程对CPU的占用:

  • 内核中用就绪队列来维护所有处于可运行状态的进程,可运行状态不包括等待IO、休眠等状态的进程。

  • 进程调度器负责从就绪队列中选择处于可运行状态的进程来执行。

  • 而所有不处于可运行状态的进程,并不占用CPU资源,这些进程都等待被相关的事件比如网络IO唤醒,唤醒之后的进程更改状态为可运行状态,同时加入到就绪队列中,然后才能被调度器算法选择执行。

    因此在一个进程的整个生命周期中,虽然进程看上去一直存在,但是并不是所有时候都在占用CPU,根据占用CPU与否分为on cpu 和 off cpu。

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e80f7428a8a741cdb8928b156300da56~tplv-k3u1fbpfcp-zoom-1.image

    进程因为各种原因(被其他进程抢占、自己调用了sleep系统调用主动进入睡眠状态、等待网络IO等)被剥夺了执行权的时候,首先会调用deactivate_task函数从就绪队列中删除,接下来调用context_switch函数进行进程的上下文切换,这个时候旧的进程失去CPU的执行权,此时正式进入off cpu时间中

其实火焰图就是分析on cpu和off cpu的时间,如果on cpu或者off cpu的时间过长,就要看看为什么进程一直持有cpu,或者进程一直拿不到cpu


4. 做一个小实验: 观测CPU上下文切换

我们想要了解火焰图生成的原理,首先就要知道怎么找到CPU上下文切换的时机,下面用一个小例子演示下

  1. 测试程序 fake_make.c:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

int get_job_num(int argc, char *const *argv);

void *busy(void *args) {
    while (1);
}

int main(int argc, char *argv[]) {
    int num = get_job_num(argc, argv);
    printf("job num: %d\n", num);
    pthread_t threads[num];
    int i;
    for (i = 0; i < num; ++i) {
        pthread_create(&threads[i], NULL, busy, NULL);
    }
    for (i = 0; i < num; ++i) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

int get_job_num(int argc, char *const *argv) {
    if (argc <= 1) {
        printf("illegal args\nusage ./fake_make -j8\n");
        exit(10);
    }
    int num;

    const char *optstring = "j:"; // 有三个选项-abc,其中c选项后有冒号,所以后面必须有参数
    int ret;
    ret = getopt(argc, argv, optstring);

    if (ret != 'j') {
        printf("illegal args\nusage ./fake_make -j8\n");
        exit(1);
    }
    num = atoi(optarg);
    return num;
}

4.1 Systemtap 观测CPU上下文切换

systemtap 提供的一个探针 (probe)和函数的内置库tapsap,可以在我们编写的 systemtap 脚本中复用他们 ,类似于C语言的库函数,我们可以使用它已经编写好的函数和功能。

我们要观测CPU上下文切换,其实就是在要找到CPU 上任务运行切换的时机,systemtap的内置库stap 中有一个和调度器相关的一些探针,其中就有有关CPU_OFF(被替换下CPU)的探针。

参考: github.com/jav/systemt…

/**
 * probe scheduler.cpu_off - Process is about to stop running on a cpu
 *
 * @name: name of the probe point
 * @task_prev: the process leaving the cpu (same as current)  
 *             保存切换之前的进程
 * @task_next: the process replacing current
 *             保存切换之前的进程
 * @idle: boolean indicating whether current is the idle process
 *             表示当前CPU是否空闲
 *
 * Context: The process leaving the cpu.
 *
 */
probe scheduler.cpu_off =
	kernel.trace("sched_switch") !,
	kernel.function("context_switch")
{
    name = "cpu_off"
    task_prev = $prev
    task_next = $next
    idle = __is_idle()
}

参考systemtap的 stap,我们自定义一个探针

global csw_count
global idle_count
global csw_total

probe scheduler.cpu_off {
      csw_count[task_prev, task_next]++
      csw_total+=1
      idle_count+=idle
}

function fmt_task(task_prev, task_next) {
   return sprintf("tid(%d)->tid(%d)", task_tid(task_prev), task_tid(task_next))
}

function print_cswtop () {
  printf ("%45s %10s\n", "Context switch", "COUNT")
  foreach ([task_prev, task_next] in csw_count- limit 5) {
    printf("%45s %10d\n", fmt_task(task_prev, task_next), csw_count[task_prev, task_next])
  }
  printf("%45s %10d\n", "csw_total", csw_total)
  printf("%45s %10d\n", "idle", idle_count)

  delete csw_total
  delete csw_count
  delete idle_count
}

probe timer.s($1) {
  print_cswtop ()
  printf("--------------------------------------------------------------\n")
}
  • 编译demo程序fake_make.c gcc fake_make.c -o fake_make

  • 执行 fake_make 程序./fake_make -j4

  • systemtap 查看上下文切换次数 stap cswstap.stap 1

1.png

另外linux中还有别的工具可以观测CPU的上下文切换

  • vmstat 观测 vmstat pid
  • pidstat 观测 pidstat -w -p pid