Linux之系统调用
这里我们只讨论:
- 硬件: Arm64
- 系统: Linux系统 (Kernel-5.15-rc1)
- 高级语言: C (glibc-2.34)
- 模式: 64位 (即未定义CONFIG_COMPAT)
2、什么是系统调用
Linux系统分为内核态和用户态,两者是相互隔离的。为了防止各种应用程序可能对系统资源的破坏,用户态的应用程序是没有权限直接去访问系统资源的,当需要访问时,就需要通过系统调用。
系统调用是内核提供给用户态应用程序的一系列统一接口,标准库或API在系统调用的基础上做了进一步抽象和封装。用户态的应用程序可以直接进行系统调用,也可以通过标准库或API来调用
一个系统调用有很多个步骤,其中一个很重要的就是用户态和内核态相互切换,包括CPU模式的切换, 内核栈、用户栈的保护与处理等
大致的流程为:
-----------------------------------------
|
用户态 | 内核态
|
标准库或API -> 模式切换 -> 调用准备
| \
| -> 处理
| <- 函数
| /
标准库或API <- 模式切换 <- 调用善后
|
-----------------------------------------
下面我们就分别讨论用户态、内核态下的一些关键处理
3、用户态的处理
那么如何陷入内核态呢?主要是通过同步异常来实现。ARM64专门定义了svc指令,用于进入同步异常,也就是说,一旦执行了svc指令,cpu立即跳转到同步异常入口地址处,从这个地址进入内核态
下面已glic里面的系统调用为例,简单看看过程:
ARM64相关的代码主要在:sysdeps/unix/sysv/linux/aarch64
比如我们常用的glibc库函数ioctl(), 在arm64下,glibc的实现:
ENTRY(__ioctl)
mov x8, #__NR_ioctl
sxtw x0, w0
svc #0x0
cmn x0, #4095
b.cs .Lsyscall_error
ret
PSEUDO_END (__ioctl)
其中#__NR_ioctl就对应的ioctl的系统调用号,其定义是在sysdeps/unix/sysv/linux/aarch64/arch-syscall.h,如下:
...
#define __NR_io_uring_setup 425
#define __NR_ioctl 29 //
#define __NR_ioprio_get 31
...
这个系统调用号(29)就是上层标准库(API)与内核联系的桥梁,和内核中的定义是对应的(arm64: include/uapi/asm-generic/unistd.h):
...
/* fs/ioctl.c */
#define __NR_ioctl 29
__SC_COMP(__NR_ioctl, sys_ioctl, compat_sys_ioctl)
...
所以相关用户态的基本流程大致为:
1.将系统调用号存放在x8寄存器中
2.执行svc指令,陷入异常,并且从el0切换到el1
4、内核态的处理
当用户态进入同步异常, 便会跳转到同步异常入口地址,从而触发内核相应的处理动作。
在内核中,arm64对应的异常向量表为(arch/arm64/kernel/entry.S):
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1h
kernel_ventry 1, t, 64, error // Error EL1t
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)
SYM_CODE_START 其实就是将其后面()里面的字符展开而已,并在这个展开之前加上一些属性(比如对齐规则),展开后就相当于”vectors:”,表示定义vectors函数,“:”后面就是vectors的具体实现.
设置不同mode下的异常向量表,异常可以分为4组,每组异常有4个,所以这里一共会设置16个entry。4组异常分别对应4种情况下发生异常时的处理。上面的4组,按照顺序分别对应如下4中情况:
(1)运行级别不发生切换,从ELx变化到ELx,使用SP_EL0,这种情况在Linux kernel都是不处理的,使用invalid entry。
(2)运行级别不发生切换,从ELx变化到ELx,使用SP_ELx。这种情况下在Linux中比较常见。
(3)异常需要进行级别切换来进行处理,并且使用aarch64模式处理,比如64位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch64模式处理异常。
(4)异常需要进行级别切换来进行处理,并且使用aarch32模式处理。比如32位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch32模式进行处理。
这里我们只讨论64位模式,所以系统调用是第3种情况
继续往下看,
展开kernel_ventry, kernel_ventry(arch/arm64/kernel/entry.S)是一个宏,通过.macro和.endm组合定义
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
...
b el\el\ht\()_\regsize\()_\label
.endm
里面会跳转到el\el\ht\()_\regsize\()_\label:
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler
.if \el == 0
b ret_to_user
.else
b ret_to_kernel
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
其中便会调用对应的el\el\ht\()_\regsize\()_\label\()_handler函数
通过解析,sync的处理对应的就是el0t_64_sync_handler()函数,跟踪代码,该函数的处理流程:
el0t_64_sync_handler() [arch/arm64/kernel/entry-common.c]
-> el0_svc()
-> do_el0_svc() [arch/arm64/kernel/syscall.c]
-> el0_svc_common()
-> invoke_syscall()
-> __invoke_syscall()
其中最主要的流程在el0_svc()函数:
static void noinstr el0_svc(struct pt_regs *regs)
{
enter_from_user_mode(regs);
cortex_a76_erratum_1463225_svc_handler();
do_el0_svc(regs);
exit_to_user_mode(regs);
}
最终会调用到invoke_syscall(), 该函数会根据传入的系统调用号, 在sys_call_table中找到对应的系统调用函数, 并执行
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
add_random_kstack_offset();
if (scno < sc_nr) {
syscall_fn_t syscall_fn;
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
ret = __invoke_syscall(regs, syscall_fn);
} else {
ret = do_ni_syscall(regs, scno);
}
syscall_set_return_value(current, regs, 0, ret);
/*
* Ultimately, this value will get limited by KSTACK_OFFSET_MAX(),
* but not enough for arm64 stack utilization comfort. To keep
* reasonable stack head room, reduce the maximum offset to 9 bits.
*
* The actual entropy will be further reduced by the compiler when
* applying stack alignment constraints: the AAPCS mandates a
* 16-byte (i.e. 4-bit) aligned SP at function boundaries.
*
* The resulting 5 bits of entropy is seen in SP[8:4].
*/
choose_random_kstack_offset(get_random_int() & 0x1FF);
}
4.1、sys_call_table
sys_call_table的定义:
/// arch/arm64/kernel/sys.c
asmlinkage long __arm64_sys_ni_syscall(const struct pt_regs *__unused)
{
return sys_ni_syscall();
}
/*
* Wrappers to pass the pt_regs argument.
*/
#define __arm64_sys_personality __arm64_sys_arm64_personality
#undef __SYSCALL
#define __SYSCALL(nr, sym) asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
首先会将sys_call_table都初始化为sys_ni_syscall(),这里使用了GCC的扩展语法:指定初始化
sys_ni_syscall()为一个空函数,未做任何操作:
/// kernel/sys_ni.c
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
然后包含asm/unistd.h的进行逐项初始化,asm/unistd.h最终会包含到uapi/asm-generic/unistd.h头文件:
...
#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif
#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
#define __NR_io_cancel 3
__SYSCALL(__NR_io_cancel, sys_io_cancel)
...
4.2 SYSCALL_DEFINEx
内核中具体的系统调用实现使用SYSCALL_DEFINEx来定义, 其中x代表传入参数的个数,
SYSCALL_DEFINEx相关代码:
/// include/linux/syscalls.h
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
而对于ARM64,__SYSCALL_DEFINEx的定义为:
/// arch/arm64/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__)); \
} \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
由上面可以看出,SYSCALL_DEFINEx来定义的函数就和sys_call_table中由__SYSCALL确定的函数对应了,即__arm64_sys##name
例如ioctl()内核态的实现为:
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
struct fd f = fdget(fd);
int error;
if (!f.file)
return -EBADF;
error = security_file_ioctl(f.file, cmd, arg);
if (error)
goto out;
error = do_vfs_ioctl(f.file, fd, cmd, arg);
if (error == -ENOIOCTLCMD)
error = vfs_ioctl(f.file, cmd, arg);
out:
fdput(f);
return error;
}
想学习更多华为鸿蒙HarmonyOS开发知识,在这里我为大家准备了华为鸿蒙HarmonyOS开发者资料大全,大家可以自行点击链接领取:《做鸿蒙应用开发到底学习些啥?》
其次就是考虑到市场上还没有系统性的学习资料,同时我也整理了一份《鸿蒙 (Harmony OS)开发学习手册》特意整理成PDF文档方式,分享给大家参考学习,大家可以根据自身情况进行获取:《做鸿蒙应用开发到底学习些啥?》
《鸿蒙 (Harmony OS)开发学习手册》
一、入门必看
1. 应用开发导读(ArkTS)
2. 应用开发导读(Java)
3.......
二、HarmonyOS 概念
1. 系统定义
2. 技术架构
3. 技术特性
4. 系统安全
5......
三、如何快速入门?《鸿蒙基础入门开发宝典!》
1. 基本概念
2. 构建第一个ArkTS应用
3. 构建第一个JS应用
4. ……
四、开发基础知识
1. 应用基础知识
2. 配置文件
3. 应用数据管理
4. 应用安全管理
5. 应用隐私保护
6. 三方应用调用管控机制
7. 资源分类与访问
8. 学习ArkTS语言
9. ……
五、基于ArkTS 开发
1. Ability开发
2. UI开发
3. 公共事件与通知
4. 窗口管理
5. 媒体
6. 安全
7. 网络与链接
8. 电话服务
9. 数据管理
10. 后台任务(Background Task)管理
11. 设备管理
12. 设备使用信息统计
13. DFX
14. 国际化开发
15. 折叠屏系列
16. ……
更多了解更多鸿蒙开发的相关知识可以参考:《鸿蒙开发学习指南》