• Linux内核5-系统调用


    Linux第5章

    5.1 与内核通信

    系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个。首先,它为用户空间提供了一种硬件的抽象接口。第二,系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其它一些规则对需要进行的访问进行裁决。第三,在第3掌中提到,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是处于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。

    在Linux中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。

    5.2

    应用程序 --->  C库  --->  内核

    5.3 系统调用

    要访问系统调用(在Linux中常称作syscall),通常通过C库中定义的函数调用来进行。它们通常都需要定义零个、一个或几个参数(输入)而且可能产生一些副作用,例如,写某个文件或向给定的指针拷贝数据等。系统调用还会通过一个long类型的返回值来表示成功或错误。通常(不绝对)用一个负的返回值来表明错误。返回一个0值通常(不绝对)表明成功。系统调用在出现错误的时候C库会把错误码写入errno全局变量。通过perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

    例如getpid()系统调用,内核中它的实现非常简单:

    SYSCALL_DEFINE0(getpid)

    {

      return task_tgid_vnr(current);  //returns current->tgid

    }

    SYSCALL_DEFINE0只是一个宏,它定义一个无参数的系统调用(因此这里为数字0),展开后的代码如下:

    asmlinkage long sys_getpid(void)

    asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,函数返回long。为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long。最后,系统调用getpid()在内核中被定义成sys_getpid()。这是Linux中所有系统调用都应该遵守的命名规则。

    5.3.1 系统调用号

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

    系统调用号,一旦分配就不能再有任何变更。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。

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

    内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。每一种体系结构中,都明确定义了这个表,在x86-64中,它定义在arch/i386/kernel/syscall_64.c文件中。这个表为每一个有效的系统调用指定了唯一的系统调用号。

    5.3.2

    Linux上下文切换较快,进出内核比较高效。

    5.4 系统调用处理程序

    用户空间的程序无法直接之星内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统的安全性和稳定性将不复存在。

    所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用。

    通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。在x86系统上预定义的软中断是中断号128,通过int $0x80指令触发该中断。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序。这个处理程序名字为system_call()。

    5.4.1 指定恰当的系统调用

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

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

    call *sys_call_table( ,%eax, 8)

    系统调用表中的表项是以64位类型存放的。

    5.4.2 参数传递

    除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。所以,在发生陷入时,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样,把这些参数也存放在寄存器里。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

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

    5.5 系统调用的实现

    5.5.1 实现系统调用

    实现一个新的系统调用的第一步是决定它的用途。

    系统调用的参数、返回值和错误码。

    5.5.2 参数验证

    系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大地考验。

    如与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。进程应当让内核去访问那些它无权访问的资源。

    最重要的一种检查就是检查用户提供的指针是否有效。在接收一个用户的指针之前,内核必须保证:

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

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

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

    内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意内核无论何时都不能轻率地接受来自用户空间的指针。

    为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数是需要拷贝的数据长度(字节数)。

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

    如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,则返回0.当出现错误时,系统掉哦那个返回标准-EFAULT。

    copy_to_user()和copy_from_user()都可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

    最后一项检查针对是否有合法权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非0值,调用者就有权进行操作,返回0则无权操作。例如capable(CAP_SYS_NICE)可以检查调用者是否有权改变其它进程的nice值。

    5.6 系统调用上下文

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

    在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占。能够休眠说明系统调用可以使用内核提供的绝大部分功能。在进程上下文中能够被抢占其实表明,像用户空间内的进程一样,当前的进程同样可以被其它进程抢占(保证系统调用可重入)。

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

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

    5.6.2 从用户空间访问系统调用

    通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。

    Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围从0到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)

    这样应用程序就可以直接使用open()。

    第一个参数对应系统调用的返回值类型。第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。_NR_open在<asm/unistd.h>中定义,是系统调用号。

  • 相关阅读:
    C#--事件驱动在上位机中的应用【一】(搭建仿真PLC环境)
    C#--事件驱动在上位机中的应用【三】(自定义控件)
    C#--事件驱动在上位机中的应用【二】(自定义控件)
    C#--属性--propfull和prop使用场所
    C#--通过Modbus TCP与西门子1200PLC通讯
    C#--简单调用WebService
    C#-- 简单新建WebService服务
    C#--发布WebService和部署IIS到本地服务器
    P1909 买铅笔
    P1089 津津的储蓄计划
  • 原文地址:https://www.cnblogs.com/cjj-ggboy/p/12347491.html
Copyright © 2020-2023  润新知