编写LXRT(RTAI-LXRT)用户空间程序
Asad Chan 2011-06-28
此文档仅为那些刚涉猎RTAI Linux的爱好者编写。在用户空间下编写RTAI-LXRT程序并不是一件困难的事,可以参考《DIAPM RTAI ProgrammingGuide 1.0》和《How to port your C++ GNU/Linux applicationto RTAI/LXRT》。而这两个文档却没有一个完整在用户空间能执行的例子,所以往往会造成看了遇到问题不知问题出在哪里。往往有的程序编写好了,也编译好了,在控制台下执行却出现了segmentation fault字样。
RTAI Linux 调度器(请参考RTAI 3.3 User Manual rev0.2)
RTAI分别在内核空间和用户空间提供了两个对等的任务调度器,一个为rtai_sched,一个为rtai_lxrt,这两个调度器主要是他们所能调度的可调度任务有所差异,而这两个调度器都能调度用户空间和内核空间的任务。rtai_sched能调度Linux下的各种任务,例如process,thread,kthread,而同时它还能调度RTAI内核空间任务(请区分Linux内核和 RTAI 内核)。而rtai_lxrt仅能调度process,thread,kthread,却不能调度RTAI内核空间任务。
此文档仅介绍在用户空间执行的实时性任务,而要使用这两个调度器,需将rtai_hal.ko,rtai_sched.ko,rtai_lxrt.ko三个模块加载进Linux内核,因为只有将这些模块加载进内核才能使这两个调度器运行起来并且能为实时性进程/线程提供调度服务。也即在用户空间编写的实时性进程/任务需要上述两个调度器中的一个提供调度服务才能正常运行。
现在开始吧!
1、建立main.c文件内容如下:
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<rtai_lxrt.h>
staticintend=1;
voidendHandler(intsig) //信号处理函数
{
end=0; //使静态变量为0,这将是主函数的死循环被break
}
intmain(void)
{
RT_TASK*task=0; //定义实时任务指针,用于指向被创建的实时性任务
intperiod=0; //定义周期变量,用于存储定时器的周期
signal(SIGKILL,endHandler); //连接几个主要关闭信号的信号处理函数
signal(SIGTERM,endHandler); //包括KILL 也即杀死进程信号,TERM关闭信号
signal(SIGALRM,endHandler); //这些信号被收到时将会break主程序的循环
if(!(task=rt_task_init_schmod(nam2num("TEST"),0,0,0,SCHED_FIFO,0x0f))) //初始化本进程
{ //为实时性任务
printf("Can't initial thetask\n");
exit(1);
}
mlockall(MCL_CURRENT|MCL_FUTURE); //锁定本进程内存,防止本进程的内存页被换出
//导致的实时性被破坏
period=start_rt_timer(nano2count(10000)); //启动定时器,设置其定时周期/频率,
//根据定时器是否是oneshot
rt_make_hard_real_time(); //使本任务为硬(hard)实时性任务
rt_task_make_periodic(task,rt_get_time()+period*10,period*100); //设置本任务的周期
while(end) //如果end为1则一直执行,end==0,则说明接收到了kill/term/alrm三个信号
{ //中的一个,证明此进程被用户终止或alrm,则break此循环,结束此任务
printf("Hello World!\n"); //打印字符,注意,此为syscall,会导致任务进入非实时性
//状态,当此调用结束时将会重新返回到实时性状态
/*
在实际应用中如果你的任务是周期性执行的,请在此处填写需要执行的周期性过程。
如果你的任务是需要等待某个事件、者信号量或者某个消息的到来,请你建立相应
的事件、信号量、消息邮箱,并在此处等待相应的量,相应的量到来时执行你需要的
操作。。。
*/
rt_task_wait_period(); //使当前任务进入休眠,知道下一次执行周期到达被唤醒,
//如果execution time>= deadline则此函数不会使
//任务进入休眠状态
}
rt_make_soft_real_time(); //是任务进入软实时状态
stop_rt_timer(); //停止定时器
rt_task_delete(task); //删除实时性任务,此时控制权将交回Linux
printf("End of theApplication!\n");
return0; //返回0值
}
2、建立Makefile文件
主要是写好makefile,有头文件路径,静态库路径,还要加载rtai三个模块,并且以root权限运行。
-I/usr/realtime/include -o 这个是编译选项,用于设置头文件的目录
-L/usr/realtime/lib -llxrt -lpthread 这个是连接选项用于设置静态库的位置, 其中“-llxrt”实为liblxrt.a的缩写。
ldconfig/usr/realtime/lib ↙
insmod rtai_hal.ko ↙
insmod rtai_sched.ko ↙
insmodrtai_lxrt.ko ↙
sudo ./在Makefile文件内如下所写:
CC = gcc
CFLAGS = -I/usr/realtime/include -o
DFLAGS = -L/usr/realtime/lib -llxrt -lpthread
TARGET = main
SOURCE = main.c
all:
$(CC) $(SOURCE) $(CFLAGS) $(TARGET) $(DFLAGS)
clean:
rmmain
注释:
CFLAGS宏用于设置编译选项,可以看到“-I/usr/realtime/include”这项,此项用于设置包含rtai_lxrt.h的路径(path),其原因在于此路径不是Linux的标准头文件路径,因此需要手动在编译选项里指定其路径,其选项为“-I”。“-o”选项用于告诉编译器编译成可执行文件。
DFLAGS宏用于设置连接选项,可以看到“-L/usr/realtime/lib”这项,这项用于指定静态库“liblxrt.a”的路径,因为此文件不在Linux的标准库路径下,需要手动设置其路径,其选项为“-L”。需要手动指定其静态库,可见“-llxrt”“-lpthread”两项,其中“-llxrt”实为liblxrt.a的缩写,此实为GNU连接器的使用规则,由此可知“-lpthread”即为libpthread.a的缩写。
“$()”符号用于对定义的宏进行解析,例如“$(CC)”在上面的例子中等价为“gcc”,“$(SOURCE)”等价为“main.c”,其他各项亦按此种形式解析。
3、编译main.c文件
假设你的main.c、Makefile两个文件放在绝对路径/usr/root/workspace/main目录里,那么在控制台下进入到此目录,执行以下命令即可:
make ↙
注:“↙”为回车的意思。
此时控制台上会显示“gcc main.c -I/usr/realtime/include -o main -L/usr/realtime/lib -llxrt -lpthread
”,同时在main目录下会生成可执行文件main。
4、执行main
此时,在上面步骤下在控制台下执行以下命令:
./main ↙
此时,会返回一个错误,类似于“can't open file liblxrt.so.1 ...”之类的字样,其原因是文件liblxrt.so.1并不是在Linux系统设定的静态库路径下,那么要解决这个问题需要将在第2步中设定路径“/usr/realtime/lib”加入到Linux系统能寻找的静态库的路径中,此可以通过以下命令设定:
ldconfig/usr/realtime/lib ↙
注:ldconfig即为设置静态库查找路径命令,其参数即为你需要设置的静态库路径。
此时,你再执行:
./main ↙
你又发现出问题了,其表现为控制台返回“segmentation fault”,也即为段错误。还记得在“RTAI Linux 调度器”一节中曾提到过rtai_hal.ko, rtai_sched.ko,rtai_lxrt.ko三个模块,根据该小节中的解释,需要运行main程序,必须要将这三个模块加载到内核空间,以使其提供调度任务服务功能。首先要切换到目录/usr/realtime/modules下,也即如下命令:
cd/usr/realtime/modules ↙
继续执行以下命令:
insmod rtai_hal.ko ↙
insmod rtai_sched.ko ↙
insmodrtai_lxrt.ko ↙
此时,再重新执行以下命令:
./main ↙
此时,控制台上将会不断打印“Hello World!”。 此时可以通过按下“Ctrl + C”来终止程序,而更建议在任务管理器下将此进程杀死,因为程序里实现了信号kill的处理函数。
疑惑还是存在
疑惑的存在主要是在main函数里的各个步骤还是难以让人理解。例如singal函数的调用是一件比较难以明白的处理,里面连接了kill信号,在kill的处理句柄里可以看到它将end赋0,而end为0将会导致main函数里的循环结束。而在Linux系统里kill信号是有默认的处理句柄的,那么我们为什么不用它的默认处理句柄呢?它同样可以使应用程序退出并释放相应的内存。
1、头文件
首先,既然在用户空间编写RTAI-LXRT可运行程序,那么意味着并且实际上是可以调用Linux的系统调用,标准库等用户空间能运行的函数,那么可以包含stdio.h, stdlib.h,这两个标准头文件,以使程序能调用标准的库函数,同时需要包含signal.h头文件,此头文件主要是提供signal连接关闭程序句柄,而需要使用RTAI-LXRT提供的实时性功能,则毫无疑问需要包含rtai_lxrt.h,请记得rtai_lxrt.h在/usr/realtime/include(假设是按照RTAI安装的默认路径)目录里,因此需要在编译选项里指定这一路径,否则将不能找到此头文件。头文件包含如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <rtai_lxrt.h>
2、RT_TASK指针
可以看到在程序的开头声明了指针task,可以知道它为RT_TASK结构体指针,此结构体指针用于指向需要设置为实时性的任务,需要这个指针的原因是当在程序中将一个process(本文档例子中即为mainProcess)/pthread/kthread向RTAI内核注册后,task将指向此prcess/pthread/kthread,在随后的过程中还需要设置task指向的任务的一些属性,比如该任务的周期等属性,那么需要使用task作为设置该任务设置的链接口,并且在程序结束时还要通过它来从RTAI内核中删除任务。请详细阅读代码。
3、signal函数
在关闭一个进程时,进程将会收到kill信号,在Linux系统里,在收到kill信号后,默认的kill句柄将会执行从Linux内核中删除任务的操作,这个操作包括关闭收到该信号的进程所打开的所有文件,释放该进程所占的内存页,将该进程的PCB从Linux内核中删除等操作,最终导致该进程的结束。
根据上述,可以知道,当本文所写的main函数的while循环没有被break,而收到的kill信号,也即是用户关闭该进程,而同时没有连接上述所写的endHandler句柄,那么将会直接执行Linux的默认kill信号句柄,此时将导致程序在while循环里被结束,而没有执行到while循环以下的过程,比如rt_make_soft_real_time(),stop_rt_timer(),
rt_task_delete(task),而最终会导致这个已经在RTAI内核注册过的任务没有从里面删除,而同时它已经不存在了。
基于上述原因,需要重新链接kill,term等信号的处理句柄,而不使程序在while过程里被关闭,使程序最终能退出while,可以调用rt_task_delete(task)过程,使得任务从RTAI内核中删除的目的。
4、向RTAI内核注册main进程
可以看到main函数里的task=rt_task_init_schmod(nam2num("TEST"),0,0,0,SCHED_FIFO,0x0f)。此过程将main进程注册进RTAI内核,并成为软实时任务。
第一个参数为任务号,我们看到nam2num(“TEST”),此调用的目的是向RTAI申请一个名为“TEST”的任务号,其返回值即为该任务号。
第二个参数为该任务的优先级,这里设置为0,即为最高优先级。
第三个参数为任务的栈大小,如果给0,将会使用默认的512(单位未知)。
第四个参数为任务的消息邮箱缓冲大小,如果为0,则默认大小为256(单位未知)。
第五个参数为Linux任务的优先级设置。(下文会有解释)
第六个参数为处理器掩码,类如为0x01时选用处理器0,为0x02时选用处理器1,为0x03时为处理器0和1。
rt_task_init_schmod函数实际可以使用以下过程分解:
struct sched_parammysched;
mysched.sched_priority =sched_get_priority_max(SCHED_FIFO) – 1;
sched_setscheduler(0,SCHED_FIFO,&mysched);
rt_task_init(nam2num(“TEST”),0,0,0); //注册和初始化本进程为软实时任务
sched_param为一个调度参数结构体,里面有进程的任务调度属性字段,在这里需要修改main进程的调度策略。在Linux系统里有两种调度策略,一种是SCHED_OTHER,一种是SCHED_FIFO,一般情况下Linux用户进程使用的是SCHED_OTHER,SCHED_OTHER为Linux的经典调度算法实现的,而SCHED_FIFO为一种软实时调度机制,它的优先级有从1-99,而SCHED_OTHER代表的是0,数值越大,则优先级越大。在SCHED_FIFO调度策略里每一个优先级形成一个FIFO(先进先出)队列,高优先级的任务FIFO队列里只要有进程为就绪状态将会抢占低优先级FIFO队列里正在运行的进程,只有当高优先级FIFO队列里的进程都被阻塞时,低优先级的进程才能得到CPU执行下去。而在每一个优先级的队列里,当前进程运行被阻塞,它将会被插入到FIFO队列尾部,而同时如果没有更高优先级的任务就绪,那么将会调度本优先级里FIFO队列头的进程。
LXRT要改善Linux进程的软实时性,则需要将Linux进程转换为SCHED_FIFO调度策略。
sched_setscheduler函数即为设置该属性的接口。第一个参数为选择运行的CPU,0即为选择CPU0,第二个参数为调度策略,第三个即为需要设置进程的FIFO进程调度策略下的优先级。
rt_task_init即为想RTAI内核注册main进程使其成为RTAI的任务,其参数同rt_task_init_schmod函数的前面几个参数相同的含义。
“TEST”任务名项主要是方便于任务之间的通信与同步。
5、锁定main进程所用的内存
Linux使用了分页管理内存和虚拟内存的机制,此机制即形成了Linux进程的第二级调度,进程的所占用的内存随时都会被Linux的内存管理单元将其换出。而在实时性系统中,当内存被换出时,会使得进程无法得到及时调度,因此将会破坏进程的实时性,因此需要将实时进程(任务)的内存锁定,使得其内存在任何时候都不会被换出,而不会影响到该进程的实时性能,如下所示:
mlockall(MCL_CURRENT |MCL_FUTURE); //MCL_CURRENT 和MCL_FUTURE宏表明
//将锁定当前分配的内存和以后所申请
//的内存
6、启动定时器
start_rt_timer(nano2count(1000)),此函数启动RT的定时器,也即实时任务的tick中断时钟。nano2count(1000)也即使用1000ns的周期,将其转换为timer可以接受的数值。
7、使main进程为硬实时任务
rt_task_make_hard_real_time()函数将使本任务成为硬实时任务,因为之前仅仅为Linux下的软实时任务。
8、设置任务的周期
rt_task_make_periodic(task,get_time() + period*10,period*100)函数用于设置任务的执行周期,可见task即为2点所说的要设置的任务指针,get_time()用于获得但前时间,period即为定时器的实际时钟周期,第二个参数为任务的第一次执行的起始时间,第三个参数即为任务的执行周期。
9、执行while循环
while循环里需要填写用户需要周期执行的操作,每一次循环执行完后,将调用rt_task_wait_period,此函数将使此实时任务进入休眠状态,并在下一次执行周期到来时被唤醒再次执行,达到周期执行任务的效果。
10、任务被kill时,使任务重新进入软实时状态
rt_task_soft_real_time(); //此时控制权将会交给Linux
11、关闭定时器
rt_stop_timer()。
12、删除任务
当任务不再需要时,需要将任务从RTAI的调度器中删除。
rt_task_delete(task);
小结:
本文主要是举一个在用户空间编写RTAI-LXRT硬实时任务的例子,首先将这个简单的程序代码呈现出来,便于总体查看,在过程中阐述了编译时用的Makefile文件,还有在运行此可执行程序时的注意事项。对于程序中给出的过程做了详细的解释,以便于读者能理解这个最简单例子里的每一步的意义所在。
本文实则为本人阅读了在开头是所提及之文档的内容的一个小结,由于这些文档都为英文原版,以本人之能力难以理解透彻,如若有解释错误之处,请见谅并改正。