linux-RT应用编程指南
1. Linux-rt介绍
RT-linux(Real-Time Linux),也称作实时Linux,是Linux中的一种硬实时操作系统,它最早由美国墨西哥理工学院的V.Yodaiken开发。而这里说的Linux-RT内核不同于通用的Real-Time Linux内核,它应用了开源的RT PREEMPT机制(补丁)。PREEMPT_RT补丁的关键是最小化不可抢占的内核代码量,同时最小化必须更改的代码量,以便提供这种附加的可抢占性。PREEMPT_RT补丁利用Linux内核的SMP功能来添加这种额外的抢占性,而不需要完整的内核重写。
2.如何构建一个简单的RT应用程序
与Non-RT Linux内核相比,Linux-RT仍然存在内核空间和用户空间的划分。Linux应用程序在用户空间中运行。Non-RT与RT常规编程方式之间的区别主要在于:调度策略、优先级和内存控制,RT应用程序使用了调度策略后,系统将根据调度策略对其进行调优。调度策略和优先级都可以在线程属性中修改,内存控制有单独的API接口,下面就线程属性和内存控制分别进行介绍。
2.1 线程属性
线程属性的设置,请参考我的“Linux应用 – 如何使用多线程编程”第3 小节线程属性部分,这里不再赘述。
2.2内存锁定在
内存锁定api允许应用程序指示内核将(部分或全部)虚拟内存页与实际页帧相关联并保持这种状态。换句话说:内存锁定api将触发必要的页面错误,将被锁定的页面引入物理内存。因此,对锁定内存的第一次访问(在mlock*()调用之后)将已经分配了物理内存,并且不会出现页面错误(在RT关键路径中)。这样就不需要显式地预先设置这些内存的故障。
进一步的内存锁定可以防止应用程序的内存页在其生命周期内的任何时候被调出,即使在整个系统面临内存压力的情况下也是如此。
应用程序可以使用mlock(…)或mlockall(…)进行内存锁定。这些C库调用的细节可以在GNU C Library:Locking页面中找到。请注意,这些调用要求应用程序具有足够的特权(即CAP_IPC_LOCK功能)才能成功。
当mlock(,)锁定特定页(由地址和长度描述)时,mlockall(…)锁定物理内存中应用程序的整个虚拟地址空间(即全局、堆栈、堆、代码)。在方便性和锁定多余内存之间的权衡应该驱动一个对另一个的选择。只锁定由RT线程访问的那些区域(使用mlock(…))可能比盲目使用mlock all(…)更便宜,后者最终将锁定应用程序的所有内存页(即,即使那些仅由非RT线程使用的内存页)。
下面的代码片段演示了mlockall(…)的用法:
/锁定所有当前和将来的页面,以防止被调换/
if(mlockall(MCL_CURRENT | MCL_FUTURE){
perror(“mlockall fail”);
/*退出(-1)或执行错误处理*/
}
内存锁定涉及的函数如下:
#include <sys/mman.h>
int mlock(const void *addr, size_t len);
int munlock(const void *addr, size_t len);
int mlockall(int flags);
int munlockall(void);
描述
mlock() 和 mlockall() 锁定调用进程的部分或全部虚拟地址空间在 RAM 内,阻止内存被页交互出去。munlock()和 munlockall()执行相反的操作,因此指定的虚拟地址可能在内核管理内存时多次被交互出去。内存锁定或解锁操作的单位都是整个分页。
mlock() 和 munlock()
mlock() 锁定开始于地址 addr 并延续长度为 len 个地址范围的内存。调用成功返回后所有包含该地址范围的分页都保证在 RAM内;这些分页保证一直在RAM 内直到后来被解锁。
munlock() 解锁开始 addr 并延续长度为 len 个字节的地址范围的分页。本调用之后,所有包含指定内存区间的分页都有可能因为额外的交互而被内核交互出去。
mlockall() 锁定调用里程所有映射到地址空间的分页。这包括代码、数据、栈片段分页,同时也包括共享库、用户空间内核数据、共享内存以及内存映射的文件。调用成功返回后所有映射的分页都保证在 RAM 中:直到后来的解锁,这些分页都保证一直在 RAM 内。参数 flags 由下面数值常量一个或多个按位或运算构造:MCL_CURRENT 锁定所有当前映射到进程地址空间的分页。 MCL_FUTURE 锁定所有接下来被映射到进程地址空间的分页。这些分页可能是堆栈增长请求的分页,内存文件映射或共享内存区域请求的分页。如果 MCL_FUTURE 被指定,那么接下来的系统调用(如 mmap(2)、sbrk(2)、malloc(3)),都有可能因为锁定的分页超限(参考下面)而失败。同样道理,栈增长也可能失败:内核将阻止栈增长并递送信号 SIGSEGV 给相应进程。
munlockall() 解锁所有映射到调用进程地址空间的分页。
返回值
成功时这些系统调用返回 0。失败时,-1 被返回,errno 被设置为合适的值,并且进程的地址空间锁没有任何变化。
注意
内存锁定有两个主要的应用:实时算法和高安全数据处理。实时算法要求精确的计时,而调度和分页都会引起不期望的执行延迟。实现算法也应该使用 sched_setscheduler(2) 切换到实时调度中去。密码安全软件通常处理诸如密码或安全密钥作为严密的字节数据结构。分页的结果将会把这些秘密持久地存入交互空间的介质,而这个地方可能在安全软件清除 RAM 内密码并终止之后长久能被敌人访问。(但是请注意在现代笔记本和一些桌面机都会保存一份系统 RAM 到磁盘上去,而不管有没有内存锁。)
实时进程应该使用 mlockall() 来阻止延迟的页失败,这样应该会在严格的实时部分锁定足够多的栈空间分页,如此不会有函数调用导致的页失败。这可以通过调用一个分配大量自动变量(或数组)的函数并写入数据到这些数组的地方以触及这些栈分页。这种方式,足够多页会被映射并被锁定在 RAM。模仿的写可以保证在严格执行部分不会发生“写时复制”的分页失败。
内存锁不会由通过 fork(2)调用创建的子进程继承并在调用execve(2) 或进程终止时被删除(解锁)。
地址区间内的内存锁会因为地址区间被 munmap(2) 反映射而自动删除。内存不会入栈,如果分页被mlock() 或 mlockall() 锁定多次,那么在相应区间内的一次 munlock() 或 一个 munlockall() 就会解锁。被映射到多处的分页或被多个进程锁定的 RAM 将锁定到最后一个区域解锁或最后一个进程解锁。
3. 实例
该实例创建一个基本的实时线程,在线程内触发LED的电平翻转,同时统计实时线程的调度延时。
#include <stdlib.h>
#include <stdbool.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <time.h>
#include <sys/time.h>
#include <memory.h>
/* actural path */
#define LED "/sys/class/leds/user-led1/brightness"
struct LatencyInfo {
long long act_us;
long long min_us;
long long max_us;
long long avg_us;
long long sum_us;
};
struct TimeInfo {
struct timespec t1;
struct timespec t2;
struct timespec latency;
struct timespec interval;
};
struct RtThreadParams {
long long cycle;
struct LatencyInfo l_info;
struct TimeInfo t_info;
};
/* exit flag */
volatile bool g_quit = false;
static void sig_handle(int signal) {
g_quit = true;
}
void print_instructions(long interval_ns) {
/* Time Unit Conversion, ns --> us */
long interval_us = interval_ns / 1000L;
printf("\n"
"This program outputs a square wave\n"
"with a pulse width of %ld us and a duty cycle of 50%%.\n\n"
"The waveform can be obtained by measuring\n"
"the corresponding output pin with an oscilloscope.\n\n"
"'ctrl + c' to exit and get error analyse results.\n", interval_us);
return;
}
void analyse_latency(struct LatencyInfo *l_info,
struct TimeInfo *t_info,
long long cycle) {
/* Calculating time difference */
t_info->latency.tv_sec = t_info->t2.tv_sec -
t_info->t1.tv_sec - t_info->interval.tv_sec;
t_info->latency.tv_nsec = t_info->t2.tv_nsec -
t_info->t1.tv_nsec - t_info->interval.tv_nsec;
if (t_info->latency.tv_nsec < 0L) {
t_info->latency.tv_sec --;
t_info->latency.tv_nsec += 1000000000L;
}
/**
* Conversion time to microseconds and
* Calculating the maximum、 minimum and average of latency
**/
l_info->act_us = t_info->latency.tv_sec * 1000000 +
t_info->latency.tv_nsec / 1000;
l_info->min_us = l_info->act_us < l_info->min_us ?
l_info->act_us : l_info->min_us;
l_info->max_us = l_info->act_us > l_info->max_us ?
l_info->act_us : l_info->max_us;
l_info->sum_us += l_info->act_us;
l_info->avg_us = l_info->sum_us / cycle;
return;
}
void *thread_func(void *arg) {
if (arg == NULL) {
printf("Arg input error.\n");
return NULL;
}
struct RtThreadParams *params = (struct RtThreadParams*)arg;
struct TimeInfo *t_info = ¶ms->t_info;
struct LatencyInfo *l_info = ¶ms->l_info;
long long cycle = params->cycle;
/* Open the LED node */
int fd = open(LED, O_WRONLY);
if (fd < 0) {
printf("LED open failed.\n");
return NULL;
}
bool flag = false;
while (! g_quit) {
clock_gettime(CLOCK_REALTIME, &t_info->t1);
clock_nanosleep(CLOCK_MONOTONIC, 0, &t_info->interval, NULL);
if (flag) {
/* turn off LED */
if (write(fd, "0\n", 2) < 0) {
printf("Fail to turn off led.\n");
}
} else {
/* turn on LED */
if (write(fd, "1\n", 2) < 0) {
printf("Fail to turn on led.\n");
}
}
clock_gettime(CLOCK_REALTIME, &t_info->t2);
/* analyse the latecy to get error range */
analyse_latency(l_info, t_info, ++cycle);
flag = !flag;
}
close(fd);
printf("\nCycles : %lld \n"
"Error results : [Min] %lld us [Max] %lld us [Avg] %lld us\n",
cycle, l_info->min_us, l_info->max_us, l_info->avg_us);
return NULL;
}
int main(int argc, char* argv[]) {
/* 100us */
long interval_ns = 100000L;
if (argc == 2)
interval_ns = atol(argv[1]) * 1000L;
print_instructions(interval_ns);
/* Ctrl+c handler */
signal(SIGINT, sig_handle);
struct RtThreadParams rt_thread_params;
/**
* Initialize the time node and set the interval
* Initialize variable for the first time,
* Set min_us to maximum and other value to minimum whenever possible
**/
memset(&rt_thread_params, 0, sizeof(struct RtThreadParams));
rt_thread_params.t_info.interval.tv_nsec = interval_ns;
rt_thread_params.l_info.min_us = LONG_MAX;
struct sched_param param;
pthread_attr_t attr;
pthread_t thread;
/* Lock memory */
if (mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {
printf("mlockall failed: %m\n");
exit(-2);
}
/* Initialize pthread attributes (default values) */
if (pthread_attr_init(&attr) != 0) {
printf("init pthread attributes failed\n");
return -1;
}
/* Set a specific stack size */
if (pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN) != 0) {
printf("pthread setstacksize failed\n");
return -1;
}
/* Set scheduler policy and priority of pthread */
if (pthread_attr_setschedpolicy(&attr, SCHED_FIFO) != 0) {
printf("pthread setschedpolicy failed\n");
return -1;
}
param.sched_priority = 80;
if (pthread_attr_setschedparam(&attr, ¶m) != 0) {
printf("pthread setschedparam failed\n");
return -1;
}
/* Use scheduling parameters of attr */
if (pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) != 0) {
printf("pthread setinheritsched failed\n");
return -1;
}
/* Create a pthread with specified attributes */
if (pthread_create(&thread, &attr, thread_func, (void *)&rt_thread_params) != 0) {
printf("create pthread failed\n");
return -1;
}
/* Join the thread and wait until it is done */
if (pthread_join(thread, NULL) != 0)
printf("join pthread failed: %m\n");
/* Unock memory */
munlockall();
return 0;
}