• Linux系统调用


    系统调用基本概念

    为了和用户空间上运行的进程进行交互,内核提供了一组接口,透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口称为系统调用。
    系统调用是用户空间和硬件设备之间添加的一个中间层,主要作用:
    1)为用户空间提供一种硬件的抽象接口。
    2)保证系统的稳定和安全。
    3)每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是基于中间层的考虑。

    Linux系统调用非常少,x86系统上大概250个系统调用(具体取决于特定体系结构)。

    [======]

    API、POSIX和C库

    一般,应用程序通过应用程序编程接口(API)而非直接通过系统调用来编程。这样,应用程序使用的编程接口,并不需要跟内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口。

    Unix中,最流行的应用编程接口是POSIX标准,而POSIX是由IEEE的一组标准组成,目的是提供一套大体上基于Unix 的可移植操作系统标准。
    Linux与POSIX兼容;Windows NT提供了POSIX兼容库。

    [======]

    系统调用

    系统调用在Linux中常称作syscalls,通常通过函数进行调用。
    参数根据定义决定;返回long类型表示成功或错误,通常,0表示成功,非0表示错误。出错时,错误码写入errno全局变量,可通过perror库函数将其翻译为用户可理解字符串。

    例如,getpid()系统调用,返回当前进程PID。内核实现:

    asmlinkage long sys_getpid()
    {
        return current->tgid;
    }
    

    注意:asmlinkage表示使用0个寄存器传递参数,也就是不用寄存器传参,常用于系统调用,确保是用栈传参。

    #define asmlinkage __attribute__((regparm(0)))
    

    系统调用号

    Linux中,每个系统调用都被赋予一个独一无二、固定不变的系统调用号,用来关联系统调用。用户空间的进程执行一个系统调用时,该系统调用号酒杯用来指明到底要执行哪个系统调用;进程不会提及系统调用名称。

    sys_ni_syscall() 代表未实现的系统调用,除了返回-ENOSYS外不做任何工作,该错误号专门针对无效的系统调用而设。如果一个系统调用被删除,或不可用,该函数就要负责“填补空位”。

    可通过查看entry.s文件中的sys_call_table,查看系统调用表中所有已注册过的系统调用的列表。

    系统调用的性能

    Linux系统调用比其他许多操作系统执行得要快:上下文切换时间;进程内核优化得简洁、高效;系统调用处理程序和系统调用本身简洁。

    [======]

    系统调用处理程序

    用户空间无法直接执行内核代码,不能直接调用内核空间中的系统调用,需要靠软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。这个异常处理程序实际上就是系统调用处理程序。
    x86上,软中断(这里是软件中断)由int $0x80指令产生。该指令会触发一个异常导致系统切换到内核态,并执行第128号异常处理程序,而该程序正是相当于处理程序,名为system_call() 。该函数与硬件体系结构紧密相关,通常在entry.S文件中用汇编编写。

    指定恰当的系统调用

    所有系统调用陷入内核的方式都一样,必须把系统调用号一并传给内核。
    x86上,系统调用号通过eax寄存器传递给内核。eax是x86汇编中通用寄存器的名称,32bit。EAX是累加器,是很多加法乘法指令的缺省寄存器。

    system_call()检查给定系统调用号的有效性:如果系统调用号 >= NR_syscalls,函数返回-ENOSYS;否则,执行相应系统调用。

    call *sys_call_table(, %eax, 4);
    

    参数传递

    除了系统调用号,大部分系统调用都还需要一些外部的参数输入。发生异常时,应该把这些参数从用户空间传递给内核。
    最简单办法就是像传递系统调用号一样,把这些参数也存放在寄存器。如x86上,ebx, ecx, edx, esi, edi按顺序存放前五个参数;超过6个参数时,可以用一个单独寄存器存放指向这些参数的用户空间地址的指针。

    给用户空间的返回值,也可以通过寄存器传递。x86上,返回值存放在eax中。

    [======]

    系统调用的实现

    一个系统调用实现时,不需要太关心它和系统调用处理程序之间的关系。
    给Linux内核添加一个系统调用很容易,难点在于设计和实现。

    编写系统调用原则

    1)决定用途
    它要做什么?每个系统调用都应有一个明确用途,不推荐多用途的系统调用。ioctl()是反例。

    2)确定新系统调用的参数、返回值、错误码
    接口应该力求简洁,参数尽可能少。

    3)设计接口时,尽量为将来多做考虑
    是不是对函数做了不必要限制?系统调用设计得越通用越好,有一定可移植性。

    4)不对系统调用做错误的假设,否则将来可能会崩溃
    别对字节长度和字节序做假设。提供机制而不是策略。

    参数验证

    系统调用必须仔细检查所有参数是否合法,阻止不合法输入传递给内核。

    1. 用户指针有效性检查
      最重要的一种检查就是检查用户提供的指针是否有效。在接收一个用户空间的指针之前,内核必须保证:
    • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
    • 指针指向的内存区域有在进程的地址空间里,进程决不能哄骗内存去读其他进程的数据。
    • 如果是读,读内存应该被标记为可读。如果是写,内存应该被标记为可写。进程决不能绕过内存访问限制。

    内核提供2个方法来完成必须的检查,内核空间与用户空间间的数据来回拷贝。

    • copy_to_user() 向用户空间写入数据,需要3个参数:1)进程空间中的目的内存地址;2)内核空间中的源地址;3)需要拷贝的数据长度(bytes)。
    • copy_from_user() 从用户空间读取数据,和copy_to_user()相似,3个参数:1)内核空间中的目的地址;2)进程空间中的源地址;3)需要拷贝的数据长度。

    如果执行成功,返回0;如果失败,2个函数返回的都是没能完成拷贝的数据的字节数。如果指针不合法,则系统调用返回标准-EFAULT (Bad address )。

    示例:silly_copy() 既用了copy_from_user() ,也用了copy_to_user() 。

    asmlinkage long sys_silly_copy(unsigned long* src, unsigned long* dst, unsigned long len)
    {
        unsigned long buf;
        /* 如果内核字长与用户字长不匹配, 则失败 */
        if (len != sizeof(buf))
            return -EINVAL;
    
        /* 将用户地址空间中的src拷贝进buf */
        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()都可能引起阻塞,当包含用户数据的页被换出到硬盘上而非物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

    1. 合法权限检查
      旧的Linux内核,可通过suser()来完成检查:检查用户是否是超级用户。
      新的Linux内核,可通过capable()检查是否有权能对指定的资源进行操作:如果返回非0值,调用者就有权进行操作;返回0,则无权操作。i.e. capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程nice值。
      默认情况下,超级用户的进程有所有权利;非超级用户的进程没有任何权利。

    例:下面系统调用展示了权能的使用

    asmlinkage long sys_am_i_popular(void)
    {
        /* 检查用户进程是否具有CAP_SYS_NICE 权能 */
        if (!capable(CAP_SYS_NICE))
            return -EPERM;
        /* 成功返回0 */
        return 0;
    }
    

    参见<linux/capability.h>,包含一份所有这些权能和其对应的权限的列表。

    [======]

    系统调用上下文

    内核在执行系统调用的时候处于进程上下文,current指针指向当前任务(引发系统调用的那个进程)。

    在进程上下文中,内核可以休眠(如系统调用阻塞或显式调用schedule())并且可以被抢占。

    • 能休眠,说明系统调用可以使用内核提供的绝大部分功能。
    • 能被抢占,说明当前进程同样可以被其他进程抢占,而新进程可能使用相同系统调用,所以必须小心,保证该系统调用是可重入的。

    当系统调用返回时,控制权仍在system_call()中,该函数最终会负责切换到用户空间并让用户进程继续执行下去。

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

    编写完一个系统调用后,怎么注册成一个正式的系统调用?
    1)在系统调用表(sys_call_table)最后加入一个表项。
    2)对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>。
    3)系统调用必须被编译进内核映像(不能被编译成模块),将其放入kernel/下一个相关文件即可。

    例:虚构系统调用foo()来考察添加一个系统调用的步骤。
    1)把sys_foo加入系统调用表
    对大多数体系结构来说,sys_call_table表位于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_mq_notify       /* 281 */
        .long sys_mq_getsetattr
        .long sys_foo             /* 283, 新添项 */
    

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

    /* 本文件包含系统调用号 */
    #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_mq_notify       281
    #define _NR_getsetattr      282
    #define _NR_foo             283  /* 新添项 */
    

    3)实现foo()系统调用,将该系统调用编译进内核映像。
    我们将其实现放入kernel/sys.c,当然也可以放进功能紧密的代码中。如果功能与调度相关,也可以放进kernel/sched.c。

    #include <asm/thread_info.h>
    
    /* 返回每个进程的内核栈大小 */
    asmlinkage long sys_foo(void)
    {
        return THREAD_SIZE;
    }
    

    OK,现在,可以在用户控件调用foo()系统调用了。

    从用户控件访问系统调用

    通常,系统调用靠C库支持,用户程序包含标准头文件和C库链接,即可使用系统调用(或者调用库函数,通过库函数实际调用)。
    glibc库不支持直接调用系统调用,需要通过Linux本身提供的一组宏(_syscalln(), n=0..6,代表参数个数),会设置好寄存器并调用陷入指令。
    e.g. open()系统调用定义

    long open(const char* filename, int flags, int mode);
    

    不靠glibc库支持,直接调用该系统调用的宏形式:

    #define _NR_open 5 /* open的系统调用号 */
    _syscall3(long, open, const char* ) /* 3代表open系统调用需要3个参数 */
    

    这样,应用程序就可以直接调用open()

    编写一个宏来用前面编写的foo()系统调用,然后写出测试代码

    #define _NR_foo 283
    _syscall0(long, foo) /* 0代表foo不需要参数 */
    
    int main()
    {
        long stack_size;
        stack_size = foo();
        printf("The kernel stack size is %ld\n", stack_size);
        return 0;
    }
    

    TIPS:建立一个新系统调用很容易,但并不不推荐通过系统调用的方式实现。

    [======]

    小结

    1)描述了系统调用到底是什么,跟库函数和API的关系。
    2)考察如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。
    3)讨论如何添加系统调用,从用户空间调用系统调用。

    [======]

  • 相关阅读:
    yii---模型的创建
    yii---控制器的创建
    yii的安装
    windows下安装composer
    wpgcms---列表页数据渲染
    Twig---基本使用
    wpgcms---详情页面数据怎么渲染
    Twig---的使用
    vue---指令怎么写
    vue---设置缩进为4个空格
  • 原文地址:https://www.cnblogs.com/fortunely/p/15859962.html
Copyright © 2020-2023  润新知