4.7POSIX进程与线程实例

4.7 POSIX进程与线程实例

4.7.1 构建latency

latency是Xenomai测试套件中的一个应用程序,用于测量系统的实时性能,特别是上下文切换和中断响应的延迟。编译并生成Xenomai应用层代码的过程中,latency会被自动编译,默认安装到usr/xenomai/bin/latency。

latency源码位于testsuite/latency/latency.c,可以作为一个实例来说明如何使用POSIX skin进行实时进程和实时线程编程。

在Xenomai源码中,latency的Makefile是通过Automake工具生成的。为了更好地模拟并演示如何构建Xenomai应用程序,手动来重新构建latency

makefilelatency.c放在同一个目录下,执行make命令,即可生成latency应用。

  • makefile文件
    • 使用POSIX skin进行编译链接
    • 使用wrap-link.sh进行链接
makefile 复制代码
XENO_DESTDIR=/root/xenomai/xenomai-v3.2.4-install

CC = aarch64-linux-gnu-gcc
CCLD = $(XENO_DESTDIR)/usr/xenomai/bin/wrap-link.sh $(CC)

XENO_CONFIG =$(XENO_DESTDIR)/usr/xenomai/bin/xeno-config

XENO_POSIX_CFLAGS =$(shell DESTDIR=$(XENO_DESTDIR) $(XENO_CONFIG) --skin=posix --cflags)

XENO_POSIX_LDFLAGS =$(shell DESTDIR=$(XENO_DESTDIR) $(XENO_CONFIG) --skin=posix --ldflags) \
                    -lm

PROJPATH = .

EXECUTABLE := latency

src = $(wildcard ./*.c)

obj = $(patsubst %.c, %.o, $(src))

all: $(EXECUTABLE)

$(EXECUTABLE): $(obj)
        $(CCLD) -g -o $@ $^ $(XENO_POSIX_LDFLAGS)

%.o:%.c
        $(CC) -g -o $@ -c $< $(XENO_POSIX_CFLAGS)

.PHONY: clean
clean:
        rm -f $(EXECUTABLE) $(obj)
  • latency.c源码

直接编译latency.c源码,会报告找不到cobalt/uapi/syscall.h头文件。

这个头文件实际上并不需要,直接移除,即可编译通过。

c 复制代码
diff --git a/testsuite/latency/latency.c b/testsuite/latency/latency.c
index fe688b9b8..0b6964bb9 100644
--- a/testsuite/latency/latency.c
+++ b/testsuite/latency/latency.c
@@ -556,8 +556,6 @@ static void faulthand(int sig)

 #ifdef CONFIG_XENO_COBALT

-#include <cobalt/uapi/syscall.h>
-
 static const char *reason_str[] = {
        [SIGDEBUG_UNDEFINED] = "received SIGDEBUG for unknown reason",
        [SIGDEBUG_MIGRATE_SIGNAL] = "received signal",
  • 为什么用户空间代码几乎不再用这个头文件了?

    因为Xenomai 3.x,尤其是使用Dovetail之后,已经提供了更好的用户空间API封装,开发者可以直接使用Xenomai的 POSIX skin 或者 Alchemy skin 来进行实时编程,而不需要直接调用底层的Cobalt内核接口。

  • 那为什么这个头文件还必须存在?

    • Cobalt 层保持ABI(Application Binary Interface)稳定性,老版本的libcobalt.so, 实用应用程序和Xenomai 2 的extensions必须能在新版本内核上继续工作,就是不需要重新编译。
    • Xenomai 的构建过程会使用 cobalt/uapi 来生成 Cobaltsyscall表, Wrapper, Trampoline 代码(系统调用从 Linux 用户态到 Cobalt co-kernel 的入口跳板) 和 Stub 码(发起系统调用的参数封装层)
    • 这是一个历史遗留但必须保留的 interface,旧的 binary 依然依赖它,内核内部的 syscall dispatching 表(系统调用分发表)仍然使用它

4.7.2 执行latency

latency能在用户态任务、内核态周期性任务、内核态定时器中断处理函数等不同模式下进行延迟测量,还能记录最小、最大、平均延迟等数据,并通过直方图、统计信息等方式展示结果。

参数 说明
-h 打印最小、平均、最大延迟的直方图,方便直观查看延迟分布情况。
-g 将直方图以 Gnuplot 格式导出到指定的 中,便于后续用 Gnuplot 工具进行可视化处理。
-s 打印最小、平均、最大延迟的统计信息,如方差、标准差等,帮助分析延迟数据特征。
-H 设置直方图的大小,默认值为 200。若最后一个桶数据已满,可增大该值以容纳更多数据。
-B 设置直方图中每个桶的大小,默认值为 1000ns。减小该值能提高延迟数据统计的分辨率。
-p <period_us> 设置采样周期,单位为微秒。程序会按照该周期进行延迟采样。
-l 设置每显示多少行数据后输出一次表头,默认值为 21。设置为 0 可抑制表头输出。
-T <test_duration_seconds> 设置测试持续时间,单位为秒。默认值为 0,表示需手动通过 ^C 结束测试。
-q 当使用 -T 参数时,抑制 RTD 和 RTH 行的输出,保持输出简洁。
-t <test_mode> 指定测试模式,0 表示用户态任务(默认),1 表示内核态周期性任务,2 表示内核态定时器中断处理函数。
-f 每当出现新的最大延迟时,冻结跟踪信息,方便调试问题。
-c 将测量任务固定到指定编号的 CPU 上运行,避免任务在不同 CPU 间迁移带来的影响。
-P 设置任务优先级,仅适用于测试模式 0 和 1。
-b 遇到模式切换时终止测试。

为了演示用户层线程的创建,使用默认的用户态任务测试模式。在执行latency时,不增加任何参数,使用默认即可。latency会持续打印延迟信息,如下是执行后的输出。因为是在QEMU下执行的,所以延迟很大,可以忽略延迟信息。

bash 复制代码
$ ./latency
== Sampling period: 1000 us
== Test mode: periodic user-mode task
== All results in microseconds
warming up...
RTT|  00:00:01  (periodic user-mode task, 1000 us period, priority 99)
RTH|----lat min|----lat avg|----lat max|-overrun|---msw|---lat best|--lat worst
RTD|     66.708|    495.528|  36315.492|      38|     0|     66.708|  36315.492
RTD|     90.538|    445.924|   2583.055|      41|     0|     66.708|  36315.492
RTD|     62.389|    454.657|   1977.077|      47|     0|     62.389|  36315.492
...snip...

latency执行后,会在当前控制台持续输出。可以通过ssh -p 56789 localhost登录到QEMU ARM64虚拟机,在新的控制台观察latency信息。

  • 执行cat /proc/xenomai/sched/threads查看Xenomai实时线程信息。
    • 主线程latency:对应于Linux中的latency进程,PID号为523。
    • 子线程sampling:用户态任务,用于延迟采样,PID号为525。
    • 子线程display:用于打印信息,PID号为526。
bash 复制代码
$ cat /proc/xenomai/sched/threads
CPU  PID    CLASS  TYPE      PRI   TIMEOUT       STAT       NAME
  0  0      idle   core       -1   -             R          [ROOT/0]
  1  0      idle   core       -1   -             R          [ROOT/1]
  2  0      idle   core       -1   -             R          [ROOT/2]
  3  0      idle   core       -1   -             R          [ROOT/3]
  0  523    rt     cobalt      0   -             X          latency
  0  525    rt     cobalt      0   -             W          display-523
  0  526    rt     cobalt     99   -             Wt         sampling-523
  • 执行top -H -b -n 1 -p 523查看Linux中PID号为523的latency进程。
    • -H:显示线程(H 表示 "Threads-mode",默认显示进程)。
    • -b:以批处理模式(非交互式)运行,适合输出到文件或管道。
    • -n 1:仅运行一次迭代后退出。
    • -p 523: Linux中latency进程的PID为523。
bash 复制代码
$ top -H -b -n 1 -p 523
top - 10:00:28 up  7:41,  2 users,  load average: 0.00, 0.00, 0.00
Threads:   4 total,   0 running,   4 sleeping,   0 stopped,   0 zombie
%Cpu(s):  6.9 us,  0.0 sy,  0.0 ni, 93.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   3671.2 total,   3477.8 free,     83.1 used,    110.3 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   3553.4 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    523 root      20   0   77976  11684   3076 S   0.0   0.3   0:00.09 latency
    524 root      20   0   77976  11684   3076 S   0.0   0.3   0:00.71 cobalt_printf
    525 root      20   0   77976  11684   3076 S   0.0   0.3   0:00.42 display-523
    526 root      rt   0   77976  11684   3076 S   0.0   0.3   0:00.00 sampling-523

相比Xenomai实时线程,在Linux中多出了一个cobalt_printf子线程。cobalt_printf子线程是latency进程在启动过程中创建的,它定期 将缓冲区内容刷新到目标流(如 stdoutstderr 或其他文件流)。

4.7.3 实时线程的创建

以创建实时线程sampling为实例,分析创建实时线程的要点。

c 复制代码
	if (test_mode == USER_TASK) {
		setup_sched_parameters(&tattr, priority);
		CPU_ZERO(&cpus);
		CPU_SET(cpu, &cpus);

		ret = pthread_attr_setaffinity_np(&tattr, sizeof(cpus), &cpus);
		if (ret)
			error(1, ret, "pthread_attr_setaffinity_np()");

		ret = pthread_create(&latency_task, &tattr, latency, NULL);
		if (ret)
			error(1, ret, "pthread_create(latency)");

		pthread_attr_destroy(&tattr);
	}
1. setup_sched_parameters()函数初始化线程调度属性

setup_sched_parameters 函数的主要功能是初始化线程属性对象,设置线程的调度继承策略、调度策略以及调度参数。

c 复制代码
static void setup_sched_parameters(pthread_attr_t *attr, int prio)
{
	struct sched_param p;
	int ret;
	
	ret = pthread_attr_init(attr);
	if (ret)
		error(1, ret, "pthread_attr_init()");

	ret = pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED);
	if (ret)
		error(1, ret, "pthread_attr_setinheritsched()");

	ret = pthread_attr_setschedpolicy(attr, prio ? SCHED_FIFO : SCHED_OTHER);
	if (ret)
		error(1, ret, "pthread_attr_setschedpolicy()");

	p.sched_priority = prio;
	ret = pthread_attr_setschedparam(attr, &p);
	if (ret)
		error(1, ret, "pthread_attr_setschedparam()");
}
  • 初始化线程属性对象

    • 调用 pthread_attr_init 函数对传入的 attr 进行初始化,若初始化失败则调用 error 函数输出错误信息并终止程序。
  • 设置调度继承策略

    • 调用 pthread_attr_setinheritsched 函数将调度继承策略设置为 PTHREAD_EXPLICIT_SCHED,表示线程将使用 attr 中显式设置的调度策略和参数,若设置失败则报错终止。
  • 设置调度策略

    • 依据 prio 的值选择调度策略,若 prio 不为 0 则使用 SCHED_FIFO(实时先进先出调度策略),否则使用 SCHED_OTHER(默认的分时调度策略)。对于sampling线程,传入的prio值为HIPRIO(99),对应的调度策略为SCHED_FIFO。
    • 调用 pthread_attr_setschedpolicy 函数进行设置,失败则报错终止。
  • 设置调度优先级

    • 将 prio 赋值给 p.sched_priority,对于sampling线程,传入的prio值为HIPRIO(99)。
    • 调用 pthread_attr_setschedparam 函数将该调度参数设置到 attr 中,失败则报错终止。
2. pthread_attr_setaffinity_np()函数设置CPU亲和性

pthread_attr_setaffinity_np 是一个非标准(_np 表示非可移植)的函数,用于设置线程属性对象的 CPU 亲和性。

c 复制代码
		CPU_ZERO(&cpus);
		CPU_SET(cpu, &cpus);

		ret = pthread_attr_setaffinity_np(&tattr, sizeof(cpus), &cpus);
		if (ret)
			error(1, ret, "pthread_attr_setaffinity_np()");
  • CPU_ZERO(&cpus);

    • 功能:CPU_ZERO 是一个宏,定义在 <sched.h> 头文件中,用于将 cpu_set_t 类型的集合 cpus 清零。cpu_set_t 类型用于表示一组 CPU 核心,将集合清零意味着初始化该集合,使其不包含任何 CPU 核心。
  • CPU_SET(cpu, &cpus);

    • 功能:CPU_SET 也是一个宏,同样定义在 <sched.h> 头文件中,用于将指定编号的 CPU 核心添加到 cpu_set_t 类型的集合中。
    • 参数:cpu 的有效取值范围要介于 0 和 CPU_SETSIZE - 1 之间,CPU_SETSIZE 同样是定义在 <sched.h> 里的宏,它规定了 cpu_set_t 集合所能表示的最大 CPU 数量。
    • 示例代码:CPU_SET(2, &cpus),将编号为2的 cpu 添加到 cpus 集合中,意味着后续要让线程在这个 CPU 2 核心上运行。
  • pthread_attr_setaffinity_np(&tattr, sizeof(cpus), &cpus);

    • 功能:pthread_attr_setaffinity_np 是一个非标准(_np 表示非可移植)的函数,用于设置线程属性对象的 CPU 亲和性。该函数会将指定线程属性对象关联的线程限制在 cpus 集合所包含的 CPU 核心上运行。
3. pthread_create()函数创建实时线程

pthread_create 是 POSIX 中的一个标准函数,用于创建并立即启动一个新的线程。

c 复制代码
		ret = pthread_create(&latency_task, &tattr, latency, NULL);
		if (ret)
			error(1, ret, "pthread_create(latency)");

参数说明:

  • 参数1:&latency_task

    • 指向 pthread_t 类型变量 latency_task 的指针,新创建线程的线程 ID 会存储在该变量中。
  • 参数2:&tattr

    • 指向 pthread_attr_t 类型变量 tattr 的指针,该变量已经通过 setup_sched_parameters 函数配置了线程的调度策略和优先级等属性。
  • 参数3:latency

    • 新线程启动时要执行的函数,函数原型为 void *latency(void *cookie)。
    • void *latency(void *cookie) 函数执行后,会设置子线程的名称为sampling-%d,其中%d传入的是子线程的PID。
  • 参数4:NULL

    • 传递给 latency 函数的参数,这里表示不传递额外参数。
4. 实时线程display的特殊性

除了实时线程sampling之外,latency也用相同的方式创建了实时线程display,但是有一处不同:实时线程display的优先级是0

当优先级为0时,调用 pthread_attr_setschedpolicy 函数设置的调度策略为SCHED_OTHER(默认的分时调度策略)。这也意味着,实时线程display在Xenomai中的调度策略为xnsched_class_weak,属于弱实时调度类。

考虑到实时线程display的作用,主要是频繁的向终端打印信息,选择优先级为0是恰当的,可以避免影响实时线程sampling的实时性!

4.7.3 主线程的处理

主线程一般不会执行实时任务,而是

c 复制代码
int main(int argc, char *const *argv)
{
  ...snip...

  sigemptyset(&mask);
  sigaddset(&mask, SIGINT);
  sigaddset(&mask, SIGTERM);
  sigaddset(&mask, SIGHUP);
  sigaddset(&mask, SIGALRM);
  pthread_sigmask(SIG_BLOCK, &mask, NULL);

  ...snip...

  __STD(sigwait(&mask, &sig));
	finished = 1;

	cleanup();

	return 0;
}
1. 阻塞指定信号
c 复制代码
  sigemptyset(&mask);
  sigaddset(&mask, SIGINT);
  sigaddset(&mask, SIGTERM);
  sigaddset(&mask, SIGHUP);
  sigaddset(&mask, SIGALRM);
  pthread_sigmask(SIG_BLOCK, &mask, NULL);
  • sigemptyset函数

    • 功能:sigemptyset 是一个函数,定义在 <signal.h> 头文件中,用于将 sigset_t 类型的信号集 mask 初始化为空集。这意味着在调用该函数之后,mask 信号集中不包含任何信号。
    • 参数:&mask 是 sigset_t 类型变量 mask 的地址,函数会对该地址指向的信号集进行操作。
  • sigaddset函数

    • 功能:sigaddset 函数同样定义在 <signal.h> 头文件中,用于将指定的信号添加到 sigset_t 类型的信号集中。
    • 参数:&mask 是信号集 mask 的地址,后面的信号常量表示要添加到信号集中的信号。
    • 这几行代码分别将 SIGINT、SIGTERM、SIGHUP 和 SIGALRM 信号添加到 mask 信号集中。
      • SIGINT:通常是用户按下 Ctrl+C 时发送给进程的中断信号。
      • SIGTERM:用于请求进程正常终止的信号,是系统关机或 kill 命令默认发送的信号。
      • SIGHUP:终端断开连接时发送给进程的信号,也可用于通知进程重新加载配置。
      • SIGALRM:定时器到期时发送的信号,在代码里由 alarm 函数触发。
  • pthread_sigmask函数

    • 功能:pthread_sigmask 函数定义在 <pthread.h> 头文件中,用于设置线程的信号掩码。信号掩码决定了哪些信号会被阻塞,即线程暂时不会响应这些信号。这里 SIG_BLOCK 表示将 mask 信号集中的信号添加到当前线程的信号掩码中,从而阻塞这些信号。
    • 参数:
      • SIG_BLOCK:操作标志,表示将指定信号集添加到当前信号掩码中。
      • &mask:指向要添加的信号集的指针。
      • NULL:若不需要获取之前的信号掩码,可传入 NULL。
2. 等待被阻塞的信号

使用 sigwait 函数来等待被阻塞的信号: SIGINT、SIGTERM、SIGHUP 和 SIGALRM 。sigwait 函数会阻塞当前线程,直到 set 信号集中的某个信号被递送。

c 复制代码
__STD(sigwait(&mask, &sig));
  • 参数说明
    • set:指向 sigset_t 类型信号集的指针,该信号集指定了 sigwait 函数要等待的信号集合。
    • sig:指向 int 类型变量的指针,用于存储实际接收到的信号编号。当 sigwait 函数返回时,这个变量会被赋值为接收到的信号值。

为了让 sigwait 正常工作,调用该函数的线程需要先阻塞 set 信号集中的所有信号,否则信号可能会被默认的信号处理函数处理,而不会被 sigwait 捕获。一旦 set 信号集中的某个信号被递送,sigwait 函数会解除阻塞,将接收到的信号编号存储到 sig 指向的变量中,然后返回 0 表示成功。

latencymain函数中,sigwait(&mask, &sig) 会阻塞主线程,并使得主线程切换到此模式运行。次模式是指线程由标准 Linux 内核调度的状态。

bash 复制代码
$ cat /proc/xenomai/sched/threads
CPU  PID    CLASS  TYPE      PRI   TIMEOUT       STAT       NAME
  0  0      idle   core       -1   -             R          [ROOT/0]
  1  0      idle   core       -1   -             R          [ROOT/1]
  2  0      idle   core       -1   -             R          [ROOT/2]
  3  0      idle   core       -1   -             R          [ROOT/3]
  0  523    rt     cobalt      0   -             X          latency
  0  525    rt     cobalt      0   -             W          display-523
  0  526    rt     cobalt     99   -             Wt         sampling-523

查看主线程latency的STAT字段,它处于X状态,代表其运行在次模式。

状态 定义 bit位 含义
X XNRELAX 0x00000080 处于secondary模式(非实时上下文)

如果用户按下 Ctrl+C 发送 SIGINT 信号、使用 kill 命令发送 SIGTERM 信号、终端断开发送 SIGHUP 信号或者 alarm 定时器到期发送 SIGALRM 信号,当接收到这些信号中的任意一个时,sigwait 解除阻塞,将接收到的信号编号存于 sig 变量。

随后程序把 finished 标记设为 1,并调用 cleanup 函数进行资源清理和测试结果输出,最后正常退出。这样做能让程序统一处理终止信号,保证资源正确释放。

相关推荐
无聊到发博客的菜鸟5 小时前
使用STM32对SD卡进行性能测试
stm32·单片机·rtos·sd卡·fatfs
切糕师学AI2 天前
Azure RTOS ThreadX 简介
microsoft·嵌入式·azure·rtos
切糕师学AI6 天前
FreeRTOS是什么?
嵌入式·rtos
aspirestro三水哥7 天前
3.5启动QEMUARM64虚拟机
rtos·xenomai
时光の尘8 天前
嵌入式面试八股文(十九)·裸机开发与RTOS开发的区别
linux·stm32·单片机·iic·rtos·spi
aspirestro三水哥10 天前
3.2编译Xenomai内核
rtos·xenomai
Jerry丶Li11 天前
NXP--S32K移植FreeRTOS
嵌入式硬件·rtos·nxp·s32k
aspirestro三水哥13 天前
3.4制作根文件系统
rtos·xenomai
aspirestro三水哥14 天前
3.1Ubuntu开发环境
ubuntu·rtos·xenomai