03. 运行环境
操作系统的运行环境
计算机系统的层次结构
现代计算机系统采用分层架构,从底层到顶层依次为:
┌─────────────────┐
│ 应用程序层 │ ← 用户程序
├─────────────────┤
│ 操作系统层 │ ← 系统软件
├─────────────────┤
│ 硬件抽象层 │ ← 驱动程序
├─────────────────┤
│ 硬件层 │ ← 物理设备
└─────────────────┘
各层功能:
- 硬件层:CPU、内存、外设等物理设备
- 硬件抽象层:设备驱动程序,隐藏硬件细节
- 操作系统层:内核,管理硬件资源
- 应用程序层:用户程序,提供具体功能
内核态与用户态
特权级的概念
现代处理器通常支持多个特权级,用于区分不同程序的权限:
1. 特权级分类
Intel x86 架构:
- Ring 0:内核态,最高权限
- Ring 1-2:系统服务层(较少使用)
- Ring 3:用户态,最低权限
ARM 架构:
- EL0:用户态
- EL1:内核态
- EL2:虚拟化层
- EL3:安全监控层
2. 内核态(Kernel Mode)
特点:
- 最高权限级别
- 可以执行所有指令
- 直接访问硬件
- 可以修改系统状态
权限:
- 访问所有内存地址
- 执行特权指令
- 控制中断和异常
- 管理硬件设备
典型操作:
- 内存管理
- 进程调度
- 设备驱动
- 系统调用处理
3. 用户态(User Mode)
特点:
- 较低权限级别
- 只能执行非特权指令
- 不能直接访问硬件
- 受系统保护
限制:
- 只能访问用户空间内存
- 不能执行特权指令
- 不能直接操作硬件
- 受操作系统监控
典型操作:
- 应用程序执行
- 用户数据处理
- 调用系统服务
- 文件操作
状态切换机制
1. 用户态到内核态的切换
触发条件:
- 系统调用
- 中断
- 异常
- 特权指令
切换过程:
用户态 -> 保存用户态上下文 -> 切换到内核态 -> 执行内核代码
具体步骤:
- 保存用户态寄存器状态
- 切换到内核栈
- 设置内核态标志
- 跳转到内核代码
2. 内核态到用户态的切换
触发条件:
- 系统调用返回
- 中断处理完成
- 异常处理完成
切换过程:
内核态 -> 恢复用户态上下文 -> 切换到用户态 -> 继续用户程序
具体步骤:
- 恢复用户态寄存器状态
- 切换到用户栈
- 清除内核态标志
- 跳转到用户代码
内存保护机制
1. 地址空间隔离
用户空间:
- 应用程序可访问的内存区域
- 受操作系统保护
- 每个进程独立
内核空间:
- 操作系统内核使用的内存区域
- 所有进程共享
- 受硬件保护
2. 内存管理单元(MMU)
功能:
- 虚拟地址到物理地址的转换
- 内存访问权限控制
- 内存保护
工作原理:
虚拟地址 -> MMU -> 物理地址
↓
权限检查 -> 允许/拒绝访问
中断与异常
中断(Interrupt)
1. 中断的概念
定义:由硬件设备或外部事件引起的程序执行流程的临时中断。
特点:
- 异步发生
- 可屏蔽
- 有优先级
- 可嵌套
2. 中断的分类
按来源分类:
- 硬件中断:由硬件设备产生
- 软件中断:由程序指令产生
按可屏蔽性分类:
- 可屏蔽中断:可以被 CPU 忽略
- 不可屏蔽中断:CPU 必须立即处理
按优先级分类:
- 高优先级中断:系统关键事件
- 低优先级中断:一般设备事件
3. 常见的中断类型
时钟中断:
- 由系统时钟产生
- 用于时间片轮转
- 定期发生
I/O 中断:
- 由 I/O 设备产生
- 表示 I/O 操作完成
- 异步通知
硬件故障中断:
- 由硬件错误产生
- 需要立即处理
- 系统保护机制
4. 中断处理过程
中断发生 -> 保存现场 -> 切换到内核态 -> 执行中断服务程序 -> 恢复现场 -> 返回
详细步骤:
- 中断检测:CPU 检测到中断信号
- 现场保存:保存当前程序状态
- 模式切换:切换到内核态
- 中断处理:执行相应的中断服务程序
- 现场恢复:恢复被中断程序的状态
- 程序返回:继续执行被中断的程序
异常(Exception)
1. 异常的概念
定义:由程序执行过程中的错误或特殊情况引起的程序执行流程的中断。
特点:
- 同步发生
- 不可屏蔽
- 与程序执行相关
- 需要立即处理
2. 异常的分类
按严重程度分类:
- 故障(Fault):可恢复的错误
- 陷阱(Trap):有意的异常
- 终止(Abort):不可恢复的错误
按来源分类:
- 程序异常:由程序错误引起
- 系统异常:由系统状态引起
3. 常见的异常类型
页错误(Page Fault):
- 访问不存在的内存页
- 访问权限不足的内存页
- 触发虚拟内存管理
除零错误(Divide by Zero):
- 除数为零的除法运算
- 程序错误
- 需要异常处理
非法指令(Illegal Instruction):
- 执行不存在的指令
- 执行特权指令
- 程序错误
栈溢出(Stack Overflow):
- 栈空间不足
- 递归过深
- 缓冲区溢出
4. 异常处理过程
异常发生 -> 保存现场 -> 切换到内核态 -> 执行异常处理程序 -> 决定后续操作
处理结果:
- 恢复执行:修复错误后继续执行
- 终止程序:无法恢复,终止程序
- 系统重启:严重错误,重启系统
中断与异常的区别
特征 | 中断 | 异常 |
---|---|---|
发生时机 | 异步 | 同步 |
可屏蔽性 | 可屏蔽 | 不可屏蔽 |
来源 | 硬件设备 | 程序执行 |
处理方式 | 中断服务程序 | 异常处理程序 |
优先级 | 有优先级 | 无优先级 |
系统调用(System Call)
系统调用的概念
1. 定义
系统调用是操作系统提供给应用程序的接口,允许用户程序请求操作系统内核的服务。
2. 作用
- 资源管理:申请和释放系统资源
- 进程控制:创建、终止、等待进程
- 文件操作:文件的创建、读写、删除
- 设备控制:设备的打开、关闭、控制
- 通信服务:进程间通信、网络通信
3. 系统调用的特点
- 特权操作:需要切换到内核态
- 标准化接口:提供统一的调用方式
- 安全性:受操作系统保护
- 可移植性:不同平台接口一致
系统调用的实现
1. 调用过程
用户程序 -> 系统调用接口 -> 内核态切换 -> 内核服务 -> 返回用户态 -> 用户程序
详细步骤:
- 参数准备:用户程序准备系统调用参数
- 调用接口:通过系统调用接口进入内核
- 模式切换:从用户态切换到内核态
- 服务执行:内核执行相应的服务
- 结果返回:将结果返回给用户程序
- 模式恢复:从内核态切换回用户态
2. 系统调用号
定义:每个系统调用都有一个唯一的编号,用于标识不同的系统调用。
示例(Linux x86-64):
#define SYS_read 0
#define SYS_write 1
#define SYS_open 2
#define SYS_close 3
#define SYS_fork 57
#define SYS_execve 59
3. 参数传递
寄存器传递:
- 系统调用号放在特定寄存器中
- 参数依次放在其他寄存器中
- 返回值放在指定寄存器中
栈传递:
- 参数压入栈中
- 系统调用号放在栈顶
- 返回值放在栈中
常见的系统调用分类
1. 进程管理
进程创建:
pid_t fork(void); // 创建子进程
int execve(const char *pathname, char *const argv[], char *const envp[]); // 执行程序
进程控制:
pid_t wait(int *status); // 等待子进程
int exit(int status); // 终止进程
进程信息:
pid_t getpid(void); // 获取进程ID
pid_t getppid(void); // 获取父进程ID
2. 文件操作
文件控制:
int open(const char *pathname, int flags); // 打开文件
int close(int fd); // 关闭文件
文件读写:
ssize_t read(int fd, void *buf, size_t count); // 读取文件
ssize_t write(int fd, const void *buf, size_t count); // 写入文件
文件定位:
off_t lseek(int fd, off_t offset, int whence); // 文件定位
3. 内存管理
内存分配:
void *brk(const void *addr); // 调整数据段大小
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); // 内存映射
内存保护:
int mprotect(void *addr, size_t len, int prot); // 设置内存保护
4. 进程间通信
管道:
int pipe(int pipefd[2]); // 创建管道
信号:
int kill(pid_t pid, int sig); // 发送信号
sighandler_t signal(int signum, sighandler_t handler); // 设置信号处理
共享内存:
int shmget(key_t key, size_t size, int shmflg); // 创建共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg); // 连接共享内存
系统调用的安全性
1. 参数验证
边界检查:
- 检查参数的有效性
- 验证内存地址的合法性
- 检查权限是否足够
示例:
// 检查文件描述符是否有效
if (fd < 0 || fd >= MAX_FILES) {
return -EBADF;
}
// 检查内存地址是否在用户空间
if (!access_ok(VERIFY_READ, buf, count)) {
return -EFAULT;
}
2. 权限检查
用户权限:
- 检查用户是否有相应权限
- 验证文件访问权限
- 检查系统资源限制
示例:
// 检查文件访问权限
if (!may_access(path, mode)) {
return -EACCES;
}
3. 资源限制
系统资源:
- 检查系统资源是否足够
- 防止资源耗尽
- 实施资源配额
示例:
// 检查内存限制
if (current->mm->total_vm > rlimit(RLIMIT_AS)) {
return -ENOMEM;
}
总结
操作系统的运行环境是理解操作系统工作原理的基础。内核态与用户态的分离提供了安全性和稳定性,中断与异常机制处理了各种突发事件,系统调用则为用户程序提供了访问操作系统服务的标准接口。
这些机制相互配合,共同构成了现代操作系统的运行环境,确保了系统的安全、稳定和高效运行。理解这些概念对于深入学习操作系统的其他内容具有重要意义。