- 与内核通信
- 什么是系统调用
- 系统调用存在的意义
- API,POSIX和C库
- 系统调用的实现原理
- 【软中断】和【硬中断】:int$0x80指令
- 【系统调用号】__NR_name
- 【响应函数】sys_name()
- 【系统调用表】
- 【寄存器eax】
- 系统调用过程
- 用户态进入内核态的方式:int$0x80 和 sysenter()
- 参数传递:eax + 其他5个寄存器
- 参数校验:【系统调用表】中计算对应【响应函数】的入口地址
5.1 与内核通信
(1)什么是系统调用
- 概况的说,系统调用就是用户程序和硬件设备之间的桥梁
- 在Linux中,系统调用是用户空间访问内核的唯一手段
(2)系统调用存在的意义
- 用户程序无需知道硬件设备怎么实现的,只需访问系统调用的接口
- 比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上
- 保证系统安全和稳定
- 内核可以基于权限对系统调用进行裁决,这样可以避免用户程序不正确的使用硬件设备,从而破坏了其他程序
- 系统调用有效的分离了用户程序和内核的开发
- 用户程序只需关心系统调用API,通过这些API来开发自己的应用,不用关心API的具体实现
- 内核则只要关心系统调用API的实现,而不必管它们是被如何调用的
5.2 API,POSIX和C库
(1)概念
- API:一组应用编程接口
- POSIX标准(Portable Operating System Interface eXecution):提供一套大体上基于Unix的可移植操作系统标准,严格意义上是为了提供应用程序的可移植性
- 关系
- 系统调用是POSIX的一个子集
- 系统调用一般通过C语言库提供,也是C语言的一个子集
- POSIX库是C库的超集
(2)总结
- 内核只与系统调用打交道
- 程序员和API打交道
- API中封装了很多系统调用(比如C库提供的函数,priintf()实际是执行write()系统调用)
5.3 系统调用的实现原理
(1)【硬中断】和【软中断】
- 中断的概念:【中断】就是一个硬件或软件请求,操作系统一般是通过【中断】从用户态切换到内核态
- 比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。
- 中断有两个重要的属性:【中断号】和【中断处理程序】
- 【中断号】用来标识不同的中断,不同的中断具有不同的【中断处理程序】
- 在操作系统内核中维护着一个中断向量表,这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。
一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关
(2)【系统调用号】和【系统调用号的编号】
- 文件路径: include/asm/unisted.h (也有可能为 unisted_32.h 或者 unisted_64.h)
- 文件内容:定义了【系统调用号】(比如__NR_socket)和【系统调用号的编号】(对应编号为340)
(3)系统调用的【响应函数】
- 内核中真正实现系统调用的函数
- 响应函数以“sys_”开头,后面跟着系统调用的名字
- 例如
- 系统调用fork() 的响应函数是 sys_fork()(见Kernel/fork.c)
- 系统调用exit() 的响应函数是 sys_exit()(见Kernel/fork.)
- 此外,Linux还有一个未实现的函数 sys_ni_syscall(),它除了返回 -ENOSYS外不作任何其他工作,这个错误号是专门针对无效的系统调用而设定的
(4)【系统调用表 sys_call_table】
- 可以理解为,以【系统调用号】为参数,去【系统调用表】中找到对应的【响应函数】的入口地址
- 文件路径:arch/i386/kernel/entry.S(不同版本可能不一样,但是名称都为entry.S)
- 文件内容
-
call *sys_ call-table(,%eax, 4)
(5)寄存器【eax】
- 用户态执行软中断0x80后,会执行系统调用进入内核态。那么内核是怎么知道执行哪个系统调用的呢
- 通过【eax】寄存器,在陷人内核之前,用户空间就把相应的系统调用号放入
eax
中
5.4 系统调用过程
(1)【用户态】进入【内核态】的方式
- 用户空间无法直接访问内核代码,即它们不能直接调用内核空间的函数。不然的话,系统的安全稳定性会受到威胁
- 目前主流两种方式:int$0x80 和 sysenter()指令
- 用户态可以通过【软中断】进入内核态:通过发起一个异常来通知内核去处理异常
- 在x86上预定义的软中断号是128,即 int$0x80
- 这条指令会让内核去执行第128号异常处理程序,而该程序就是系统调用处理程序,叫 system_call()
- 除了int$0x80之外,最近x86又新增了一条 sysenter()指令
- 与int中断指令相比,这条指令提供了更快,更专业的陷入内核执行系统调用的方式
(2)参数传递
- 进入内核态时,需要把参数传递给内核态,这里通过寄存器的方式,主要有两种参数
- 【eax】寄存器:专门存放系统调用号
- 【ebx】【ecx】【edx】【esi】【edi】:存放系统调用的参数,按照顺序存放前五个参数,需要六个或六个以上的情况很少见。
- 如果遇到大于等于六个参数,则用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针
(3)参数校验
- system_call()收到了【eax】的系统调用号后,会检查其有效性
- 参数校验失败:
- system_call()将 系统调用号 与 NR_syscalls 作比较,如果它大于或等于 NR_syscalls,应该返回 -ENOSYS
- NR_syscalls表示系统调用号的最大值,假如有190个系统调用号,对应编号为0-189,NR_syscalls=190
- 参数校验成功
- 执行响应系统调用(以x86-32为例):call *sys_call_table(, %eax, 4)
- 上面这行代码代表:去【系统调用表】,根据【系统调用号】,计算偏移量,找到对应【响应函数】的入口地址
(4)系统调用流程图