OS概述
什么是操作系统
操作系统的两个职责:
对硬件进行管理与抽象
管理硬件:内存分配、设备驱动
对硬件进行抽象:将有限的、离散的资源高效地抽象为无限的、连续的资源。将硬件通过易用的接口提供给上层的
应用,从而使应用无须关心硬件的具体细节。
为应用提供服务并管理
- 服务于应用:提供接口(如系统调用),不同类型的访问控制、应用间交互等服务
- 管理应用:应用生命周期管理,包括应用的加载、启动、切换、调度、销毁等。
狭义的OS:操作系统内核 + Shell(命令行界面)
广义的OS:
- 操作系统内核(对硬件资源的管理与抽象,为操作系统框架提供基础的系统服务)
- 操作系统框架(基于操作系统内核提供的服务,为不同的应用领域提供编程接口与运行环境)
OS发展历史:
批处理操作系统 -> 分时处理操作系统(UNIX), shell命令行交互 -> 个人PC(macOS/Windows),人机交互更好
操作系统接口
- 系统调用接口 向内核申请服务
- POSIX接口 可移植操作系统接口,通常通过C library(libc)来实现。
- 领域应用接口 封装面向不同领域的领域应用接口
硬件结构
计算机硬件结构主要为冯·诺伊曼结构
- 中央处理器 CPU
- 存储器(内存)
- 输入输出 I/O
CPU缓存,比物理内存访问速度快。
设备与中断
内存映射输入输出 MIMO:
把输入输出设备和物理内存放到同一个地址空间,为设备内部的内存和寄存器也分配相应的地址。
轮询与中断
让CPU不断通过MIMO查看是否有输入,但会使CPU长时间处于等待状态造成浪费。
获得输入后,向CPU发送一个中断。
MMIO使得CPU可以主动地访问设备,中断使得设备能够主动地通知CPU,这两种机制是CPU与设备之间交互的重要方式。
内存
进程 Process
与每个进程相关的是地址空间
地址空间中存放可执行程序、程序所需要的数据和栈
进程可以看作是容纳运行一个程序所有信息的容器
进程表: 数组或链表结构,存放进程信息
进程树:一个进程可以创建多个进程(子进程),树形结构
系统管理器授权每个进程一个给定的UID,子进程与父进程拥有一样的UID
地址空间
管理进程,每个进程有一些可以使用的地址集合,典型值从0开始直到某个最大值。一个进程可以拥有的最大地址空间小于主存。
虚拟内存:操作系统把部分地址空间装入主存,部分留在磁盘上,并在需要的时候交换回来
文件
抽象文件模型
创建文件、删除文件、读和写文件 都需要系统调用。
文件描述符: 读写文件前,检查权限可以打开,系统返回一个小整数,供后续操作使用;若禁止使用返回一个错误码
管道: 一种虚文件,可以连接两个进程
系统调用
操作系统的两大功能:
- 为用户提供应用程序抽象
- 管理计算机资源
只有系统调用能够进入内核态而过程调用则不能进行内核态
用于进程管理的系统调用
UNIX中唯一可以在POSIX中创建进程的途径:fork
fork调用返回一个值,在子进程中为0,在父进程中等于子进程的进程标识符PID。使用返回的PID可以看出哪个是父进程和子进程。
POSIX:可移植操作系统接口
waitpid系统调用:为等待子进程完成,父进程执行waitpid
execve系统调用:实现系统执行,三个参数:将要执行的文件名称、一个指向变量数组的指针、一个指向环境数组的指针。
一个shell指令:
1 | cp file1 file2 |
此命令将file1复制到file2文件中,在shell执行fork之后,子进程定位并执行文件拷贝,将将源文件和目标文件的名称传递给它。
cp的主程序包含声明:
1 | main(agrc, argv, envp) |
argc:命令行中参数数目的计数,包括程序名称。对于上面的例子,argc是3。
argv:数组的指针,该数组的元素i为第i个字符串的指针,例如,argc[0]指向字符串cp,argc[1]指向字符串file1,argc[2]指向字符串file2。
envp:指向环境的指针,该环境是一个数组,含有name = value的赋值形式,例子中,没有环境参数传递给execve,所以execve的第三个参数为0。
UNIX中的进程将内存划分为三部分:
- text segment,文本区,例如程序代码
- data segment,数据区,例如变量
- stack segment,栈区域,数据向上增长而堆栈向下增长。
用于文件管理的系统调用
与某个文件有关的系统调用
常用的调用read和write
UNIX为每个文件保存了该文件的类型、大小、最后修改时间以及其他信息,程序可以通过stat系统调用查看这些信息。
用于目录管理的系统调用
与整个文件系统有关的系统调用
mkdir和rmdir分别用于创建和删除空目录。
mount系统调用将两个文件系统合并为一个。
其他系统调用
chdir调用更改当前工作目录
chmod系统调用提供改变文件模式的操作
UNIX与Win32系统调用API
UNIX | Win32 | 说明 |
---|---|---|
fork | CreateProcess | 创建一个进程 |
waitpid | WaitForSingleObject | 等待一个进程退出 |
execve | none | CraeteProcess = fork + service |
exit | ExitProcess | 终止执行 |
open | CreateFile | 创建一个文件或打开一个已有的文件 |
close | CloseHandle | 关闭文件 |
read | ReadFile | 从单个文件中读取数据 |
write | WriteFile | 向单个文件中写数据 |
lseek | SetFilePointer | 移动文件指针 |
stat | GetFileAttributesEx | 获得不同的文件属性 |
mkdir | CreateDirectory | 创建一个新的目录 |
rmdir | RemoveDirectory | 移除一个空的目录 |
link | none | Win32不支持link |
unlink | DeleteFile | 销毁一个已有的文件 |
mount | none | Win32不支持mount |
umount | none | Win32不支持mount |
chdir | SetCurrentDirectory | 切换当前工作目录 |
chmod | none | Win32不支持安全 |
kill | none | Win32不支持信号 |
time | GetLocalTime | 获取当前时间 |
操作系统结构
- 单体系统
- 分层系统
- 微内核
- 客户-服务端系统
- 虚拟机
- 外核
单体系统
整个系统在内核态以单一程序的方式运行
整个操作系统是以程序集合来编写的,链接在一块形成一个大的二进制可执行文件
在单体系统中构造实际目标程序时,会首先编译所有单个过程(或包含这些过程的文件),然后使用系统链接器将它们全部绑定到一个可执行文件中。
三层模型:
- 主程序:调用请求服务程序
- 服务程序:执行系统调用
- 实用程序:辅助服务过程调用
分层系统
使用层来分隔不同的功能单元,每一层只与该层的上层和下层通信。
每一层都使用下面的层来执行其功能,层之间的通信通过预定义的固定接口通信。
层号 | 功能 |
---|---|
5 | 操作员 |
4 | 用户程序 |
3 | 输入/输出管理 |
2 | 操作员-进程通信 |
1 | 存储器和磁鼓管理 |
0 | 处理器分配和多道程序编程 |
微内核
微内核运行在内核态,其余模块可以作为普通用户进程运行。
机制与策略分离,比如系统调度,一个简单的调度算法是对每个进程赋予优先级,让内核执行优先级最高的进程。内核的机制是寻找最高的优先级进程并运行,而策略(赋予进程优先级)可以在用户态中的进程完成。策略与机制分离,使得内核变得更小。
客户-服务器模式
把进程划分为两类:
- 服务器,每个服务器用来提供服务
- 客户端,使用这些服务
两种载体:
- 计算机既是客户端又是服务器
- 客户端与服务器在不同机器上(普遍情况),通过局域网或广域网连接
客户端发送请求并得到回应。
进程与线程
进程
进程:对正在运行中的程序的抽象,操作系统最核心的概念。
伪并发:单核或多核处理器同时执行多个进程,从而使程序更快。以非常有限的时间间隔在程序之间快速切换CPU。
缺点是CPU时间可能分配给下一个进程,也可能不分配给下一个进程。
进程模型
计算机上运行的软件包括操作系统,被组织为若干顺序进程
进程包括:程序计数器、寄存器、变量的当前值
在进程不断切换的过程中,程序计数器也在不同的变化
任何一个给定的瞬间仅有一个进程真正运行
一个CPU只能真正一次运行一个进程的时候,即使有2个核(或CPU),每一个核也只能一次运行一个线程。
进程的创建
- 系统初始化(init)
- 正在运行的程序执行了创建进程的系统调用(比如fork)
- 用户请求创建一个新进程
- 初始化一个批处理工作
系统初始化
前台进程:同用户进行交互并替他们完成工作的进程
守护进程:进程运行在后台用来处理一些活动如Email、web网页、打印等
UNIX中,ps程序可以列出正在运行的进程,Windows中使用任务管理器
系统调用创建
一个正在运行的进程会发出系统调用,用来创建一个或多个新进程来帮助其完成工作。
多处理器中,每个进程运行在不同的CPU上可以使工作更快。
UNIX中,仅有一个系统调用可以创建一个新进程:fork ,该调用会创建一个与调用进程相关的副本。
用户请求创建
交互式系统中,输入一个命令或双击图标可以启动程序,这些操作可以选择开启一个新的进程。
新进程将接管启动它的窗口,每个窗口都可以运行进程。
批处理创建
大型机的批处理系统
用户提交批处理作业,当操作系统决定它有资源来运行另一个任务时,将创建一个新进程并从其中的输入队列中运行下一个作业。
进程的终止
- 正常退出(自愿的)
- 错误退出(自愿的)
- 严重错误(非自愿的)
- 被其他进程杀死(非自愿的)
正常退出
多数进程是由于完成了工作而终止
UNIX调用是exit。
错误退出
例如编译foo.c 但是该文件不存在,编译器会发出声明并退出。
通常会弹出对话框告知用户发生系统错误
严重错误
通常由于程序中的错误导致。例如,执行了一条非法指令,引用不存在的内存,除数为0等。
UNIX中,这类错误,进程会收到信号(中断),而不是在这类错误出现时直接终止进程
被其他进程杀死
UNIX中调用kill,某个进程执行系统调用告诉操作系统杀死某个进程。
进程状态
每个进程是一个独立的实体,有其自己的程序计数器和内部状态。
进程之间需要相互帮助,例如,一个进程的结果可以作为另一个进程的输入
1 | cat chapter1 chapter2 chapter3 | grep tree |
第一个进程cat,将三个文件级联并输出。
第二个进程grep,从输入中选取包含有关键字tree的内容。
可能出现grep准备就绪开始执行,但是输入还未完成,于是必须阻塞grep进程,直到输入完毕。
进程状态的切换:
进程状态有三种:
- 运行态:进程实际占用CPU时间片运行时
- 就绪态:可运行,但因为其他进程正在运行而处于就绪状态
- 阻塞态:除非某种外部事件发生,否则进程不能运行
运行态与就绪态很相似,都表示进程可运行,区别在于就绪状态没有获得CPU时间分片。
阻塞态与前两个不同,进程不能运行,CPU空闲时也不能运行。
在操作系统发现进程不能继续执行时发生转换1,某些系统执行系统调用如pause,获取一个阻塞的状态。UNIX中,当进程从管道或特殊文件(如终端)中读取没有可用的输入时,该进程自动终止。
转换2和3由进程调度程序引起。
进程等待一个外部事件发生(如从外部输入一些数据后),发生转换4;若此时没有其他进程在运行,则立即触发转换3,该进程开始运行,否则该进程处于就绪阶段,等待CPU空闲后再轮到它运行。
操作系统最底层的就是调度程序。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。
进程的实现
进程表 进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状态、所打开文件的状态、调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后可以再次启动,就像从未中断过一样。
关键字段:
- 进程管理
- 存储管理
- 文件管理
线程
线程模型
线程thread,线程也会有程序计数器、寄存器、堆栈。
进程用于把资源集中到一起,线程是CPU上调度执行的实体。
线程比进程创建快。
线程不像进程那样有较强的独立性。由于每个线程都可以访问进程地址空间的每个内存地址,因此一个线程可以读取、写入甚至擦除另一个线程的堆栈。
线程的状态:运行中、阻塞、就绪和终止。线程之间的状态转换与进程之间的状态转换是一样的。
线程无法利用时钟中断强制让线程让出CPU。
线程创建:调用库函数,线程创建函数会要求指定新创建线程的名称,创建的线程通常会返回一个线程标识符,该标识符就是新线程的名字。
线程实现
主要三种实现方式
- 在用户空间中实现线程
- 在内核空间中实现线程
- 在用户和内核空间中混合实现线程
在用户空间中实现线程
把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。
运行时系统:也叫运行时环境,提供了程序在其中运行的环境。
线程在运行时系统之上运行,运行时系统是管理线程过程的集合。
每个进程需要有其专用的线程表,用来跟踪该进程中的线程。
优势:
- 启动线程比内核调用效率更高,不需要切换到内核,也不需要上下文切换,不需要对内存高速缓存进行刷新,线程调用非常便捷,因此效率高
- 允许每个进程有自己定制的调度算法,用户线程具有较好的可扩展性。
劣势:
- 阻塞系统调用问题,使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程
- 缺页中断问题,如果某个程序发生函数调用或跳转指令到了一条不在内存的指令上,会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,称为缺页故障。
- 如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,单进程内部没有时钟中断,不能使用轮转调度的方式调度线程。
在内核空间中实现线程
不需要运行时环境,每个进程中也没有线程表,在内核中会有记录系统中所有线程的线程表。某个线程希望创建新线程或撤销一个已有线程,会执行一个系统调用,完成对线程表的更新。
所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,例如运行同一个进程中的另一个线程或者运行另一个进程中的一个线程。
内核中创建或销毁线程的开销比较大,某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把他标志为不可运行的状态,但其内部结构没有收到影响,稍后在创建一个新线程时,就会重新启用旧线程,标志为可用状态。
混合实现
内核级线程,将用户级线程与某些或全部内核线程多路复用起来。
编程人员可以自由控制用户线程和内核线程的数量,灵活度高。内核只识别内核级线程,对其进行调度,其中一些内核级线程会被多个用户级线程多路复用。
进程间通信IPC
三个问题:
- 进程如何传递消息给其他进程
- 确保两个或多个线程之间不会相互干扰
- 数据的先后顺序问题
竞争条件
存在共享资源、共享文件、共享内存
两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性。
如何避免:
禁止一个或多个进程在同一时刻对共享资源(包括共享文件、共享内存等)进行读写。
需要互斥条件。
临界区
临界区(临界区域):对共享内存进行访问的程序片。
使两个不同进程不可能同时处于临界区,就能避免竞争条件。
使用临界区的互斥
忙等互斥
屏蔽中断
单核处理器,每个进程在进入临界区后立即屏蔽所有中断,并在离开临界区之前重新启用他们。屏蔽中断后CPU不会切换到其他进程。一旦某个进程屏蔽中断后,就可以检查和修改共享内存,而不担心其他进程介入访问共享数据。
屏蔽中断对于操作系统本身是一种有用的技术,但是对于用户线程来说,屏蔽中断不是一项通用的互斥机制。
锁变量
考虑有单个共享的锁变量,初始值为0。当一个线程想要进入关键区域时,会首先查看锁的值是否为0,如果锁的值为0,进程会把它设置为1并让进程进入关键区域;如果锁的状态为1,进程会等待直到锁变量的值为0。
锁变量值为0,表示没有线程进入关键区域,如果为1,表示有进程在关键区域内。
会发生竞争条件,临界区可能会有两个进程同时运行,set-before-check 不是一种原子性操作。
严格轮询法
先抛出一段代码,用c语言编写。
进程0的代码:
1 | while(TRUE){ |
进程1的代码:
1 | while(TRUE){ |
变量turn初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新内存。
忙等待:连续检查一个变量直到某个值出现为止,但是这种方式浪费CPU时间,通常应该避免。
只有在有理由认为等待时间是非常短的情况下,才能使用忙等待。用于忙等待的锁,,称为自旋锁。
可能会出现违反 位于临界区外的进程不得阻塞其他进程 的情况,不算一个好的方案。
互斥量
互斥量mutex,一些共享资源和一段代码保持互斥。
两种状态:解锁和加锁。
当一个线程(或进程)需要访问关键区域时,会调用mutex_lock进行加锁。如果互斥锁处于解锁状态(表示关键区域可用),则调用成功,并且调用线程可以自由进入关键区域。
如果mutex互斥量已经锁定,调用线程会阻塞直到关键区域内的线程执行完毕并且调用了mutex_unlock。如果多个线程在mutex互斥量上阻塞,将随机选择一个线程并选择一个线程并允许它获得锁。