对于多核cpu,linux系统的负载自均衡会将进程或线程的压力分摊到各个cpu核。整体上来看极大的提高了系统性能,充分利用了cpu资源,但是频繁的上下文切换对实时性要求高的应用很不友好,如何针对实时性应用做优化是本文核心。
要想让我们的实时应用不受干扰的运行需要做到一下几点:
1.隔离指定CPU核,使其不在操作系统调度范围内
2.将实时进程或线程绑定到隔离cpu上
3.梳理中断,确保所有中断绑定在非隔离的cpu上
4.开启内核Feature “NO_HZ_FULL”,降低Timer中断对应用的影响
1. 隔离cpu核
1.1 通过修改内核启动参数isolcpus将从线程调度器中移除选定的CPU
这里我们分两个场景,使用linux发行版我们可以通过配置文件及相应工具快速实现核孤立;如果我们使用的时自己编译的嵌入式系统我们可以通过修改dts文件来修改内核启动参数,进而实现核孤立。
1.1.1 Linux发行版
我们使用发行版一般不需要我们自己编译内核,可以通过配置文件/etc/default/grub来配置内核启动参数。
①在/etc/default/grub中的下面一行添加最后的isolcpus参数
GRUB_CMDLINE_LINUX_DEFAULT=“quiet splash isolcpus=2”(cpu序号从0开始)
也可以添加到:
GRUB_CMDLINE_LINUX=“isolcpus=2”
②sudo update-grub
检查/boot/grub/grub.cfg时间戳,查看更新是否成功
③重启操作系统
④查看 /proc/cmdline里是不是有isolcpu参数,有的话说明本次重启确实带了这个参数
1.1.2 嵌入式系统
在我们自己的嵌入式系统里不必依赖grub来设置isolcpus, 直接通过修改内核配置项 CONFIG_CPU_ISOLATION=y和dts文件来改变内核启动参数。
内核编译配置:
修改dts文件:
我这里使用的是一个8核cpu,在chosen节点下bootargs参数里加上isolcpus的定义即可
/ { chosen: chosen { bootargs = "earlycon=uart8250,mmio32,0xfeb50000 console=ttyFIQ0 irqchip.gicv3_pseudo_nmi=0 root=PARTLABEL=rootfs rootfstype=ext4 ro rootwait overlayroot=device:dev=PARTLABEL=userdata,fstype=ext4,mkfs=1 coherent_pool=1m systemd.gpt_auto=0 isolcpus=1,2,3,4,5,6,7 nohz_full=1,2,3,4,5,6,7 cgroup_enable=memory swapaccount=1"; }; |
重新编译内核启动后,通过cat /proc/cmdline可以查看到启动参数的变化。
从上图我们也可以看到,被隔离的cpu基本都是处于空闲状态。
1.2 修改init进程cpu亲和性实现独占
另一方法利用了CPU亲和性的继承性,即子进程会继承父进程的CPU亲和性.由于所有进程都是init的子进程,我们可以设置init的CPU亲和性,这样一来,所有的进程都具有了与init相同的CPU亲和性.然后我们可以更改我们需要的进程的CPU亲和性来达到独占。
2. 将实时进程或线程绑定到隔离cpu上
前面我们已经将cpu核隔离出来了,如果只有一个进程绑定到隔离的cpu核上,我们就基本实现了独占。那么我们如何让指定的进程或线程和某一个cpu绑定呢?这里涉及到我们在编码过程中设置进程的cpu亲和性。
CPU亲和性就是进程在某个给定的CPU上尽量长时间的运行而不被迁移到其它处理器的倾向性。Linux内核进程调度器天生就具有”软CPU亲和性”,这意味着进程通常不会在处理器之间频繁迁移,这也以为着我们将某一进程绑定到多个核上,最后他实际总是在一个核上跑。下面来将具体的操作方法。
首先,用到头文件sched.h,并且要将_GNU_SOURCE定义在文件开头
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sched.h>
用到的函数如下:
//初始化,设为空
void CPU_ZERO (cpu_set_t *set);
//将某个cpu加入cpu集中
void CPU_SET (int cpu, cpu_set_t *set);
//将某个cpu从cpu集中移出
void CPU_CLR (int cpu, cpu_set_t *set);
//判断某个cpu是否已在cpu集中设置了
int CPU_ISSET (int cpu, const cpu_set_t *set);
进程绑核以及获取进程亲和性
int sched_setaffinity(pid_t pid, size_t cpusetsize,
const cpu_set_t *mask);
int sched_getaffinity(pid_t pid, size_t cpusetsize,
cpu_set_t *mask);
线程绑核以及获取线程亲和性
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
cpu_set_t *cpuset);
示例代码如下:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <time.h>
#include <signal.h>
#include <string.h>
#include <sched.h>
#include <pthread.h>
#define MAX_CPU_NUM 8
int num;
double waste_time(long n)
{
double res = 0;
long i = 0;
while(i <n * 200000) {
i++;
res += sqrt (i);
}
return res;
}
void *thread_func1(void *arg) {
cpu_set_t mask; //CPU核的集合
cpu_set_t get; //获取在集合中的CPU
int *a = (int*)arg;
printf("thread:%d\n",*a); //显示是第几个线程
CPU_ZERO(&mask); //置空
CPU_SET(*a,&mask); // 将当前线程和CPU绑定
if(sched_setaffinity(0, sizeof(mask), &mask)) {
printf("warning ! set affinity failed! \n");
}
else {
while (1)
{
CPU_ZERO(&get);
if (sched_getaffinity(0, sizeof(get), &get) == -1)//获取线程CPU亲和力
{
printf("warning: cound not get thread affinity, continuing...\n");
}
int i;
for (i = 0; i < num; i++)
{
if (CPU_ISSET(i, &get))//判断线程与哪个CPU有亲和力
{
printf("this thread %d is running processor : %d\n", i,i);
}
}
printf ("result: %f\n", waste_time (1000));
}
}
return NULL;
}
void *thread_func2(void *arg) {
cpu_set_t mask; //CPU核的集合
cpu_set_t get; //获取在集合中的CPU
int *a = (int*)arg;
printf("thread %d\n",*a); //显示是第几个线程
CPU_ZERO(&mask); //置空
CPU_SET(*a,&mask);
if(pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) == -1) {
printf("warning ! set affinity failed! \n");
}
else
{
while (1)
{
printf ("result: %f\n", waste_time (1000));
}
}
return NULL;
}
int main()
{
int i = 0;
int tid[MAX_CPU_NUM];
pthread_t thread[MAX_CPU_NUM];
// 获取CPU核数
num = sysconf(_SC_NPROCESSORS_CONF);
for(i=1;i<num;i++)
{
tid[i] = i;
pthread_create(&thread[i], NULL, (void *)thread_func2,&tid[i]);
}
for(i=1;i<num;i++)
{
pthread_join(thread[i],NULL); //等待所有的线程结束,线程为死循环所以CTRL+C结束
}
printf("main thread exit.\n");
return 0;
}
可以看到任务被分配到孤立的cpu1 ~ cpu7。
3. 中断绑核
被隔离的CPU虽然没有线程运行在上面,但是仍会收到interrupt。Interrupt request是硬件级别的服务请求,IRQ有一个亲和度属性smp_affinity.,smp_affinity决定允许哪些CPU核心处理IRQ。
特定的IRQ的亲和度值储存在/proc/irq/IRP_NUMBER/smp_affinity文件中。此文件仅ROOT用户可见。储存的值是一个十六进制位掩码(hexadecimal bit-mask),代表着系统的所有CPU核心。
命令cat /proc/interrupts可以看到所有设备的interrupts信息,第一列即为IRP_NUMBER。
命令cat /proc/irq/32/smp_affinity可以看到IRQ号为32的亲和度。默认值为ff,代表这个IRQ能被所有CPU接受处理。
命令echo 1 > /proc/irq/32/smp_affinity把IRQ号为32的亲和度值设为1,代表这个IRQ仅能被CPU0接受处理。
照此,我们可以据要求任意绑定IRQ到CPU。
虽然我们已经做了很大努力,但是仍有一部分中断没有被绑定,例如: Single function call interrupts, Local timer interrupts等,后面我们会逐步分析核解决。
软中断,除cpu0外,其他cpu核不再增加。
4. 降低Timer中断对应用的影响
Timer中断频发会频繁产生上下文切换,我们需要通过是能核配置NO_HZ_FULL来关闭孤立核上的timer中断,具体方法与1.1节类似,本节做简要介绍。
(1)通过修改 /etc/default/grub 文件实现。
(2)修改内核配置项 CONFIG_NO_HZ_FULL=y和dts文件。