Lec 04 系统调用
(参考来源:上海交通大学并行与分布式系统研究所+操作系统课程ppt)
Creative Commons Attribution 4.0 License
Contents
4.1 系统调用
- 硬件提供了一对指令svc/eret指令在用户态/内核态间切换
- 系统调用
(1) 用户与操作系统之间,类似于过程调用的接口
(2) 通过受限的方式访问内核提供的服务
4.1.2 AArch64下常见的Linux系统调用
4.1.3 系统调用举例
int main()
{
write(1, "hello, world\n", 13);
_exit(0);
}
转换成汇编语言
.section .rodata
.LC0:
.string "hello, world\n"
.text
.align 2
.global main
.type main, %function
main:
// First, call write(1, "hello, world\n", 13)!
movq x8, #0x40 // write is system call 64
movq x0, #0x1 // Arg1:stdout has descriptor 1
adrp x3, .LC0
add x1,x3,:lo12:.LC0 // Arg2:Hello world string
movq x2, #0xd // Arg3:string length
svc // Make the system call
// Next, call exit(0)
movq x8, #0x5d // _exit is system call 93
movq x0, #0x0 // Arg1:exit status is 0
svc // Make the system call
4.1.3 系统调用的参数传递(常见软件的约定)
- 最多允许8个参数
(1) x0-x7寄存器
(2) x8用于存放系统调用编号 - 返回值存放于x0寄存器中
4.1.4 系统调用返回值与errno
-
系统调用通过寄存器向应用传递返回值
(1) 一般设置为 -errno
(2) 库对系统调用的 wrapper code 会将系统调用的返回值转换为库函数形式的返回值 -
寄存器放不下,只能通过内存传参
(1) 将参数放在内存中,将指针放在寄存器中传给内核
(2) 内核通过指针访问相关参数
(3) 存在安全的隐患(后续课程会进一步介绍)
4.1.4 如何跟踪系统调用
4.1.5 系统调用流程图
4.2 系统调用优化(Virtual Dynamic Shared Object)
内核将一部分数据通过只读的形式共享给应用,允许应用直接读取。
4.2.1 动机
- 系统调用的时延不可忽略
(1) 尤其是调用非常频繁的情况
(2) 系统调用实际执行逻辑很简单 - 如何降低系统调用的时延?
(1) 特权级切换造成的时间开销
(2) 如果没有特权级切换,那么就不需要保存恢复状态
4.2.2 举例:gettimeofday
- 内核定义
(1) 在编译时作为内核的一部分 - 用户态运行
(2) 将gettimeofday的代码加载到一块与应用共享的内存页
(3) 这个页称为:vDSO
-- Virtual Dynamic Shared Object
(4) Time 的值同样映射到用户态空间(只读)
-- 只有在内核态才能更新这个值
4.2.3 Linux的vDSO
4.3 系统调用优化:FLEX-SC
4.3.1 动机
- 如何进一步降低系统调用的时延?
(1) 不仅仅是 gettimeofday() - "时间都去哪儿了?"
(1) 大部分是用来做状态的切换
(2) 保存和恢复状态 + 权限的切换
(3) Cache pollution - 是否有可能在不切换状态的情况下实现系统调用?
4.3.2 Flexible System Call
- 一种新的syscall机制
(1) 引入 system call page ,由 user & kernel 共享
(2) 用户进程可以将系统调用的请求 push 到 system call page
(3) 内核会从system call page poll system call 请求 - Exception-less syscall
(1) 将系统调用的调用和执行解耦,可分布到不同的CPU核
4.3.3 exception less system call
举例
Kernel 填充syscall的返回值
4.3.4 单核上的单线程/多线程系统调用
- 单内核单线程
(1) 用户应用将多个系统调用推至系统调用页表
(2) 切换到内核线程,内核线程从系统调用页表拉取系统调用。
(3) 执行完系统调用后切换到应用态 - 单内核多线程
(1) 线程1将系统调用推至系统调用页表,并且切换到下一个线程,直到所有线程推送结束。
(2) 下陷到内核态。内核拉取页表中的system call。
(3) 执行完所有系统调用后返回内核态。
4.4 从应用视角看操作系统抽象
4.4.1 进程
1. 分时复用有限的CPU资源
(1) CPU核心数量少于应用程序数量,如何运行?
(2) 单个CPU核心如何运行多个应用程序?
- 分时复用CPU
(1) 让多个应用程序轮流使用处理器核心
(2) 何时切换:操作系统决定
-- 运行时间片(例如100ms)
(3) 高频切换:看起来是多个应用“同时”执行
2. 应用程序与进程
- 通常一个应用程序对应一个进程
(1) 在shell中输入可执行文件的名称
-- shell创建新进程,可执行文件在新进程中执行
(2) 在图形界面双击应用图标 - 多进程程序:应用程序亦可自行创建新进程
(1) 创建新进程,在新进程中运行其他应用程序或与自己一样的程序
3. 进程在操作系统中的实现:状态数据
- 操作系统提供进程的抽象用于管理应用程序
(1) 进程标识号(Process ID, PID)
(2) 运行状态
-- 处理器上下文(CPU Context)
(3) 地址空间
(4) 打开的文件
4. 进程展示效果
- 进程抽象为应用程序提供了“独占CPU”的假象
(1) 程序开发不用考虑如何与其他程序共享CPU
(2) 简化编程 - 进程相关的系统调用
(1) 创建进程
(2) 让进程执行指定的程序
(3) 退出进程
(4) 进程间通信
4.5 进程切换
4.5.1 处理器上下文(CPU Context)
- 操作系统为每个进程维护处理器上下文
(1) 包含恢复进程执行所需要的状态
(2) 思考:进程A执行到main函数任意一条指令,切换到进程B执行,一段时间后,再切回到进程A执行
-- 为完成此过程,有哪些状态需要保存?
(3) 具体包括:
-- PC寄存器值,栈寄存器值,通用寄存器值,状态寄存器值
4.5.2 进程切换的时机
-
异常导致的上下文切换
(1) Timer中断(如基于时间片的多任务调度) -
用户执行系统调用并进入内核
(1) 如:read/sleep等会导致进程阻塞的系统调用
(2) 即使系统调用不阻塞执行,内核也可以决定执行上下文切换,而不是将控制权返回给调用进程
举例1
举例2
4.6 进程相关接口
4.6.1 获取进程ID
- 进程 ID
(1) 每个进程都有唯一的正数PID - Getpid()
(1) 返回调用进程的PID - Getppid()
(1) 返回调用进程父进程的PID
(2) 父进程:创建该进程的进程
4.6.2 Exit 函数
exit函数终止进程并带上一个status状态
4.6.3 Fork 函数
- 调用一次
(1) 在父进程中 - 返回两次
(1) 在父进程中,返回子进程的PID
(2) 在子进程中,返回0 - 返回值提供了唯一明确地区分父进程和子进程执行的方法
4.6.4 execve 函数
- 加载和运行
(1) filename:可执行文件名;argv:参数列表,envp:环境变量列表 - execve 只调用一次,且永远不会返回
(1) 仅仅在运行报错的时候,返回调用程序
(2) 例:找不到filename标识的文件
4.6.5 Linux下的僵尸进程
- 进程终止后,内核不会立刻销毁该进程
(1) 不再运行,但仍然占用内存资源 - 进程以终止态存在,等待父进程回收
- 当父进程回收终止的子进程
(2) 内核把子进程的exit状态传递给父进程
(3) 内核移除子进程,此时子进程才被真正回收 - 终止状态下还未被回收的进程就是僵尸进程
- 如果父进程在自己终止前没有回收僵尸子进程
(4) 内核会安排init进程回收这些子进程 - init进程
(5) PID为1
(6) 在系统初始化时由内核创建
4.6.6 waitpid函数
返回值:成功返回子进程 PID,出错返回 -1
- pid>0:等待集合中只有pid子进程
- pid=-1:等待集合包括所有子进程
- 如果没有子进程:返回-1,errno = ECHILD
- 如果等待被中断:返回-1,errno = EINTR
- options=0
(1) 挂起调用进程,等待集合中任意子进程终止
(2) 如果等待集合中有子进程在函数调用前已经终止,立刻返回
(3) 返回值是导致函数返回的终止子进程pid
(4) 该终止子进程被内核回收 - options=WNOHANG
(1) 如果等待集合中没有终止子进程,立刻返回0 - options=WUNTRACED
(2) 除了返回终止子进程的信息外,还返回因信号而停止的子进程信息 - options=WNOHANG|WUNTRACED
(1) 带回被回收子线程的exit状态
(2) status指针不为NULL
(3) status包含导致子进程进入终止状态的信息
(4) wait.h文件包含了若干宏定义,用于解释status
4.6.7 小结
4.7 内存
4.7.1 虚拟内存
-
虚拟地址空间
(1) 应用进程使用虚拟地址访问内存
(2) 所有应用进程的虚拟地址空间都是统一的(方便开发) -
地址翻译
(1) CPU按照OS配置的规则把虚拟地址翻译成物理地址
(2) 翻译对于应用进程是不可见的(无需关心)
4.7.2 虚拟内存和物理内存
虚拟内存具有独立而统一的地址空间
5. ELF 文件格式
5.1 目标文件
- 可执行目标文件
- 可重定位目标文件(.o)
- 共享目标文件(.so)
(1) 特殊的可重定位目标文件
(2) 可以加载到内存中
(3) 支持动态链接:加载时或运行时
5.2 ELF:可执行可链接格式
- 目标文件的标准二进制格式
统一格式,又被称为ELF二进制文件 - 用于BSD Unix和Linux
最早用于AT&T System
5.3 ELF格式:可重定位目标文件
(1) ELF 头部(ELF Header)
- ELF文件的第一个部分
- 通常用于存元数据
-- Magic number (‘0x7f’ ‘E’ ‘L’ ‘F’)
-- 类型(.o, .so, 可执行)
-- 机器架构
-- 字节顺序(大小端)
-- 节头部表的位置(文件内偏移)
(2) 节头部表(Section Header Table)
- 节(section)
-- ELF文件中除了头部和头部表划分为若干区域
-- 每一个节在文件中时一块连续的字节(可能为空)
-- 互不重叠 - 每一个节都有一个节头部描述
-- 节头部的一项
- sh_name
-- 节名称(在.strtab节中的偏移) - sh_addr
-- 节在加载到内存后,在内存的起始地址 - sh_offset
-- 节在文件中的偏移(字节数) - sh_size
-- 节的大小(字节数) - sh_addralign
-- 对齐要求
(3) ELF字符串表(.strtab)
- 记录一系列C风格字符串
-- 以'\0’结尾 - 表示符号名或节名
-- 使用时记录字符串在表中的偏移(index)
(4) 用以调试的节
- .debug
-- 调试符号表,包括变量(全局、局部)、typedef、C源文件
-- gcc –g生成 - .line
-- C源文件的行数与.text节中指令的映射
(5) 代码与数据节
- .text:代码
- .rodata:只读数据
- .data:初始化的全局变量和静态变量
- .bss:未初始化的全局变量和静态变量
-- “Block Started by Symbol”,“Better Save Space”
-- 不占文件空间,但是在节头部表中有记录
-- 运行时分配内存,默认为0
5.4 可执行目标文件
- ELF头部(ELF header)
-- 整体信息
-- 程序入口 (e_entry)
-- 程序头部表信息- 在文件中的起始位置(e_phoff)
- 大小(e_ehsize)
- 每个条目的大小(e_phentsize)
- 条目数量(e_phnum)
程序头部表
- Program (Segment) Header Table
p_type - PT_LOAD (1): 可加载段
p_flags
-- 运行时权限 (rwx) - p_offset
-- 段在文件中的起始偏移 - p_filesz
-- 段在文件中的大小( 段在内存中的大小p_memsz) - p_vaddr
-- 段在内存中的起始(虚拟)地址 - p_paddr (不常用,x86、ARM 下不用)
- p_memsz
-- 段在内存中的大小(p_filesz) - p_align
-- 段起始地址的对齐要求
通常为2^12 (4K) 或 2^21 (2M)
6. 文件
6.1 UNIX 文件
- Unix 文件是一串字节序列。
-- \(B_0,B_1,\ldots,B_k,\ldots,B_m\) - 所有的IO设备都被抽象成文件。
-- 如:网络设备,硬盘,终端
-- Unix提供一个基于文件的底层应用接口,即UNIX I/O - 所有输入,输出都是通过读,写文件完成
-- 所有的输入输出都具有统一的表现形式
6.2 文件类型1
- 普通文件(regular file):包含任意数据
-- 从应用程序的角度来看有两种- 文本文件:仅包含ASCII和UNICODE字符
- 二进制文件:除了文本文件以外的所有
-- 从内核的角度来看没有区别
6.3 文件类型2
- 目录(directory)也是一个文件
-- 由有链接(links)构成
-- 每个链接将一个文件名映射到一个文件(或目录)
-- 每个目录至少有两个链接:- . (dot):到文件夹本身的链接
- . . (dot-dot):到上一层文件夹的链接
-- 目录相关指令:mkdir、ls、rmdir等
6.4 目录层级
- Linux内核使用层次化目录来组织所有文件
-- /:代表根目录
-- 每个文件都是根目录直接或间接的后代
6.5 文件类型3
- 套接字(Socket)也是文件,用于跨网络进程交互
- 其他文件类型包括:
-- 命名管道(named pipes)
-- 符号连接(symbolic links)
-- 字符/块设备(character/block devices)
...
6.6 打开文件
- 应用准备访问一个IO设备
-- 内核打开相关文件,并返回一个非负整数,作为文件标识符(file descriptor, fd)- fd代表该文件,用于之后对文件进行操作
-- 内核跟踪记录每个进程的所有打开文件的信息 - 对于每个打开文件,维护一个文件内偏移k
- 应用可以通过seek函数,显式改变当前文件内偏移k
- 应用只需要记录内核返回的文件标识符
- fd代表该文件,用于之后对文件进行操作
6.7 关闭文件
- 不再需要访问文件
- 内核操作如下
-- 释放在文件打开时创建的数据结构
-- 把文件标识符返回到可用的标识符池 - 进程终止时的默认行为
-- 内核关闭所有打开的文件
-- 内核释放内存资源
6.8 读写文件
- 读操作:
-- 从文件中复制m>0个字节到内存中- 从当前文件的位置k开始,并更新k+=m
-- 如果从k开始到文件末尾的长度小于m,触发条件end-of-file(EOF) - EOF可以被应用检测
- 但是文件末尾实际上不存在EOF字符
- 从当前文件的位置k开始,并更新k+=m
6.9 文件:对所有设备的抽象
- 存储设备
-- File - 网络设备
-- socket - 其他设备
-- 同样是fd