System Calls [LKD 05]

8 篇文章 0 订阅

无论什么系统,都会向user space提供一些interface,用来和kernel系统交互,从而可以实现某些特定功能,比如访问硬件,获取系统资源等等。通过定义好的interface访问系统,有助于系统的稳定性。

Communicating with the Kernel


系统调用是位于用户态程序和kernel/hardware的中间层,这个中间层有三个目标:

1. 向user space提供统一的硬件抽象;

2. 保证系统的安全性和稳定性;

3. 可以实现向用户提供虚拟系统(虚拟内存,虚拟地址等等)。

系统调用是除了异常和陷入之外,唯一可以访问kernel的方式。

APIs, POSIX, and the C Library


应用程序都是基于API来编程,这个API是由上层的lib库提供,跟kernel提供的interface并没有直接关系。API的interface可以由多个kernel的system call实现,也可能没有使用system call。

Linux kernel和其他的操作系统类似,都是使用libc作为上层应用程序直接使用的接口,这些接口对system call做了封装。对于应用程序而言,它只需要关心libc的interface,不需要关心kernel的system call。

此外,要记住一个原则,kernel提供机制,而不是策略。也就说kernel提供了system call来实现某些特定的功能,但是这些system call如何使用取决于user  mode。

Syscalls


系统调用,是通过libc中已经定义好的function来实现的。这些function可能有0个,1个,或者多个参数,在执行时可能有1个或多个side effect(比如修改系统状态,或者写文件等)。系统调用提供了一个long类型的返回值,用来表明system call是否成功完成,如果出错了,就会返回error code(一般是负值),libc会把special error code写到全局变量errno中,这样用户态程序可以知道发生了错误。

定义一个系统调用,一般是如下这种格式:

SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current); // returns current->tgid
}

SYSCALL_DEFINE0是一个宏:

#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void)

以getpid为例,宏展开就是

asmlinkage long sys_getpid(void)
{
    return task_tgid_vnr(current); // returns current->tgid
}

其中asmlinkage是一个指令,用于告诉编译器在stack上查找函数的参数,system call都需要设置;其次,getpid返回值是一个long类型,是为了兼容32bit和64bit;再次,所有system call的实体都是sys_##name这种形式。

System Call Numbers

在Linux kernel中,每一个系统调用都有一个syscall number,这个number是唯一的值,可以用来指定一个系统调用。当用户态的程序执行系统调用时,是通过syscall number来指定要调用的system call,而不是名字。

这个syscall number非常重要,一旦指定就不能再更改;如果这个系统调用函数废弃不用,它对应的syscall number也不会被别的system call使用,相反的,kernel会为这个syscall number指定一个空的实现sys_ni_syscall,这样当用户态程序调用时,kernel就会返回-ENOSYS。

kernel保存了自己实现的所有system call,他们都用sys_call_table来存储和记录,不过这个数据结构是架构相关的,x86的位于arch/x86/entry/syscall_64.c:

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

似乎asm/syscalls_64.h是build kernel的时候生成的头文件,在clean的kernel code中没有找到这个头文件。

System Call Performance

没说啥有用的东西,就说Linux的系统调用执行的很快,主要原因是Linux的context switch很快,kernel简单,处理效率高,etc。

System Call Handler


kernel的地址空间不能被user space直接访问,所以系统调用函数不能像普通的函数调用一样直接被用户态调用。kernel的地址空间是被保护的,不能直接访问,如果用户态程序需要执行系统调用该怎么办呢?答案是发送signal,告诉kernel它需要执行系统调用,然后程序切换到kernel mode,代表原来的用户态程序在kernel中执行。

这种通过signal通知kernel的机制就是软件中断,当发生了异常,系统就会切换到kernel,然后执行exception handler。每个系统调用,其实就是exception handler。在x86平台上,这个软中断是128,也就是常见的int $0x80中断,当80中断产生,系统就切换到kernel,执行128这个异常向量,这个异常向量就是系统调用。

x86上后来实现了一个新的feature:sysenter,通过sysenter可以更快的实现系统调用。

基于kernel 4.15,看了一下系统调用的实现,主要涉及到的代码有:arch/x86/entry/entry_64.S,系统调用的入口应该是entry_SYSCALL_64,这个函数使用汇编实现,函数很大,核心的代码是:


ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
        ...
	call	do_syscall_64		/* returns with IRQs disabled */
        ...
	popq	%rdi
	popq	%rsp
	USERGS_SYSRET64
END(entry_SYSCALL_64)

汇编里调用了do_syscall_64:

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	/*
	 * NB: Native and x32 syscalls are dispatched from the same
	 * table.  The only functional difference is the x32 bit in
	 * regs->orig_ax, which changes the behavior of some syscalls.
	 */
	nr &= __SYSCALL_MASK;
	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);
		regs->ax = sys_call_table[nr](regs);
	}

	syscall_return_slowpath(regs);
}
#endif

这里直接通过sys_call_table加上nr,找到被调用的系统调用,然后把参数通过pt_regs传递给系统调用函数,并调用它,返回值记录在rax中。

Denoting the Correct System Call

这里将的是如何把syscall number传递给kernel,从而让kernel找到正确的系统调用,实现很简单,就是eax寄存器。在用户态trap进kernel之前,先把syscall number写到exa寄存器之中,然后再trap到kernel,这样kernel从eax寄存器中就可以找到正确的syscall number。

Parameter Passing

除了要传递syscall number,kernel还需要直到用户态程序调用系统调用是传递的参数,这些参数也是通过寄存器来传递的。系统调用最多支持6个寄存器,ebx, ecx, edx, esi和edi,按照顺序存储参数,系统调用的返回值也是通过寄存器传递,x86上使用eax传递返回值。

System Call Implementation


这本书这里讲个例子,如何添加自己的system call。这里不再赘述。

System Call Context


之前提到过,当kernel在执行系统调用时,是在process context中执行,current指针指向当前的task,也就是触发系统调用的进程描述符。在process context中,允许被block,sleep,从而可能会发生调度或者抢占。实际上,如果允许sleep,调度或者抢占,对kernel来说逻辑反而简单,不过要注意的是,系统调用中被调度或者抢占以后,新执行的process也可能调用同样的系统调用,所以这些系统调用要做好资源的保护。

当系统调用返回时,代码会切换到user space继续执行。

Final Steps in Binding a System Call

实现自己的系统调用,不再赘述。

Accessing the System Call from User-Space

一般而言,应用程序都通过glibc或者类似的C库来调用系统调用,但是如果是自己添加的系统调用,在glibc中是没有封装的,此时可以直接在应用程序中调用这个系统调用。

比如要调用long open(const char *filename, int flags, int mode),那么可以这么调用:

#define __NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)

首先得知道open函数的syscall number,这个可以查询头文件;然后syscall3中的3,用来告诉编译这个函数调用有几个参数,后面的参数,可以看到有很多,第一个long是返回值类型,第二个open是函数名字,第三个参数是open系统调用的第一个参数的类型,然后是参数的名字,在之后是open系统调用的第二个参数类型,和参数名字,后面是第三个参数类型和名字。

_syscall3这个宏在展开以后,就是汇编指令,这些汇编指令会把syscall number和其他的参数都放在寄存器中,然后触发软件中断或者trap,通知kernel。

#define __NR_foo 283
__syscall0(long, foo)

int main () {
    long stack_size;
    stack_size = foo ();
    printf (“The kernel stack size is %ld\n”, stack_size);
    return 0;
}

上面是一个简单的调用自己实现的系统调用的一个例子。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值