10 系统调用

  • OS为在
    • 用户态运行的进程与硬件设备(CPU、磁盘、打印机)进行交互
    • 提供一组接口。
  • 在应用程序和硬件之间
    • 设置一个额外层好啊。
  • 使编程更易,把用户从学习硬件设备的低级编程特性中解放
  • 提高系统安全性,因为内核在试图满足某个请求之前在
    • 接口级就可检査这种请求的正确性。
  • 这些接口使程序有可移植性,
    • 只要内核提供的接口相同,
    • 则在任一内核上就可正确地编译和执行程序。

Unix向内核发出系统调用

  • 实现用户态进程和硬件设备之间的大部分接口。
  • 讨论Linux内核如何实现这些
    • 由用户态进程向内核发出的系统调用。

POSIX API和系统调用

  • 应用编程接口(API)与系统调用不同。
  • 前者函数定义
  • 后者通过软中断向内核态发出明确请求。
  • Unix给程序员提供很多API的库函数
  • libc的标准C库所定义的一些API引用了封装例程( wrapper routine)(其唯一目的就是发布系统调用)。
  • 每个系统调用对应一个封装例程,封装例程定义应用程序使用的API。
  • 反之不然
  • 一个API没必要对应一个特定的系统调用。
  • API可能直接提供用户态的服务(如抽象的数学函数,没必要用系统调用)。
  • 一个单独的API可能调用几个系统调用。
  • 几个API可能调用封装了不同功能的同一系统调用。
  • Linux的libc库函数实现malloc、calloc和free等 POSIX API,都用brk()系统调用来扩大或缩小进程的堆(见九章“堆的管理”)

POSIX标准针对API不针对系统调用。

  • 判断一个系统是否与 POSIX兼容要看它
    • 是否提供一组合适的应用程序接口,不管对应的函数是如何实现。
  • 一些非Unix系统被认为是与POSIX兼容的,
  • 因为它们在用户态的库函数中提供了传统Unix能提供的所有服务。

编程者观点看,

  • API和系统调用之间的差别是没关系的
    • 唯一相关的是函数名、参数类型及返回代码的含义
  • 内核设计者观点看,这种差别有关系,
    • 系统调用属于内核,用户态的库函数不属于内核。

大部分封装例程返回一个整数,值含义依赖相应的系统调用。

  • -1:内核不能满足进程的请求。
  • 系统调用处理程序的失败
    • 由无效参数引起,也可能因为缺乏可用资源,或硬件出问题。
  • libc库中定义的errno变量含特定的出错码

出错码都定义为:常量宏(产生一个相应的正整数值)。

  • POSIX标准指定了很多出错码的宏名。
  • 80x86的linux中,include/asm-i386/errno.h中定义这些宏。
  • 为使各种Unix系统上的C程序可移植
    • 在标准的C库头文件/usr/include/errno.h中也含include/asm-i386/errno.h
  • 其他的系统有它们自己专门的头文件子目录。

系统凋用处理程序及服务例程

  • 当用户态进程调用一个系统调用时,
    • CPU切换到内核态并开始执行一个内核函数。
  • 80x86体系结构中,两种不同的方式调用Linux的系统调用。
  • 两种方式的最终结果都是
    • 跳转到所谓
    • 系统调用处理程序的汇编语言函数。

内核实现了不同的系统调用,

  • 进程须传递一个名为系统调用号的参数来识别所需的系统调用
  • eax寄作此目的
  • 当调用一个系统调用时通常还要传递另外的参数。

系统调用都返整数。

  • 与封装例程返回值的约定不同。
  • 内核中,正数或0表示系统调用成功结束,负数:一个出错条件。
  • 后一种情况,就是存放在errno变量中必须返回给应用程序的负出错码。
  • 内核没有设置或使用errno变量,封装例程从系统调用返回后设置这个变量

系统调用处理程序与其他异常处理程序same

  • 在内核态栈保存多数寄内容(此操作对所有的系统调用都通用,用汇编写)。
  • 调用
    • 名为系统调用服务例程( system call service routine)的
    • 相应的C函数来处理系统调用。
  • 退出系统调用处理程序:
    • 用保存在内核栈中的值加载寄存器,
    • CPU从态切回用态
    • (所有的系统调用都要执行这一相同的,用汇编语言代码实现)。
      10 系统调用

把系统调用号与相应的服务例程关联,内核用了一个系统调用分派表

  • 此表存放在sys_all_table数组中,有NR_ syscalls个表项(在
    Linux2.6.11内核中是289)
  • 第n个表项含系统调用号为n的服务例程的地址

NR_syscalls宏对可实现的系统调用最大个数的限制,不表已实现的系统调用个数。

  • 分派表中的任一个表项也可包含sys_ni_syscall()函数的地址,
  • 这个函数是“未实现”系统调用的服务例程,
    • 仅返回出错码- ENOSYS。

进入和退出系统调用

本地应用(注1)两种方式系统调用:

  • int $0x80。
    • 内核老版本,从用态切到内态的唯一
  • sysenter
    • Pentium II中引入
    • 2.6内核支持这条指令。

内核可通过两方式从系统调用退出,使CPU切回用态

  • iret
  • sysexit,sysenter同时在Pentium II引入。

支持进入内核的两种不同方式并不像看起来那么简单,

  • 内核既支持只用int $0x80指令的旧函数库,
    • 也支持可用sysenter的新函数库。
  • sysenter的标准库必须能处理仅支持int$0x80指令的旧内核。
  • 内核和标准库须既能运行在不含sysenter的旧处理器上,
    • 也能运行在含它的新处理器上。

本章“通过sysenter指令发出系统调用”一节,

  • 看Linux内核是如何解决这些兼容性问题的。

int $0x 80发系统调用

  • 调用系统调用的传统方法是
    • 用汇编语言指令int,
    • 在“中断和异常的硬件处理”曾论过

128(0x80)对应内核入口点。

  • 在内核初始化期间调用的函数trap_init()

  • 用下面的方式建立对应于向量128的

    • 中断描述符表表项
  • set_system__gate(0x80, &system-call)

  • 该调用把下列值存入这个门描述符的相应字段(见四章的“中断门、陷阱门及系统门”一节)

  • Segment Selector

  • 内核代码段__KERNEL_CS的段选择符。

  • Offset

  • 指向system_call()系统调用处理程序的指针,

  • Type

  • 置为15。

  • 表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断。

  • DPL(描述符特权级)

    • 置为3。
    • 这就允许用户态进程调用这个异常处理程序(见四章中的“中断和异
      常的硬件处理”)。
  • 当用户态进程发出int s0x80时,

    • CPU切换到内核态并从system_call处开始执行。

参数传递

内核封装例程