向下之旅(七):系统调用

  为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。应用程序发送各种请求,内核负责满足这些请求,系统调用在用户空间进程和硬件设备之间添加了一个中间层。主要作用有三个:第一,为用户空间提供了一种硬件的抽象接口。第二,系统调用保证了系统的稳定和安全。作为硬件和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。第三,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样的一层公共接口。

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

  API、POSIX和C库

  一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。给外界提供相同的API,内部实现可以通过不同的方式。

  在Unix世界中,最流行的应用编程接口是基于POSIX标准的,Linux是与POSIX兼容的。Linux的系统调用像大多是Unix系统一样,作为C库的一部分提供,下图所示C库实现了Unix系统的主要API,包括标准C库函数和系统调用。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便的把它们封装起来使用。此外,C库提供了POSIX的绝大部分API。

程序员无需考虑系统调用,只需跟API打交道就可以了。内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用的不是内核所关系的。但是,内核必须时刻牢记系统调用所有齐纳在的用途并保证它们有良好的通用性和灵活性。

  系统调用

  系统调用通常通过函数进行调用,然后通过返回一个long类型的值来表示成功或者错误(long型为与64位的硬件体系结构兼容),通常用一个负的返回值来表示错误,用0值表示成功。内核必须提供系统调用所希望完成的功能,但它完全可以按照自己预期的方式去实现,只要最后的结果正确就行了。

  每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用,进程不会提及系统调用的名称。系统调用号十分关键,一旦被分配就不在有任何的变更,即使一个系统调用被删除,系统调用号也不会被回收,此时Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何工作,防止一个系统调用被删除后,应用程序调用以前的系统调用出错。

  内核记录了系统调用表中的所有已经注册过的系统调用的列表,存储在sys_call_table中。

  Linux系统调用比其他许多操作系统执行的都要快。Linux上下文切换时间是一个重要的原因,进出内核都被优化的简洁高效;另一个原因是系统调用处理程序和系统调用本身都十分的简洁。

  系统调用处理程序

  用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间的函数,因为内核驻留在受保护的地址空间上。否则系统安全就会失去控制。

  应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用。希望系统切换到内核态,这样内核就可以代表应用程序来执行系统调用了。通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。实际上就是系统调用处理程序。

  x86系统上的软中断由int $0x80指令产生,触发一个异常导致系统切换到内核态去执行第128号异常处理程序,也就是系统调用处理程序,叫system_call()。最近增加了一条就做sysenter的指令,这条指令提供了更快,更专业的陷入内核执行系统调用的方式。

  指定恰当的系统调用

  因为所有的系统调用陷入内核的方式都一样,所以必须把系统调用号一并传给内核,在x86上,系统调用号通过eax寄存器传给内核。在陷入内核之前,将调用号放入eax中,当系统调用处理程序就会从eax中获得数据。

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

  由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置。

  除了系统调用号之外,大部分系统调用还需要传入一些外部的参数输入,其方式和传入系统调用号方式相同,通过ebx、ecx、edx、esi和edi按照顺序存放前五个参数。六个以上的话,应该用一个单独的寄存器存放指向所有这些参数在用户空间的指针。给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。

  系统调用的实现

  在Linux中,不提倡采用多用途的系统调用(一个系统调用传递不同的参数值来完成不同的工作),尽可能的做到单一职责原则。ioctl()就应该被视为一个反例。并且尽可能的考虑它的可移植性和健壮性。不仅现在实用,将来也要实用。

  参数验证

  系统调用必须仔细检查传给它们的参数的合法性,系统调用在内核空间执行,若是参数不合法,系统的安全和稳定将面临极大的考验。最重要的是对用户提供的指针检查,内核必须保证:

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

  2.指针指向内存区域属于当前进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。

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

  内核提供两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。为向用户空间写入数据,内核提供了copy_to_user()。它需要三个参数,第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个是需要拷贝数据的长度(字节数)。从用户空间读取数据,内核提供了copy_from_user()。与前者相似,该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。成功则返回0,失败则是返回的未能完成拷贝的字节数。两者都可能引起阻塞。

  最后一项检查真滴是否有合法权限。新系统允许检查针对特定资源的特定权限。调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非零值,调用者就有权进行操作,返回零则无权操作。

  系统调用上下文

  内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠(比如咋系统调用阻塞或者显示调用schedule()的时候)并且可以被抢占。

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

  1.首先,在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作(大部分的系统调用都针对所有的体系结构)。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用分配到的系统调用号为9。

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

  3.系统调用必须被编译进内核映像(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以。

  从用户空间访问系统调用

  通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。

  Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围是从0到6。代表需要传递给系统调用的参数个数。例:

open()系统调用的定义是:

_NR_open在<asm/unistd.h>中定义,是系统调用号。

  为什么不通过系统调用的方式实现

  Linux绝不提倡通过建立一个新的系统调用来实现某种接口功能。通常都会有更好的办法用来代替新建一个系统调用以作实现。其中利与弊如下:

  好处:

  1.系统调用创建容易且使用方便

  2.Linux系统调用的高性能显而易见

  问题是:

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

  2.系统调用被加入稳定内核后就被固化了,为了避免应用程序崩溃,它的界面不允许做改动

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

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

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

  替代方法:

  1.创建一个设备节点,通过read()和write()访问它。用ioctl()进行特别的设置操作和获取特别的信息

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

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

  这就是Linux系统调用接口如此简洁的原因,从另一方面也反映出Linux是一个相对较为稳定的并且功能已经较为完善的操作系统。

 

  参考自:《Linux Kernel Development》.

转载于:https://www.cnblogs.com/blkspm/p/5280355.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值