OS基础总结

128 阅读22分钟

OS


进程 vs 线程

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位。

  • 进程与线程最大的区别是: 进程拥有自己的地址空间,某进程内的线程对于其他进程不可见,即进程A不能通过传地址的方式直接读写进程B的存储区域。进程之间的通信需要通过进程间通信(Inter-process communication,IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。

  • 进程作为操作系统中拥有资源和独立调度的基本单位,可以拥有多个线程。 通常操作系统中运行的一个程序就对应一个进程。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 相比进程切换,线程切换的开销要小很多。线程于进程相互结合能够提高系统的运行效率。

  • 进程的特点

    • 进程是程序的一次执行过程。若程序执行两次甚至多次,则需要两个甚至多个进程。
    • 进程是是正在运行程序的抽象。它代表运行的CPU,也称进程是对CPU的抽象(虚拟技术的支持,将一个CPU变幻为多个虚拟的CPU)。
    • 系统资源(如内存、文件)以进程为单位分配。
    • 操作系统为每个进程分配了独立的地址空间
    • 操作系统通过“调度”把控制权交给进程。
  • 线程的属性

    • 标识符ID
    • 有状态及状态转换,所以需要提供一些状态转换操作
    • 不运行时需要保存上下文环境,所以需要程序计数器等寄存器
    • 有自己的栈和栈指针
    • 共享所在进程的地址空间和其它资源
  • 进程与线程区别

    • 定义方面:进程是程序在某个数据集合上的一次运行活动;线程是进程中的一个执行路径。(进程可以创建多个线程)
    • 角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是CPU调度的单位。
    • 资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
    • 独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。
    • 开销方面。进程切换的开销较大。线程相对较小。(前面也提到过,引入线程也出于了开销的考虑。)
  • 线程可以分为两类:

    1. 用户级线程:对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。 在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。 在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
    2. 内核级线程:对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。 内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是:内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
  • 事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。


PCB

  • 为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。PCB是进程存在的唯一标志。
    PCB一般包括:
    1. PID(程序ID、进程句柄):它是唯一的,一个进程都必须对应一个PID。PID一般是整形数字。
    2. 特征信息:一般分系统进程、用户进程、或者内核进程等
    3. 进程状态:运行、就绪、阻塞,表示进程现的运行情况
    4. 优先级:表示获得CPU控制权的优先级大小
    5. 资源需求、分配控制信息等

上下文切换

  • 对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是内核在CPU上对进程或者线程进行切换。上下文切换过程中的信息被保存在进程控制块(PCB)中。
  • 从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

系统调用与库函数的区别

  • 系统调用(System call)是程序向系统内核请求服务的方式。 可以包括硬件相关的服务(例如,访问硬盘等),或者创建新进程,调度其他进程等。系统调用是程序和操作系统之间的重要接口。
  • 库函数: 把一些常用的函数编写完放到一个文件里,编写应用程序时调用,这是由第三方提供的,发生在用户地址空间。
  • 在移植性方面,不同操作系统的系统调用一般是不同的,移植性差;而在所有的ANSI C编译器版本中,C库函数是相同的。
  • 在调用开销方面,系统调用需要在用户空间和内核环境间切换,开销较大;而库函数调用属于“过程调用”,开销较小。
  • 库函数是语言或应用程序的一部分,可以运行在用户空间中。而系统调用是操作系统的一部分,是内核提供给用户的程序接口,运行在内核空间中,而且许多的库函数都会使用系统调用实现功能,如在linux下C中的fopen、fclose、fwrite等文件操作函数其底层就是通过open、close、write等系统调用是实现的。没有使用系统调用的库函数,执行效率通常比系统调用高。因为使用系统调用时,需要通过中断进行上下文的切换以及由用户态向内核态的转移。

守护、僵尸、孤儿进程的概念

守护进程: 运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。创建守护进程时有意把父进程结束,然后被1号进程init收养。 🔗
僵尸进程: 一个进程 fork 子进程,子进程退出,而父进程没有wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。
孤儿进程: 一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程收养并对它们完成状态收集工作)。
区分: 一个正常运行的子进程,如果此刻子进程退出,父进程没有及时调用wait或waitpid收回子进程的系统资源,该进程就是僵尸进程,如果系统收回了,就是正常退出,如果一个正常运行的子进程,父进程退出了但是子进程还在,该进程此刻是孤儿进程,被init收养,如果父进程是故意被杀掉,子进程做相应处理后就是守护进程。


分时系统与实时系统的区别

  • 分时系统(Sharing time system) :系统把CPU时间分成很短的时间片,轮流地分配给多个作业。优点:对多个用户的多个作业都能保证足够快的响应时间,并且有效提高了资源的利用率。
  • 实时系统(Real-time system):系统对外部输入的信息,能够在规定的时间内(截止期限)处理完毕并做出反应。优点:能够集中地及时地处理并作出反应,高可靠性,安全性。
  • 通常计算机采用的是sharing time,即多个进程/用户之间共享CPU,从形势上实现多任务。各个用户/进程之间的调度并非精准度特别高,如果一个进程被锁住,可以给它分配更多的时间。而实时操作系统则不同,软件和硬件必须遵从严格的deadline,超过时限的进程可能直接被终止。在这样的操作系统中,每次加锁都需要仔细考虑。

Semaphore(信号量) Vs Mutex(互斥锁)

  • 当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加锁的方式,控制临界区(critical section)的访问权限。 对于semaphore而言,在初始化变量的时候可以控制允许多少个线程/进程同时访问一个临界区,其他的线程/进程会被堵塞,直到有人解锁。
  • Mutex相当于只允许一个线程/进程访问的semaphore。 此外,根据实际需要,人们还实现了一种读写锁(read-write lock),它允许同时存在多个阅读者(reader),但任何时候至多只有一个写者(writer),且不能于读者共存。

逻辑地址 Vs 物理地址 Vs 虚拟内存

  • 逻辑地址,是指计算机用户(例如程序开发者),看到的地址。 例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
  • 另一个重要概念是虚拟内存。操作系统读写内存的速度可以比读写磁盘的速度快几个量级。但是,内存价格也相对较高,不能大规模扩展。于是,操作系统可以通过将部分不太常用的数据移出内存,“存放到价格相对较低的磁盘缓存,以实现内存扩展。 操作系统还可以通过算法预测哪部分存储到磁盘缓存的数据需要进行读写,提前把这部分数据读回内存。虚拟内存空间相对磁盘而言要小很多,因此,即使搜索虚拟内存空间也比直接搜索磁盘要快。唯一慢于磁盘的可能是,内存、虚拟内存中都没有所需要的数据,最终还需要从硬盘中直接读取。这就是为什么内存和虚拟内存中需要存储会被重复读写的数据,否则就失去了缓存的意义。现代计算机中有一个专门的转译缓冲区(Translation Lookaside Buffer,TLB),用来实现虚拟地址到物理地址的快速转换。

抖动(颠簸)、驻留集、工作集

  • 抖动(颠簸):如果多道程度过高,页面在内存与外存之间频繁调度,以至于调度页面所需时间比进程实际运行的时间还多,此时系统效率急剧下降,甚至导致系统崩溃。
  • 驻留集:指请求分页存储管理中给进程分配的物理页面(块)的集合。当一个进程在运行的时候,操作系统不会一次性加载进程的所有数据到内存,只会加载一部分正在用,以及预期要用的数据。其他数据可能存储在虚拟内存,交换区和硬盘文件系统上。被加载到内存的部分就是resident set。
  • 工作集:指在某段时间间隔 ∆ 里,进程实际要访问的页面的集合。

文件系统

  • Unix风格的文件系统利用树形结构管理文件。每个节点有多个指针,指向下一层节点或者文件的磁盘存储位置。文件节点还附有文件的操作信息(metadata),包括修改时间,访问权限等等。
  • 用户的访问权限通过能力表(Capability List)和访问控制表(Access Control List)实现。前者从文件角度出发,标注了每个用户可以对该文件进行何种操作。后者从用户角度出发,标注了某用户可以以什么权限操作哪些文件。
  • Unix的文件权限分为读、写和执行,用户组分为文件拥有者,组和所有用户。可以通过命令对三组用户分别设置权限。

死锁的条件、如何处理死锁

死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。   一般来说,处理死锁问题有三种方法:
    1. 通过协议来预防或避免死锁,确保系统不会进入死锁状态。(银行家算法)
    2. 可以允许系统进入死锁状态,然后检测它,并加以恢复。
    3. 可以忽视这个问题,认为死锁不可能在系统内发生。(为大多数操作系统所采用,包括 Linux 和 Windows)

动态链接库与静态链接库的区别

  • 本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。windows上对应的是.lib .dll linux上对应的是.a .so link.png
  • 静态库是一个外部函数与变量的集合体。静态库的文件内容,通常包含一堆程序员自定的变量与函数,即很多目标文件经过压缩打包后形成的一个文件。之所以成为静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

静态库特点总结:
1. 静态库对函数库的链接是放在编译时期完成的。
2. 程序在运行时与函数库再无瓜葛,移植方便。
3. 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

为什么需要动态库,其实也是静态库的特点导致。

  1. 空间浪费是静态库的一个问题。 动态库1.jpg

  2. 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。 如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

    动态库2.jpg

  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

进程间通信

  • 管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
  • 信号量: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 信号: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 消息队列: 消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存: 共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
  • 套接字: 套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

中断与系统调用

  • 中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保存现场后自动去执行相应的处理程序,处理完该事件后再返回中断处继续执行原来的程序。  
中断{外部中断(硬件中断)内部中断(软件中断、异常){故障(fault陷阱(trap)👈系统调用中断\left\{ \begin{aligned} &外部中断(硬件中断)\\ &内部中断(软件中断、异常)\left\{ \begin{aligned} &故障(fault)\\ &陷阱(trap)👈系统调用 \end{aligned} \right. \end{aligned} \right.

 

  • 中断分外部中断(硬件中断)内部中断(软件中断),内部中断又称为异常(Exception),异常又分为故障(fault)陷阱(trap)。系统调用就是利用陷阱(trap)这种软件中断方式主动从用户态进入内核态的。
    • 外部中断指来自CPU执行指令以外的事件的发生,如外部设备(外设是硬件,所以也叫硬件中断)发出的I/O结束中断,表示设备输入/输出处理已经完成,希望处理机能够向设备发下一个输入 / 输出请求,同时让完成输入/输出后的程序继续运行。时钟中断,表示一个固定的时间片已到,让处理机处理计时、启动定时运行的任务等。
    • 内部中断。是由CPU内部事件所引起的中断,例如进程在运算中发生了上溢或者下溢,有如程序出错,如非法指令,地址越界等。

🔥 中断和陷入的主要区别是信号的来源,看是来自CPU外部,还是CPU内部。

 

  • 中断处理一般分为中断响应中断处理两个步骤,中断响应由硬件实施,中断处理主要由软件实施。
  • 中断处理程序: 当中断发生的时候,系统需要去对中断进行处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序了。
  • 中断的优先级: 中断的优先级也表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理某一中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。

典型的中断优先级:机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断

  • 用户态和内核态: 内核态,所有的指令包括特权指令都可以执行,可以访问任意内存。相应的,用户态的进程能存取它们自己的指令和数据,但不能存取内核指令和数据。

进程的三种状态

  • 阻塞态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用

  • 就绪态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行

  • 执行态:进程处于就绪状态被调度后,进程进入执行状态

    procedure_state_small.PNG

🔥 如果进程运行时间片使用完也会进入就绪状态。

执行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
执行态→就绪态:由于外界原因使运行的进程让出处理器。例如时间片用完,更高优先级的进程来抢占处理器等。
就绪态→执行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了执行态。


进程调度

调度种类

  • 高级调度:主要任务是按照某种算法从外存的后备队列上选择一个或多个作业调入内存,并为其创建进程、分配必要的资源,然后再将所创建的进程控制块插入就绪队列中;
  • 低级调度:又称为进程调度,它决定把就绪队列的某进程获得CPU;
  • 中级调度:引入中级调度的目的是为了提高内存利用率和系统吞吐量。其功能是,让那些暂时不能运行的进程不再占用宝贵的内存资源,而是调其到外存上等候。此时的进程状态为挂起状态。当这些进程重新具备运行条件且内存空闲时,由中级调度选择一部分挂起状态的进程调入内存并将其状态变为就绪状态。  
  • 非抢占式调度与抢占式调度
    • 非抢占式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生进程调度进程调度某事件而阻塞时,才把处理机分配给另一个进程。
    • 抢占式:操作系统将正在运行的进程强行暂停,由调度程序将CPU分配给其他就绪进程的调度方式。
  • 调度策略的设计
    • 响应时间: 从用户输入到产生反应的时间
    • 周转时间: 从任务开始到任务结束的时间
    • CPU任务可以分为交互式任务和批处理任务,调度最终的目标是合理的使用CPU,使得交互式任务的响应时间尽可能短,用户不至于感到延迟,同时使得批处理任务的周转时间尽可能短,减少用户等待的时间。

进程调度算法

 

进程调度算法{先来先服务算法(FCFS最短作业优先算法(SJF高响应比优先算法(HRRF时间片轮转算法(RR最高优先级算法(HPF多级反馈队列算法(MFQ进程调度算法\left\{ \begin{aligned} &先来先服务算法(FCFS)\\ &最短作业优先算法(SJF)\\ &高响应比优先算法(HRRF)\\ &时间片轮转算法(RR)\\ &最高优先级算法(HPF)\\ &多级反馈队列算法(MFQ) \end{aligned} \right.

  链接👉🔗


临界资源与临界区

  • 在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。 典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
  • *对于临界资源的访问,必须是互斥进行。*也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。