系统调用(二)
(五):系统调用的实现
1:实现系统调用
实现一个系统调用就是考虑他的用途,每一个系统调用都有一个确定的用途,在Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)。
2:参数验证
系统调用必须仔细检查他们所有的参数是否合法有效。最重要的一项检查就是检查用户提供的指针是否有效。
在接收一个用户空间的指针之前,内核必须保证:
1:指针指向的内存区域属于用户空间,进程决不能洪骗内核去读内核空间的数据
2:指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据
3:如果是读,该内存应被标记为可读,如果是写,该内存应被标记为可写,如果是可执行,该内存应被标记为可执行。进程决不能绕过内存访问限制。
内核提供了两种方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。
为了向用户空间写入数据,内核提供了copy_to_user(),他需要三个参数,第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,第三个参数是需要拷贝的数据的长度(字节数)。
为了从用户空间读取数据,内核提供了copy_from_user(),他和copy_to_user()相似,该函数把第二个参数指定位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据的长度由第三个参数指定。
如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,则返回0。当出现上述错误的时候,系统调用返回标准-EFAULT。
下面我们看一个例子,silly_copy()函数。
/*
* silly_copy没有实际价值的系统调用,他把len字节的数据从'src'拷贝的'dst',
* 毫无理由的让内核空间作为中转站。
*/
SYSTEMCALL_DEFINE3(silly_copy,
unsigned long *src,
unsigned long *dst,
unsigned long len )
{
unsigned long buf;
/ * 将用户地址中的src拷贝进dst */
if(copy_from_user(&buf,src,len))
return -EFAULT;
/* 将buf拷贝进用户地址空间中的dst */
if(copy_to_user(dst,&buf,len))
return -EFAULT;
/* 返回拷贝的数据量 */
return len;
}
注意,copy_to_user()和copy_from_user()函数都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生,此时,进程就会休眠,知道缺页处理程序将该页从硬盘换回到物理内存。
最后一项是检查针对是否有合法权限。在现在linux系统中,可以使用capable()函数来检查是否有权限对指定的资源进程操作。如果返回非0值,调用者就有权进程操作,返回0表示无权操作。
下面我们来看一下在reboot系统调用中capality()函数的使用。
/*
* Reboot system call: for obvious reasons only root may call it,
* and even root needs to set up some magic numbers in the registers
* so that some mistake won't make this reboot the whole machine.
* You can also set the meaning of the ctrl-alt-del-key here.
*
* reboor系统调用:由于一些显著的原因,仅仅root用户才能调用他。
* 甚至是root用户,也需要在寄存器中设置一些参数,所以一些错误不会使
* 整个机器重启。
*
* reboot doesn't sync: do that yourself before calling this.
*
* reboot不是协作的:在调用这个之前自己做那些事情。
*/
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
void __user *, arg)
{
char buffer[256];
int ret = 0;
/* We only trust the superuser with rebooting the system. */
/* 我们仅仅相信启动系统的超级用户 */
if (!capable(CAP_SYS_BOOT))
return -EPERM;
/* For safety, we require "magic" arguments. */
/* 为了安全起见,我们需要“magic”参数 */
if (magic1 != LINUX_REBOOT_MAGIC1 ||
(magic2 != LINUX_REBOOT_MAGIC2 &&
magic2 != LINUX_REBOOT_MAGIC2A &&
magic2 != LINUX_REBOOT_MAGIC2B &&
magic2 != LINUX_REBOOT_MAGIC2C))
return -EINVAL;
/* Instead of trying to make the power_off code look like
* halt when pm_power_off is not set do it the easy way.
*
* 当pm_power_off未被设置的时候,请不要尝试让power_off的代码
* 看起来像是可以停机,而应该采用更简单的方式
*/
if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)
cmd = LINUX_REBOOT_CMD_HALT;
mutex_lock(&reboot_mutex);
switch (cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restart(NULL);
break;
case LINUX_REBOOT_CMD_CAD_ON:
C_A_D = 1;
break;
case LINUX_REBOOT_CMD_CAD_OFF:
C_A_D = 0;
break;
case LINUX_REBOOT_CMD_HALT:
kernel_halt();
do_exit(0);
panic("cannot halt");
case LINUX_REBOOT_CMD_POWER_OFF:
kernel_power_off();
do_exit(0);
break;
case LINUX_REBOOT_CMD_RESTART2:
if (strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1) < 0) {
ret = -EFAULT;
break;
}
buffer[sizeof(buffer) - 1] = '';
kernel_restart(buffer);
break;
#ifdef CONFIG_KEXEC
case LINUX_REBOOT_CMD_KEXEC:
ret = kernel_kexec();
break;
#endif
#ifdef CONFIG_HIBERNATION
case LINUX_REBOOT_CMD_SW_SUSPEND:
ret = hibernate();
break;
#endif
default:
ret = -EINVAL;
break;
}
mutex_unlock(&reboot_mutex);
return ret;
}
首先就是确定调用进程是否具有CAP_SYS_REBOOT的权利。在linux/capality.h文件中,包含一份所有这些权限和所对应的权限的列表。我们稍微看一下:
/**
** POSIX-draft defined capabilities.
**/
/* In a system with the [_POSIX_CHOWN_RESTRICTED] option defined, this
overrides the restriction of changing file ownership and group
ownership. */
#define CAP_CHOWN 0
/* Override all DAC access, including ACL execute access if
[_POSIX_ACL] is defined. Excluding DAC access covered by
CAP_LINUX_IMMUTABLE. */
#define CAP_DAC_OVERRIDE 1
/* Overrides all DAC restrictions regarding read and search on files
and directories, including ACL restrictions if [_POSIX_ACL] is
defined. Excluding DAC access covered by CAP_LINUX_IMMUTABLE. */
#define CAP_DAC_READ_SEARCH 2
/* Overrides all restrictions about allowed operations on files, where
file owner ID must be equal to the user ID, except where CAP_FSETID
is applicable. It doesn't override MAC and DAC restrictions. */
#define CAP_FOWNER 3
//........
(六):系统调用上下文
在上文中,我们知道,在进程系统调用的时候,会由sys_call()进程处理,当系统调用并返回之后,控制权仍然在system_call()手中,他最终会负责切换到用户空间,并让用户进程继续执行下去。
1:绑定一个系统调用的最后步骤
当编写玩一个系统调用的时候,把他注册成一个正式的系统调用是一件繁琐的事情:
1):首先,在系统调用表的最后加入一个表项。
2):对于所支持的体系结构,系统调用表必须定义在asm/unistd.h中
3):系统调用必须被编译进内核映象(不能被编译成模块)。只要放入kernel/下的一个相关文件中就可以,比如sys.c,他包含了各种各样的系统调用
首先我们虚构一个系统调用foo(),来使用一下这些步骤。
首先将sys_foo系统调用加入到调用表的最后一个表项,该表位于kernel/syscall_table_32.S文件中。
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_creat
.long sys_link
.long sys_unlink /* 10 */
// ..............................................
.long sys_rt_tgsigqueueinfo /* 335 */
.long sys_perf_event_open
.long sys_recvmmsg
.long sys_foo /* 338 */
很明显,我们的系统调用的系统调用号是338。这个系统调用是与体系结构相关的,所以需要放在合适的体系结构的文件中。
其次,我们将系统调用号加入到asm/unistd.h文件中。
#define __NR_rt_tgsigqueueinfo 240
__SYSCALL(__NR_rt_tgsigqueueinfo, sys_rt_tgsigqueueinfo)
#define __NR_perf_event_open 241
__SYSCALL(__NR_perf_event_open, sys_perf_event_open)
#define __NR_accept4 242
__SYSCALL(__NR_accept4, sys_accept4)
#define __NR_recvmmsg 243
__SYSCALL(__NR_recvmmsg, sys_recvmmsg)
#undef __NR_syscalls
#define __NR_syscalls 244
#define _NR_foo 338
最后来实现系统调用函数foo()。根据函数的功能,我们可以放入到有关的文件中,这个foo()函数我放入到kernel/sys.c文件中。
#include <asm/page.h>
/*
* sys_foo -返回内核栈的大小
*
*/
asmlinkage long sys_foo(void)
{
return THREAD_SIZE;
}
这样就可以启动内核,并在用户空间调用foo()系统调用了。
2:从用户空间访问系统调用
linux内核提供了一个宏来在用户空间调用系统调用,下面我们通过这种方法来测试前面的foo()系统调用
#define _NR_foo 338
__syscall0(long,foo)
int main()
{
long stack_size;
stack_size = foo();
printf("The kernel stack size is %ld.
",stack_size);
return 0;
}
其中,#define _NR_foo 338 代表foo系统调用的系统调用号
__syscall0(long,foo) : 其中0表示传递给foo系统调用0个参数,该值表示传递给系统调用的参数的个数。
3:为什么不通过系统调用的方式实现
首先我们先看一下系统调用的好处:
1:系统调用创建容易,并且使用方便
2:Linux系统调用的高性能显而易见
系统调用的问题:
1:需要一个系统调用号,这需要在内核开发过程中有官方分配
2:系统调用被加入稳定内核后被固化了,为了避免程序v崩溃,他的接口不允许做改动
3:每个需要支持的体系结构都需要注册该系统调用
4:在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用
5:由于系统调用号的存在,在主内核之外很难维护和使用系统调用
6:如果仅仅是信息交换的话,系统调用有些大才小用