参考文件:
arch/mips/kernel/scall32-o32.S
sys_call_table定义了系统调用的函数列表。在这个表中反复引用sys这个汇编宏定义二元组。
.macro sys function, nargs
PTR /function
LONG (/nargs << 2) - (5 << 2)
.endm
这个表相当于一个二元组列表(或二维数组)。二元组的第一个是系统调用的函数地址,第二个是系统调用的参数。
为什么参数个数要列在这里?
因为参数的多少决定了在进入系统调用前要做的必要处理。
在MIPS ABI规范中,少于5个参数的函数调用,参数通过寄存器来传递,超出部分将通过栈来传递。因此系统调用首要的任务是要判断是否有超出的参数在用户栈中。
handle_sys 函数是系统调用的通用入口。
1)what?
handle_sys做了什么工作:
subu v0, v0, __NR_O32_Linux //从v0寄存器读入系统调用号,减去__NR_O32_Linux 得到在linux系统调用列表中的个数偏移。
sll t0, v0, 3 // t0=v0<<3; 得到在linux系统调用列表中的地址偏移。系统调用列表每个二元组的大小是8字节。
la t1, sys_call_table // t1 <-- 系统调用列表的地址
addu t1, t0 // t1 <-- 当前请求的系统调用二元组地址。
lw t2, (t1) # syscall routine
lw t3, 4(t1) # >= 0 if we need stack arguments
// t2是当前请求的系统调用函数地址,
// t3是(当前请求的系统调用函数参数个数 - 5),参见sys宏的定义
bgez t3, stackargs // 如果t3>=0(即:参数>=5),跳转stackargs去处理。
//stackargs 完成:从用户栈将其余参数copy到内核栈。
jalr t2 // 调用用户请求的系统调用函数,完毕后返回。
剩下的工作:检查返回值,根据需要设置错误标志,准备从内核返回到用户。
2)where?
arch/mips/kernel/traps.c函数:
set_except_vector(8, handle_sys);用于向CPU注册系统调用的统一入口。
8是系统调用的异常向量号(待考)
3)how?
以getpid()系统调用为例,用户API以uClibc-0.9.31库为例,
参考uClibc/libc/sysdeps/linux/common/getpid.c
其中,
uClibc/libc/sysdeps/linux/common/bits/syscalls-common.h
uClibc/libc/sysdeps/linux/mips/bits/syscalls.h
是两个很重要的文件,定义所有系统调用用户层API的框架。大多数系统调用只需用根据这些宏填写函数名,参数个数就可以了。
比如我要重新写一个getpid的函数为my_getpid,只需用如下两句就可以了。
#define __NR_my_getpid 4020
_syscall0(pid_t, my_getpid)
这个my_getpid 和标准的 getpid 是一模一样的。
仔细阅读syscalls.h,可以注意到:
系统调用号是作为第0个参数,即v0传入的。
从internal_syscall5其,才开始有栈的操作:
"subu/t$29, 32/n/t" / 栈指针(SP)寄存器减32
"sw/t%6, 16($29)/n/t" / 将参数arg5压入栈(新的栈指针+16偏移处)
syscall是系统调用的汇编指令,当该指令执行到时,CPU从用户态进入核心态,也即发生了自陷(trap)
MIPS CPU根据在启动时注册的trap入口,和此时的trap类别,找到linux内核的系统调用统一入口,于是程序指令指针(PC寄存器)被赋予handle_sys的地址。
在程序员看来,就是handle_sys开始执行了。
其他参考
/opt/sigma-design/mips-4.3/mips-linux-gnu/libc/usr/include/asm/unistd.h
linux-2.6.22.19/include/asm/unistd.h
asm/unistd.h:#define __NR_O32_Linux 4000
asm/unistd.h:#define __NR_O32_Linux_syscalls 319
__NR_O32_Linux 第一个syscall的号,也即,前4000个是reserved的。按MIPS ABI32
__NR_O32_Linux_syscalls 最后一个syscall的偏移,也即总共有320个syscall
从上文可以看出,重写一个getpid系统调用非常容易。
同理:增加一个系统调用也很容易。
你要做的工作其实相当的少:
linux内核:
1)unistd.h中增加一个系统调用号,并且__NR_Linux_syscalls要加1.
2)在linux 内核中增加一个系统调用函数。仿照其他的函数来写,一般用C就可以。
3)在sys_call_table表末尾,添加系统调用函数的入口地址、参数个数。
用户库:
根据你的参数个数,选择_syscall0 ~ _syscall6来写你的系统调用函数。
注意,这里的系统调用号应该和内核保持一致。
参考:
butterfly/infra/misc/syscalls/kcall.c
我们的初衷是要运用内核里面的某些功能,如:调用函数,获取状态,设置变量等等。
为什么要增加一个系统调用?其实还有一个更标准的选择就是写一个基于文件系统I/O的驱动。
如果按照文件系统IO的方式,我们的用户层该做的是:
通过设备文件名,调用open来打开一个特殊设备
通过文件描述符,调用read、write、ioctl来读、写、控制这个设备文件。
当我们不再用它的时候,调用close来关闭。
当然,你应该清楚的是:open、read、write、close其实都还是系统调用。
对于特别的CPU、设备;他们都不具有通用性。
一个更标准、一个更高效;你会如何选择呢?