一、引言
PX4程序是基于实时操作系统(Real-time operating system, RTOS)的上层应用程序,PX4飞控程序的很多重要模块都是在Nuttx操作系统的调度下运行的。因此,想要弄懂PX4源码,需要了解一些操作系统的相关知识。
二、任务调度
早期计算机由于技术限制,大部分处理器都是单一核心,即CPU在同一时间只能处理一个工作。例如,在没有任务调度的打印程序中,程序需要等待打印机就绪,之后才能向打印机发送文字。但由于打印机是外部设备,其响应速度与执行速度远低于CPU,这会导致CPU需要等待打印机响应,导致CPU资源浪费,为了解决这个问题,人们提出了一系列任务调度的方法。
1.时间片轮转调度
时间片轮转调度是最为简单的调度方法,当某任务执行一小段时间后,CPU保存当前任务,然后去执行另一个任务,再执行一小段时间后,再将这个任务保存,再转回执行第一个任务。每个被执行的任务都会被操作系统分配一个时间片,CPU在执行任务时会计算当前任务执行的时间,当时间片耗尽之后,会对当前任务进行保存,并对另一个任务恢复现场并执行。例如,任务A与B,处理器先载入A任务相关参数并执行,当执行一段时间后,转入任务B执行,并保存A任务状态,再过一段时间,将任务B执行状态保存,转去执行任务A,重复此操作,示意图如下所示。
2.多级反馈队列
在实际应用中,简单的时间片轮转调度有很大的局限性,在这种调度下,每一个任务所占用的CPU执行时间相同,所有任务都是平等的,但这种方法不切合实际,因此人们提出了多级反馈队列的调度方式。这一部分可以参考多级反馈队列调度算法.
3.抢占式优先级
在嵌入式操作系统中,通常对任务的实时性有要求。为此,我们可以为每一个任务分配一个优先级,操作系统优先执行高优先级的任务,只有高优先级任务主动休眠,或者等待资源时才将其挂起,运行低优先级的任务。这种调度策略就是抢占式优先级调度,即高优先级任务抢占低优先级任务的运行权。
PX4中Nuttx系统采取的就是抢占式优先级调度,每一个运行的任务都可以为其指定优先级,Nuttx在调度时按优先级调度。如果多个任务具有相同优先级,则采取时间片轮转的方式。
这种调度方式的优点在于可以保证高优先级任务运行的实时性,对嵌入式系统具有重要意义。例如,在无人机中,导航任务与控制任务的优先级就要高于数据传输和网络通信任务,只有这样才可以最大程度保证无人机飞行安全。
三、多进程与多线程
1.进程
进程,简单来说,就是运行中的程序。我们在计算机上播放音乐,上网冲浪,查看照片,像这样操作系统为用户提供的多个任务,被称为操作系统中的进程。举个简单的例子,我们在linux中创建c文件,命名为test1.c,并输入如下代码
#include <stdio.h>
#include <unistd.h>
int main()
{
for(int i = 0; i<1000; i++)
{
printf("%d\n",i);
sleep(1);
}
return 0;
}
该程序通过for循环输出i的值,将程序保存,通过gcc命令将其编译为一个可执行的文件test1
gcc test1.c -o test1
执行过后,会在当前文件夹下生成名为test1的可执行文件。在操作系统没有执行之前,它作为普通文件存放在计算机中,当执行./test1后,计算机会将test1文件载入内存,并开始运行,此时该可执行文件就成了可以运行的进程,运行结果如图
在linux中,我们可以采用ps命令查看进程
在PX4飞控程序中,许多模块作为一个进程(任务)运行在pixhawk中。链接pixhawk硬件,输入top命令,可以得到
通过top命令可以清晰看到每一个进程(任务)的ID号,名称,CPU占用百分比,进程优先级等。有关进程更为详细的应用我会后续介绍
2.线程
通常我们并不希望操作系统的最小调度单位为进程。换句话来说,我们希望一个进程在执行时,这个进程可以并行执行多个任务,而不是只能执行一个任务,于是就有了线程的概念。还是以个人电脑为例,电脑中有一个记事本的程序,我们打开运行后可以在其中输入内容,同时我们还可以将输入的内容进行保存,在使用时,输入与保存功能往往是可以同时进行的。事实上,输入与保存就是记事本进程下的两个并行任务,我们称为线程。
操作系统允许一个进程中创建多个可并行的调度单元,即线程。所以线程机制使进程在同一时间执行多个任务。多个进程在操作系统运行的过程通常称为多进程,一个进程中的多个线程运行的过程称为多线程,下面以一段代码讲解多线程的创建与运行。首先创建test2.c文件,在其中输入如下内容:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* pth_func1(void *arg)
{
for (int i = 0; i < 2; i++)
{
printf("%s: i = %d\n",__func__,i);
sleep(1);
}
return NULL;
}
void* pth_func2(void *arg)
{
int cnt = (int)(long int)arg;
for (int i = 0; i < cnt; i++)
{
printf("%s: i = %d\n",__func__,i);
sleep(1);
}
return NULL;
}
int main(int argc, char *argv[])
{
//定义3个多线程描述符变量
pthread_t pth1, pth2, pth3;
//创建进程,进程名称为pth1,执行函数为pth_func1(),输入参数为NULL(无)
pthread_create(&pth1, NULL, &pth_func1, NULL);
pthread_create(&pth2, NULL, &pth_func2, (void*)3);
pthread_create(&pth3, NULL, &pth_func2, (void*)5);
//调用pthread_join()函数等待线程运行结束,当三个线程都结束后,主进程才会继续运行
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
pthread_join(pth3, NULL);
return 0;
}
保存文件然后输入如下代码进行编译
gcc test2.c -o test2 -lpthread
随后输入./test2运行代码,运行结果如下
程序一共运行5s,然后退出,运行过程为:线程1运行2s,输出两次;线程2运行3s,输出三次;线程3运行5s,输出五次。可以看到,在同一进程下的可以同时运行多个任务(线程)
3.PX4中进程与线程
大致了解了进程与线程,看看PX4代码中是如何调用这一功能吧。PX4代码版本为V1.11,打开/PX4_Firmware/platforms/posix/src/px4/common/tasks.cpp文件,你会看到PX4中创建进程的函数px4_task_spawn_cmd(),具体函数注释如下
px4_task_t px4_task_spawn_cmd(const char *name, int scheduler, int priority, int stack_size, px4_main_t entry,
char *const argv[])
//函数功能:创建进程
//返回值:返回创建进程的ID号
//输入值:name:进程名称
// scheduler:线程调度调度方式,RR或FIFO,RR是指时间片轮转,FIFO是指先进先出的调度策略
// priority:进程优先级
// stack_size:分配堆栈内存大小
// entry:进程函数
// argv:进程参数
此外,px4_task_t在PX4中为int的别名,后续文章我将更新PX4如何使用进程与线程以及怎么在PX4中创建自己的进程
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)