LINUX 源码解读:系统调用的设计与实现
总结:1. 内核不是进程(一定要理解,内核是通过硬件的能力来协调用户程序的)
2. 系统调用,可以理解为一个中断处理函数,系统调用的发生就是通过软件中断实现的
如果想了解其具体的实现过程,可以阅读下面的原文
介绍
系统调用是操作系统内核提供的,为了和用户空间上运行的进程进行交互的一组接
口,通过该接口,应用程序可以访问硬件设备和其他操作系统资源。
系统调用主要有三个作用:
a. 为用户空间提供一种硬件的抽象接口。
b. 保证了系统的稳定与安全。
c. 实现多任务和虚拟内存。
对于用户空间的进程,在一般情况下是通过应用编程接口(API)而不是系统调用
来进行编程,有些 API 往往直接封装了系统调用,但这并不意味着两者是一一对应的。
当前最流行的 API 是基于 POSIX 标准的。
原理
本节通过对 getpid()的跟踪,来由外向内的了解 linux 系统调用的原理。
库函数
首先,用户进程调用 glibc 中的 getpid()函数,这个函数在 include/unistd.h 中声明,
其函数原型为:
由于其实现是平台相关的,我们关注的实现在
/glibc/nptl/sysdeps/unix/sysv/linux/getpid.c 中。
extern __pid_t __getpid (void);
其中引发系统调用的是 INTERNAL_SYSCALL 宏,定义在
/glibc/nptl/sysdeps/unix/sysv/linux/i386/sysdep.h 中。
这一段主要是通过汇编来完成的,高亮的两句为重点,前一句将系统调用号放入
eax 寄存器,后一句进行一个中断号为 0x80 的软中断,这里的中断处理程序正是
系统调用处理程序。
系统调用处理程序
这个系统调用处理程序 system_call 定义在/arch/x86/kernel/entry_32.S 中。
首先给出一些宏的说明。
# define INTERNAL_SYSCALL(name, err, nr, args...) \
({ \
register unsigned int resultvar; \
EXTRAVAR_##nr \
asm volatile ( \
LOADARGS_##nr \
"movl %1, %%eax\n\t" \
"int $0x80\n\t" \
RESTOREARGS_##nr \
: "=a" (resultvar) \
: "i" (__NR_##name) ASMFMT_##nr(args) : "memory", "cc"); \
(int) resultvar; })
pid_t
__getpid (void)
{
#ifdef NOT_IN_libc
INTERNAL_SYSCALL_DECL (err);
pid_t result = INTERNAL_SYSCALL (getpid, err, 0);
#else
pid_t result = THREAD_GETMEM (THREAD_SELF, pid);
if (__builtin_expect (result <= 0, 0))
result = really_getpid (result);
#endif
return result;
}
系统调用例程:
#define CFI_STARTPROC .cfi_startproc
//用在每个函数的开始,用于初始化一些内部数据结构
#define CFI_ENDPROC .cfi_endproc
//在函数结束的时候使用与.cfi_startproc 相配套使用
#define CFI_DEF_CFA .cfi_def_cfa //定义计算 CFA 的规则
#define CFI_DEF_CFA_REGISTER .cfi_def_cfa_register
//xx reg ,offset reg 中的值保存在 offset 中,offset 是 CFA 的
#define CFI_DEF_CFA_OFFSET .cfi_def_cfa_offset
//xx offset 修改计算 CFA 的规则,reg 中的值不发生变化,之改变 offset
#define CFI_ADJUST_CFA_OFFSET .cfi_adjust_cfa_offset
//与上面相似但是修改前面一个 offset
#define CFI_OFFSET .cfi_offset
//xx reg ,offset reg 中的值保存在 offset 中,offset 是 CFA 的
#define CFI_REL_OFFSET .cfi_rel_offset
#define CFI_REGISTER .cfi_register
#define CFI_RESTORE .cfi_restore
#define CFI_REMEMBER_STATE .cfi_remember_state
#define CFI_RESTORE_STATE .cfi_restore_state
#define CFI_UNDEFINED .cfi_undefined
系统调用结束例程:
# system call handler stub
ENTRY(system_call)
RING0_INT_FRAME. # can’t unwind into user space anyway
pushl %eax # save orig_eax //保存原来的 eax 中的值
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL //保存系统寄存器信息
GET_THREAD_INFO(%ebp) //获取 thread_info 结构的地址
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
//检测 thread_info 中的相关标志看是否有系统跟踪
jnz syscall_trace_entry
//有系统跟踪则先执行系统跟踪的代码,然后转跳回来(这在 syscall_trace_entry 中有写的)
cmpl $(nr_syscalls), %eax
//比较请求的系统调用号和最大系统调用号(验证系统调用号是否有效)
jae syscall_badsys
//如果请求系统调用号无效则退出
syscall_call:
call *sys_call_table(,%eax,4)
//跳转到系统调用表中,系统调用表是 4 字节对齐。
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
//用于调试使用,只有在开启调试的时候才会去检测系统调用深度
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don’t miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
//#define ENABLE_INTERRUPTS(x) sti
//#define DISABLE_INTERRUPTS(x) cli
TRACE_IRQS_OFF //关闭中断跟踪
movl TI_flags(%ebp), %ecx //
testl $_TIF_ALLWORK_MASK, %ecx # current->work
//检测是否可以返回用户空间
jne syscall_exit_work
在系统调用处理程序中,在进行了一些初始化之后(初始化内部数据结构,检
查系统调用号是否在范围之内),便开始进行 syscall_call 例程,此例程主要便
是根据存放在 eax 中的系统调用号到系统调用表 sys_call_table 找到相应的系统
调用函数并执行,然后由于系统调用函数的返回值也将通过 eax 传回,所以需
要将返回值保存下来。
另外,在系统调用结束,返回用户空间的时候,系统应当检查当前进程的
need_resched 标志,如果此标志设了,系统将调用 schedule()进行调度,这一
部分便是在 resume_userspace 中做的。
系统调用表
# perform work that needs to be done immediately before resumption
ALIGN
RING0_PTREGS_FRAME # can't unwind into user space anyway
work_pending:
testb $_TIF_NEED_RESCHED, %cl
jz work_notifysig
work_resched:
call schedule
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx
jz work_pending
TRACE_IRQS_ON //开启系统中断跟踪
ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call
# schedule() instead
//允许中断
movl %esp, %eax
call syscall_trace_leave
jmp resume_userspace
END(syscall_exit_work)
在最新的 2.6.39 内核中,系统调用表是在/arch/x86/kernel/syscall_table_32.S
中定义的。系统调用表规定了每个系统调用的调用函数,并且暗示了其系统调
用号。
同时在/arch/x86/include/asm/unistd_32.h 中定义了所有的系统调用号。
值得一提的是,对于 64 位的机器,linux 提供了一个更好的实现。
/arch/x86/kernel/syscall_64.c
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
……
#define __NR_getgid 47
……
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for
restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
…….
.long sys_getpid /* 20 */
……
/arch/x86/include/asm/unistd_64.h
/* System call table for x86-64. */
#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <asm/asm-offsets.h>
#define __NO_STUBS
#define __SYSCALL(nr, sym) extern asmlinkage void sym(void) ;
#undef _ASM_X86_UNISTD_64_H
#include <asm/unistd_64.h>
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = sym,
#undef _ASM_X86_UNISTD_64_H
typedef void (*sys_call_ptr_t)(void);
extern void sys_ni_syscall(void);
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
*Smells like a like a compiler bug -- it doesn't work
*when the & below is removed.
*/
[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/unistd_64.h>
};
这样一来,我们若要改动系统调用的话就在 unistd_64.h 里面做就可以了。
系统调用函数
系统调用函数便是系统调用处理程序引导运行的函数,以 getpid()系统调用为
例便是 sys_getpid,但是在 linux 源码中并不存在任何对 sys_getpid 这个函数的
直接定义,事实上该函数的定义是通过宏 SYSCALL_DEFINE0 来做的,该宏的定
义位于/include/linux/syscalls.h 中。
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H
#ifndef __SYSCALL
#define __SYSCALL(a, b)
#endif
/*
* This file contains the system call numbers.
* Note: holes are not allowed.
*/
/* at least 8 syscall per cacheline */
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4
__SYSCALL(__NR_stat, sys_newstat)
……
#define __NR_getpid 39
__SYSCALL(__NR_getpid, sys_getpid)
……
由此我们可以看出为什么系统调用函数同一的为“sys_“开头。在 syscall.h 中
SYSCALL_DEFINEx 系列(x=1..6),这是用来处理带参系统调用的。
用 SYSCALL_DEFINE0 定义的 sys_getpid 位于/kernel/timer.c 中。
实现
在了解了系统调用的原理之后,实现一个系统调用就相对简单了。主要有以下几
步
a) 在内核源码树中实现系统调用函数。
注意:函数定义的位置要选好,可以将其写入到/kernel/中的某个 c 文件中,
也可以自己新建一个,但这样就要注意修改 Makefile。
可以直接定义 sys_mysyscall(),也可以通过 SYSCALL_DEFINE0(mysyscall)宏来实
现,若直接定义的话不要忘了加上 asmlinkage 限定词。
b) 在/include/linux/syscalls.h 中加入对 sys_mysyscall()的声明。
c) 在/arch/x86/include/asm/unistd_32.h 和
/arch/x86/kernel/syscall_table_32.S 的末尾分别添加 mysyscall 的系统调用表
项和系统调用号,并将 unistd_32.h 中 NR_syscalls 的值加一。
注意:这是根据 x86 的 32 位机来说的,不同的平台可能稍有不同(比如 x86 64
位机上只需修改/arch/x86/include/asm/unistd_64.h)。
系统调用表项和系统调用号要对应。
d) 重新编译加载内核。
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
#define SYSCALL_DEFINE0(sname) \
SYSCALL_TRACE_ENTER_EVENT(_##sname); \
SYSCALL_TRACE_EXIT_EVENT(_##sname); \
static const struct syscall_metadata __used \
__attribute__((__aligned__(4))) \
__attribute__((section("__syscalls_metadata"))) \
__syscall_meta__##sname = { \
.name = "sys_"#sname, \
.nb_args = 0, \
.enter_event = &event_enter__##sname, \
.exit_event = &event_exit__##sname, \
}; \
asmlinkage long sys_##sname(void)
e) 从用户空间访问系统调用。
由于是“野生的“系统调用,所以 glibc 肯定不会提供 API 支持,但是我们可
以直接对系统调用进行访问。 老版本的 linux 中提供了一组宏_syscall(),其中 n
从 0 到 6,代表需要传递给系统调用的参数个数。
但在新版本中这个宏没有了,但是我们可以把这些宏原来的定义直接包括进程
序中,效果还是一样的。
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-MAX_ERRNO)) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
/* XXX - _foo needs to be __foo, while __NR_bar could be _NR_bar. */
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
__syscall_return(type,__res); \
}
#define _NR_mysyscall 250
_syscall0(int, mysyscall)
Int main()
{
mysyscall();
return 0;
}
#define _NR_mysyscall 250
_syscall0(int, mysyscall)
Int main()
{
mysyscall();
return 0;
}