在user利用syscall指令(x86)或svc指令(Arm)发起系统调用后,如果kernel在处理系统调用的过程中被异常打断(如果有信号量pending或被异常唤醒等),此时是retry还是返回?由于是在user的进程上下文中,根据机制和策略分离的原则,kernel一般不会在系统调用处理函数中做retry动作,而是返回user space后再次发起系统调用。不同的操作系统实现方式不一样,我们以Magenta和Linux为例说明。
Magenta的syscall流程
用户空间系统调用接口的实现
我们先来看看magenta的系统调用流程(以x86_64为例)。正如在Magenta-Userboot中所描述,user的系统调用接口实现在libmagenta.so中。通过一些宏定义以及工具,生成了系统调用接口函数。后面假设编译后生成的目录是“build-magenta-pc-x86-64”。
在文件
build-magenta-pc-x86-64/gen/include/magenta/syscalls-x86-64.S
中定义了接口函数,形如:
...
m_syscall mx_futex_wait 42 3 0
m_syscall mx_futex_wake 43 2 1
...
其中m_syscall是个宏
.macro m_syscall name, num, nargs, public
...
.endm
它的参数“public”用来表示是否定义全局的函数名。以mx_futex_wake为例,展开后生成如下函数名:
hidden: SYSCALL_mx_futex_wake
_mx_futex_wake = SYSCALL_mx_futex_wake
weak: mx_futex_wake = SYSCALL_mx_futex_wake
hidden: VDSO_mx_futex_wake = SYSCALL_mx_futex_wake
而mx_futex_wait 只生成了:
hidden: SYSCALL_mx_futex_wait
为什么要做“public”的区分尼?我们来看看如下文件:
build-magenta-pc-x86-64/gen/include/magenta/syscall_vdso_wrappers.inc
文件的内容形如:
mx_status_t _mx_futex_wait(mx_futex_t* value_ptr, int current_value, mx_time_t deadline) {
mx_status_t ret;
do {
ret = SYSCALL_mx_futex_wait(value_ptr, current_value, deadline);
} while (unlikely(ret == MX_ERR_INTERRUPTED_RETRY));
return ret;
}
VDSO_INTERFACE_FUNCTION(mx_futex_wait);
宏VDSO_INTERFACE_FUNCTION生成了如下的函数名:
mx_futex_wait = _mx_futex_wait
VDSO_mx_futex_wait = _mx_futex_wait
从文件syscall_vdso_wrappers.inc来看,定义的函数都是形如:
do {
ret = XXX;
} while (unlikely(ret == MX_ERR_INTERRUPTED_RETRY));
可见,对于这一类的系统调用,如果返回值是MX_ERR_INTERRUPTED_RETRY,则会继续尝试发起系统调用。
由上分析可见,Magenta的系统调用retry,是在用户的vdso lib中实现的。
kernel系统调用的实现
在kernel层面,系统调用的总入口函数是x86_syscall,根据系统调用号,直接跳转到系统封调用表Lcall_wrapper_table总对应的系统调用函数。Lcall_wrapper_table是由宏 start_syscall_dispatch 和 syscall_dispatch来定义的,可参见文件:
build-magenta-pc-x86-64/gen/include/magenta/syscalls-kernel-branches.S
可见接口函数的定义形如:wrapper_\syscall。 比如 wrapper_futex_wait。这些wrapper函数定义在文件:
build-magenta-pc-x86-64/gen/include/magenta/syscalls-kernel-wrappers.inc
形如:
...
x86_64_syscall_result wrapper_futex_wait(mx_futex_t* value_ptr, int current_value, mx_time_t deadline, uint64_t ip) {
return do_syscall(MX_SYS_futex_wait, ip, &VDso::ValidSyscallPC::futex_wait, [&]() {
return static_cast<uint64_t>(sys_futex_wait(make_user_ptr(value_ptr), current_value, deadline));
});
}
...
do_syscall主要是做了权限检查,确认发起系统调用的是否是vdso代码。检查通过后,调用 sys_futex_wait。
Linux的系统调用retry实现
Linux的retry可以分为2大类:kernel处理和user处理。从系统调用的返回值也可见一斑。
#define ERESTARTSYS 512
#define ERESTARTNOINTR 513
#define ERESTARTNOHAND 514
以上的返回值由kernel负责处理,是不会返回给user的。所以对这些返回值有如下要求:
当设置返回值为以上的值时,一定要同步设置TIF_SIGPENDING,即指示signal pending,以便kernel可以进入signal处理函数。
在处理函数中,将用户空间的返回地址做了减2处理。系统调用是利用指令syscall进入kernel的,原来的用户返回地址是syscall后的地址;syscall指令占2个bytes,做了减2处理后,返回地址又指向了syscall指令。从而当返回到用户空间后,又会发起syscall进入系统调用流程。
可见Linux的retry方法和Magenta的不同。
而如下的返回值是返回给用户的,由用户程序决定是否retry还是直接abort。
#define EINTR 4
#define EAGAIN 11