《Linux内核设计与实现》读书笔记(三)--系统调用

来源:互联网 发布:大尺度知乎收藏 编辑:程序博客网 时间:2024/06/06 00:57

  系统调用:为保证系统稳定可靠,避免应用程序肆意妄行。用户进程与内核进行交互的一组接口。

1、与内核通信

  系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层有三个主要作用:

  1. 为用户空间提供了一种硬件的抽象接口;
  2. 保证了系统的稳定和安全;
  3. 保证每个进程都运行在虚拟系统中。

  系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。
  用户程序,系统调用,内核,硬件设备的调用关系如下图:
这里写图片描述

2、API、POSIX和C库

  应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。一个API定义了一组应用程序使用的编程接口,可对应多个系统调用。
  POSIX是一套应用编程接口(API)的实现标准。
  C库实现了linux系统的主要API,包括标准C库函数和系统调用接口。
  从程序员角度看,系统调用无关紧要,只需要跟API打交道就可以了,相反,内核只跟系统调用打交道;库函数及应用程序怎么使用系统调用,内核无需关心;但内核必须时刻牢记系统调用所有潜在的用途,并保证它们有良好的通用性和灵活性。

3、系统调用

  要访问系统调用(syscall),通常通过C库中定义的函数调用来进行。
  asmlinkage:用于函数声明的限定词,一个编译指令,通知编译器仅从栈中提取该函数的参数。所有系统调用都需要这个限定词。

3.1 系统调用号

  在Linux中,每个系统调用都被赋予一个系统调用号。通过这个独一无二的号可以准确的关联系统调用。
  系统调用号非常重要,一旦分配就不能再有任何变更。系统调用就算被删除,其所占用的系统调用号也不允许被回收利用。
  内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。该表为每一个有效的系统调用指定了唯一的系统调用号。

3.2 系统调用的性能

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

4、系统调用处理程序

  用户空间通知内核的机制:依靠软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序system_call()。

4.1 指定恰当的系统调用

  在x86上,系统调用号通过eax寄存器传递到内核。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax寄存器中。
  system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或等于NR_syscalls,该函数就返回-ENOSYS。否则,就执行相应的系统调用:

    call *sys_call_table(,%rax,8);

4.2 参数传递

  除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。类似系统调用号的做法,也把这些参数存放在寄存器里。在x86-32系统上,ebx、ecx、edx、esi核sdi按照顺序存放前五个参数。

5、系统调用的实现

5.1 实现系统调用

  实现一个新的系统调用首先要决定它的用途。每个系统调用都应该有一个明确的用途。
  确定新系统调用的参数、返回值和错误码。系统调用的接口应该力求简洁,参数尽可能少。稳定!!!
  设计接口时要尽量为将来多做考虑。系统调用设计的越通用越好。不要假设这个系统调用现在怎么用将来也一定就是这么用。永远记住Uinx的格言:提供机制而不是策略!
  实现新的系统调用时,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。基本的Unix系统调用经受住了时间的考验;他们中的很大一部分现在还和30年前一样适用和有效。

5.2 参数验证

  系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核控件执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。
  最重要的检查就是检查用户提供的指针是否有效。在接受一个用户空间的指针之前,内核必须保证:

  1. 指针指向的内存区域属于用户空间。
  2. 指针指向的内存区域在进程的地址空间里。
  3. 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。

  copy_to_user():向用户空间写入数据;
  copy_from_user():从用户空间读取数据。

6、系统调用上下文

  内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
  在进程上下文中,内核可以休眠并且可以被抢占。(一)能够休眠说明系统调用可以使用内核提供的绝大部分功能;(二)在进程上下文中能被抢占其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。

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

  当编写完一个系统调用后,把它注册成一个正式的系统调用是件琐碎的工作:

  1. 首先,在系统调用表的最后加入一个表项;
  2. 对于所支持的各种体系结构,系统调用号都必须定义于《asm/unistd.h》中;
  3. 系统调用必须被便衣进内核映像(不能被编译成模块)。

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

  系统调用通常靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用。

6.3 系统调用的利弊

  建立一个新的系统调用的好处:

  • 系统调用创建容易且使用方便;
  • Linux系统调用的高性能显而易见。

  缺点:

  • 需要一个系统调用号,而这需要一个内核在处于开发版本的时候由官方分配给你;
  • 系统调用被加入稳定内核后就被固化了,为避免应用程序的崩溃,它的接口不允许做改动;
  • 需要将系统调用分别注册到每个需要支持的体系结构中去;
  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用;
  • 由于需要系统调用号,因此在主内核树之外是很难维护和使用系统调用的;
  • 如果仅仅进行简单的信息交换,系统调用就大材小用了。

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

  • 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作;
  • 把增加的信息作为一个文件放在sysfs的合适位置。
阅读全文
0 0
原创粉丝点击