Linux实时任务性能优化

本文介绍了如何在Linux系统中针对实时应用进行CPU核隔离、进程绑定、中断管理和降低Timer中断影响的策略,以提升实时性并减少上下文切换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        对于多核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文件。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值