5.2 ARM64 Xenomai系统调用
5.2.1 IPIPE区分双核系统调用(复用x8寄存器)
回顾一下《5.1 ARM64 Linux系统调用》,当程序在用户层(EL0)执行 SVC 指令时,它会触发一个同步异常(即软件中断),导致处理器进入更高的特权级别(通常是 EL1,即操作系统运行的级别)。
以write 系统调用为例,其中的关键代码片段如下:
0x0000fffff7f08c7c <+44>: mov x8, #0x40 // #64
0x0000fffff7f08c80 <+48>: svc #0x0
在发起 SVC 指令之前,应用程序需要将系统调用号以及任何必要的参数放置在特定的寄存器中。对于 AArch64 来说:系统调用号通常放在 x8 寄存器。在当前的例子中,x8 被设置为 0x40。
IPIPE是不可能改变上述指令和规则的,只能在兼容的基础上创新。
具体的方法是:x8是64位的通用寄存器,其中传递的系统调用号,以__COBALT_SYSCALL_BIT(0x10000000)为界限划分,0x10000000以下的范围给Linux使用,0x10000000及以上的范围给Xenomai使用。
实际上,ARM64 Linux根本用不了这么大的范围。宏定义__NR_syscalls它表示系统调用表的大小,即支持的最大系统调用编号加一,定义在include/uapi/asm-generic/unistd.h,当前的值是294(#define __NR_syscalls 294)。所以只要发现x8中的系统调用号超过__NR_syscalls,就必然是Xenomai系统调用。
接下来看一下Xenomai系统调用号和系统调用表的定义。
Xenomai系统调用号定义在include/xenomai/cobalt/uapi/syscall.h,起始系统调用号sc_cobalt_bind竟然是0,说好的从0x10000000开始的呢?

别着急,来看以下Xenomai是怎么通过x8传递系统调用号的。
应用程序自身一般是不会直接使用汇编指令svc来触发系统调用的,而是通过调用库函数来间接触发系统调用。这里就有操作空间了。
在用户层,Xenomai3的实时核是cobalt,它在用户层的库是libcobalt。参考lib/cobalt/arch/arm64/include/asm/xenomai/syscall.h,使用svc触发系统调用之前,总是使用宏__xn_syscode(__nr),把Xenomai原始的系统调用号,与0x10000000进行逻辑或运算,赋值到w8即x8寄存器的低32位。

在内核层,通过宏__xn_syscall (__regs),对x8寄存器的值执行与用户层相反的操作,即与~0x10000000进行逻辑与运算,得到原始的系统调用号。下图的例子是以sc_cobalt_read(#define sc_cobalt_read 81)为例子,进行说明。

第1186行,两个判断条件都成立,会依次调用ipipe_fastcall_hook->handle_head_syscall。

在handle_head_syscall中,第498行,通过宏__xn_syscall(__regs)得到原始系统调用号0x51。
接下来的章节,会用实例从用户层和内核层来演示Xenomai系统调用。
5.2.2 用户层:用latency演示Xenomai系统调用
在《5.1.1 用户层:用hello world演示系统调用》章节中,用最简单的hello world来演示Linux的系统调用。
在本章节,使用Xenomai自带的测试程序latency来演示Xenomai系统调用。它的源码在xenomai-v3.2.1/testsuite/latency/latency.c,编译完成并默认安装到usr/xenomai/bin/latency。
latency 是 Xenomai/Cobalt 实时内核提供的一个工具,用于测量系统的实时性能,特别是上下文切换和中断响应的延迟。它启动后,会新建一个display线程用于打印信息;会新建一个sampling线程用于延迟采样。
使用gdb来观察samping线程的系统调用read。
(1)在shell下启动# gdb latency

(2)执行(gdb) run,在确保开始打印延迟信息时,按Ctrl+C中断latency的执行。用(gdb) info threads可以看到此时有4个线程,其中sampling-727就是用测试测量延迟的线程。并且,sampling-727线程中断的位置是Xenomai系统调用库函数__wrap_read。

(3)执行(gdb) disassemble显示当前位置的汇编

其中和系统调用号相关的两行汇编摘出来:
mov w8, #0x51 // w8 = 0x0051
mov 指令将立即数 #0x51 (十进制为81)直接移动到32位寄存器 w8 中。这意味着 w8 的低16位会被设置为 0x51,而高16位被清零。
movk w8, #0x1000, lsl #16 // w8 = 0x10000051
movk 指令用于修改寄存器中指定位置的16位字段,而不改变其他位。这里的 lsl #16 表示立即将立即数 #0x1000 左移16位,然后将其放入寄存器 w8 的相应位置。movk 将不会影响已经存在于 w8 中的低16位。
结合两条指令的效果如下:
第一条指令 mov w8, #0x51 设置 w8 的值为 0x0051。
第二条指令 movk w8, #0x1000, lsl #16 修改 w8 的高16位为 0x1000,同时保留低16位的 0x51。
最终结果是 w8 的值变为 0x10000051。
这个结果与《5.2.1 IPIPE区分双核系统调用(复用x8寄存器)》的分析,是完全匹配的。接下来分析,在内核层Xenomai系统调用的流程。
5.2.3 内核层:Xenomai系统调用的流程
参考《5.1.2 内核层:ARM64 Linux系统调用的流程》,先回顾一下ARM64 Linux系统调用的过程:
第一步,从el0_sync跳转到el0_svc。
第二步,从el0_svc跳转到c函数el0_svc_handler
第三步,从el0_svc_handler调用el0_svc_common
第四步,el0_svc_common调用invoke_syscall
在第四步的el0_svc_common函数中,IPIPE是在调用invoke_syscall之前,调用ipipe_handle_syscall进行系统调用的劫持。如下图所示。

(1)ipipe_handle_syscall

第1186行,两个判断条件都成立。其中_TIP_HEAD代表当前线程是实时进程。
(2) ipipe_fastcall_hook->handle_head_syscall
函数比较长,分成两张图:


第498行,通过宏__xn_syscall(__regs),对x8寄存器的值执行与用户层相反的操作,即与~0x10000000进行逻辑与运算,得到原始的系统调用号0x51(81),存到变量code。
第512行,以code为下标,从Xenoami系统调用表cobalt_syscalls中找到系统调用函数指针,赋值给handler。此时handler指向CoBaLt_read。
第600行,执行handler即CoBaLt_read,跳转到了由COBALT_SYSCALL定义的Xenomai系统调用read。

至此,走完了Xenomai实时进程调用Xenomai系统调用的过程。
但是这个是最简单的过程!
Xenomai实时线程如果切换到root domain?
或者Xenomai调用了Linux系统调用?
考虑所有因素,可能有如下组合:
| Linux系统调用 | Xenomai系统调用 | |
|---|---|---|
| Linux线程 仅能运行在Root domain | todo | todo |
| Xenomai实时线程 运行在Head domain | todo | 已分析,最简单 |
| Xenomai实时线程 运行在Root domain | todo | todo |
后面的章节逐步讨论!
5.2.4 内核层:Xenomai系统调用的定义
为了方便对比ARM64 Linux和Xenomai,以系统调用read为例,分别总结如下。
根据《5.1.3 内核层:ARM64 Linux系统调用函数的定义》,Linux 系统调用总结如下。
- 系统调用号
include/uapi/asm-generic/unistd.h:
#define __NR_read 63
- 系统调用函数
fs/read_write.c:
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
- 系统调用表
arch/arm64/kernel/sys.c:
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
- 系统调用号和系统调用函数插入系统调用表
include/uapi/asm-generic/unistd.h:
__SYSCALL(__NR_read, sys_read)
作为对比,针对Xenomai总结如下。相比Linux多了一个系统调用模式的定义。
- 系统调用号
include/xenomai/cobalt/uapi/syscall.h:
#define sc_cobalt_read 81
- 系统调用函数
kernel/xenomai/posix/io.c:
COBALT_SYSCALL(read, handover,
(int fd, void __user *buf, size_t size))
{
return rtdm_fd_read(fd, buf, size);
}
- 系统调用表
kernel/xenomai/posix/syscall.c:
#include "syscall_entries.h"
static const cobalt_syshand cobalt_syscalls[] = {
__COBALT_CALL_NI
__COBALT_CALL_ENTRIES
#ifdef CONFIG_XENO_ARCH_SYS3264
#include <asm/xenomai/syscall32-table.h>
#endif
};
- 系统调用号和系统调用函数插入系统调用表
kernel/xenomai/posix/syscall_entries.h:
__COBALT_CALL_ENTRIES
- 系统调用模式
kernel/xenomai/posix/syscall.c:
static const int cobalt_sysmodes[] = {
__COBALT_CALL_NFLAGS
__COBALT_CALL_MODES
};
接下来对 Xenomai 系统调用逐项展开:
(1)系统调用号
系统调用号定义在include/xenomai/cobalt/uapi/syscall.h,文件比较长,这里分别截取头部和尾部两张图。


第21行,包含头文件cobalt/uapi/asm-generic/syscall.h,此头文件中定义了宏定义__COBALT_SYSCALL_BIT(0x10000000)。在用户层通过x8通用寄存器传递Xenomai系统调用号时,总是与__COBALT_SYSCALL_BIT做逻辑或运算。
第139行,定义了宏__NR_COBALT_SYSCALLS,表示Xenomai包含的系统调用号最大数量为128。从第137行可知,实际只用到了0~114。
(2)系统调用函数
kernel/xenomai/posix/io.c:
COBALT_SYSCALL(read, handover,
(int fd, void __user *buf, size_t size))
{
return rtdm_fd_read(fd, buf, size);
}
在kernel/xenomai/posix/io.c中,通过宏COBALT_SYSCALL来定义read系统调用的实现。
kernel/xenomai/posix/syscall.h:
/* Regular (native) syscall handler implementation. */
#define COBALT_SYSCALL(__name, __mode, __args) \
long CoBaLt_ ## __name __args
宏COBALT_SYSCALL展开后,得到函数名为CoBaLt_read。这里的前缀突然用了大小写交替的方式,不知道为什么!
(3)系统调用表
Xenomai系统调用表cobalt_syscalls[]数组的定义,与Linux的系统调用表有异曲同工之妙。

第455行,宏定义__COBALT_CALL_NI的目标,就是把cobalt_syscalls[]数组的所有元素全部初始化,指向CoBaLt_ni函数(直接return -ENOSYS)。相比Linux,系统调用表默认所有元素都指向__arm64_sys_ni_syscall函数。
第456行,宏定义__COBALT_CALL_ENTRIES从字面理解,应该是很多__COBALT_CALL_ENTRY的集合,事实也是如此。__COBALT_CALL_ENTRIES定义在第452包含的头文件syscall_entries.h中。此文件的绝对路径是kernel/xenomai/posix/syscall_entries.h。展开后就是对数组的元素进行赋值,和Linux的套路完全相同。上述宏定义展开的时候,涉及到兼容32位ABI的宏都按空处理。

备注:syscall_entries.h默认是不存在的,是在编译过程中使用脚本gen-syscall-entries.sh处理各个.c文件中的COBALT_SYSCALL宏,生成的一个头文件。
(4)系统调用模式
Xenomai的每个系统调用,都有自己对应的调用模式,定义在数组cobalt_sysmodes[],数组的索引值就是系统调用号。
所以,根据系统调用号,从cobalt_syscalls[]数组可以找到对应的系统调用函数,从cobalt_sysmodes[]数组可以找到系统调用模式。
cobalt_sysmodes[]数组同样定义在kernel/xenomai/posix/syscall.c中。

第462行,宏定义__COBALT_CALL_NFLAGS就是把cobalt_sysmodes[]数组的所有元素全部初始化0。
第463行,宏__COBALT_CALL_MODES定义在第452包含的头文件syscall_entries.h中。此文件的绝对路径是kernel/xenomai/posix/syscall_entries.h。展开后就是对数组的元素进行赋值。宏定义展开的时候,涉及到兼容32位ABI的宏都按空处理。

其中所涉及的__xn_exec_primary、__xn_exec_current等模式的定义,同样定义在kernel/xenomai/posix/syscall.c中。

6817

被折叠的 条评论
为什么被折叠?



