Skip to main content
A cup of beer
  1. Posts/

操作系统学习笔记-ch1-3

·457 words·3 mins

Ch1-导论 #

什么是操作系统 #

  • 在用户视角,操作系统为了方便用户使用
  • 在系统视角,操作系统是控制程序
  • 人们认为操作系统就是一直运行在计算机上的一个程序(称作kernel),其他东西都是应用程序。因此,微软公司曾因内置浏览器而被垄断处罚。

开机过程(简化版) #

位于ROM中的引导程序(bootstrap program)(也称为系统固件)首先运行,先初始化硬件,然后负责找到操作系统并装入内存,然后CPU执行内存中的这一段程序,操作系统逐渐加载。

事件 #

  • 事件通常通过中断实现。
  • 硬件可以直接向CPU提出中断请求。
  • 软件可以提出系统调用来出发中断。
  • 可以认为,现代操作系统是由中断来驱动的,因为中断就是事件。OS经常会等待“事件”的发生。

中断向量是什么 #

  • 中断向量是低地址内存(大约在前100左右的位置)中连续存储了若干中断服务程序的地址。由于是连续存储很多个,所以这片区域构成的~数组~称为中断向量。
  • 中断时,可以通过中断设备号索引到该数组的特定元素,并根据此地址访问到中断服务程序。

虚拟内存的主要优点 #

  • 程序可以比物理内存要大。
  • 程序员在编写程序时,不需要考虑实际内存的物理特征。因为内存已经变成了人们理解的逻辑内存,也就是一个数组。程序员只需要跟这个数组打交道就行,具体的物理实现由OS完成。

死锁 #

两个进程互相无限地等待彼此。

Ch2-操作系统结构 #

shell #

  • shell是独立于内核的一个命令解释程序。当一个任务开始时或用户首次登陆时,其运行。
  • Unix类系统的shell中的每个命令都对应一个文件。例如输入ls时,会自动搜索ls这个文件。
    • 这样子非常方便。因为如果需要增加新命令,只需要增加新文件即可。
    • 解释程序不必非常复杂。

系统调用 #

Introduction #

  • 实际上是(操作)系统调用,是操作系统提供服务的接口。每当需要进行较为底层的操作,或者是用户程序不可能有权利进行的操作,就要用到系统调用,让OS==帮你做==。
  • 在调用层面,使用C编写;在需要直接操作硬件的层面,由汇编编写。
  • 通常,OS每秒执行上千个系统调用。即使是一个简单的程序,实际上也随时在进行系统调用。比如在屏幕上显示任何信息都需要系统调用;去操作任何文件都需要系统调用。
  • 程序员感知不到系统调用,因为它们被封装在对应操作系统所留出的API中。
    • 为何使用API编程?因为可移植性;因为系统调用的细节过多。
  • 每个系统调用都对应一个编号,每个编号对应特定的一种系统调用。例如某程序提出13号系统调用,则先通过某种方式把调用参数传给OS,然后OS找到13号系统调用的对应代码开始执行。

Runtime Library #

  • 运行支持系统。是和编译器一起事先构造好的函数库。其作用是提供系统调用接口,供运行中的程序访问,然后其再代替此程序向OS提出调用请求。相当于是一个==中介==。
  • 每个语言都有自己的Runtime Library,例如 Java SE Runtime。

系统调用时向OS传递参数的三种方法 #

  • 通过寄存器:把参数分别放在寄存器里。但是如果参数个数比寄存器个数还多,就不行了。
  • 通过内存的块和表:将参数放在内存中,然后利用寄存器传递所在内存的块地址即可。
  • 通过堆栈:将参数压入堆栈,并在需要时由OS弹出。
  • 后两个方式的好处是不受传递参数的数量、长度的限制。

系统调用的5大功能 #

  • 进程控制
    • 进程的正常退出和非正常退出:前者称为end,后者称为abort。
    • 程序如果结束(包括end和abort),则会返回其返回值,例如return 0,或非0错误代码给调用者。
    • 程序如果出错,则会abort系统调用,或陷入陷阱。此时内存信息==转储==,产生错误信息,写入磁盘,供调试器使用,便于程序员调试程序。
    • 如果进程出错:
      • 交互式系统会继续读除了此程序外的下一个指令,因为它们假定下一条指令是用户用于处理错误的。
      • 批处理系统会直接跳过此job,执行下一个job。
    • 进程等待:可能进程会等待一个事件(中断)的出现,才具备继续执行的条件,例如打印程序。
    • CPU单步执行模式:即每个指令运行后都会陷入一次陷阱。这种陷阱是为调试程序所用。
    • 程序时间表:支持程序跟踪和定时中断。定时中断的用途同ch1所述,即防止程序死循环。即程序每执行一段时间,CPU都要监督一下。
  • 文件管理、设备管理
    • 系统不允许用户程序对设备进行直接调用
    • 在类Unix系统中,设备当作文件来看待,并和文件使用同一类的调用接口
  • 信息维护
    • 很多系统调用只是为了传递信息。
    • 几乎所有系统都存在请求日期时间、请求当前用户数、操作系统版本等信息的系统调用。
  • 通信
    • 消息传递模型
    • 共享内存模型

系统程序的6大功能 #

  • 文件管理
  • 状态信息
  • 文件修改
  • 程序语言支持
  • 程序装入和执行
  • 通信

设计和实现操作系统 #

机制和策略 #

  • 机制:系统怎么做。就像是提供了一套算法,这个算法的各个细节由很多参数可以动态调整。但是不管这些参数,算法本身也是完整能够正常运行的。
  • 策略:系统做什么。对其中的参数根据实际应用情况进行适配,就是制定策略。
  • 例如CPU定时中断是一种机制,但是定时器具体设置多大的值就是一种策略。

用什么语言编写操作系统? #

  • 早期操作系统采用汇编语言、低级语言编写。现代操作系统采用C等高级语言,外加一点汇编语言(用于最需要速度的地方、或不得不直接与硬件交互的地方,例如设备驱动程序、保存和恢复寄存器内容(即进程的切换))。
  • 高级语言编写系统的优缺点
    • 优点:便于调试和升级、便于移植(例如MS - DOS采用Intel 8088汇编语言编写,所以只支持Intel系列处理器;但是Linux采用C编写,凡是能运行C的地方就有Linux,所以可以在大多数CPU上运行)。
    • 缺点:运行比汇编慢,对内存要求比汇编高。后者在当代已经不成问题,但是前者需要重视。这就需要通过调试操作系统找出性能瓶颈,把这段瓶颈用高效的汇编语言代替即可。

操作系统结构 #

简单结构VS模块化设计 #

  • 简单结构即粗暴地划定在硬件和用户程序之间为一层操作系统内核。操作系统的所有功能都在这里,这导致内核层过于庞大,扩展性维护性极差。例如MS-DOS。
  • 模块化设计的方法之一就是分层法。操作系统由若干层构成,每层负责自己的事情。上层能够调用下层,下层给上层提供服务。缺点是层难以准确一次性给出定义;并且效率较低(因为参数在多层中传递,每走一层都有时间开销。)
  • 现在使用层数较少的分层设计,这样能够体现模块化设计的优点,同时淡化了分层设计的一些缺点。

微内核设计 #

  • 随着Unix系统扩充,其内核逐渐变大,更加难以维护和管理。因此有人提出微内核,即只保留最基本的内核所需要做的功能(通常是最小的进程管理、内存管理、通信功能)。注意,微内核应该保持哪些功能,并没有一个标准的定义。
  • 其他被移出内核的区域以普通进程的方式在系统中运行,和用户进程没有区别。
  • 优点
    • 极佳的可扩展性:如果要添加新功能只需要增加用户程序即可,无需对内核做出改变,以至于影响稳定性。
    • 内核便于维护:即便是要对内核进行一定的修改,也不需要太复杂,因为内核本来就很小。
    • 极佳的跨平台性:同“内核便于维护”,即不需要做太多修改
    • 极佳的稳定性和安全性:由于不同功能由不同的用户进程实现,其中一个功能如果GG了,也不影响其他功能,更不会影响内核的稳定性。(要知道如果内核GG了系统就崩溃了)
  • 缺点
    • 性能下降:功能程序需要使用系统调用与微内核进行通信。
    • 因此微内核并不能对性能带来提升。

内核模块化设计 #

  • 随着现代操作系统采用面向对象语言编写,出现这种结构,即若干功能内核围绕着一个核心内核。这些功能内核可以动态链接到核心内核上来,同时又能相互通信(且不需要走系统调用,因为大家都是内核,只是分了主从而已。)。
  • 即:类似于内核化的微内核架构。这里的核心内核和微内核实现完全相同的功能。只是其他功能改用动态链接的内核模块实现。

系统启动 #

bootstrap程序 #

  • 这段程序能够找到系统loader(在较复杂的系统上),或者直接找到操作系统内核本身。
  • 当CPU加电或重启(即接收到重置事件后),该bootstrap程序启动。
  • 然后进行自检,以及进行初始化(例如CPU、寄存器、内存内容)。
  • 之后它找到对应程序(可以是系统loader,也可以是内核本身)后(现代计算机都是找loader,因为这样比较容易修改系统。如果要更换新系统,只需要重写loader(磁盘引导扇区,0号扇区)即可,不需要管ROM的事情),强行把其所在扇区从硬盘上读进内存并开始执行。

固件 #

  • 我们称放在ROM芯片上的程序为固件,因为它不可重复写入,不受计算机病毒影响,不需要初始化(因为是非易失的)。
  • 固件运行的速度介于RAM和硬盘之间。
  • 嵌入式系统,例如街机,它们的OS直接作为固件。但是现代计算机一般都是bootstrap作为固件,然后系统loader和内核放在磁盘上(这样便于更改)。

系统loader #

  • 具有寻找操作系统内核位置,并装入内存运行之的功能。当内核真正开始运行了,我们说操作系统真正开始启动了。
  • 一般位于引导扇区(0区块)。

引导磁盘或系统磁盘 #

具有系统loader扇区的磁盘称为此。

Ch3-进程 #

批处理和进程设计 #

  • 早期批处理,每个job占用系统全部资源,完全掌控整个计算机系统。
  • 现代计算机采用进程设计,能够使很多进程==并发==(因为只有一个CPU核心,所以不可能是并行的,只可能是并发的)执行,对CPU进行多路复用(由进程切换实现),系统更加高效。
    • 分时设计,能够使得无论何时都永远有进程能够运行,让各IO设备和CPU都不空闲。
    • OS会快速地切换CPU的进程,使得用户感觉可以与任何进程都无障碍地交互。但是实际上它们是时分并发。CPU如何选择下一个要放入CPU的进程的过程,就是进程调度。进程调度通过CPU的“就绪队列”进行。

进程的组成 #

  • 代码段(文本段)又称程序代码。
  • 当前活动:存储了当前PC的值和其他寄存器的值。
  • 堆栈段:~临时~数据
    • 函数参数和局部变量
    • 函数返回地址
  • 堆段:动态创建的数据
    • 例如new出来的新空间
  • 数据段:静态变量、全局变量
    • 数据段和堆栈段的区别是:堆栈段中的数据只要一旦不需要了,就会弹出。相对而言,数据段不具有这个功能,一旦创建就是静态地放在那里。

程序和进程的关系 #

  • 程序是一段可执行的文件。是静态的一段数据。
  • 进程是一个动态的实体,由当前活动+相关资源构成。
    • 当前活动:其对应的PC的值和其他寄存器的值。(注意,CPU中 PC等各寄存器的值会时刻在不同程序之间改变,以实现并发。这由OS完成。)
    • 相关资源:
      • 内存空间资源:即堆栈段、堆段、代码段、数据段、活动段。
      • 打印机等外部设备资源。
  • 程序一旦装入内存开始运行就变成了进程。==进程是现代分时OS的工作单元。==

PCB Process Control Block 进程控制块在==上下文切换==中的作用 #

  • 进程控制块包括了进程的所有信息。当其从CPU中被调出时,进程的所有信息都保存在这个进程PCB中;同样,当CPU要调此进程来执行时,也会先从这个进程的PCB中复制信息以恢复上下文。
  • CPU在两个进程之间切换时,执行上述信息复制操作(即把原有进程的状态信息复制到原有进程的PCB中,并把新进程的状态信息复制到CPU中)。这段复制操作的时间,CPU不能执行其他工作,~所以称为进程切换的时间消耗~。等新进程的上下文信息复制完成,CPU才能开始执行。
  • 请注意,~一个CPU只能运行一个进程,其余进程都可以处在等待或就绪状态。进程的并发通过进程切换实现。PCB的内容足以程序所有的上下文(PID、进程状态、CPU寄存器、资源调度信息、审计信息)。这种进程的切换称为“==上下文切换==。”~
  • 当进程调用exit(),PCB和资源都消失了。
  • PCB的主要内容
    • PID
    • 进程状态:例如ready
    • CPU寄存器信息
      • PC的值
      • 其他寄存器的值
    • 资源调度信息
      • CPU调度信息:进程优先级、调度队列指针等
      • 内存调度信息:内存空间地址
      • IO调度信息:IO设备列表、已打开的文件
    • 审计信息:CPU时间等

进程调度 #

“队列” #

  • 进程调度中的队列实际上都是链表。链表表头由head域和tail域组成,分别指向其下一节点和整个链表的最后一个节点。
  • 链表的节点都是PCB结构体,而非进程本身。PCB结构体的“CPU调度”字段中的指针负责将这个链表连接起来。
  • 当进程调用exit(),由于其PCB被释放,所以链表节点消失,它从任何的队列中都消失不见了。

PCB的角色 #

  • PCB就类似于一个进程的一个代表。它代替这个进程去参与调度,而不必进程这个庞大的个体去跑上跑下。
  • PCB在各个队列之间来回切换,体现了CPU对进程的调度。同一时刻,同一进程的PCB不能出现在两个队列中。

作业队列 #

  • 当你双击一个可执行文件,会产生一个作业。该作业首先放在作业队列里,等待准备就绪,进入ready状态。从而它就可以进入就绪队列。(但是不会从作业队列里消失。)
  • 作业队列包括了系统中所有的进程,但是就绪队列不一定(它只会包括状态为ready的进程。例如正在IO设备队列中等待的进程就不会出现在就绪队列中。)

就绪队列 #

  • CPU从就绪队列中拿进程来执行。而非其他队列。

IO设备队列 #

  • 每个IO设备都不能保证在进程发出请求时是空闲的。如果进程发出请求时,IO设备已经在为其他进程服务,则需要排队。因此对于每个IO设备,都有其相应的一个IO设备队列。
  • 当进程提出IO请求,其PCB被放入IO设备队列,而从其他队列中拿走。

进程执行过程中释放CPU的情况 #

  • 定时器到时,时分并发的时间片结束:则进程被强行切换回ready。(时间片结束。)
  • 中断:则中断处理后,回到ready。
  • 有更高优先级进程抢占CPU:被强行切换回ready。
  • IO请求:会进入IO设备队列。IO完成后回到ready。

长程调度程序(Job调度程序) #

长程调度程序一般用于批处理系统。在现代的分时系统中基本不再需要。

  • 主要工作是从作业池中选择作业,装入内存中等待资源分配。
    • 控制多道程序的程度(内存中进程的数量):尽量保证内存中的进程数量的稳定,以不至于使负荷过低或过高。因此需要控制进程进入内存和离开内存的平均速度大致相同。
    • 控制系统CPU资源和IO资源的均衡分配:有的作业主要占用CPU资源,有的作业主要占用IO资源,因此需要合理分配作业,使得不至于让CPU队列老是空闲(使短程调度程序无事可做),或者不至于让IO队列老是空闲。
  • 对速度要求不高,因为通常情况下得每过5分钟才会有新的作业等待调度。
  • 现代分时系统例如Microsoft Windows,不存在长程调度程序。内存中进程的数量由用户自行负责。如果用户觉得卡了,就会自己关闭一些程序。

短程调度程序(CPU调度程序) #

  • 短程调度程序为在内存中的进程分配CPU资源,使之占用CPU。
  • 要求程序执行速度一定要快。因为进程如果占用CPU100ms,则如果程序需要10ms才能决定应该把哪个进程调度给CPU,那么就已经花费了9%的CPU时间去做调度了。这个损失是极大的。
  • 短程程序和长程程序的主要区别是执行频率。CPU每100ms就要执行一次短程调度,但是可能数分钟也不会进行长程调度。

中程调度程序(Swap交换程序) #

  • 为了改善内存中的~进程组合~,或是某些程序使用了过多内存~导致内存不足~,OS会执行中程调度程序。(0号进程swapper)。
  • 当一个进程被swap,其就从CPU竞争中脱离,从主存被交换到辅存中。
  • 当辅存中的进程被换回主存,它才能回到ready状态。被调度后可以被换出的~中断处继续~其执行过程。
进程概念的新理解 #

进程是可并发执行的程序在数据结构上的执行行为。

进程的5个基本特征 #
  • 动态性
    • 程序的执行包括创建、ready等状态,它是动态的周期。在生命周期中,状态不断转化。
    • 动态性是进程最基本的特征。
  • 并发性
    • 是进程存在的意义。
  • 独立性
    • 进程作为实体可以独立运行,独立获得资源和接受调度(如果是多线程,那么实际上线程才是被调度的基本单位:辎重部队和作战部队。)。
  • 异步性
    • 不同的进程会独立地运行、相互制约(使彼此具有执行的间断性和顺序的不可预知性)
    • 正是多进程的异步性,导致执行结果的不可再现性 —— 因此引入“进程同步”
    • 异步是同步的反义词。大家之间步调不一致,且这个不一致完全不可预知,称为异步。
  • 结构性
    • 每个进程都由PCB描述。具有一定的内存结构。
关于进程的异步性,可以参考此程序理解 #
#include <stdio.h>
main(){
    pid_t pid;
    pid=fork();//创建完子进程,以fork()函数返回继续执行。
    if(pid==0)//如果是子进程就执行
        printf("hello\n");
    printf("world\n");//大家都执行
    return 0;
}
  • 以函数fork()返回继续执行:父进程就正常返回正常继续了,子进程假装自己也执行完fork了也假装自己正常返回,同样继续。返回的时候顺带赋值。
  • 通常用pid来区分父进程或子进程执行的代码。
  • 这段程序输出结果,两个world和一个hello,但是这三个单词是随机排列的,其结果可能分布在三个单词的一个全排列里面。==这是因为没有人知道并发的进程具体会以什么顺序执行。==
程序缓冲区 #
  • IO缓冲区只有当遇到==换行符('\n')或者主动调用flush或者程序退出或者缓冲区真的满了==才会被清空并输出。大小一般是==1KB==。
  • 文件缓冲区同理。另外,如果关闭文件,那么也会被清空并输出。因此一定要使用完文件就去关闭它。否则在一些较老的OS上,你可能输出失败。因为这些OS直到程序运行结束也不会检查是否还有未关闭的文件。这导致程序退出后,缓冲区的数据仍然在缓冲区,并且再也写不到文件里去了。另外,关闭文件还用于释放系统分配的资源,例如文件描述符、文件控制块等。
#include <stdio.h>
main(){
    printf("hello world!");
    pid=fork();
}
  • 会输出两次hello world。因为子进程继承了父进程的缓冲区。
系统调用:pause() #

这个调用使程序进入阻塞状态,等待特定的信号。

进程的状态 #
  • New
    • 进程创建了,但是没有完全创建
    • 相对于ready而言,现在这个状态下程序还不能运行起来,因为PCB、栈等空间、PC的值都没有完成初始化或被分配。
    • 相当于我刚来IR Lab,没有桌牌,没有门卡,只是代表有这个人了,但是关于这个人的一些资源还没有到位。
    • 在Nachos系统中,进程的一些成员刚刚被定义,但是仍然没有被初始化。在其源代码中,这些成员变例如stackTop、stack等的初始值是NULL。因为要等待系统分配。
  • 从Ready到Running的情况
    • 进程调用fork(),使得其跑起来.
  • 从Running到Sleep的状态
    • 调用sleep(). 此时一般是在等待某个事件的发生.
  • 从Running回到Ready的情况
    • 进程可以调用Yield()来让出CPU.(如果此时还有其他进程等着执行的话)
    • 其他情况(pad版本上面已经描述)
Linux系统中的一些有意思的状态标志 #
  • TASK_UNINTERRUPTIBLE
    • 标志这个进程正在执行,除非等到了一个特殊事件的发生,否则其执行不允许被打断.
    • 保证这段代码执行的完整性.
  • TASK_TRACED
    • 当一个程序员正在调试代码,他让这个程序暂停运行了.(可恢复)
  • EXIT_ZOMBIE
    • 僵尸进程.等待父进程回收之.
  • EXIT_DEAD
    • 当僵尸进程被回收完成,这个进程被系统删除.称为DEAD,
PCB #
  • 进程和PCB是一一对应的.

  • PCB虽然代替进程在各个调度队列中游走,但是实际上只有它自己一个.

  • 系统PCB的总数代表了系统的并发度.

系统调度的PCB结构 #
  • 链接结构:PCB真的在各个链表之间穿梭,例如阻塞链表,就绪链表…
  • 索引结构:每个调度队列只记录PCB存储所在地址.进行了间接的链接.在这种结构中,PCB表甚至可以同时参与多个队列(只是理论可行,实际上如果真这样那么调度就乱了).
  • 注意,只有Ready队列中的进程是分配好了资源,已经迁移到主内存中了的.他们只是在等待CPU.
子进程的课本知识 #
  • PCB是进程存在的唯一标志.CPU通过PCB感知进程的存在.
  • 进程通过系统调用得已创建子进程.
  • Unix系统中系统启动与进程的关系
    • 系统有一个root进程.当系统引导时,0号进程作为root的一个子进程出现.
    • 0号进程使用fork()创建1号进程(init进程),然后0号进程自己变成swapper进程.可见,0号进程时唯一一个不适用fork()系统调用创建的进程.
    • 1号进程称作init,就是因为它为每个用户创建用户进程.当用户登录系统时,就为它创建一个用户根进程.其他用户来了也一样.因此init下面挂了若干用户根进程.
    • 用户程序都是上述其用户根进程的子进程.系统所有的进程的祖先都是init进程.包括用户根进程.
  • 进程必然由一个父进程创建而来。因此产生了父子结构,这是一棵树。
  • 子进程的资源可以由OS直接分配,也可以是来自父亲的一部分。有的OS为了防止父进程创建过多子进程导致系统资源瞬间枯竭,会强迫子进程的资源只能来自于其父亲。
  • 当进程创建一个子进程,其子进程的行为可以是:
    • 子进程和父进程并发执行
    • 子进程自己执行,而父进程等待子进程执行完成后回收此僵尸进程.
  • 新进程的地址空间可以是:
    • 可以是父亲地址内装着的东西的副本
    • 也可以是自己又调用exec()从而将新的二进制程序放入内存空间进行执行(不再使用父亲的程序.这种情况下父子进程各司其职了属于是.)
父进程创建子进程后的关系 #
  • 三种可能的资源分享关系

    • 父子可以共有原有的资源
    • 子的资源可以是父亲资源的固定的一部分子集
    • 父亲可以拒绝和子分享自己的资源
  • 两种可能的内存关系

    • 子可以直接复制父亲的内存内容
      • 注,虽然内存是这样的,但是他们还是分了不同的内存空间去各自执行各的.他们之间只是内容上的相同.
    • 子可以自己装入新的内容,执行自己的程序
  • 两种可能的执行关系

    • 父子可以并发执行(上阵父子兵)
    • 父可以一直等待子执行完毕(成吉思汗在帐篷里等其4个儿子凯旋而归)
系统调用 :fork() #

系统调用的作用就是新建一个PCB和新划定一段内存空间,然后初始化他们.

父亲创建子进程的过程就是先继承再独立的过程。继承是造孩子的过程,独立是造完了,则孩子降生了,继续自己执行自己的程序了。

如果fork()失败,则返回-1.因为进程PID都是正整数。所以可用 if(pid<0)来判断是否创建成功。

为了防止子进程的嵌套创建,一般如果需要多次fork是不允许写在一起的,一般要判断 if(pid!=0),确保在父进程的执行范围内继续fork。否则子进程也会执行fork,就形成了嵌套创建,会==非常混乱==。

  • 为子进程新建一空PCB,然后:

    • 为子进程分配一个新的PID

    • 为子进程分配一段专属的地址空间

  • 然后,把父亲的内存空间复制到新的地址空间.把父亲的PCB复制到子的PCB中.

    • 即完全克隆一个父进程.
    • 注意:由于PCB表被复制,所以其中的打开的文件,CPU各寄存器的数值,临时变量等都是完全相同的.可以认为,子进程除了可以和父进程执行不同的程序,在初始状态下是完全克隆! 这个完全克隆涉及到程序运行的方方面面,让程序根本感受不到自己搬家了.
    • 代码可能不会复制,因为子进程往往都是用来做和父进程不同的事情的。因此子进程往往只取父进程的代码的一个==引用==。
    • PID是非零的正整数。
fork前后,需要注意的几个继承点 #
  • PCB
  • 变量
  • 打开的文件和设备(利用文件描述符实现)
    • 父亲打开了,子也打开了。
  • 缓冲区
    • 如果不清空缓冲区,父亲调用printf然后子进程被创建,则子进程又会printf和父亲缓冲区中相同的内容。
int value=5;
int main(){
    pid_t pid;
    pid=fork();
    if(pid==0)value+=15;//子进程执行
    else if(pid>0){
        wait(NULL);//等待子进程执行结束,回收子进程
        printf("parent:value=%d",value);
        exit(0);
    }
}

函数输出:

5
  • 这就说明了:父亲和子的变量是存在于两个空间中的独立的。虽然刚开始的继承值一样,但是当孩子降生,他们就完全分离了。
继承和分离 #

父亲复制自己的内容给孩子,这一过程称为继承。一切都复制完成,孩子诞生了,并且在继承来的数据基础上修改成为自己的东西,称为分离。

  • 文件描述符
    • 文件描述符的分离意味着分离后,父进程和子进程的文件管理完全分离了,它们分别独立地操纵文件。
    • 注意,虽然是独立操纵文件,但是仍然因为并发进程的异步性,如果对同一文件操作还是会产生干扰的。
  • 局部变量和缓冲区
父进程对子进程的回收 #
  • 子进程把其执行结束的返回值(例如return 0)传递给父进程.(这个过程就叫做回收了.)
  • 系统释放子进程的一切资源.(只有当子进程被父进程回收了,其PCB表才被删除.)
  • 除了子进程主动调用exit(),父进程也可以主动结束掉自己的子进程.(注意,只有父进程能结束自己的子进程.否则各个进程就可以随便结束彼此了,就乱套了.)
如果父进程提前终止 #
  • 级联终止:例如VMS系统,如果检测到父进程结束了,则其子进程也会跟着结束.
  • 孤儿院:在类Unix系统中,如果父亲进程提早结束,则子进程会直接归为1号进程(init进程)的子进程.
进程挂起 #
  • 进程挂起有两种方式:就绪挂起状态Ready Suspended和阻塞挂起状态Blocked Suspended.从原来五状态的基础上增加这两种状态,就变成了七状态图.

  • 进程挂起指的是PCB等一切数据都从内存清退,从而全部转存到外存.当需要激活时,再把它从外存中读取进来.(这里注意,挂起的反义词是激活,然而阻塞的反义词是唤醒.)

  • 关于阻塞挂起状态和就绪挂起状态

    • 就绪挂起状态可以从就绪状态转换,也可以直接从执行状态转换.这就相当于程序正在正常执行,突然被挂起了,或者在Ready等待执行,但是突然被挂起了.虽然在大多数情况下挂起一个阻塞状态进程更加合理,但是如果内存实在不够用也只能这样了.
    • 阻塞挂起状态只能从阻塞状态转化而来.这种情况下一般是内存不足的情况下,阻塞的进程又不能运行,只能空空占有内存,则让他们让出位置.(反正又执行不了)
    • 阻塞挂起状态和就绪挂起状态之间可以互相转化.实际上这种转化和阻塞状态向就绪状态之间的转化是一样的.即IO结束或事件发生时.
  • 进程挂起和阻塞的区别

    • 阻塞一般是不得已而为之.因为它需要等待再次具备执行条件的发生.例如网络IO,你必须等对方回传信息.但是挂起是主动的.
    • 阻塞是对CPU的释放,因为它现在不具备继续执行的条件了,把机会让给别人.但是挂起是属于"占着CPU不干活.“如果这个挂起的进程具有足够高的优先级,那么其他进程怎么也不没法使用它的这块CPU权力了.
    • 阻塞进程仍然接受OS的调度,因为他要等待分配空间.但是OS在调度时会直接忽略挂起的进程.因此可以认为只要挂起了,进程调度就不管他了.
  • 挂起的应用场景

    • 定时任务:如果不执行就可以直接挂起,节约内存.
系统调用:exec()系列函数 #
  • exec的本意是“execute”,即所谓“exe”,即将一段指定的可执行程序覆盖掉子进程原来的执行代码。

  • 它和函数调用的区别是:==老代码直接被覆盖了。它不可能再“return”了==。即:调用完成后也不能返回调用它的代码继续跑了。

    • 这就意味着,只要是写了exec(),其下方的代码就写了白写了
  • 如果exec失败,那么exec()函数在此进程中返回-1.

  • 注意:由于exec之后已经在跑一段全新的程序了,所以在调用后,==其原来拥有的上下文都不能再合法访问了,除了传递给新程序的参数==。

系统调用:wait() #
  • 父进程调用wait,是希望与子进程==终止同步==。所谓终止同步,就是父亲==不想管==这个儿子了,他希望他进入DEAD状态。
  • 如果父亲调用wait()时,父进程还尚未创建过子进程,则wait()函数没有实际作用,==直接返回==.
  • 如果父亲调用wait()时,父进程创建过子进程:
    • 如果子进程已经结束工作,则子进程把其执行时间等审计信息累加到父进程的PCB表中(==子债父还==),然后系统回收PCB等资源(子进程 进入DEAD状态)
    • 如果子进程尚未结束执行,则父进程进入Blocked状态,专门等待子进程执行完毕再说.(==成吉思汗等儿子==)