0.相关理论

  • 80%结果由20%原因引起
  • 先找木桶短板再优化
  • 采样->分析瓶颈和热点->定位问题->优化->测试

1.内核自带统计信息

1.1 proc文件系统

让用户和内核内部数据结构交互,并不是真正意义上的文件系统,在内存且不占用磁盘,文件本身显示0字节,但查看就会返回大量信息。ps、top等shell命令就是从proc文件系统中读取信息的。

  • /proc/pagetypeinfo 这是URL链表上有多少活跃和不活跃页面的统计
  • /proc/zoneinfo zone结构体的相关信息,按照node来显示的,会显示伙伴系统管理的页面数量、各种水位线页面的数量等
  • /proc/slabinfo slab分配的小的内存的信息
  • /proc/cpuinfo processer如果是7则是指有8个核心
  • /proc/[pid]/maps 或者 pmap pid 命令来查看进程用户态虚拟内存空间的实际分布
  • /proc/iomem 命令来查看进程内核态虚拟内存空间的的实际分布,还能查看内存的物理地址,不用sudo竟然地址全是0
  • /proc/buddyinfo命令来查看NUMA节点中不同内存区域zone的伙伴系统当前状态,迁移类型,链表成员数量
  • /proc/meminfo显示的是当前时刻物理页的信息,包括LRU的、slab的、hugepage的等等,meminfo_proc_show函数实现
  • proc/[pid]/status | grep -E 'Name|Pid|Vm*|Rss*|Vm*|Hu*'是进程和内存相关的信息。数据结构mm_struct成员rss_stat会记录进程内存的使用情况。(VmRSS进程使用的最大物理内存、RssAnon进程使用的匿名页、RssFile、RssShmem共享页、VmPTE页表大小、VmSwap交换区大小等)
  • proc/mounts已加载的文件系统列表
  • proc/modules已加载的模块
  • proc/cmdline系统启动时输入的内核命令行参数
  • proc/kmsg内核日志信息

写一个内核模块在/proc创建benshushu目录,通过read节点读取内核模块某个全局变量值。通过write修改某个全局变量值

//procfs的常用API在fs/proc/internal.h文件中有定义
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>

#define NODE "benshushu/my_proc"

static int param = 100;
static struct proc_dir_entry *my_proc;
static struct proc_dir_entry *my_root;

#define KS 32
static char kstring[KS];	/* should be less sloppy about overflows :) */

static ssize_t
my_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
	int nbytes = sprintf(kstring, "%d\n", param);
	return simple_read_from_buffer(buf, lbuf, ppos, kstring, nbytes);
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t lbuf,
			loff_t *ppos)
{
	ssize_t rc;
	rc = simple_write_to_buffer(kstring, lbuf, ppos, buf, lbuf);
	sscanf(kstring, "%d", &param);
	pr_info("param has been set to %d\n", param);
	return rc;
}

//文件函数操作集
static const struct file_operations my_proc_fops = {
	.owner = THIS_MODULE,
	.read = my_read,
	.write = my_write,
};

static int __init my_init(void)
{
    //proc_mkdir在父目录中创建一个benshushu的目录,这里父目录NULL那就是指在/proc根目录下创建了
	my_root = proc_mkdir("benshushu", NULL);
	if (IS_ERR(my_root)){
		pr_err("I failed to make benshushu dir\n");
		return -1;
	}

    //proc_create创建一个新的文件节点,名称是NODE宏定义,节点访问权限0(UGO模式),父进程的proc_dir_entry对象NULL,my_proc_fops指向文件的操作函数。
	my_proc = proc_create(NODE, 0, NULL, &my_proc_fops);
	if (IS_ERR(my_proc)){
		pr_err("I failed to make %s\n", NODE);
		return -1;
	}
	pr_info("I created %s\n", NODE);
	return 0;
}

static void __exit my_exit(void)
{
	if (my_proc) {
		proc_remove(my_proc);
		proc_remove(my_root);
		pr_info("Removed %s\n", NODE);
	}
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
BASEINCLUDE ?= /lib/modules/`uname -r`/build

proc-test-objs := proc_test.o 

obj-m	:=   proc-test.o
all : 
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;

clean:
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
	rm -f *.ko;

1.2 sys文件系统

子系统、设备驱动程序将sysfs作为与用户空间交互的接口。
功能和上一个一样,sysfs的API又有所不同。

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/sysfs.h>

#define NODE "benshushu/my_sysfs"

static int param = 100;
static struct proc_dir_entry *my_sysfs;
static struct proc_dir_entry *my_root;
static struct platform_device *my_device;

#define KS 32
static char kstring[KS];	/* should be less sloppy about overflows :) */

static ssize_t
my_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
	int nbytes = sprintf(kstring, "%d\n", param);
	return simple_read_from_buffer(buf, lbuf, ppos, kstring, nbytes);
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t lbuf,
			loff_t *ppos)
{
	ssize_t rc;
	rc = simple_write_to_buffer(kstring, lbuf, ppos, buf, lbuf);
	sscanf(kstring, "%d", &param);
	pr_info("param has been set to %d\n", param);
	return rc;
}

//文件的操作集
static const struct file_operations my_proc_fops = {
	.owner = THIS_MODULE,
	.read = my_read,
	.write = my_write,
};

static ssize_t data_show(struct device *d,
		struct device_attribute *attr,  char *buf)
{
	return sprintf(buf, "%d\n", param);
}

static ssize_t data_store(struct device *d,
		 struct device_attribute *attr,
		 const char *buf, size_t count)
{
	sscanf(buf, "%d", &param);
	dev_dbg(d, ": write %d into data\n", param);

	return strnlen(buf, count);
}

static DEVICE_ATTR_RW(data);

//用于描述文件属性的数据结构
static struct attribute *ben_sysfs_entries[] = {
	&dev_attr_data.attr,
	NULL
};

//一组属性类型,这个数据结构定义在include/linux/sysfs.h
static struct attribute_group mydevice_attr_group = {
	.name = "benshushu",
	.attrs = ben_sysfs_entries, //attribute数据结构
};

static int __init my_init(void)
{
	int ret;

	my_root = proc_mkdir("benshushu", NULL);
	my_sysfs = proc_create(NODE, 0, NULL, &my_proc_fops);
	if (IS_ERR(my_sysfs)) {
		pr_err("I failed to make %s\n", NODE);
		return PTR_ERR(my_sysfs);
	}
	pr_info("I created %s on procfs\n", NODE);

    //kobject_create_and_add()生成kobject数据结构,注册到sysfs文件系统,参数为:文件名、父目录的kobject
	my_device = platform_device_register_simple("benshushu", -1, NULL, 0);
	if (IS_ERR(my_device)) {
		printk("platfrom device register fail\n");
		ret = PTR_ERR(my_device);
		goto proc_fail;
	}

    //(属性集合,一组属性类型)
	ret = sysfs_create_group(&my_device->dev.kobj, &mydevice_attr_group);
	if (ret) {
		printk("create sysfs group fail\n");
		goto register_fail;
	}

	pr_info("create sysfs node done\n");

	return 0;

register_fail:
	platform_device_unregister(my_device);
proc_fail:
	return ret;
}

static void __exit my_exit(void)
{
	if (my_sysfs) {
		proc_remove(my_sysfs);
		proc_remove(my_root);
		pr_info("Removed %s\n", NODE);
	}

	sysfs_remove_group(&my_device->dev.kobj, &mydevice_attr_group);

	platform_device_unregister(my_device);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
BASEINCLUDE ?= /lib/modules/`uname -r`/build

sysfs-test-objs := sysfs_test.o 

obj-m	:=   sysfs-test.o
all : 
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;

clean:
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
	rm -f *.ko;

1.3 debugfs文件系统

调试内核的内存文件系统,在运行中修改某些内核数据,将关心的数据映射到用户空间。mount -t debugfs none /sys/kernel/debug

相关API定义在include/linux/debuggfs.h

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/debugfs.h>

#define NODE "benshushu"

static int param = 100;
struct dentry *debugfs_dir;

#define KS 32
static char kstring[KS];	/* should be less sloppy about overflows :) */

static ssize_t
my_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
	int nbytes = sprintf(kstring, "%d\n", param);
	return simple_read_from_buffer(buf, lbuf, ppos, kstring, nbytes);
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t lbuf,
			loff_t *ppos)
{
	ssize_t rc;
	rc = simple_write_to_buffer(kstring, lbuf, ppos, buf, lbuf);
	sscanf(kstring, "%d", &param);
	//pr_info("param has been set to %d\n", param);
	return rc;
}

static const struct file_operations mydebugfs_ops = {
	.owner = THIS_MODULE,
	.read = my_read,
	.write = my_write,
};

static int __init my_init(void)
{
	struct dentry *debug_file;

	debugfs_dir = debugfs_create_dir(NODE, NULL);
	if (IS_ERR(debugfs_dir)) {
		printk("create debugfs dir fail\n");
		return -EFAULT;
	}

	debug_file = debugfs_create_file("my_debug", 0444,
			debugfs_dir, NULL, &mydebugfs_ops);
	if (IS_ERR(debug_file)) {
		printk("create debugfs file fail\n");
		debugfs_remove_recursive(debugfs_dir);
		return -EFAULT;
	}

	pr_info("I created %s on debugfs\n", NODE);
	return 0;
}

static void __exit my_exit(void)
{
	if (debugfs_dir) {
		debugfs_remove_recursive(debugfs_dir);
		pr_info("Removed %s\n", NODE);
	}
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
BASEINCLUDE ?= /lib/modules/`uname -r`/build

debugfs-test-objs := debugfs_test.o 

obj-m	:=   debugfs-test.o
all : 
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;

clean:
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
	rm -f *.ko;

2.查看系统内存信息工具

2.1 top

前几行是free -h的信息,还有些可以交互的命令,比如f是显示隐藏的统计信息比如缺页等,M是按照使用内存排序
top显示的排序是:

  • PR进程优先级
  • NI nice值
  • VIRT进程使用的虚拟内存总量
  • RES进程使用的没有被换出的物理内存大小
  • SHR共享内存大小
  • S进程状态(D不可中断睡眠,R运行,S睡眠,T跟踪/停止,Z僵尸)
  • %MEM进程使用物理内存百分比
  • %CPU上次更新到现在的CPU时间占用百分比
  • TIME+ 进程使用CPU的时间总计,单位10ms
  • CMOMAND命令名或者命令行

2.2 vmstat

第一个参数是时间间隔,第二个参数是采样次数。
-S M指按照兆字节来显示。

  • r正在执行和等待的进程数
  • b阻塞的进程
  • swpd通常而言,最优内存管理所需的最小交换空间取决于程序固定在内存中而又很少访问到的匿名页面的数量, 以及回收这些匿名页面换来的价值。后者大体上来说是问哪些页面不再会因为要保留这些很少访问的匿名页面而 被回收掉腾出空间。
  • free空闲物理内存
  • buff用于磁盘缓存的大小
  • cache用于页面缓存的内存大小
  • si从磁盘换入的内存大小,so交换到磁盘的内存大小。页的换入换出
  • 磁盘读写块数量bibo从块设备
  • 每秒中断次数算不算太大in
  • cs 每秒上下文切换次数数百到一万以内都是正常的,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。下图来自这篇,很详细的找瓶颈 线程数不要超过物理机的2-3倍
  • 内核系统进程sy和用户进程us执行时间百分比
  • wa是CPU的I/O等待时间百分比
  • id空闲CPU百分比

man手册

2.3 iostat

  • iowait CPU 等待磁盘 IO 操作的时间
  • rrqm/s:每秒合并的读请求数。这表示磁盘在读取数据时是否进行了请求的合并操作。较高的值可能表示磁盘正在优化读取性能。
  • wrqm/s:每秒合并的写请求数。类似于rrqm/s,这表示磁盘在写入数据时是否进行了请求的合并操作。
  • r/s:每秒完成的读请求数。这表示磁盘每秒实际执行的独立的读取操作数量。
  • w/s:每秒完成的写请求数。这表示磁盘每秒实际执行的独立的写入操作数量。
  • rkB/s:每秒读取的数据量(以KB为单位)。表示从磁盘读取的数据量。
  • wkB/s:每秒写入的数据量(以KB为单位)。表示向磁盘写入的数据量。
  • avgrq-sz:平均请求大小(以扇区为单位)。表示平均每个I/O请求的大小。
  • avgqu-sz:平均I/O队列长度。表示等待磁盘服务的请求的平均数量。
  • await:平均每个I/O请求的等待时间(以毫秒为单位)。较低的值通常表示更好的性能。
  • svctm:平均每个I/O请求的服务时间(以毫秒为单位)。这表示磁盘实际执行请求所需的时间。
  • %util:磁盘利用率。表示磁盘的负载程度,较高的值表示磁盘正在饱和。
    iowait过高的查找和解决方案

3. printk输出函数和动态输出

3.1 printk最简单有效的调试方法

在Linux内核中,printk函数可以使用不同的优先级来记录日志消息。以下是一些常用的printk优先级:

  1. KERN_EMERG:紧急情况。用于记录最紧急的系统消息,通常表示系统崩溃或无法继续运行的情况。这个优先级的消息通常会导致系统宕机。
  2. KERN_ALERT:警报。用于记录需要立即采取行动的情况,通常表示严重的系统问题,但不一定导致宕机。
  3. KERN_CRIT:关键。用于记录严重的系统问题,但没有导致系统崩溃。这可以是一个需要紧急关注的问题。
  4. KERN_ERR:错误。用于记录错误消息,表示系统遇到了一个问题,但仍然可以继续运行。
  5. KERN_WARNING:警告。用于记录警告消息,表示可能存在一些问题,但系统仍在正常运行。
  6. KERN_NOTICE:通知。用于记录一般信息,通常表示系统中发生的一些重要事件。
  7. KERN_INFO:信息。用于记录普通信息,通常用于调试和跟踪系统行为。
  8. KERN_DEBUG:调试。用于记录调试信息,通常只在开发和调试阶段使用。

这些优先级可以与printk函数一起使用,例如:

printk(KERN_ERR "This is an error message.\n");

在这个例子中,KERN_ERR表示这是一个错误消息,该消息将被记录到系统日志中。

对于新驱动程序,建议使用更方便的打印API,这些API在其名称中嵌入了日志级别。这些打印辅助函数包括pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_warn、pr_notice、pr_info、pr_debug或pr_dbg
除了比等效的printk()调用更简洁外,它们还可以通过pr_fmt()宏 对格式字符串使用
通用定义,例如,在源文件的顶部(在任何#include 指令之前)定义它:

#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__

这将在该文件中的每个pr_*消息前加上产生该消息的模块和函数名称。内核得是用DEBUG编译的,否则它们被替换为空语句。

3.2动态输出

由系统维护者动态地打开和关闭指定的printk输出。配置内核时打开CONFIG_DYNAMIC_DEBUG宏,挂载debugfs文件系统。

动态输出语句由pr_debug()/dev_dbg()这些函数定义。

具体来说动态输出会在debugfs文件系统的control文件节点被记录,输出包括文件路径名、输出语句所在行号、模块名(函数名)、将要输出的语句。cat /sys/kernel/debug/dynamic_debug/control

#打开svcsock.c文件中的所有动态输出语句
echo 'file svcsock.c +p' > /sys/kernel/debug/dynamic_debug/control
#打开usbcore模块中的所有动态输出语句
echo 'module usbcore +p' > /sys/kernel/debug/dynamic_debug/control
#打开svc_process()函数中的所有动态输出语句
echo 'func svc_process +p' > /sys/kernel/debug/dynamic_debug/control
#关闭svc_process()函数中的所有动态输出语句
echo 'func svc_process -p' > /sys/kernel/debug/dynamic_debug/control
#打开文件路径中包含usb的文件里的所有动态输出语句
echo -n '*usb* +p' > /sys/kernel/debug/dynamic_debug/control
#打开系统所有的动态输出语句
echo -n '+p' > / sys/kernel/debug/dynamic_debug/control
  • p打开关闭动态输出语句
  • l输出行号
  • f输出函数名
  • m输出模块名
  • t输出线程ID

对于那些开机就已经完成运行的代码,调试方式是在qemu启动时加入命令,比如SMP topology的动态输出语句打开方式在qemu启动命令加上topology.dyndbg=+plft这样输入dmesg | grep 'cluster' 就能看见输出。

4. cppcheck

这是一个用于静态检查代码错误的工具,可以避免越界访问等一些问题,减少后期在内核调试的成本。在改改内核的时候用起来操作手册

5. ftrace

使用set_ftrace_pid

# cd /sys/kernel/debug/tracing/
# cat set_ftrace_pid
no pid
# echo 3111 > set_ftrace_pid //跟踪PID为3111的进程
# cat set_ftrace_pid
3111
# echo function > current_tracer
# echo 1 > tracing_on
# usleep 1
# echo 0 > tracing_on
# cat trace

frace还支持种更为直观的跟踪器,名为function_gaph,使用方法和function跟踪器类似。

动态ftrace
只要在配置内核时打开CONFIG_DYNAMIC_FTRACE选项,就可以支持动态ftrace功能。set_ftrace_filter和set_ftrace_notrace这两个文件可以配对使用,其中,前者设置要跟踪的函数,后者指定不要跟踪的函数。如果函数太多,需要设置跟踪函数的过滤器,过滤器还支持通配符。available_filter_functions文件可以列出当前系统支持的所有函数,假如现在我们只想关注sys_nanosleep()和hrtimer_interrupt()这两个函数。

# cd /sys/kernel/debug/tracing/
# echo sys_nanosleep hrtimer_interrupt > set_ftrace_filter
# echo function > current_tracer
# echo 1 > tracing_on
# usleep 1

也可以将要跟踪的事件添加到相应文件,差不多的方式。例子在samples/trace_events目录里面。

kernelshark图形化工具
sudo apt-get install trace-cmd kernelshark
分析ftrace数据用

6. kdump

内核转储工具。kdump核心实现基于kexec, kexec 的全称是kernel execution,非常类似于Linux内核中的exec系统调用。kexec可以快速启动新的内核,并且跳过BIOS或bootloader等引导程序的初始化阶段。这个特性可以让系统上崩溃时快速切换到备份的内核,这样第一个内核的内存就得到了保留。在第二个内核中,可以对第一个内核产生的崩溃数据进行继续分析。

kdump会在内存中保留一块区域,这块区域用来存放捕获内核。当生产内核在运行过程中遇到崩溃等情况时,kdump会通过kexec机制自动启动到捕获内核,这时会绕过BIOS,以免破坏第一个内核的内存,然后把生产内核的完整信息(包括CPU寄存器、栈数据等)转储到指定文件中。接着,使用crash工具分析这个转储文件,就可以快速定位宕机问题了。

kdump+crash工具的适用范围:主要用来分析系统宕机黑屏、无响应(unresponsive)等问题,比如SSH、串口、鼠标键盘无响应等。有一类宕机情况kdump无能为力,比如因硬件错误导致CPU崩溃。也就是说,系统不能正常地热重启,只能通过重新关闭和开启电源才能启动,这种情况下kdump就不适用了。因为kdump需要在系统崩溃的时候快速启动到捕获内核,但前提条件就是系统能热启动,并且内存中的内容不会丢失。

7. 常见的Linux性能测试工具

工具 描述
kernel-selftests 内核源代码目录自带的测试程序
perf-bench perf工具自带的测试程序,包含对内存、调度等的测试
phoronix-test-suit 综合性能测试程序
sysbench 综合性能测试套件,包含对CPU、内存、多线程等的测试
unixbench 综合性能测试套件,UNIX系统的一套传统的测试程序
pmbench 用来测试内存性能的工具
iozone 用来测试文件系统性能的工具
AIM7 一套来自UNIX系统的测试系统底层性能的工具
iperf 用来测试网络性能的工具
linpack 用来测试CPU的浮点运算的性能
vm-scalability 用来测试Linux内核内存管理模块的扩展性
glbenchmark 用来测试GPU性能
GFXbenchmark 用来测试GPU性能
DBENCH 用来测试I/O性能

内容来自《奔跑吧Linux内核基础篇》这些工具记录在这里,有一部分也没用过。

8. perf和火焰图

性能计数器是 CPU 硬件寄存器,用于计算硬件事件,例如执行的指令、遭受的缓存未命中或错误预测的分支。利用这些数据可以跟踪动态控制流和识别热点。Perf 提供每个任务、每个 CPU 和每个工作负载计数器、基于这些计数器的采样和源代码事件注释。

跟踪点是放置在代码中的逻辑位置的检测点,例如用于系统调用、TCP/IP 事件、文件系统操作等。这些在不使用时开销可以忽略不计,并且可以通过 perf 命令启用以收集包括时间戳和堆栈跟踪在内的信息。Perf 还可以使用 kprobes 和 uprobes 框架动态创建跟踪点,用于内核和用户空间动态跟踪。

perf 功能强大:它可以检测 CPU 性能计数器、跟踪点、kprobes 和 uprobes(动态跟踪)。它能够进行轻量级分析。它包含在 Linux 内核中的 tools/perf 下,并且经常更新和增强。对于perf子系统更深入的剖析在笔者另一篇读源码的文章中。这里主要讲怎么使用封装好的用户空间的perf工具。以page fault为例子:

# -I 1000 每1000ms输出一次,
# -a 采集全部CPU上的事件
# -p <pid> 可以指定进程
> perf stat -e page-faults -I 1000 -a
> perf stat -e page-faults -I 1000 -a -p 10102

使用FlameGraph更加直观的看到各部分代码触发pagefault的比例

# 采集进程10102的30秒pagefault触发数据
> perf record -e page-faults -a -p 10102 -g -- sleep 30
# 导出原始数据,此步必须在采集机器上进行,因为需要解析符号。
> perf script > out.stacks
# 下面的步骤就可以移动到其他机器上了
> ./FlameGraph/stackcollapse-perf.pl < out.stacks | ./FlameGraph/flamegraph.pl --color=mem \
    --title="Page Fault Flame Graph" --countname="pages" > out.svg
  • stat 收集性能计数器的统计数据
  • record Run a command and record its profile into perf.data
  • -e 事件选择器。使用 “perf list “来列出可用的事件。
  • -p 对现有进程ID的统计事件
  • -a 从所有CPU收集全系统的信息
  • -i -I 子任务不继承计数器
  • -o 输出文件名和文件路径

dTLB是数据TLB,iTLB是指令TLB,因为CPU在处理指令和数据时访问方式不同,所以指令页和数据页是分开访问的。
TLB是用于快速查找虚拟地址到物理地址的映射关系的,他们分别映射的是指令页和数据页的物理地址。
指令和数据都存放在主存,它们都以二进制代码形式出现,对于CPU而言,区分的方法为:(1)取指令或数据时所处的机器周期不同:取指周期取出的是指令;分析、取数或执行周期取出的是数据。(2)取指令或数据时地址的来源不同:指令地址来源于程序计数器;数据地址来源于地址形成部件。
当CPU执行一条指令时,会将下一条指令的地址预取到缓存中,这个预取的过程称为指令预取。iTLB用于缓存指令页的映射信息,可以加速指令预取和指令执行的过程。
常用事件的解释参考博客

  • branches :这段时间内发生分支预测的次数。现代的CPU都有分支预测方面的优化
  • branch-instructions:分支预测成功次数
  • branch-misses :这段时间内分支预测失败的次数,这个值越小越好
  • cache-references:cache命中次数
  • cache-misses:cache失效次数
  • context-switches:下文切换次数,前半部分是切换次数,后面是平均每秒发生次数(M是10的6次方)
  • cpu-cycles:统计cpu周期数,cpu周期:指一条指令的操作时间。
  • cycles:程序消耗的处理器周期数
  • instructions:执行的指令条数,insns per cycle: 即IPC,每个cpu周期执行的指令条数,IPC比上面的CPU使用率更能说明CPU的使用情况,(很多指令需要多个处理周期才能执行完毕),IPC越大越好,说明程序充分利用了处理器的特征。
  • L1-dcache-loads :一级数据缓存读取次数
  • L1-dcache-load-missed :一级数据缓存读取失败次数
  • LLC-loads:last level cache 读取次数
  • LLC-load-misses:last level cache 读取失败次数
  • major-faults:页错误,内存页已经被swap到硬盘上,需要I/O换回
  • minor-faults :页错误,内存页在物理内存中,只是没有和逻辑页进行映射
  • page-faults:缺页异常的次数。当应用程序请求的页面尚未建立、请求的页面不在内存中,或者请求的页面虽然在内存中,但物理地址和虚拟地址的映射关系尚未建立时,都会触发一次缺页异常。另外TLB不命中,页面访问权限不匹配等情况也会触发缺页异常。
  • stalled-cycles-frontend和stalled-cycles-backend表示CPU停滞统计
  • task-clock (msec): cpu处理task所消耗的时间,表示目标任务真正占用处理器的时间,单位ms,CPUs utilized表示cpu使用率, 该值越高代表程序是CPU bound(计算密集型)而非IO bound(I/O密集型)

具体命令写法参考文档里面写了各种可以测量的事件。比如:

perf stat -e dTLB-loads,dTLB-load-misses,iTLB-load-misses,cpu-cycles,cache-misses,cache-references,page-faults,major-faults,minor-faults,LLC-loads,LLC-loads-misses -a ./test.sh

确定 CPU 繁忙的原因是一项性能分析的例行任务,这通常涉及分析堆栈跟踪。火焰图是采样堆栈跟踪的可视化,可以快速识别热代码路径。火焰图可以与任何操作系统上的任何 CPU 分析器一起使用。我在这里的例子使用Linux perf (perf_events),DTrace,SystemTap和ktap。有关其他探查器示例,请参阅更新列表,有关火焰图软件,请参阅 github火焰图使用不同工具获得的信息在生成火焰图时命令不同,参考博客

y轴表示调用栈, 每一层都是一个函数。 调用栈越深, 火焰就越高, 顶部就是正在执行的函数, 下方都是它的父函数。
x轴表示抽样数, 如果一个函数在x轴占据的宽度越宽, 就表示它被抽到的次数多, 即执行的时间长。 注意x轴不代表时间, 而是所有的调用栈合并后, 按字母顺序排列的。
火焰图就是看顶层的哪个函数占据的宽度最大。 只要有 “平顶”(plateaus), 就表示该函数可能存在性能问题。
颜色没有特殊含义, 因为火焰图表示的是CPU的繁忙程度, 所以一般选择暖色调。

# 采样,1s10次
sudo perf recoed -F 10 -o ${LOG_DIR}/perf.data -a -g ${BENCH_RUN}

# 画图,因为写入是默认名字所以这里处理的输入也就默认名
sudo perf script | ./tools/FlameGraph/stackcollapse-perf.pl | ./tools/FlameGraph/flamegraph.pl > perf.svg

9. Intel MLC (Memory Latency Checker)

决定应用程序性能的一个重要因素是应用程序从处理器的缓存层次结构和内存子系统获取数据所需的时间。在启用非统一内存访问 (NUMA) 的多插槽系统中,本地内存延迟和跨插槽内存延迟会有很大差异。除了延迟之外,带宽 (b/w) 在决定性能方面也发挥着重要作用。因此,测量这些延迟和带宽对于为被测系统和性能分析建立基线非常重要。

英特尔® 内存延迟检查器(英特尔® MLC)是一种用于测量内存延迟和带宽以及它们如何随着系统负载增加而变化的工具。它还为更细粒度的调查提供了多种选项,还可以测量从一组特定核心到缓存或内存的带宽和延迟。

root权限来运行./mlc >> /home/xmu3/Documents/res/mlc.out 不加后缀的话是都可以测量的。

这些命令在运行工作负载和空机运行时应该会有所不同,用ycsb的数据对比一下

mlc --latency_matrix
# 打印本地和跨插槽理想情况下内存延迟的矩阵,请求来自每个socket并且发送到每个空闲socket
mlc --bandwidth_matrix
# 打印本地和跨插槽内存带宽的矩阵
mlc --peak_injection_bandwidth
# 针对所有本地访问的各种读写比率打印峰值内存带宽(核心以尽可能快的速率生成请求)
mlc --max_bandwidth
# 针对所有本地访问的各种读写比率打印最大内存带宽(通过自动改变负载注入速率)
mlc --idle_latency
# 打印平台的空闲内存延迟
mlc --loaded_latency
# 打印平台加载的内存延迟
mlc --c2c_latency
# 打印平台的缓存到缓存传输延迟(cache-to-cache transfer latencies )
mlc -e
# 不要修改预取器设置
mlc --memory_bandwidth_scan 
# 打印整个内存中每个 1 GB 地址范围的内存带宽

默认命令的解释:

Intel(R) Memory Latency Checker - v3.8
*** Unable to modify prefetchers (try executing 'modprobe msr')
*** So, enabling random access for latency measurements
Measuring idle latencies (in ns)...
# 理想情况下内存延迟的矩阵,请求来自每个socket并且发送到每个可用socket。mlc --latency_matrix
		Numa node
Numa node	     0	     1	     2	     3	
       0	  40.0	  61.5	 108.5	 139.5	
       1	  61.8	  39.8	 141.8	 108.5	

Measuring Peak Injection Memory Bandwidths for the system
Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec)
Using all the threads from each core if Hyper-threading is enabled
Using traffic with the following read-write ratios
# 针对具有不同读取和写入量的请求(每个核心尽可能快地生成请求)测量峰值注入内存带宽(对本地内存的所有访问)。mlc --peak_injection_bandwidth
ALL Reads        :	161501.6	
3:1 Reads-Writes :	194996.6	
2:1 Reads-Writes :	211301.9	
1:1 Reads-Writes :	256226.9	
Stream-triad like:	147516.0	

# 源自每个socket并寻址到每个可用socket的请求的内存带宽矩阵。mlc --bandwidth_matrix
Measuring Memory Bandwidths between nodes within system 
Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec)
Using all the threads from each core if Hyper-threading is enabled
Using Read-only traffic type
		Numa node
Numa node	     0	     1	     2	     3	
       0	80686.1	54665.8	25508.8	 2529.8	
       1	54657.7	80956.5	 2571.2	25510.1	

Measuring Loaded Latencies for the system
Using all the threads from each core if Hyper-threading is enabled
Using Read-only traffic type
Inject	Latency	Bandwidth
# 不同带宽点的延迟
Delay	(ns)	MB/sec
==========================
 00000	455.70	 161519.3
 00002	458.47	 161556.4
 00008	454.24	 161501.8
 00015	448.57	 161506.9
 00050	405.38	 161417.2
 00100	396.14	 161388.8
 00200	135.20	 152952.1
 00300	 62.77	 116188.2
 00400	 53.61	  88853.4
 00500	 51.12	  71974.5
 00700	 48.78	  52310.7
 01000	 47.38	  37255.9
 01300	 46.68	  29088.7
 01700	 45.94	  22646.6
 02500	 45.75	  15891.3
 03500	 45.14	  11785.4
 05000	 44.83	   8691.3
 09000	 44.16	   5489.5
 20000	 42.18	   3338.6
 
 # 处理器中缓存之间的延迟。mlc --c2c_latency
Measuring cache-to-cache transfer latency (in ns)...
Local Socket L2->L2 HIT  latency	35.8
Local Socket L2->L2 HITM latency	59.9
Remote Socket L2->L2 HITM latency (data address homed in writer socket)
			Reader Numa Node
Writer Numa Node     0	     1	
            0	     -	  54.5	
            1	  54.7	     -	
Remote Socket L2->L2 HITM latency (data address homed in reader socket)
			Reader Numa Node
Writer Numa Node     0	     1	
            0	     -	  53.7	
            1	  55.3	     -	

10. 使用SystemTap软件跟踪内核函数

衡量 TPH 如何影响性能和延迟的另一种有效方法是跟踪/探测 Linux 内核功能。为此使用SystemTap,它是“用于动态检测正在运行的生产Linux内核操作系统的工具”。这个工具有它自己的脚本语言,使用非常灵活。

第一个函数是__alloc_pages_slowpath。当没有可用于分配的连续内存块时,将执行它。反过来,此函数调用可能导致延迟峰值的昂贵页面压缩和回收逻辑。第二个函数是khugepaged_scan_mm_slot。它由后台“khugepaged”内核线程执行。它扫描巨大的页面并试图将它们折叠成一个。
脚本如下:

#! /usr/bin/env stap
global start, intervals

probe $1 { start[tid()] = gettimeofday_us() }
probe $1.return
{
  t = gettimeofday_us()
  old_t = start[tid()]
  if (old_t) intervals <<< t - old_t
  delete start[tid()]
}

probe timer.ms($2)
{
    if (@count(intervals) > 0)
    {
        printf("%-25s:\n min:%dus avg:%dus max:%dus count:%d \n", tz_ctime(gettimeofday_s()),
             @min(intervals), @avg(intervals), @max(intervals), @count(intervals))
        print(@hist_log(intervals));
    }
}

__alloc_pages_slowpath函数结果示例

[~]# ./func_time_stats.stp 'kernel.function("__alloc_pages_slowpath")' 1000

Thu Aug 17 09:37:19 2017 CEST:
 min:0us avg:1us max:23us count:1538
value |-------------------------------------------------- count
    0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  549
    1 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  541
    2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                 377
    4 |@@@@                                                54
    8 |@                                                   12
   16 |                                                     5
   32 |                                                     0
   64 |                                                     0
...

SystemTap软件用于收集运行的Linux的信息

11.追踪函数调用

追踪相关的系统调用,strace -h可以看到相关的操作。可以展示系统调用消耗的总时间占比、具体时间s、调用返回的错误。相比而言调用次数比时间更可靠。ltrace是对库函数调用的追踪。

Over View


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
  目录