一 亲和性
高性能的应用有个技巧就是做任务和cpu核心的绑定, 即设置CPU亲和性,绑定之后就可以让这个任务尽可能在这个核心上长时间运行,而不被切换到其他核心上去.
为什么要设置亲和性那, 要想让程序高性能运行,必须利用好高速缓存,而像L1,L2这类高速缓存,在进行定时切换任务的时候,必然会失效,从而影响程序的性能;另外减少了调度,也提升了此任务的实时性。
是不是设置了任务的亲和性就不会有其他任务调度到这个核心上了嘛?也不是的,亲和性也只是尽力保证任务长时间在这个核心上运行,比如一个cpu一个核心,如果一个任务绑定了核心,那岂不是其他任务就不要执行了,还有个办法就是做CPU核心的隔离。隔离CPU核心即这些CPU不再被用户空间的进程使用,内核仍然可以用,从而禁止用户空间的进程(除非手动设置affinity)调度到DPDK的核心上去.
二 隔离CPU
2.1 Linux内核隔离CPU
vim /etc/default/grub
#编辑
GRUB_CMDLINE_LINUX="crashkernel=auto resume=/dev/mapper/cl-swap rd.lvm.lv=cl/root rd.lvm.lv=cl/swap rhgb quiet iommu=pt intel_iommu=on transparent_hugepage=never default_hugepagesz=2M hugepagesz=2M hugepages=1024 isolcpus=4,5,6,7"
增加的选项,即隔离CPU4,5,6,7核心。
isolcpus=4,5,6,7
然后执行:
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot -h now
重启后就生效了,现在可以通过top
或htop
命令,查看cpu只有0-3有任务,4-7上没有任务:
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 99.1 id, 0.0 wa, 0.9 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.8 us, 0.8 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
写个shell来验证下:
while true; do echo hello>/dev/null; done;
CPU调度情况:
%Cpu0 : 7.7 us, 3.3 sy, 0.0 ni, 87.0 id, 0.0 wa, 1.0 hi, 1.0 si, 0.0 st
%Cpu1 : 15.1 us, 7.0 sy, 0.0 ni, 75.5 id, 0.0 wa, 0.3 hi, 2.0 si, 0.0 st
%Cpu2 : 17.3 us, 9.0 sy, 0.0 ni, 72.0 id, 0.0 wa, 0.3 hi, 1.3 si, 0.0 st
%Cpu3 : 23.6 us, 11.0 sy, 0.0 ni, 62.5 id, 0.0 wa, 0.3 hi, 2.7 si, 0.0 st
%Cpu4 : 0.0 us, 0.0 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.3 hi, 0.0 si, 0.0 st
%Cpu5 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 0.0 us, 0.0 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.3 hi, 0.0 si, 0.0 st
%Cpu7 : 0.0 us, 0.0 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.3 hi, 0.0 si, 0.0 st
2.2 查看进程绑定的核心
有没有命令查看进程绑定的核心嘛,当然是有的,命令如下:
[root@localhost spiderFlow]# ps -eo pid,args:50,psr
PID COMMAND PSR
1 /usr/lib/systemd/systemd --switched-root --system 0
2 [kthreadd] 2
3 [rcu_gp] 0
4 [rcu_par_gp] 0
6 [kworker/0:0H-kblockd] 0
9 [mm_percpu_wq] 2
....
48 [ksoftirqd/6] 6
49 [kworker/6:0-events] 6
50 [kworker/6:0H] 6
51 [cpuhp/7] 7
52 [watchdog/7] 7
PSR:当前分配给该进程的处理器,即进程在哪颗CPU上运行。
可以看到内核任务,也就是用[]扩起来的任务可以运行在4,5,6,7,而用户任务不能运行在4,5,6,7上。
也就是说isolcpus只能隔离用户任务,不能隔离内核任务。 运行在4,5,6,7核心上的任务多是中断任务,我们仍然可以命令,修改中断不跑在这些核心上。
2.3 设置任务的亲和性
刚才我们聊到,隔离的cpu 上可以跑内核任务,也可以跑设置亲和性的任务,那么如何设置任务的cpu亲和性那。
通过sched_setaffinity函数,可以设置CPU的亲和性,这个函数可以决定线程在指定的 cpu 中运行。在多进程系统中,适当的为线程指定 cpu 可以提升效率,比如,指定线程 A 在 cpu 0 中运行,限定其他线程在其他 cpu 运行,那么线程 A 的执行速度和实时性就可以得到最大程度的保障。另外,cpu 也是有高速缓存的,执行线程频繁切换 cpu 也会导致缓存的命中率大大降低,同样影响执行效率。
#include <sched.h>
#include <stdio.h>
int main()
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(1, &mask);
if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1)
printf("sched_setaffinity set error!");
while (1) {
}
return 0;
}
将进程绑定到cpu1上,编译和查看结果:
g++ setaff.c -o setaf
[root@localhost ctest]# ./setaf
# 在另一个中断查看:
[root@localhost ~]# ps -eo pid,args:50,psr |grep set
46061 ./setaf 1
从结果来看是cpu号,从1号开始,可以修改下,CPU_SET(5,&mask);,然后编译后重新运行:
[root@localhost ~]# taskset -p `pgrep setaf`
pid 46683 的当前亲和力掩码:20
>>> bin(0x20)
'0b100000'
[root@localhost ~]# ps -eo pid,args:50,psr |grep set
46764 ./setaf 5
可以看到隔离的线程可以通过这种绑定的形式进行进程和cpu核心的绑定。
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上.如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上.第二个参数cpusetsize是mask所指定的数的长度.通常设定为sizeof(cpu_set_t).如果当前pid所指定的进程此时没有运行在mask所指定的任意一个CPU上,则该指定的进程会从其它CPU上迁移到mask的指定的一个CPU上运行.
sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中.即获得指定pid当前可以运行在哪些CPU上.同样,如果pid的值为0.也表示的是当前进程
设置线程亲和性
和进程设置cpu亲和性比较像,在网上找到一段代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int GetCpuCount()
{
return (int)sysconf(_SC_NPROCESSORS_ONLN);
}
void* thread_fun(void*)
{
int i;
while (1) {
i = 0;
}
return NULL;
}
int main()
{
int cpu_num = 0;
cpu_num = GetCpuCount();
printf("The number of cpu is %d\n", cpu_num);
pthread_t t1;
pthread_t t2;
pthread_attr_t attr1;
pthread_attr_t attr2;
pthread_attr_init(&attr1);
pthread_attr_init(&attr2);
cpu_set_t cpu_info;
CPU_ZERO(&cpu_info);
CPU_SET(4, &cpu_info);
if (0 != pthread_attr_setaffinity_np(&attr1, sizeof(cpu_set_t), &cpu_info)) {
printf("set affinity failed");
return -1;
}
CPU_ZERO(&cpu_info);
CPU_SET(5, &cpu_info);
if (0 != pthread_attr_setaffinity_np(&attr2, sizeof(cpu_set_t), &cpu_info)) {
printf("set affinity failed");
}
if (0 != pthread_create(&t1, &attr1, thread_fun, NULL)) {
printf("create thread 1 error\n");
return -1;
}
if (0 != pthread_create(&t2, &attr2, thread_fun, NULL)) {
printf("create thread 2 error\n");
return -1;
}
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
编译运行:
g++ thsetaff.c -o thset -lpthread
分析下绑定核心:
[root@localhost ~]# top -Hp 54962
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
54963 root 20 0 32328 1872 1716 R 99.9 0.0 0:23.09 thset
54964 root 20 0 32328 1872 1716 R 99.9 0.0 0:23.09 thset
54962 root 20 0 32328 1872 1716 S 0.0 0.0 0:00.00 thset
[root@localhost ~]# taskset -p 54963
pid 54963 的当前亲和力掩码:10
[root@localhost ~]# taskset -p 54964
pid 54964 的当前亲和力掩码:20
[root@localhost ~]# taskset -p 54962
pid 54962 的当前亲和力掩码:f
[root@localhost ~]# python3.6
Python 3.6.8 (default, Aug 24 2020, 17:57:11)
[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> bin(0x20)
'0b100000'
>>> bin(0x10)
'0b10000'
>>> bin(0xf)
'0b1111'
三 临时设置程序亲和性
taskset可以更改进程的cpu亲和性。
taskset -h
用法:taskset [选项] [掩码 | cpu列表] [pid|命令 [参数...]]
显示或更改某个进程的 CPU 亲和力。
Options:
-a, --all-tasks operate on all the tasks (threads) for a given pid
-p, --pid operate on existing given pid
-c, --cpu-list display and specify cpus in list format
-h, --help display this help
-V, --version display version
默认行为是运行一条新命令:
taskset 03 sshd -b 1024
您可以获取现有任务的掩码:
taskset -p 700
或设置掩码:
taskset -p 03 700
使用逗号分隔的列表格式而不是掩码:
taskset -pc 0,3,7-11 700
列表格式中的范围可以带一个跨度参数:
例如 0-31:2 与掩码 0x55555555 等效
我们简单编写个程序,运行:
#include <stdio.h>
int main()
{
while (1) {
}
return 0;
}
g++ loop.c -o loop
./loop
更改loop程序的亲和性:
[root@localhost ~]# ps -ef|grep loop
root 45894 45694 99 22:46 pts/4 00:00:09 ./loop
root 45939 45898 0 22:46 pts/6 00:00:00 grep --color=auto loop
[root@localhost ~]# taskset -pc 4,5 45894
pid 45894 的当前亲和力列表:0-3
pid 45894 的新亲和力列表:4,5
[root@localhost ~]# ps -eo pid,args:50,psr |grep loop
45894 ./loop
再次查看loop运行的核心,已经运行到了cpu4上。
[root@localhost ~]# taskset -p 1 `pgrep loop`
pid 45894 的当前亲和力掩码:30
pid 45894 的新亲和力掩码:1
注意这里面是掩码的方式,开始时候掩码为30即0011 0000 刚好对应刚才cpu列表4和5.
改成了1 掩码为0000 0001,就只能运行在cpu0上,验证:
[root@localhost ~]# ps -eo pid,args:50,psr |grep loop
45894 ./loop 0
四 去掉Time Tick中断
如果核心上只跑一个线程,就没有必要用TimeTick进行中断,所以可以继续更改去掉Time Tick中断。
GRUB_CMDLINE_LINUX="crashkernel=auto resume=/dev/mapper/cl-swap rd.lvm.lv=cl/root rd.lvm.lv=cl/swap rhgb quiet iommu=pt intel_iommu=on transparent_hugepage=never default_hugepagesz=2M hugepagesz=2M hugepages=1024 isolcpus=4,5,6,7 nohz_full=4,5,6,7"
#更新和重启
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot -h now
添加:
nohz_full=4,5,6,7
重启后,多次运行cat /proc/interrupts发现时钟中断已经停止,如果多个线程都绑定到隔离的cpu上,仍然会启动tick。
LOC: 97153 17867 18737 21131 840 842 836 843 Local timer interrupts
五 诗词欣赏
将进酒
[李白] [〔唐代〕]
君不见黄河之水天上来,奔流到海不复回。
君不见高堂明镜悲白发,朝如青丝暮成雪。
人生得意须尽欢,莫使金樽空对月。
天生我材必有用,千金散尽还复来。
烹羊宰牛且为乐,会须一饮三百杯。
岑夫子,丹丘生,将进酒,杯莫停。
与君歌一曲,请君为我倾耳听。(倾耳听 一作:侧耳听)
钟鼓馔玉不足贵,但愿长醉不愿醒。(不足贵 一作:何足贵;不愿醒 一作:不复醒)
古来圣贤皆寂寞,惟有饮者留其名。(古来 一作:自古;惟 通:唯)
陈王昔时宴平乐,斗酒十千恣欢谑。
主人何为言少钱,径须沽取对君酌。
五花马、千金裘,呼儿将出换美酒,与尔同销万古愁。