linux内核之系统调用
1、系统调用简介
系统调用在用户空间进程和硬件设备之间添加的一个中间层。主要有三个作用:
A.它为用户空间提供了一种硬件的抽象接口
B.隔离用户态和内核态,保证系统的稳定和安全
C.每个进程都运行在虚拟地址,实现进程独立和方便虚拟内存管理
在linux系统中,系统调用是用户空间主动访问内核的唯一方法;除异常和中断外,是内核唯一的合法入口。下图是简单的调用流程(以getpid为例):
A. 每个系统调用都有一个调用号(getpid=20),目前,内核已经有383个系统调用(部分废除)
B.通过swi指令产生软中断进入内核态,后面的调用号存放在swi指令的低24位(即最大支持16777215个系统调用号)
C.每个系统调用参数一般不能超过6个。
系统调用命名规则:在用户空间定义成getpid则内核加前缀编程sys_getpid(SYSCALL_DEFINE宏定义),展开后的定义如下:
asmlinkage long sys_getpid(void),asmlinkage一个编译指令,通知编译器仅从栈中提取该函数的参数。
#define SYSCALL_DEFINE0(sname)
#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__)
宏定义后面的0~6表示系统调用的函数参数个数
Linux系统调用比其他许多操作系统执行的要快,linux很短的上下文切换时间是一个重要的原因,进出内核都被优化的简介高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁。
2、系统调用服务例程
上一节已经提到系统调用通过软中断实现:通过swi产生一个软中断促使系统切换到内核态去执行中断处理程序(vector_swi)。
(1)swi指令
指令格式如下:SWI {cond} immed_24
Cond域:是可选的条件码 (参见 ARM汇编指令条件执行详解).
immed_24域:24位立即数(即0-16777215),立即数作用:区分用户不同操作,执行不同内核函数。如果用户程序调用系统调用时传递参数,根据ATPCSC语言与汇编混合编程规则将参数放入R0~R4即可。
使用SWI指令时,通常使用以下两种方法进行传递参数,SWI异常中断处理程序要通过读取引起软件中断的SWI指令,以取得24为立即数。
A.指令中的24位立即数指定了用户请求的服务类型,参数通过通用寄存器传递.
mov r0,#34 ;设置子功能号位34
SWI 12 ;调用12号软中断
B.指令中的24位立即数被忽略,用户请求的服务类型有寄存器R0的值决定,参数通过其他的通用寄存器传递.
movr0,#12 ;调用12号软中断
movr1,#34 ;设置子功能号位34
SWI 0
在SWI异常中断处理程序中,取出SWI立即数的步骤为:首先确定引起软中断的SWI指令是ARM指令还是Thunb指令,这可通过对SPSR访问得到;然后取得该SWI指令的地址,这可通过访问LR寄存器得到;接着读出指令,分解出立即数。
(2)OABI和EABI
CONFIG_OABI_COMPAT代表old ABI:通过跟随在swi指令中的调用号来进行。
swi(#num | 0x900000)
CONFIG_AEABI代表新的方式为EABI,根据r7的值
mov r7, #num
swi0
两种宏可以同时配置,可以不配,也可以配置任何一种。以old ABI方式的系统调用会执行sys_oabi_call_table表中的系统调用函数,EABI方式的系统调用会用sys_call_table中的函数指针。因此配置存在一下4中情况:
第一种两个宏都配置,行为就是上面说的
第二种只配置了CONFIG_OABI_COMPAT,那么以old ABI方式调用的会用sys_oabi_call_table,以EABI方式调用的用sys_call_table,和情况1实质相同,只是情况1更加明确
第三种只配置CONFIG_AEABI系统中不存在sys_oabi_call_table,对于old ABI方式调用不兼容,只能以EABI方式调用,用sys_call_table
第四种两个都没有配置,系统默认会只允许old ABI方式,但是不存在sys_oabi_call_table,最终会通过sys_call_table完成函数调用
(3)源码分析
源码目录:arch/arm/kernel/entry-common.S。该中断处理程序在《linux内核之进程详解》一文中讲解过部分,这里主要简介系统调用部分。
.align 5
ENTRY(vector_swi) //中断服务例程
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
THUMB( mov r8, sp )
THUMB( store_user_sp_lr r8, r10, S_SP ) @calling sp, lr
mrs r8, spsr @ calledfrom non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Savecalling PC
str r8, [sp, #S_PSR] @ SaveCPSR
str r0, [sp, #S_OLD_R0] @ SaveOLD_R0
zero_fp
#ifdefCONFIG_ALIGNMENT_TRAP
ldr ip, __cr_alignment
ldr ip, [ip]
mcr p15, 0, ip, c1, c0 @ update control register
#endif
enable_irq
ct_user_exit
get_thread_info tsk
//获取系统调用号
#ifdefined(CONFIG_OABI_COMPAT)
//查看swi指令值判断是EABI还是old ABI
#ifdefCONFIG_ARM_THUMB
tst r8, #PSR_T_BIT
movne r10, #0 @ no thumbOABI emulation
USER( ldreq r10, [lr, #-4] ) //获取swi指令
#else
USER( ldr r10, [lr, #-4] ) //获取swi指令
#endif
#elif defined(CONFIG_AEABI)
//如果只定义了AEABI,则系统调用号存放在r7
#elif defined(CONFIG_ARM_THUMB)
/* Legacy ABI only, possibly thumb mode. */
tst r8, #PSR_T_BIT @ thisis SPSR from save_user_regs
addne scno, r7,#__NR_SYSCALL_BASE @ put OS number in
USER( ldreq scno, [lr, #-4] )
#else
/* Legacy ABI only. */
USER( ldr scno, [lr, #-4] ) @ get SWI instruction
#endif
adr tbl,sys_call_table //加载EABI方式的系统调用表
#if defined(CONFIG_OABI_COMPAT)
如果swi低24位参数为0,则是EABI方式
如果不为0,则计算并存放系统调用号到scno中,同时获取系统调用表sys_oabi_call_table
bics r10, r10, #0xff000000 //获取swi低24位立即数
eorne scno, r10,#__NR_OABI_SYSCALL_BASE //去除幻数0x900000
ldrne tbl, =sys_oabi_call_table //获取old ABI的系统调用表
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000 @mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endif
local_restart:
/*查看是否需要跟踪系统调用*/
ldr r10, [tsk,#TI_FLAGS] @ check for syscalltracing
stmdb sp!, {r4, r5} @ push fifth and sixth args
tst r10, #_TIF_SYSCALL_WORK @are we tracing syscalls?
bne __sys_trace
cmpscno, #NR_syscalls //判读系统调用号是否超出总系统调用号
adr lr, BSYM(ret_fast_syscall) //系统调用返回地址
ldrcc pc, [tbl, scno, lsl #2] //调用相应的系统调用sys_*
//下面是对系统调用号异常处理(未识别,或者系统调用使用默认处理方式)
add r1, sp, #S_OFF
2: mov why, #0 @ no longer a realsyscall
cmp scno, #(__ARM_NR_BASE -__NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back
bcs arm_syscall
b sys_ni_syscall @ notprivate func
#ifdefined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
/*
* We failed to handle a fault trying toaccess the page
* containing the swi instruction, butwe're not really in a
* position to return -EFAULT. Instead,return back to the
* instruction and re-enter the user faulthandling path trying
* to page it in. This will likely resultin sending SEGV to the
* current task.
*/
9001:
sub lr, lr, #4
str lr, [sp, #S_PC]
b ret_fast_syscall
#endif
ENDPROC(vector_swi)
3、系统调用实现
一个linux的系统调用在实现时不需要太关心它和系统调用处理程序之间的关系。添加一个新的系统调用是件相对容易的工作,怎样设计和实现一个系统调用处理程序是难题所在,基本原则如下:
A.每个系统调用都应该有一个明确的用途
B.系统调用的接口应该力求简洁、参数尽可能少,明确返回值和错误码。
C.必须仔细检查所有的参数,通过copy_to_user和copy_from_user来实现用户和内核数据拷贝
D.检查操作的合法权限
E.系统调用必需是可以重入的。
实现系统调用基本步骤:
A.arch/arm/kernel/calls.S的系统调用表最后添加如表项
CALL(sys_seccomp)
CALL(sys_mysyscall)
B.arch/arm/include/uapi/asm/unistd.h添加调用号
#define__NR_seccomp (__NR_SYSCALL_BASE+383)
#define __NR_mysyscall (__NR_SYSCALL_BASE+384)
arch/arm/include/asm/unistd.h总系统调用数:
#define __NR_syscalls (388)
这里为啥是+4,而不是加1,是因为calls.S中会自动填充系统调用表
C.在源文件中实现系统调用处理程序
SYSCALL_DEFINE0(mysyscall)
{
printk("test syscall\n");
return THREAD_SIZE;
}
D.include/linux/syscalls.h申明函数
asmlinkage long sys_ mysyscall (void);
E.用户态使用
[email protected]:~/test$vi mysyscall.c
#include<stdio.h>
#include<unistd.h>
#include<sys/syscall.h>
//#define__NR_mysyscall 384
//__syscall0(long,mysyscall)
intmain()
{
long stack_size;
stack_size = syscall(384);
printf("the kernel stack size: %#x\n",stack_size);
return 0;
}