• 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)讨论如何添加系统调用,从用户空间调用系统调用。

    [======]

  • 相关阅读:
    java操作生成jar包 和写入jar包
    jboss配置jndi连接池
    windows 域的LDAP查询相关举例
    LDAP error Code 及解决方法
    HDU 6417
    CF1299D Around the World
    codechef Chef and The Colored Grid
    Educational Codeforces Round 82 (Rated for Div. 2)
    CF1237F Balanced Domino Placements
    CF1254E Send Tree to Charlie
  • 原文地址:https://www.cnblogs.com/fortunely/p/15859962.html
Copyright © 2020-2023  润新知