编辑推荐:
本文主要介绍了嵌入式中的多核开发和核间通信总结相关内容。
希望对您的学习有所帮助。
本文来自于CSDN,由火龙果软件Linda编辑、推荐。
随着市场对嵌入式设备功能需求的提高,市面上出现了集成嵌入式处理器和单片机的主控方案,以兼顾性能和效率。
在实际应用中,嵌入式处理器和单片机之间需要进行大量且频繁的数据交换,如果采用低速串行接口,则数据传输效率低,这将严重影响产品的性能;而如果采用高速并口,则占用管脚多,硬件成本将会增加。
为解决这一痛点,各大芯片公司陆续推出了兼具A核和M核的多核异构处理器,如NXP的i.MX8系列、瑞萨的RZ/G2L系列以及TI的AM62x系列等等。虽然这些处理器的品牌及性能有所不同,但多核通信原理基本一致,都是基于寄存器和中断传递消息,基于共享内存传输数据。
以配电终端产品为例,A核负责通讯和显示等人机交互任务,M核负责采样和保护等对实时性要求较高的任务,双核间交互模拟量、开关量和录波文件等多种信息,A核+M核的方案既满足了传统采样保护功能,又支持多种接口通信及新增容器等功能,符合国家电网现行配电标准。
通过物理内存DDR分配,将硬件层分为了两部分:TXVring Buffer(发送虚拟环状缓冲区)和RXVring
Buffer(接收虚拟环状缓冲区);其中M核从TXVring区发送数据,从RXVring区读取接收数据,A核反之。
硬件层通信实现机制
以imx8为例,在最底层硬件上,A核和M核通讯是靠硬件来进行的,称为MU,如图
处理器支持消息传递单元(MessagingUnit,简称MU)功能模块,通过MU传递消息进行通信和协调,芯片内的M控制核和A处理核通过通过寄存器中断的方式传递命令,最多支持4组MU双向传递消息,既可通过中断告知对方数据传递的状态,也可发送最多4字节数据,还可在低功耗模式下唤醒对方,是保证双核通信实时性的重要手段。
(1)CoreA写入数据;
(2)MU将Tx 空位清0,Rx满位置1;
(3)产生接收中断请求,通知CoreB接收状态寄存器中的接收器满,可以读取数据;
(4)CoreB响应中断,读取数据;
(5)CoreB读完数据后,MU将Rx满位清0,Tx空位置1;
(6)状态寄存器向CoreA生成发送中断请求,告知CoreB读完数据,发送寄存器空。
通过以上步骤,就完成了1次从CoreA向CoreB 传递消息的过程,反之亦然。
驱动层Virtio下RPMsg通信实现
Virtio是通用的IO虚拟化模型,位于设备之上的抽象层,负责前后端之间的通知机制和控制流程,为异构多核间数据通信提供了层的实现。
RPMsg消息框架是Linux系统基于Virtio缓存队列实现的主处理核和协处理核间进行消息通信的框架,当客户端驱动需要发送消息时,RPMsg会把消息封装成Virtio缓存并添加到缓存队列中以完成消息的发送,当消息总线接收到协处理器送到的消息时也会合理地派送给客户驱动程序进行处理。
在驱动层,对A核,Linux采用RPMsg框架+Virtio驱动模型,将RPMsg封装为了tty文件供应用层调用;在M核,将Virtio移植,并使用简化版的RPMsg,因为涉及到互斥锁和信号量,最终使用FreeRTOS完成过程的封装,流程框图如下方所示。
主处理核与协处理核数据传递流程图
(1)Core0向Core1发送数据,通过rpmsg_send函数将数据打包至Virtioavail链表区;
(2)在avail链表寻找共享内存中空闲缓存,将数据置于共享内存中;
(3)通过中断通知Core1数据到来,共享内存由avail链表区变至used区;
(4)Core1收到中断,触发rpmsg的接收回调函数,从used区获取数据所在的共享内存的物理地址,完成数据接收;
(5)通过中断通知Core0数据接收完成,共享内存缓存由used区变为avail区,供下次传输使用。
应用层双核通信实现方式
在应用层,对A核可使用open、write和read函数对 /dev下设备文件进行调用;对M核,可使用rpmsg_lite_remote_init、rpmsg_lite_send和rpmsg_queue_recv函数进行调用,不做重点阐述。
实际使用效果
通过程序实测,M核和A核可以批量传输大数据。同样以配电产品为例——128点采样的录波文件大约为43K,若通过传统的串行总线传输方式,需要数秒才可完成传输。
而使用i.MX8MP的双核异构通信方案,只需要不到0.5秒即可传输完成,数据传输效率提升数十倍!同时还避免了串行总线易受EMC干扰的问题,提高了数据传输稳定性,简化了应用编程,可满足用户快速开发的需求。
ARM内核A核、R核和M核的对比
直接参考:ARM内核A核、R核和M核的异同点和应用场景 - 知乎 (zhihu.com)
如何将进程、线程与CPU核进行绑定
参考:如何将进程、线程与CPU核进行绑定 - 知乎 (zhihu.com)
CPU绑定指的是在多核CPU的系统中将进程或线程绑定到指定的CPU核上去执行。在Linux中,我们可以利用CPU
affinity属性把进程绑定到一个或多个CPU核上。
CPU Affinity是进程的一个属性,这个属性指明了进程调度器能够把这个进程调度到哪些CPU上。该属性要求进程在某个指定的CPU上尽量长时间地运行而不被迁移到其他处理器。
CPU Affinity分为2种:soft affinity和hard affinity。soft
affinity只是一个建议,如果不可避免,调度器还是会把进程调度到其它的CPU上去执行;hard
affinity则是调度器必须遵守的规则, 2.6以上版本的Linux内核可以让开发人员可以编程实现hard
affinity。
使用hard affinity的意义
提高CPU缓存命中率
CPU各核之间是不共享缓存的,如果进程频繁地在多个CPU核之间切换,则会使旧CPU核的cache失效,失去了利用CPU缓存的优势。如果进程只在某个CPU上执行,可以避免进程在一个CPU上停止执行,然后在不同的CPU上重新执行时发生的缓存无效而引起的性能成本。
适合对时间敏感的应用
在实时性要求高应用中,我们可以把重要的系统进程绑定到指定的CPU上,把应用进程绑定到其余的CPU上。这种做法确保对时间敏感的应用程序可以得到运行,同时可以允许其他应用程序使用其余的计算资源。
如何将进程与CPU核进行绑定
系统函数
在Linux中,用结构体cpu_set_t来表示CPU Affinity掩码,同时定义了一系列的宏来用于操作进程的可调度CPU集合:
void CPU_ZERO (cpu_set_t *set) ;
void CPU_SET (int cpu, cpu_set_t *set) ;
void CPU_CLR (int cpu, cpu_set_t *set) ;
int CPU_ISSET (int cpu, cpu_set_t *set) ;
int CPU_COUNT (cpu_set_t *set) ;
具体的作用如下:
CPU_ZERO ():清除集合的内容,让其不包含任何CPU。
CPU_ISSET () :测试cpu是否在集合中。
CPU_COUNT ():返回集合中包含的CPU数量。
在Linux中,可以使用以下两个函数设置和获取进程的CPU Affinity属性:
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) ;
另外可以通过下面的函数获知当前进程运行在哪个CPU上:
如果调用成功,该函数返回一个非负的CPU编号值。
例程
int main(int argc, char * argv[])
fprintf(stderr, "Usage: %s parent-cpu child-cpu\n" , argv[0 ]);
parentCPU = atoi(argv[1 ]);
childCPU = atoi(argv[2 ]);
fprintf(stderr, "fork error\n" );
if (sched_setaffinity(getpid(), sizeof(set ), & set ) = = -1 ) {
fprintf(stderr, "child sched_setaffinity error\n" );
if (-1 != (cpu_num = sched_getcpu())) {
fprintf(stdout, "The child process is running on cpu %d\n" , cpu_num);
CPU_SET (parentCPU, & set );
if (sched_setaffinity(getpid(), sizeof(set ), & set ) = = -1 ) {
fprintf(stderr, "parent sched_setaffinity error\n" );
if (-1 != (cpu_num = sched_getcpu())) {
fprintf(stdout, "The parent process is running on cpu %d\n" , cpu_num);
wait(NULL ); / * Wait for child to terminate * /
程序首先用CPU_ZERO清空CPU集合,然后调用fork()函数创建一个子进程,并调用sched_setaffinity()函数给父进程和子进程分别设置CPU
Affinity,输入参数parentCPU和childCPU分别指定父进程和子进程运行的CPU号。指定父进程和子进程运行的CPU为1和0,程序输出如下:
The parent process is running on cpu 1
The child process is running on cpu 0
如何将线程与CPU核进行绑定
系统函数
前面介绍了进程与CPU的绑定,那么线程可不可以与CPU绑定呢?当然是可以的。在Linux中,可以使用以下两个函数设置和获取线程的CPU
Affinity属性:
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) ;
例程
static void * thread_start (void * arg) {
struct thread_info * tinfo = arg;
thread = tinfo-> thread_id;
CPU_SET (tinfo-> thread_num, & cpuset);
s = pthread_setaffinity_np(thread, sizeof(cpu_set _t), & cpuset);
handle_error _en(s, "pthread_setaffinity_np" );
s = pthread_getaffinity_np(thread, sizeof(cpu_set _t), & cpuset);
handle_error _en(s, "pthread_getaffinity_np" );
for (j = 0 ; j < cpu_num; j+ + ) {
if (CPU_ISSET(j, & cpuset)) { / / 如果当前线程运行在CPU j上,则输出信息
printf(" thread %d is running on cpu %d\n" , tinfo-> thread_num, j);
int main(int argc, char * argv[])
cpu_num = sysconf(_SC_NPROCESSORS_CONF); / / 获取系统的CPU数量
tinfo = calloc(cpu_num, sizeof(struct thread_info));
handle_error _en(0 , "calloc" );
for (j = 0 ; j < cpu_num; j+ + ) { / / 有多少个CPU就创建多少个线程
s = pthread_create(& tinfo[j].thread_id, NULL , thread_start , & tinfo[j]);
handle_error _en(s, "pthread_create" );
for (j = 0 ; j < cpu_num; j+ + ) {
s = pthread_join(tinfo[j].thread_id, NULL );
handle_error _en(s, "pthread_join" );
程序首先获取当前系统的CPU数量cpu_num,然后根据CPU数量的数量创建线程,有多少个CPU就创建多少个线程,每个线程都运行在不同的CPU上。在4核的机器中运行结果如下:
thread 1 is running on cpu 1
thread 0 is running on cpu 0
thread 3 is running on cpu 3
thread 2 is running on cpu 2
用taskset命令实现进程与CPU核的绑定
Linux 的taskset命令用于设置或检索由pid指定的运行进程的CPU Affinity,或者以给定的CPU
Affinity属性启动新的进程。CPU Affinity属性用位掩码来表示,其中最低位对应第一逻辑CPU,最后一位与最后一个逻辑CPU对应。检索到的掩码仅反映与物理系统上的CPU相对应的位。如果给出无效的掩码(即当前系统上没有对应的有效的CPU掩码),则返回错误。掩码通常以十六进制形式给出。例如:
0 x00000003 表示CPU #0 和 #1 ,
taskset命令的选项如下:
设置或检索所有由pid指定的进程的CPU Affinity属性。
指定处理器的数值列表,而不是位掩码。数字用逗号分隔,可以包括范围。比如:0 ,5 ,8 -11 。
下面以Ubuntu16.04中的taskset命令说明该命令的使用方法:
显示进程运行的CPU核
结果:pid 1 ‘s current affinity mask: f
指定进程运行在某个特定的CPU核上
结果:pid 7737 's current affinity list: 0-3
pid 7737' s new affinity list: 1,2
说明:该操作把进程7737 限定在CPU#1 ~CPU#2 上运行。
进程启动时指定CPU核
命令:taskset -c 1 -2 ./ get _affinity
结果:This process is running on cpu 1
This process is running on cpu 2
说明:get _affinity程序通过sched_getaffinity()函数获取当前进程的CPU Affinity属性并输出提示信息。
本文通过几个简单的例子介绍了Linux环境下进程、线程与CPU的绑定方法,希望对大家有参考意义。