摘要:解释了什么是系统调用,分析了系统调用的具体流程,在内核中实现一个自己定义的系统调用。
一、什么是系统调用
Linux被分为内核空间和用户空间,我们在用户空间使用open,read,write这些操作的时候,会调用内核空间的系统调用函数,因为内核是受到保护的,所以我们的应用程序是以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。这种通知内核的机制就叫做“软中断”。在arm当中,是通过“swi”软中断这个指令实现的。
在这个机制当中,因为我们有很多系统调用,所以每个系统调用都被赋予一个系统调用号,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,通过swi这个指令传入一个参数,也就是系统调用号,这个系统调用号就被用来指明到底是要执行哪个系统调用,内核接收到这个调用号的时候,就会去执行。
arm中,在产生“swi”软中断的时候,进程会通知内核,我们把调用号放在了r7寄存器里面,然后内核去读取r7当中的系统调用号,再根据一个调用table去找到这个号对应的系统调用函数执行,这样就完成了一次系统调用。
二、分析内核源码中系统调用机制
主要是以下两个文件实现:
1.entry-common.S
当用户空间进行系统调用,产生“swi”中断,这时候entry-common.S当中,在第187行:
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_lrr8, r10, S_SP ) @ calling sp, lr
mrs r8, spsr @called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp
会对swi进行响应,因为响应之后,我们要切换到内核去执行系统调用,所以这里先对当前用户态执行的一些寄存器和状态进行堆栈操作,保存r0到r12的值,保存PC,CPSR,和OLD_R0。然后下面从第226行到第249行,会根据不同的情况进行具体的操作,我们这里是从r7当中获取系统调用号:
/*
* Pure EABI user space always put syscall number into scno(r7).
*/
A710( ldr ip, [lr, #-4] @get SWI instruction )
A710( and ip, ip, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#elifdefined(CONFIG_ARM_THUMB)
/* Legacy ABI only, possibly thumb mode. */
tst r8,#PSR_T_BIT @ this is SPSR fromsave_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno,[lr, #-4]
#else
/* Legacy ABI only. */
ldr scno,[lr, #-4] @ get SWI instruction
A710( and ip, scno, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#endif
然后在259行,载入了我们的系统调用表:
adr tbl, sys_call_table @ load syscall table pointer
这里sys_call_table在第470行:
.type sys_oabi_call_table, #object
ENTRY(sys_oabi_call_table)
#include "calls.S"
这时候指向了我们的calls.S文件。
2.calls.S
是由各个系统调用函数函数指针组成的表格。
/*0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork_wrapper)
CALL(sys_read)
CALL(sys_write)
/* 5*/ CALL(sys_open)
CALL(sys_close)
.........
/*355 */ CALL(sys_signalfd4)
CALL(sys_eventfd2)
CALL(sys_epoll_create1)
CALL(sys_dup3)
CALL(sys_pipe2)
/* 360*/ CALL(sys_inotify_init1)
CALL(sys_preadv)
CALL(sys_pwritev)
CALL(sys_rt_tgsigqueueinfo)
CALL(sys_perf_event_open)
这里面都是系统调用函数,我们有了调用号作为偏移地址,有了表格的基地址,那么就可以找到对应的系统调用。
3.unist.h
这里面也包含了各个系统调用的系统调用号,和calls.S是一致的。
#define__NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define__NR_exit (__NR_SYSCALL_BASE+ 1)
#define__NR_fork (__NR_SYSCALL_BASE+ 2)
#define__NR_read (__NR_SYSCALL_BASE+ 3)
.. . . . .
#define__NR_preadv (__NR_SYSCALL_BASE+361)
#define__NR_pwritev (__NR_SYSCALL_BASE+362)
#define__NR_rt_tgsigqueueinfo (__NR_SYSCALL_BASE+363)
#define__NR_perf_event_open (__NR_SYSCALL_BASE+364)
三、在内核中实现自己的系统调用
目的:实现系统调用以后,打印一行信息。
第一步:修改/kernel/printk.c
在中后空闲位置插入如下系统调用函数实现体:
void sys_pk(void)
{
printk("this is a sys call!\n");
}
第二步:修改/arch/arm/kernel/calls.S
在接近末尾处:
CALL(sys_perf_event_open)
CALL(sys_pk)
第三步:修改/arch/arm/include/asm/unistd.h
也是在末尾处:
#define__NR_perf_event_open (__NR_SYSCALL_BASE+364)
#define __NR_pk (__NR_SYSCALL_BASE+365)
第四步:编译内核
#make clean
#make zImage然后转化成uImage
第五步:编写应用程序syscall.c调用系统调用
因为实现系统调用要使用swi指令,这里涉及到硬件操作,所以需要内嵌汇编代码,代码如下:
void pk()
{
__asm__ (
"ldr r7,=365 \n"
"swi \n"
:
:
:"memory");
}
void main()
{
pk();
}
这里ldr r7 , =365是把我们刚才指定的系统调用号传进去,然后产生swi软中断,最后这三行,大神解释说是内嵌汇编代码需要真么写,表示输入,输出,和破坏描述。
编写完了之后执行:
#arm-linux-gccsyscall.c -o syscall
第六步:下载测试
将刚才编译好的内核下载到开发板,然后通过nfs起根文件系统,将生成的syscall拷贝到共享目录下,执行:
# ./syscall
我的开发板输出信息如下:
/ # ./syscall
this is a sys call!
/ #
表示实验成功,这篇帖子就写到这里吧,主要是对系统调用的理解和具体的实现流程,要想真正实现一个系统调用要做的工作其实不止这么简单,如有不正确的地方还请指出,大家共同进步。