系统调用

来源:互联网 发布:新世界网络教育费用 编辑:程序博客网 时间:2024/06/04 19:17

系统调用

在现代操作系统中.内核提供了用户进程与内核进行交互的一组接口。这些接口让应用程序受限地访向硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请操作系统其他资源的能力。

与内核通信

系统调用相当于是中间层。

  • 它为用户空间提供了一种硬件的抽象接口。举例来说,当需要读写文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。

  • 系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决。

  • 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。

API,POSIX,C库

一般情况下,应用程序通过在用户空间实现的应用编程接口(A}I而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用对应。一个AFl定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。

Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。此外,C库提供了POSIX的绝大部分API。

系统调用

要访问系统调用(在Linux中常称作syscall) ,通常通过C库中定义的函数调用来进行。它们通常都需要定义零个、一个或几个参数(输入)而且可能产生一些副作用。系统调用在出现错误的时候C库会把错误码写入errno全局变量。通过调用可以把该变量翻译成用户可以理解的错误字符串。

SYSCALL_DEFINE0(getpid){        return task_tgid_vnr(current); // returns current->tgid}

注意,定义中并没有规定它要如何实现。内核必须提供系统调用所希望完成的功能,但它完全可以按照自己预期的方式去实现,只要最后的结果正确就行了。当然,上面的系统调用太简单.也没有什么更多的实现手段。

系统调用号

在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用:进程不会提及系统调用的名称。

系统调用号不能改变,否则全部需要重新编译。即使一个系统调用现在不用了也不能改,应该使用sys_ni_syscall().它只返回-ENOSYS.

系统调用处理程序

用户空间的程序没有办法直接执行内核代码。必须要陷入内核态才能够执行相关的代码。通过软件中断引发异常使系统切换到内核态,实际上异常处理程序就是系统调用处理程序。x86是中断号128.

指定恰当的系统调用

因为所有的系统调用陷人内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

system_call()数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等NR_syscall,该函数就返回-ENOSYS。否则,就执行相应的系统调用:

call *sys_call_table(,%rax,8)

参数传递

一般来说把参数也放进寄存器中,如果不够就放进用户空间,传入一个用户空间的指针。

系统调用的实现

  • 参数校验

    • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。

    • 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。

    • 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。进程决不能绕过内存访问限制。

为了从用户空间读取数据,内核提供了copy_from_user(),它和copy_to_user()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

/** silly_copy - pointless syscall that copies the len bytes from* ‘src’ to ‘dst’ using the kernel as an intermediary in the copy.* Intended as an example of copying to and from the kernel.*/SYSCALL_DEFINE3(silly_copy,                unsigned long *, src,                unsigned long *, dst,                unsigned long len){    unsigned long buf;    /* copy src, which is in the user’s address space, into buf */    if (copy_from_user(&buf, src, len))            return -EFAULT;    /* copy buf into dst, which is in the user’s address space */    if (copy_to_user(dst, &buf, len))            return -EFAULT;    /* return amount of data copied */    return len;}

针对是否有合法权限使用capable()函数检验。capable(CAP_SYS_NICE)

SYSCALL_DEFINE4(reboot,                int, magic1,                int, magic2,                unsigned int, cmd,                void __user *, arg){    char buffer[256];    /* We only trust the superuser with rebooting the system. */    if (!capable(CAP_SYS_BOOT))    return -EPERM;    /* For safety, we require “magic” arguments. */    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.    */    if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)        cmd = LINUX_REBOOT_CMD_HALT;        lock_kernel();    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();            unlock_kernel();            do_exit(0);            break;        case LINUX_REBOOT_CMD_POWER_OFF:            kernel_power_off();            unlock_kernel();            do_exit(0);            break;        case LINUX_REBOOT_CMD_RESTART2:            if (strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1) < 0) {                unlock_kernel();                return -EFAULT;            }            buffer[sizeof(buffer) - 1] = ‘\0’;            kernel_restart(buffer);            break;        default:            unlock_kernel();            return -EINVAL;    }    unlock_kernel();    return 0;}

系统调用上下文

系统调用的上下文就是进程上下文,是同一个东西。(进程因为系统调用陷入内核)。因为内核可以休眠,抢占。所以必须保证系统调用是可重入的。

绑定一个系统调用的最后步骤

  1. 首先,在系统调用表的最后加入一个表项。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用分配到的系统调用号为9。

  2. 对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中。

  3. 系统调用必须被编译进内核映象(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用。

下面例子实现foo()函数。加入系统调用表entry.S:

ENTRY(sys_call_table)      .long sys_restart_syscall /* 0 */      .long sys_exit      .long sys_fork      .long sys_read      .long sys_write      .long sys_open /* 5 */      ...      .long sys_eventfd2      .long sys_epoll_create1      .long sys_dup3 /* 330 */      .long sys_pipe2      .long sys_inotify_init1      .long sys_preadv      .long sys_pwritev      .long sys_rt_tgsigqueueinfo /* 335 */      .long sys_perf_event_open      .long sys_recvmmsg      .long sys_foo

加入系统调用号<asm/unistd.h>

/** This file contains the system call numbers.*/#define __NR_restart_syscall 0#define __NR_exit 1#define __NR_fork 2#define __NR_read 3#define __NR_write 4#define __NR_open 5...#define __NR_signalfd4 327#define __NR_eventfd2 328#define __NR_epoll_create1 329#define __NR_dup3 330#define __NR_pipe2 331#define __NR_inotify_init1 332#define __NR_preadv 333#define __NR_pwritev 334#define __NR_rt_tgsigqueueinfo 335#define __NR_perf_event_open 336#define __NR_recvmmsg 337//The following is then added to the end of#define __NR_foo 338

实现,看和什么功能相关,就放在kernel的哪个目录下:

#include <asm/page.h>/** sys_foo – everyone’s favorite system call.** Returns the size of the per-process kernel stack.*/asmlinkage long sys_foo(void){    return THREAD_SIZE;}

至此,完成所有工作,可以去调用了。

从用户空间访问系统调用

通常,系统调用依赖C库。用户使用标准头文件和C库连接,就可以使用系统调用。

还是一种方式是Linux提供的一组宏。可以直接对系统调用进行访问。它会设置好寄存器并陷入指令。(These macros are named _syscalln(), where n is between 0 and 6. 代表需要的参数个数)。

调用open的时候:

long open(const char *filename, int flags, int mode)#define __NR_open 5_syscall3(long, open, const char *, filename, int, flags, int, mode)

相当于该宏有2 + 2*n个参数。第一个为返回值,第二个为系统调用名。后面分别为类型以及参数名。

上面foo()函数的调用。因为其中的参数为空,2+2*0 = 2.

#define __NR_foo 338__syscall0(long, foo)int main (){  long stack_size;  stack_size = foo ();  printf (“The kernel stack size is %ld\n”, stack_size);  return 0;}

最好不要轻易的增加系统调用

  • 你需要一个系统调用一号,而这需要一个内核在处于开发版本的时候由官方分配给你。

  • 系统调用被加入稳定内核后就被固化了,为了避免应用程J序的崩溃,它的接口不允许做改动。

  • 需要将系统调用分别注册到每个需要支持的体系结构中去。

  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。

  • 由于你需要系统调用号,因此在主内核树之外是很难维护和使用系统调用的。

  • 如果仅仅进行简单的信息交换,系统调用就大材小用了。

替换方案:

  • 实现一个设备节点,并对此实现read()和write()使用ioctl()对特定的设置进行操作或者对特定的信息进行检索。

  • 像信一号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作

  • 把增加的信息作为一个文件放在sysfs的合适位置。

原创粉丝点击