【车载性能优化】将线程&进程运行在期望的CPU核心上

文章介绍了如何通过Linux的sched_setaffinity函数动态调整Android应用的CPU亲和性,以适应车载环境中的性能需求。在不同场景下,通过绑定进程或线程到特定CPU核心,可以实现应用在交互时全速运行,后台时降低资源占用。实验结果显示,绑定到大核心能提高执行效率,而绑定到小核心则可降低CPU峰值但可能影响性能。需要注意的是,这种方法需谨慎使用,避免造成资源争抢,并且可能不适用于所有Android设备。

车载Android应用开发中,可能会出现一种奇葩的要求:与用户交互时应用需要全速运行,保证交互的流畅性,但是如果应用进入后台就需要怠速运行,让出更多的资源保证系统或前台应用的流畅度。那么基于这种需求,我们需要实现一种可以动态调节应用执行效率的框架。

众所周知,当前使用最广泛的车载SOC-高通骁龙8155,采用1+3+4的8核心设计,其中大核主频为 2.96GHz,三个高性能核心主频为 2.42GHz,四个低功耗核心主频为 1.8GHz。

如果我们能够将程序的进程线程运行在指定的CPU核心上,原则上就可以实现动态调节应用的执行效率。实现这种需求要用到一个Linux的函数—sched_setaffinity

这里的芯片规格数据源自中文互联网,与我个人接触到的骁龙SA8155P量产型的实际频率存在不小的出入。

sched_setaffinity简介

在介绍sched_setaffinity之前,需要先介绍一个新概念 - CPU 亲和性

CPU亲和性

CPU亲和性是指进程或线程在运行时倾向于在某个或某些CPU核心上执行,而不是随机或频繁地在不同的核心之间切换。CPU亲和性可以提高进程或线程的性能,因为它可以利用CPU缓存的局部性,减少缓存失效和进程迁移的开销。

CPU亲和性分为软亲和性硬亲和性

  • 软亲和性是Linux内核进程调度器的默认特性,它会尽量让进程在上次运行的CPU核心上继续运行,但不保证一定如此,因为还要考虑各个核心的负载均衡。
  • 硬亲和性是Linux内核提供给用户的API,它可以让用户显式地指定进程或线程可以运行在哪些CPU核心上,或者绑定到某个特定的核心上。

在Linux内核系统上,要设置或获取CPU亲和性,可以使用以下函数:

  • sched_setaffinity():设置进程或线程的CPU亲和性掩码,表示它可以运行在哪些核心上。
  • sched_getaffinity():获取进程或线程的CPU亲和性掩码,表示它当前可以运行在哪些核心上。
  • CPU_ZERO():操作CPU亲和性掩码的宏,用于清空某个核心是否在掩码中。
  • CPU_SET():操作CPU亲和性掩码的宏,用于设置某个核心是否在掩码中。
  • CPU_CLR():操作CPU亲和性掩码的宏,用于清除某个核心是否在掩码中。
  • CPU_ISSET():操作CPU亲和性掩码的宏,用于检查某个核心是否在掩码中。

使用方式

第一步:创建一个cpu_set_t类型的变量mask,用于表示CPU亲和性掩码。

第二步:然后使用CPU_ZEROCPU_SET宏来清空和设置mask,使得只有core对应的位为1,其他位为0。

第三步:调用sched_setaffinity函数来设置当前线程的CPU亲和性,如果成功返回0,否则返回-1。

    // cpu 亲和性掩码
    cpu_set_t mask;
    // 清空
    CPU_ZERO(&mask);
    // 设置 亲和性掩码
    CPU_SET(core, &mask);
    // 设置当前线程的cpu亲和性
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        return -1;
    }

sched_setaffinity函数的原理是通过设置进程或线程的CPU亲和性掩码,来指定它可以运行在哪些CPU核心上。CPU亲和性掩码是一个位图,每一位对应一个CPU核心,如果某一位为1,表示该进程或线程可以运行在该核心上,否则不能。

sched_setaffinity函数可以用于提高进程或线程的性能,避免频繁地在不同的核心之间切换。

sched_setaffinity函数的原型如下:

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

pid:表示要设置的进程或线程的ID,如果为0,则表示当前进程或线程;

cpusetsize:表示mask指针指向的数据的长度,通常为sizeof(cpu_set_t);

mask:是一个指向cpu_set_t类型的指针,cpu_set_t是一个不透明的结构体,用于表示CPU亲和性掩码,需要使用一些宏来操作它,如CPU_ZERO, CPU_SET, CPU_CLR等。

sched_setaffinity函数成功时返回0,失败时返回-1,并设置errno为相应的错误码。可能的错误码有:

  • EFAULT: mask指针无效
  • EINVAL: mask中没有有效的CPU核心
  • EPERM: 调用者没有足够的权限

Android实现

在Android 应用中我们需要借助JNI来调用sched_setaffinity函数。使用AndroidStudio创建一个NDK的默认工程,Cmake脚本如下:

cmake_minimum_required(VERSION 3.22.1)

project("socaffinity")

add_library(${CMAKE_PROJECT_NAME} SHARED
        native-lib.cpp)

target_link_libraries(${CMAKE_PROJECT_NAME}
        android
        log)

Native-lib源码如下:

#include <jni.h>
#include <unistd.h>
#include <pthread.h>

// 获取cpu核心数
int getCores() {
    int cores = sysconf(_SC_NPROCESSORS_CONF);
    return cores;
}

extern "C" JNIEXPORT jint JNICALL Java_com_wj_socaffinity_ThreadAffinity_getCores(JNIEnv *env, jobject thiz){
    return getCores();
}
// 绑定线程到指定cpu
extern "C" JNIEXPORT jint JNICALL Java_com_wj_socaffinity_ThreadAffinity_bindThreadToCore(JNIEnv *env, jobject thiz, jint core) {
    int num = getCores();
    if (core >= num) {
        return -1;
    }
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(core, &mask);
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        return -1;
    }
    return 0;
}

// 绑定进程程到指定cpu
extern "C"
JNIEXPORT jint JNICALL
Java_com_wj_socaffinity_ThreadAffinity_bindPidToCore(JNIEnv *env, jobject thiz, jint pid,
                                                     jint core) {
    int num = getCores();
    if (core >= num) {
        return -1;
    }
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(core, &mask);
    if (sched_setaffinity(pid, sizeof(mask), &mask) == -1) {
        return -1;
    }
    return 0;
}

然后再将JNI调用方法,封装在一个独立的单例中,如下所示:

object ThreadAffinity {

    private external fun getCores(): Int

    private external fun bindThreadToCore(core: Int): Int

    private external fun bindPidToCore(pid: Int, core: Int): Int

    init {
        System.loadLibrary("socaffinity")
    }

    fun getCoresCount(): Int {
        return getCores()
    }

    fun threadToCore(core: Int, block: () -> Unit) {
        bindThreadToCore(core)
        block()
    }

    fun pidToCore(pid: Int, core: Int){
        bindPidToCore(pid, core)
    }

}

通过上面的代码,我们就是实现了一个最简单的修改CPU亲和性的demo。接下来我们来运行测试。

运行测试

假设有两个需要密集计算的任务,分别为Task1和Task2,逻辑都是计算从0到1000000000的累加和,然后把将消耗时间输出在控制台上。测试代码如下:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    task1()
    task2()
}

// 耗时任务1
private fun task1() {
    Thread {
        var time = System.currentTimeMillis()
        var sum = 0L
        for (i in 0..1000000000L) {
            sum += i
        }
        time = System.currentTimeMillis() - time
        Log.e("SOC_", "start1: $time")
        runOnUiThread {
            binding.sampleText.text = time.toString()
        }
    }.start()
}

// 耗时任务2
private fun task2() {
    Thread {
        var time = System.currentTimeMillis()
        var sum = 0L
        for (i in 0..1000000000L) {
            sum += i
        }
        time = System.currentTimeMillis() - time
        Log.e("SOC_", "start2: $time")
        runOnUiThread {
            binding.sampleText.text = time.toString()
        }
    }.start()
}

情景一:不做任何处理,直接执行耗时任务

该场景下,我们不做额外操作,线程调度采用Android内核默认的方式,得到如下结果:

耗时任务散布在不同的CPU上执行,此时CPU峰值约为207 / 600 %

Task1耗时4037ms,Task2耗时4785ms

情景二:将进程绑定到小核心上

该场景下,我们使用ThreadAffinity将应用进程绑定CPU5上(在我的设备上CPU4、CPU5都是小核心)。

class MyApp: Application() {

    override fun onCreate() {
        // 注意确定你的CPU核心 大核心、小核心的标号。
        ThreadAffinity.pidToCore(android.os.Process.myPid(), 5)
        super.onCreate()
    }

}

耗时任务基本聚集在CPU5上执行,此时CPU峰值约为102 / 600 %

Task1耗时18276ms,Task2耗时18272ms。可以看出这种方式虽然显著降低了CPU峰值,但是任务的执行效率也剧烈下降了。

情景三:将进程、耗时任务绑定到大核心上

该场景下,将进程绑定在CPU2上,Task1、Task2分别绑定在CPU0和CPU1上(在我的设备上,CPU0-CPU3都属于大核心)。

class MyApp: Application() {

    override fun onCreate() {
        // 注意确定你的CPU核心 大核心、小核心的标号。
        ThreadAffinity.pidToCore(android.os.Process.myPid(), 2)
        super.onCreate()
    }
}
private fun start1() {
    // 将线程绑定到核心0上
    ThreadAffinity.threadToCore(0) {
        Thread {
            var time = System.currentTimeMillis()
            var sum = 0L
            for (i in 0..1000000000L) {
                sum += i
            }
            time = System.currentTimeMillis() - time
            Log.e("SOC_", "start1: $time")
            runOnUiThread {
                binding.sampleText.text = time.toString()
            }
        }.start()
    }
}

private fun start2() {
    // 将线程绑定到核心1上
    ThreadAffinity.threadToCore(1) {
        Thread {
            var time = System.currentTimeMillis()
            var sum = 0L
            for (i in 0..1000000000L) {
                sum += i
            }
            time = System.currentTimeMillis() - time
            Log.e("SOC_", "start2: $time")
            runOnUiThread {
                binding.sampleText.text = time.toString()
            }
        }.start()
    }
}

耗时任务基本聚集在CPU0和CPU1上执行,此时CPU峰值约为193 / 600 %

Jul-21-2023 10-15-25.gif

Task1耗时3193ms,Task2耗时3076ms。可以看出相比于Android内核的默认性能调度,手动分配核心可以获得更高的执行效率。

综合上述三种情况我们可以得到以下结论:

  1. 进程绑定到小核心上会显著降低CPU峰值消耗,压制应用消耗系统资源,但是也会拖慢应用程序的执行效率。
  2. 线程指定到不同线程上执行,可以在尽量不提高CPU峰值的情况下,提升应用程序的执行效率。

总结

本文介绍了使用动态调节CPU亲和性的方法,原本是我个人用于车载Android应用性能优化的一种尝试,本身带有一定的「实验性」,具体的缺点相信会在以后地运用中进一步显现,所以目前仅供参考。

请注意以下两点,第一,如果需要运用在你项目中,切记要与所有的应用开发进行协调,尽可能小规模地使用在一些对性能非常敏感的应用上,防止出现大量应用争抢某个CPU的情况。第二,本文介绍的方法不适用于手机,因为手机厂商对于内核的修改,导致不同品牌设备间的CPU调度策略并不一致,在手机上使用可能会失效。

以上就是本篇文章所有的内容了,感谢你的阅读,希望对你有所帮助。

本文中源码地址:https://github.com/linxu-link/SocAffinity

参考资料

Linux中CPU亲和性(affinity)

CPU亲和性的使用与机制

C++性能榨汁机之CPU亲和性

&lt;think&gt;我们正在讨论车载导航系统中的线程进程的关系与区别。首先,我们需要明确进程线程的基本概念,然后结合车载导航系统的具体应用场景来分析。 ### 基本概念 1. **进程(Process)**:进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的地址空间、内存、文件描述符等系统资源。一个进程崩溃通常不会影响其他进程。 2. **线程(Thread)**:线程进程中的一个执行单元,是CPU调度和分派的基本单位。同一个进程中的多个线程共享该进程的资源(如内存空间、文件描述符等)。线程间的通信更加高效,但一个线程崩溃可能导致整个进程崩溃。 ### 车载导航系统中的应用 在车载导航系统中,通常需要同时处理多个任务,例如: - 地图渲染 - 路线规划 - GPS数据接收 - 用户界面响应 - 语音提示 - 网络通信(如实时交通信息) #### 进程线程的关系 在车载导航系统中,可能会采用多进程或多线程的架构。例如: - **多进程架构**:不同的功能模块可能运行在不同的进程中,以提高系统的稳定性和容错性。比如,导航引擎作为一个进程,用户界面作为另一个进程,两者通过进程间通信(IPC)交换数据。 - **多线程架构**:在一个进程内,多个线程可以并行执行不同的任务。例如,在导航引擎进程中,一个线程负责GPS数据接收,另一个线程负责路线计算,还有一个线程负责语音提示。 #### 区别 1. **资源开销**:进程的创建和销毁开销较大,而线程的创建和销毁开销较小。在资源受限的车载系统中,线程更轻量级。 2. **通信方式**:进程间通信(IPC)比较复杂(如管道、消息队列、共享内存等),而线程间通信可以直接通过共享内存(进程内的全局变量)进行,效率更高。 3. **稳定性**:一个进程崩溃不会直接影响其他进程,因此多进程架构可以提高系统的稳定性。而一个线程崩溃可能导致整个进程崩溃,进而导致该进程的所有线程都终止。 4. **并发性**:多线程可以充分利用多核CPU的优势,提高系统的响应速度和实时性。 ### 车载导航系统中的具体实现 引用[1]中提到:&ldquo;利用Linux系统的多线程技术,可以解决系统中多个任务并行处理的问题,保障了系统的稳定性、可靠性,提高了系统的运行速度。&rdquo; 这说明在车载导航系统中,多线程技术被用于处理并行任务,以提高效率。 同时,考虑到系统的稳定性,可能采用多进程架构将关键模块隔离。例如,导航核心模块和用户界面模块可能分别运行在不同的进程中,这样即使界面崩溃,导航功能仍能继续运行。 ### 总结 在车载导航系统中: - **进程**是资源分配的基本单位,用于隔离不同的功能模块,提高系统稳定性。 - **线程**是CPU调度的基本单位,用于在一个模块内实现并行处理,提高效率。 两者关系:一个进程可以包含多个线程,这些线程共享进程的资源;多个进程之间相互独立,通过IPC进行通信。 &sect;&sect;相关问题&sect;&sect; 1. 车载导航系统中如何实现进程间通信? 2. 多线程编程在车载导航系统中需要注意哪些问题? 3. 如何平衡车载导航系统中多进程和多线程的使用?&lt;/think&gt;### 车载导航系统中线程进程的关系与区别 在嵌入式车载导航系统中(如引用[1]提到的ARM9+Linux架构),**进程**和**线程**是并发任务管理的核心机制,二者既有联系又有本质区别: #### 一、基本概念 1. **进程(Process)** - 是操作系统中**资源分配的基本单位**,拥有独立的虚拟地址空间、文件描述符等资源。 - 在车载导航系统中,不同功能模块(如地图渲染、GPS数据处理、语音导航)可能作为独立进程运行[^1]。 - 例如:导航主程序、实时交通更新服务可分别作为两个进程。 2. **线程(Thread)** - 是进程内的**执行单元**,共享同一进程的资源(内存、文件等),但拥有独立的栈和寄存器。 - 引用[1]指出,Linux多线程技术用于解决**多任务并行处理**问题(如同时处理用户输入和路径计算)。 #### 二、关键区别 | **特性** | **进程** | **线程** | |------------------|-----------------------------------|-----------------------------------| | 资源隔离 | 完全隔离,崩溃不影响其他进程 | 共享资源,单线程崩溃可能导致整个进程终止 | | 通信开销 | 需进程间通信(IPC),如消息队列 | 可直接读写共享内存,效率高 | | 创建/切换开销 | 高(需分配独立资源) | 低(资源复用) | | 适用场景 | 需高隔离性的模块(如安全关键任务)| 需高效协作的任务(如实时数据处理)| #### 三、车载导航系统中的协作关系 1. **多进程架构**: - 系统启动时加载多个进程(如地图服务、传感器数据采集)。 - 通过**进程调度服务**协调运行(引用[3]),确保高优先级任务(如碰撞预警)及时响应。 2. **多线程优化**: - 单个进程内使用多线程处理并发任务: - 主线程:响应用户界面操作 - 工作线程:后台计算导航路径 - I/O线程:处理GPS数据接收 - 如引用[1]所述,此设计可**提升系统运行速度并保障可靠性**。 3. **混合模式**: - 关键模块(如导航引擎)以独立进程运行,内部再拆分多线程。 - 例如:地图渲染进程包含渲染线程、数据加载线程和缓存管理线程。 #### 四、实际应用挑战 - **稳定性**:线程间资源竞争需同步机制(如互斥锁),否则引发死锁。 - **实时性**:进程/线程优先级需合理配置(如紧急事件处理线程设为最高优先级)。 - **调试复杂度**:多线程问题(如竞态条件)比多进程更难追踪(引用[3]的调试模块可辅助定位)。 &gt; 总结:在车载导航系统中,**进程提供资源隔离和容错能力,线程实现轻量级并发**。二者协同满足系统对实时性、可靠性的严苛要求[^1]。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值