系统调用

系统调用是用户空间访问内核的唯一手段;除了异常和陷入外,他们是内核唯一的合法入口。

系统调用主要是为用户空间提供一种硬件的抽象接口,保证系统的稳定与安全,为应用程序实现多任务和虚拟内存提供接口。要访问系统调用,通常通过C库中定义的函数调用来进行。大多数系统调用都会产生某种副作用(使系统的状态发生某种变化)。

getpid()系统调用的内核实现为:SYSCALL_DEFINE0(getpid) { return task_tgid_vnr(current);}

而只是一个宏,定义了一个无参数的系统调用,展开后的代码如下:

#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void);			\
	ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);		\
	asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

getpid()展开后代码为:asmlinkage long sys_getpid(void),其中asmlinkage是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。

系统调用号

在linux中,每个系统调用都被赋予一个独一无二的系统调用号。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用:进程不会提及调用的名称。

如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。Linux有一个未实现系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,这个错误号就是专门针对无效系统调用而设的。虽然很罕见,但如果一个系统调用被删除,或者变得不可用,这个函数就要负责“填补空缺”。

为什么Linux的系统调用比其他许多操作系统执行得快?

1、Linux的上下文切换时间很短,进入内核被优化得很简洁高效;2、系统调用程序和每个系统调用本身也很简洁。

系统调用处理程序

        用户空间的程序无法直接执行内核代码。应用程序应该以某种方式通知系统,告诉内核自己需要一个系统调用,希望系统切换到内核态,这个内核就可以代表应用程序在内核空间执行系统调用。

        通知内核的机制靠软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。

        所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,预定义的软中断是中断号128,通过int$0x80指令触发该中断。该指令会触发一个异常导致系统切换到内核态并执行128号异常处理程序(system_call(),系统调用处理程序)。系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间把相应系统调用所对应的号放入eax中。这样系统调用处理程序就能从中得到数据。system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果大于或等于NR_syscalls,而函数返回-ENOSYS,否则就执行相应系统调用:

call *sys_call_table(,%rax, 8)

        由于系统调用表中的表项是以64位存放,所以内核需要将给定的系统调用号乘8,然后用所得到的结果在该表中查询其位置.如图:

 在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放系统调用的前五个参数。需要六个或六个以上参数的情况不多见,此时应该用一个单独的寄存器存放指向所有参数在用户空间地址的指针

 系统调用的实现

        内核必须保证:1、指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。2、指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。3、如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存应被标记为可执行;进程决不能绕过内存访问限制。

        内核提供copy_to_user()向用户空间写入数据,其第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,第三个是需要拷贝的数据长度(字节数)。

        内核提供copy_from_user()向用户空间写入数据,其第一个参数是内核空间内的源地址,第二个是进程空间中的目的内存地址,第三个是需要拷贝的数据长度(字节数)。

        进程最后一项检查针对是否有合法权限。新的系统允许检查针对特定资源的特殊权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作。如,capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。

在<linux/capability.h>包含一份所有权能和其对应的权限列表。

系统调用上下文

        内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行。

当编写完一个系统调用的时候,需要把它注册成一个正式的系统调用:

1、在系统调用表的最后加入一个。2、将系统调用号定义到<asm/unistd.h>中。3、系统调用必须被编译进内核映像。这只要把它放进kernel下的一个文件中即可,比如sys.c。系统调用表形式如下:

 可以把新的系统调用加到表末:.long sys_foo

将系统调用号加入到<asm/unistd.h>中,格式如下:

在列表中加入#define __NR_foo 338

最后将系统调用foo()编译到核心的内核映像中,可放在kernel/sys.c文件中

 从用户空间访问系统调用

 Linux提供了一组宏,用于直接对系统调用进行访问。这些宏是_syscalln(),其中n取值范围是[0,6],代表传递给系统调用的参数个数。举个例子,open()系统调用的定义是:

long open(const char *filename, int flags, int mode)

不靠库支持,直接调用此系统调用的宏的形式为:

#define NR_open 5

_syscall3(long, open, const char*, filename, int, flags , int, mode),这样应用程序就可以直接使用open()。对于每个宏来说都有2+2xn个参数。_NR_open在<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数;由汇编语言执行前面内容中所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中即可。

 创建一个系统调用的好处:

1、系统调用创建容易且使用方便;2、Linux系统调用的高性能显而易见

问题:

1、需要一个系统调用号,需要内核处于开发版本时官方分配;

2、系统调用被加入稳定内核后就被固化了;

3、需要将系统调用分别注册到每个需要支持的体系结构中;

4、脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用;

5、如果仅是简单的信息交换,系统调用是大材小用;

6、在内核树之外很难维护和使用系统调用。

SYSCALL_DEFINEx与syscall ? 

SYSCALL_DEFINEx是系统调用的定义,如getpid()的现实为:

SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);    
}

其中x代表参数的个数,系统调用的参数个数决定其定义的SYSCALL_DEFINEx,而SYSCALL_DEFINEx的第一个参数为系统调用的名称。如mkdir有两个参数,SYSCALL_DEFINE2就存在5个参数。

SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
	return do_mkdirat(AT_FDCWD, pathname, mode);
}

 那么 SYSCALL_DEFINE2相关的定义如下:   

#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void);			\
	ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);		\
	asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

#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__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)

/*
 * The asmlinkage stub is aliased to a function named __se_sys_*() which
 * sign-extends 32-bit ints to longs whenever needed. The actual work is
 * done within __do_sys_*().
 */
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage 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;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

 其中__MAP(x, __SC_DECL, __VA_ARGS__)定义如下,即将__MAP(n, m, t1, a1, t2, a2, ..., tn, an)转换成 m(t1, a1), m(t2, a2), ..., m(tn, an)。

#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

#define __SC_DECL(t, a)	t a
#define __TYPE_AS(t, v)	__same_type((__force t)0, v)
#define __TYPE_IS_L(t)	(__TYPE_AS(t, 0L))
#define __TYPE_IS_UL(t)	(__TYPE_AS(t, 0UL))
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a)	(__force t) a
#define __SC_ARGS(t, a)	a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐