前言
本文只用于顽固和复习(预习)操作系统基础内容,不具备原创性。
操作系统概念
这一部分有点像是看历史故事
1946年,世界第一台计算机问世。它重30吨,170平方米,十分巨大,同时他是没有任何操作系统的。这种计算机设计出来就是协助科学家进行复杂计算。因为没有操作系统,当时的人为了操作这台电脑,需要很多人力成本,而且为了操作人也要学习很多专业知识。
因此操作系统的其中一个目的就是为了对计算机硬件资源进行管理、分配和调度。
有了操作系统后,一台计算机里面的某个程序需要多少内存,要分配哪个内存空间等等问题用户都不需要关心也不用去学习那些底层专业知识。
简单地说,操作系统将所有的底层硬件封装成一个黑盒子,让普通人也能够简单操控。
早期的操作系统Disk Operating System,DOS(磁盘操作系统)跟今天的cmd命令行没什么区别,这种操作系统虽然比其1946年的计算机要好很多,不需要底层专业知识就可以操控计算机,但是这样的CLI(命令行界面)必定无法符合大部分用户的口味,因此到了今天,推行界面(GUI)成了主流。
所以操作系统的另一个目的就是为用户提供一个友好、清晰且简单的操作界面(专业叫法是壳 Shell)。用户通过 Shell 执行该操作系统提供的所有的功能。对于命令行来说,就需要提供足够多的命令;对于图形界面来说,就需要提供足够多数量的按钮。
计算机硬件介绍
处理器
处理器,也叫CPU,可以说是计算机的大脑。CPU 从内存中提取指令并执行它
一句话概括就是一个程序需要放入内存并给它分配 CPU 才能执行
指令周期
是指计算机从取指到指令执行完毕的时间
一般分以下几个步骤:
- Fetch(取指),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中,然后把 PC 寄存器自增,好在未来执行下一条指令。
- Decode(译码),也就是根据指令寄存器里面的指令,解析成要进行什么样的操作,是 R、I、J 中的哪一种指令,具体要操作哪些寄存器、数据或者内存地址。
- Execute(执行指令),也就是实际运行对应的 R、I、J 这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。
在取指令的阶段,我们的指令是放在存储器(也就是内存)里的,而通过 PC 寄存器和指令寄存器取出指令的过程,是由控制器(Control Unit)操作的。指令的解码过程,也是由控制器进行的。一旦到了执行指令阶段,无论是进行算术操作、逻辑操作的 R 型指令,还是进行数据传输、条件分支的 I 型指令,都是由算术逻辑单元(ALU)操作的,也就是由运算器处理。但如果是一个简单的无条件地址跳转,那么我们可以直接在控制器里面完成,不需要用到运算器。
CPU周期
CPU周期亦称机器周期,在计算机中,为了便于管理,常把一条指令的执行过程划分为若干个阶段,每一阶段完成一项工作。
时钟周期
时钟周期也称为振荡周期,定义为时钟频率的倒数。时钟周期是计算机中最基本的、最小的时间单位。在一个时钟周期内,CPU仅完成一个最基本的动作。
周期之间的关系
对于一个指令周期来说,我们取出一条指令,然后执行它,至少需要两个 CPU 周期。取出指令至少需要一个 CPU 周期,执行至少也需要一个 CPU 周期,复杂的指令则需要更多的 CPU 周期。而一个CPU周期是若干时钟周期之和。
存储器
理想状态下,存储器应该是非常快速的。比执行一条指令要快,从而使得 CPU 的执行效率不会收到存储器的影响,而且足够大且非常便宜
但很遗憾,目前没法同时实现这三个目标。所以现在的存储器系统采用一种分层次的结构
顶层的存储器速度最快、容量最小、成本最高,越往下层存储器的速度越慢、容量越大、成本也越低

存储器之所以快,是因为它的材料跟CPU是一样的,所以它和访问CPU比起来基本是没有时延。
缓存。
当CPU要读取一个数据时,首先会从CPU缓存中查找,找到立刻读取并送给CPU处理,若是没有找到就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,让以后对这块数据的读取都可以从缓存中进行,不需要再调用内存。
内存
内存,也叫做主存通常被称为随机访问存储器(Random Access Memory,RAM)。所有不能在缓存中得到满足的访问请求都会转往内存。除了RAM外,许多计算机还有少量的ROM(Read Only Memory,非易失性随机存取存储器),跟RAM不同,在断电后ROM并不会丢失内容,其中的内容一旦存储后就不会在被修改,而且ROM不但非常快还很便宜。
硬盘
硬盘容量很大,唯一的问题就是随机访问数据的时间比内存大约慢了三个数量级,其低速的原因是因为磁盘是一种机械装置并且拥有一种特殊的构造

一个硬盘中有一个或者多个金属盘片,它们以5400、7200、10800rpm或更高的速度旋转。从边缘开始有一个机械臂悬横在盘面上(跟唱片机很像,特别是那种黑胶唱片机)。信息会写在磁盘一系列的同心圆上,在任意一个给定臂的位置,每个磁头可以读取一段环形区域,称之为磁道(track)。把一个给定臂位置上所有磁道合并起来就组成了一个柱面(cylinder)
每个磁道划分出若干个扇区,扇区的值是512字节。现代磁盘中,较外部的柱面比较内部的柱面有更多的扇区。机械臂从一个柱面移动到相邻的柱面大约需要 1ms。而随机移到一个柱面的典型时间为 5ms 至 10ms,其具体时间取决于驱动器。磁臂到达正确的磁道上后,驱动器必须等待所需的扇区旋转到磁头之下(这大概需要 5ms 至 10ms 的时延),再开始读写,低端硬盘的速率是 50MB/s,而高速磁盘的速率是 160MB/s。

I/O设备
I/O设备一般包括两个部分:设备控制器和设备本身
设备控制器就是一块芯片或者一组芯片,它能够接收操作系统的指令并控制物理设备。
I/O 设备另一部分是设备本身,设备本身有一个相对简单的接口,这是因为接口既不能做很多工作,而且也已经被标准化了,标准化后任何一个磁盘控制器就可以适配任意一种磁盘
一句话简单说明:I/O设备就是字符设备【人机交互设备,键盘、显示屏打印机等都是这一类】、块设备【外部存储器,如磁盘磁带光盘等】、网络通信设备【网卡、调制解调器等】
四个特征
操作系统拥有 4 个鲜明的特征:并发、共享、虚拟和异步。其中并发和共享是最基本特征,二者互为存在条件
并发
- 并发:指宏观上在一段时间内能够同时运行多个程序
- 并行:指同一时刻能运行多个指令,指两个或多个事件在同一时刻同时发生
1 | 举个例子: |
顺便说说经常看见的单核和多核
单核CPU:同一时刻只能执行一个程序,各个程序只能并发执行
多核CPU:同一时刻可以同时执行多个程序,多个程序可以并行执行
所以所谓的四核其实就是并行地执行4个程序
共享
共享即资源共享,指系统中地资源可供内存中多个并发执行的进程共同使用
共享一共有两种主要方式
- 互斥共享方式:系统中的某些资源,虽然可以提供给多个进程使用,但是 一个时间段内,只允许一个进程访问
- 同时共享方式:系统中的某些资源,允许一个时间段内,多个进程”同时”对该资源进行访问
1 | 举个例子: |
并发和共享的关系
仍然是上面的那个例子,QQ发送A,微信发送B
- 两个进程正在并发的执行(并发性)
- 需要共享的访问硬盘资源(共享性)
如果失去并发性,则系统只有一个进程在运行,那么共享性就没有意义。
如果失去共享性,则QQ和微信不能同时访问硬盘资源,就无法同时发送文件,即不能并发。
这就是并发性和共享性互为存在条件的原因。
虚拟
虚拟是指把一个物理上的实体变为若干个逻辑上的对应物。其中物理实体是实际存在的,逻辑上对应物是用户感受到的。
1 | 举个例子: |
所以虚拟技术又可以分为:
- 空分复用技术,如虚拟存储器技术
- 时分复用技术,如虚拟处理器
显而易见的是,如果失去了并发性,就失去了实现虚拟性的意义。因此,没有并发性,就谈不上虚拟性。
异步
异步是指在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。
1 | 举个例子: |
只有系统用户并发性,才有可能导致异步性。所以只有系统用户并发性,才有可能导致异步性。
进程管理
进程就是程序的一次执行过程,它是暂时的。不仅包含正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,比如说 CPU、内存、网络资源等
1 | 举个例子: |
一个进程中可以有多个线程,它们共享进程资源
1 | 举个例子: |
进程主要包含两个内容:
- 进程通信
- 进程调度
进程通信
进程通信就是进程之间的相互交流。CPU 作为计算机最宝贵的资源,一个进程需要放入内存并给它分配 CPU 才能执行,而一个程序可能包含多个进程,这就需要进程之间进行相互交流,彼此同步,共同完成这个程序。
1 | 举个例子: |
进程调度
通常情况下,会有多个进程或者线程同时竞争CPU,如果恰好只有一个CPU克用,这就导致CPU必须对下一个运行的进程或线程做出选择,这个选择的过程就是进程调度。
内存管理
高速缓存作为除寄存器外最底层的存储器,其管理是由硬件完成的。
内存一般用来保存正在执行的程序,在非常简单的操作系统中,内存中每次只能运行一个程序,如果要运行第二个程序,第一个程序就必须被移除内存,再把第二个程序装入内存。但这样效率很低,因此引入了虚拟内存技术,把内存作为高速缓存来使用,只用来保存最频繁使用的部分程序,把程序的大部分放在磁盘上。
所以内存管理就只做以下两件事:
- 把使用频繁的部分程序放入内存
- 当内存满的时候,替换掉内存中的某些部分
文件系统管理
文件其实是进程创建的信息逻辑单元,一个磁盘可能含有几千甚至几百万个文件,每个文件都是独立于其他文件的(事实上可以把每个文件理解成一个地址空间)
文件同样是受操作系统管理的,有关文件的构造、命名、存取、使用、保护、实现和管理方法都是操作系统设计的内容
I/O设备管理
操作系统必须高效的管理 I/O 设备,它需要向 I/O 设备发送命令,捕捉中断,并处理设备的各种错误,它还应该在设备和系统的其他部分之间提供简单且易用的接口
内核态和用户态
现代操作系统为了更好的处理系统的并发性、共享性等,并使进程能够协调地工作,仅依靠计算机硬件提供地功能是远远不够地。因此我们需要利用软件对硬件资源进行改造,而这个软件就是内核
内核就是操作系统中的一组程序模块,作为可信软件来提供支持进程并发执行的基本功能和基本操作,具有访问硬件设备和所有内存空间的权限。
内核态(kernel mode)
当 CPU 处于内核态时,这是操作系统管理程序(也就是内核)运行时所处的状态。运行在内核态的程序可以访问计算机的任何资源,不受限制,为所欲为,例如协调 CPU 资源,分配内存资源,提供稳定的环境供应用程序运行等
用户态(user mode)
应用程序基本都是运行在用户态的,或者说用户态就是提供应用程序运行的空间。运行在用户态的程序只能访问当前 CPU 上执行程序所在的地址空间,这样有效地防止了操作系统程序受到应用程序的侵害
什么样的程序应该放在内核态呢
这取决于对资源的需求、时间的紧迫和效率高低等因素。比如 CPU、内存、设备等资源管理器程序应该在内核态运行,否则安全性没有保证。对于文件系统和数据来说,文件系统数据和管理必须放在内核态,但是用户的数据和管理可以放在用户态。
中断机制
在合适的情况下,操作系统的内核会把 CPU 的使用权主动让给应用程序,也就是使 CPU 从内核态转换到用户态。而 CPU 要想从用户态回到内核态,只能通过中断机制完成,如果没有中断机制,那么一旦应用程序上 CPU 运行(用户态),CPU 就会一直运行这个应用程序。
一句话概括:中断是让操作系统内核夺回 CPU 使用权的唯一途径。也可以说操作系统是由中断驱动的
中断机制非常广义,包括了下述三种手段
- 程序请求操作系统服务,执行系统调用
- 程序运行时产生外中断事件(比如 I/O 操作完成),运行程序被中断,转向中断程序处理
- 在程序运行时发生内中断(异常)事件,运行程序被打断,转向异常处理程序工作
除了三种手段外,中断也分为两种类型
- 外中断(也称中断,狭义上的中断,也叫硬中断)
外中断与当前执行的指令无关, 中断信号来源于 CPU 外部。如 I/O 完成中断,表示设备输入/输出处理已经完成,CPU 能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等
这个中断的流程如下:- 外设 将中断请求发送给中断控制器
- 中断控制器 根据中断优先级,有序地将中断传递给 CPU
- CPU 终止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中
- CPU 根据中断向量,从中断向量表中查找中断处理程序的入口地址,执行中断处理程序
- CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行
- 内中断(也称异常、例外,也叫软中断)
内中断与当前执行的指令有关, 中断信号来源于 CPU 内部。如非法操作码、地址越界、算术溢出,除数为 0 等
流程如下:- 无
- 无
- CPU 终止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中
- CPU 根据中断向量,从中断向量表中查找中断处理程序的入口地址,执行中断处理程序
- CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行
中断机制原理
不同的中断信号,肯定是需要用不同的中断处理程序来处理的。那么当 CPU 检测到中断信号后,就会根据中断信号的类型去查询中断向量表,以此来找到相应的中断处理程序在内存中的存放位置

系统调用
系统调用(Syscall) 是一种软中断处理程序,用于让程序从用户态陷入内核态,以执行相应的操作。
隔离
处于安全性和稳定性考虑,用户空间(用户态)程序无法直接执行内核代码(例如:I/O 读写、创建新进程/线程),也无法访问内核数据,必须通过系统调用。
系统调用的作用
当发生系统调用时,会让程序从用户态陷入内核态,以执行相应的操作。
详细地说,就是操作系统作为计算机硬件之上的第一层软件,需要向上层提供一些简单易用的服务,这个上层包括用户和应用程序:
给用户提供的接口有图形界面 GUI 和命令接口,给应用程序提供的是程序接口,这个程序接口就是由一组系统调用组成的,是操作系统提供给开发人员使用的。可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务。
1 | 举个例子: |
流程
- 程序从用户态陷入内核态
- 根据系统调用号,在系统调用表中查找对应的系统调用函数的内存地址,执行系统调用函数
- 程序从内核态返回用户态
功能分类
- 设备管理。完成设备的请求或释放,以及设备启动等功能。
- 文件管理。完成文件的读、写、创建及删除等功能。
- 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
- 进程通信。完成进程之间的消息传递或信号传递等功能。
- 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
线程和进程
- 计算机的核心是 CPU,它承担了所有的计算任务,就像一座工厂,时刻在运行。
- 工厂的电力是有限的,一次只能提供给一个车间使用。也就是说一个车间工作的时候,其他车间都是停工状态,这就是单个CPU一次只能运行一个程序。
- 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态
- 一个车间里,可以有很多工人。他们协同完成一个任务
- 线程就好比车间里的工人。一个进程可以包括多个线程
- 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存
- 但是每个房间的大小是不同的,有些房间只能容纳一个人。这表示一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存【简单来说就是厕所有人,你要在外面排队】
- 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域
- 也有一些房间可以容纳多人,这代表某些内存区域,只能供给固定数目的线程使用
- 这时的解决方法,就是在门口挂 n 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做 “信号量”(Semaphore),用来保证多个线程不会互相冲突
进程
进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位
简单地说,进程就是程序一次执行过程
程序是静态的,他作为系统中的一种资源是永远存在的。进程是动态的,它是动态的产生、变化和消亡,拥有属于自己的生命周期
1 | 举个例子: |
进程不仅包含正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,比如说 CPU、内存、网络资源等
进程的组成
进程主要由三个部分组成
- 进程控制块PCB
- 进程描述信息
- 进程控制和管理信息
- 资源分配清单
- CPU相关信息
- 数据段(进程运行过程中各种数据)
- 程序段(程序的代码)
1 | 举个例子: |
PCB 是提供给操作系统用的,而程序段、数据段是给进程自己用的。
进程控制块PCB
每一个进程都有且仅有一个进程控制块(Process Control Block,PCB),也叫进程描述符。他是进程存在的唯一标识,也是操作系统用来记录和刻画进程状态及环境信息的数据结构,更是操作系统掌握进程的唯一资料结构和管理进程的主要依据。
简单地说,操作系统需要对各个进程进行管理,但凡管理时所需要的信息,都会被放在 PCB 中,PCB 是进程存在的唯一标志。创建进程和撤销进程等都是指对 PCB 的操作,当进程被创建时,操作系统为其创建 PCB,当进程结束时,会回收其 PCB。
PCB包含四类信息:
- 进程描述信息:用来让操作系统区分各个进程
- 当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的 “身份证号”— PID(ProcessID,进程 ID)
- 另外,进程描述信息还包含进程所属的用户 ID(UID)
- 进程控制和管理信息:记录进程的运行情况【CPU的使用事件、磁盘使用情况、网络流量使用情况等】
- 资源分配清单:记录给进程分配了哪些资源。【分配了多少内存、正在使用哪些 I/O 设备、正在使用哪些文件等】
- CPU相关信息:进程在让出 CPU 时,必须保存该进程在 CPU 中的各种信息【各种寄存器的值。用于实现进程切换,确保这个进程再次运行的时候恢复 CPU 现场,从断点处继续执行】
进程的状态
虽然每一个进程都有自己的PCB和内部状态,但是进程之间经常需要互相作用
1 | 举个例子: |
进程有三个态
- 运行态(running):进程占有 CPU 正在运行。
- 就绪态(ready):进程具备运行条件,等待系统分配 CPU 以便运行。
- 阻塞态(wait):进程不具备运行条件,正在等待某个事件的完成。
进程是并发执行的,宏观上在一段时间内能同时运行多个程序,但在微观上却是交替发生的(CPU一般不会让一个进程一次性执行完,为了确保所有进程可以得到公平的调度,CPU时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程)。某个进程的时间片用完,它就会进入就绪态,而其他被分配到时间片的进程就会进入运行态。就绪态的进程需要等待进程调度程序的下一次调度,为其分配 CPU 时间片后才能再次恢复运行。
阻塞态是由于缺少需要的资源从而由运行态转换而来,但是该资源不包括 CPU 时间片,缺少 CPU 时间片会从运行态转换为就绪态
但现在一般都是五态,即在运行态、就绪态和阻塞态上多了以下的两个态
- 新建态(new):进程正在被创建时的状态
- 终止态(exit):进程正在从系统中消失时的状态
它们的关系可以用下面这张图来直观解释

只有就绪态和运行态可以相互转换,其它的都是单向转换
而这些不同状态的进程就是通过PCB被操作系统管理的。
进程的 PCB 会通过某种方式组织起来,操作系统会把处于同一状态的所有进程的 PCB 链接在一起,这种数据结构就称为进程队列(Process Queue)
进程控制
进程控制就是对系统中的所有进程实施有效的管理,实现进程状态转换功能。包括创建进程、阻塞进程、唤醒进程、终止进程等。这些功能均由原语来实现,同时进程原理也是由原语来实现的,如进程的同步和互斥、进程的通信和管理。
原语:一种特殊的程序,它的执行具有原子性。就是这段程序的运行必须一气呵成,不可中断
进程的创建
操作系统初始启动时会创建承担系统资源分配和控制管理的一些系统进程,同时还会创建一个所有用户进程的祖先,其他用户进程是在应用程序运行时创建的。
简单地说就是操作系统一开始先弄一些系统进程来进行系统资源分配和控制管理,然后同时在弄一个所有用户进程的祖先。
操作系统允许一个进程创建另一个进程,也允许子进程继承父进程所拥有的资源。当子进程终止时,它从父进程处继承的资源应当还给父进程。如果父进程被种子,那么这个父进程的所有子进程也会被终止
PS:无法理解的话就想象这是一棵树,父节点没了那么子节点也没了。
所以创建进程的过程如下所示:
- 在进程列表中增加一项,从 PCB 池中申请一个空闲的 PCB(PCB 是有限的,若申请失败则创建失败),为新进程分配一个唯一的进程标识符
- 为新进程分配地址空间,由进程管理程序确定加载至进程地址空间中的程序
- 为新进程分配各种资源
- 初始化 PCB,如进程标识符、CPU 初始状态等
- 把新进程的状态设置为就绪态,并将其移入就绪队列,等待被调度运行
进程的终止
进程的终止也称为撤销,进程完成特定工作或出现严重错误后必须被终止
引起进程终止的事件有三种:
- 正常结束:进程自己请求终止(exit 系统调用)
- 异常结束:比如整数除 0,非法使用特权指令,然后被操作系统强行终止
- 外界干预:Ctrl + Alt + delete 打开进程管理器,用户手动杀死进程
终止(撤销)进程的过程如下:
- 从 PCB 集合中找到终止进程的 PCB
- 若进程处于运行态,则立即剥夺其 CPU,终止该进程的执行,然后将 CPU 资源分配给其他进程
- 如果其还有子进程,则应将其所有子进程终止
- 将该进程所拥有的全部资源都归还给父进程或操作系统
- 回收 PCB 并将其归还至 PCB 池
进程的阻塞和唤醒
进程阻塞是指进程让出 CPU 资源转而等待一个事件,如等待资源、等待 I/O 操作完成等。进程通常使用阻塞原语来阻塞自己,所以阻塞是进程的自主行为,是一个同步事件。当等待事件完成时会产生一个中断,激活操作系统,在系统的控制下将被阻塞的进程唤醒,也就是唤醒原语。
进程的阻塞和唤醒显然是由进程切换来完成的
进的阻塞步骤如下:
- 找到将要被阻塞的进程对应的 PCB
- 护进程运行现场,将 PCB 状态信息设置为阻塞态,暂时停止进程运行
- 将该 PCB 插入相应事件的阻塞队列(等待队列)
进程的唤醒步骤如下:
- 在该事件的阻塞队列中找到相应进程的 PCB
- 将该 PCB 从阻塞队列中移出,并将进程的状态设置为就绪态
- 把该 PCB 插入到就绪队列中,等待被调度程序调度
阻塞原语和唤醒原语的作用正好相反,阻塞原语使得进程从运行态转为阻塞态,而唤醒原语使得进程从阻塞态转为就绪态。
所以阻塞和唤醒是成对出现的(陷入了阻塞就需要唤醒,不然就会永远处于阻塞)。
进程上下文切换
进程上下文切换,就是各个进程之间是共享 CPU 资源的,不可能一个进程永远占用着 CPU 资源,不同的时候进程之间需要切换,使得不同的进程被分配 CPU 资源
所以简单地说就是一个进程切换到另一个进程运行。
进程上下文的切换步骤如下:
- 首先,将进程 A 的运行环境信息存入 PCB,这个运行环境信息就是进程的上下文(Context)
- 然后,将 PCB 移入相应的进程队列
- 选择另一个进程 B 进行执行,并更新其 PCB 中的状态为运行态
- 当进程 A 被恢复运行的时候,根据它的 PCB 恢复进程 A 所需的运行环境
引起进程上下文切换的原因有四种:
- 当前进程的时间片到了
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
线程
线程就是一个进程中可以有多个线程,它们共享这个进程的资源
线程有什么用
线程又称为迷你进程,但是它比进程更容易创建,也更容易撤销
早期的操作系统都是以进程作为独立运行的基本单位的,直到后期计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。这就好比物理学家研究物质组成一样:先发现了分子,然后继续细分发现原子,再后来是原子核和电子、夸克等等。
因为进程是拥有资源的基本淡唯,还能进行独立调度,因此它的执行速度会并不会快(大概就是一个士兵背着武器又背着口粮,太重了导致行动速度比起只有武器或者只有口粮的时候要慢很多)。
因此线程就是把进程作为资源分配单位和调度单位这两个属性分开处理,减少进程切换的开销。进程依旧作为资源分配的基本单位,但是不作为调度的基本单位,把调度执行与却换的责任交给了线程。
所以线程成为独立调度的基本单位
优点
进程有的优点它都有,例如
- 线程具有就绪、阻塞、运行三种基本状态,同样具有状态之间的转换关系
- 线程间可以并发执行
- 在多 CPU 环境下,各个线程也可以分派到不同的 CPU 上并行执行
进程没有的它却有的优点
- 一个进程中可以同时存在多个线程,这些线程共享该进程的资源。进程间的通信必须请求操作系统服务(因为 CPU 要切换到内核态),开销很大。而同进程下的线程间通信,无需操作系统干预,开销更小。
但是当进程A里面的线程B要跟进程C里面的线程D通信时,必须请求操作系统服务 - 线程间的并发比进程的开销更小,系统并发性提升
不同进程的线程间切换,它是会导致进程切换的,因此开销也大
缺点
当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃
进程调度算法
调度的概念
当 CPU 有一堆任务要处理时,由于其资源有限,这些事情就没法同时处理。这时就需要确定某种规则来决定处理这些任务的顺序,这个规则就是调度需要研究的问题。
进程调度
进程调度,就是从进程的就绪队列(阻塞)中按照一定的算法选择一个进程并将 CPU 分配给它运行,以实现进程的并发执行。
这是操作系统中最基本的一种调度,也是最低级的。一般在操作系统中都必须配置进程调度,而且进程调度的频率很高,差不多几十毫秒就进行一次。
非抢占式进程调度算法
非抢占式指的是当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件发生而被阻塞时,才会把 CPU 让给其他进程。
先到先服务(FCFS)
先来先服务调度算法(First Come First Serve,FCFS):按照进程到达的先后顺序进行调度,先到的进程就先被调度,也就是说,等待时间越久的越优先得到服务。
优点
公平、算法实现简单
缺点
对短进程不利。
排在长进程后面的短进程需要等待很长时间,短进程的响应时间太长了,用户交互体验会变差
最短作业优先(SJF)
最短作业/进程优先调度算法(Shortest Job First,SJF):每次调度时选择当前已到达的、且运行时间最短的进程
最短作业优先算法跟先到先服务算法恰好相反,前者对长进程不利,后者对短进程不利。
高响应比优先(HRRN)
高响应比优先算法(Highest Response Ratio Next,HRRN):只有当前运行的进程主动放弃 CPU 时(正常/异常完成,或主动阻塞),才需要进行调度,调度时计算所有就绪进程的响应比,为响应比最高的进程分配 CPU
响应比 = (进程的等待时间 + 进程需要的运行时间) / 进程需要的运行时间
抢占式进程调度算法
抢占式就是当进程正在运行的时,可以被打断,把 CPU 让给其他进程。
抢占的原则有三种
- 时间片原则
- 优先权原则
- 短作业优先原则
最短剩余时间优先(SRTN)
最短剩余时间优先(Shortest Remaining Time Next,SRTN):当一个新的进程到达时,把它所需要的整个运行时间与当前进程的剩余运行时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程,否则新的进程等待
简单地说就是最短作业优先的抢占式版本。
轮转调度算法(RR)
轮转调度算法(Round Robin,RR,也叫时间片调度算法)调度程序每次把 CPU 分配给就绪队列首进程使用规定的时间间隔,称为时间片,通常为 10ms ~ 200ms,就绪队列中的每个进程轮流地运行一个时间片,当时间片耗尽时就强迫当前运行进程让出 CPU 资源,转而排到就绪队列尾部,等待下一轮调度。
所以,一个进程一般都需要多次轮转才能完成。
这个算法对每个进程一视同仁,但需要注意的是时间片的长度是一个很关键的因素
- 如果时间片设置得太短,就会导致频繁的进程上下文切换,降低了 CPU 效率
- 如果时间片设置得太长,那么随着就绪队列中进程数目的增加,轮转一次消耗的总时间加长,即每个进程的相应速度放慢。甚至时间片大到让进程足以完成其所有任务,RR 调度算法便退化成 FCFS 算法
最高优先级调度算法(HPF)
最高优先级调度算法(Highest Priority First,HPF):从就绪队列中选择最高优先级的进程进行运行
这个优先级是怎么规定的呢?主要是分为静态优先级和动态优先级
- 静态优先级:创建进程时候,就预先规定优先级,并且整个运行过程中该进程的优先级都不会发生变化。一般来说,内核进程的优先级都是高于用户进程的
- 动态优先级:根据进程的动态变化调整优先级。比如随着进程的运行时间增加,适当的降低其优先级;随着就绪队列中进程的等待时间增加,适当的升高其优先级
需要注意的是,最高优先级算法并非是固定的抢占式策略或非抢占式,系统可预先规定使用哪种策略
- 非抢占式:当就绪队列中出现优先级高的进程,则运行完当前进程后,再选择该优先级高的进程
- ‘抢占式:当就绪队列中出现优先级高的进程,则立即强制剥夺当前运行进程的 CPU 资源,分配给优先级更高的进程运行
PV操作
进程同步
在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。
但是异步也会带来一些问题
1 | 举个例子: |
进程同步(synchronization)就是用来解决这个问题的。
所谓进程同步就是指协调这些完成某个共同任务的并发线程,在某些位置上指定线程的先后执行次序、传递信号或消息
1 | 举个例子: |
进程同步和进程调度的区别:
- 进程调度是为了最大程度的利用 CPU 资源,选用合适的算法调度就绪队列中的进程
- 进程同步是为了协调一些进程以完成某个任务,比如读和写,你肯定先写后读,不能先读后写吧,这就是进程同步做的事情了,指定这些进程的先后执行次序使得某个任务能够顺利完成
进程互斥
因为进程的并发性,并发执行的线程不可避免地需要共享一些系统资源(例子参考上面讲过的打印机例子)
进程互斥(mutual exclusion)就是用来这个例子的问题的。在进程A通过C开始工作好,如果进程B也要通过C进行工作,那么它就会控制B,让B等A工作完了才能去访问C。
这种在一个时间段内只允许一个进程使用的资源,我没称其为临界资源。对临界资源进行访问的那段代码称为临界区。

进程互斥和进程同步的区别:
- 进程同步:进程 A 应在进程 B 之前执行
- 进程互斥:进程 A 和进程 B 不能在同一时刻执行
进程互斥是一种特殊的进程同步,即逐次使用临界资源,也是对进程使用资源的先后执行次序的一种协调。
常见的进程同步与互斥的机制
常见的进程同步与互斥机制有两种:
- 信号量与PV操作
- 管理
信号量与PV操作
信号量机制(Semaphore)首次由荷兰学者 Dijkstra 在1965年提出。这里面的信号量就是一个变量,即使用一个信号量来表示系统中某种资源的数量。
用户进程可以通过使用操作系统提供的PV操作来对信号量进行操作,从而很方便的实现进程互斥或同步
- P操作:将信号量值减1,表示申请占用一个资源。如果结果小于0,表示已经没有克用资源,则执行P操作的进程被阻塞。如果结果大于等于0,表示现在有的资源足够使用,则执行P操作的进程继续执行
- V操作:将信号量值加1,表示释放一个资源,即用完资源后归还资源。若加完的信号量值小于等于0,表示有某些进程正在等待该资源,由于我没已经释放出一个资源了,因此需要唤醒一个等待使用该资源(就绪态)的进程,使之运行下去。
可以这么理解,当信号量的值为 2 的时候,表示有 2 个资源可以使用,当信号量的值为 -2 的时候,表示有两个进程正在等待使用这个资源
1 | 【关于V操作的问题】 |
1 | //信号量定义 |
实现进程互斥
实现进程的互斥只需要两步:
- 定义一个互斥信号量,并初始化为1
- 把对于临界资源的访问置于P操作和V操作之间
1 | semaphore mutex = 1; //初始化互斥信号量,初始化为1 |
P 操作和 V 操作必须成对出现。缺少 P 操作就不能保证对临界资源的互斥访问,缺少 V 操作就会导致临界资源永远得不到释放、处于等待态的进程永远得不到唤醒。
实现进程同步
进程同步,就是要各并发进程按要求有序地运行。
1 | P1(){ |
上述代码假设“代码4”要在得到“代码1”和“代码2”的结果才能运行,那么就要必须保证 “代码4” 一定是在 “代码2” 之后才会执行
这也就是进程同步
使用信号量和 PV 操作实现进程的同步也非常方便,只需要三步:
- 定义一个同步信号量,并初始化为当前可用资源的数量
- 在优先级较高的操作的后面执行 V 操作,释放资源
- 在优先级较低的操作的前面执行 P 操作,申请占用资源
1 | seamphore S = 0; //初始化同步信号量,表示当前可用资源为0 |
生产者和消费者问题
生产者消费者问题(Producer-consumer problem)也叫有限缓冲问题(Bounded-buffer problem),这是一个多线程同步问题的经典案例。
问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决这个问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。
解决这个问题需要注意的几个点:
- 在缓冲区为空时,消费者不能再进行消费
- 在缓冲区为满时,生产者不能再进行生产
- 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步
- 注意条件变量与互斥锁的顺序
管程
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程将永远不能使用管程。也就是说管程天生支持进程互斥。
其实使用管程是能够实现信号量的,并且也能用信号量实现管程。但是管程封装的比较好,相比起信号量来需要我们编写的代码更少,更加易用,这也就是 Java 采用管程机制的原因
synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。把管程翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
六大进程通信机制
进程通信
进程通信( InterProcess Communication,IPC)就是进程之间的信息交换,实际上进程的同步与互斥本质就是一种进程通信。只不过它传输的仅仅是信号量,通过修改信号量,使得进程之间建立联系,相互协调和协同工作,缺少了传递数据的能力。
在大多数情况下,进程之间需要交换大量的数据,因此我们就需要一个新的通信机制来完成,这就是进程通信。
因为每个进程的用户地址空间都是独立的(为了安全),所以一个进程理论上是不能直接访问另一个进程的地址空间。但是每一个进程都共享一个内核空间,因此进程之间想要进行信息交换就必须通过内核
Linux 内核提供的常见的进程通信机制如下:
- 管道(也称作共享文件)
- 消息队列(也称作消息传递)
- 共享内存(也称作共享存储)
- 信号量和 PV 操作
- 信号
- 套接字(Socket)
管道
匿名管道
在Linux里面,它的管道使用竖线|来连接多个命令,这个也被成为管道符
1 | command1 | command2 |
上面就是一个通道,它的功能是将前一个命令(command1)的输出,作为后一个命令(command2)的输入。
不难看出,管道中的数据只能单向流动,也就是半双工通信。如果要实现互相通信(全双工通信),则需要创建两个通道才行。
通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。同时匿名通道只能在具有亲缘关系(父子进程)的进程中使用。
一句话解释:匿名管道只能用于父子进程之间的通信
在Linux的实际编码中,是通过pipe这个函数来创建匿名通道的。如果成功则返回0,否则返回-1.
1 | int pipe (intfd[2]); |
这个函数拥有一个空间为2的文件描述符组数:
fd[0]指向管道的读端,fd[1]指向管道的写端fd[1]的输出是fd[0]的输入
实现步骤如下:
- 父进程创建两个匿名管道,即管道1
fd1[0]和fd1[1])和管道 2(fd2[0]和fd2[1])
PS:因为管道的数据是单向流动的,所以要想实现数据双向通信,就需要两个管道,每个方向一个 - 父进程fork出子进程,对于这两个匿名管道,子进程也分别有两个文件描述符指向匿名管道的读写两端
- 父进程关闭管道 1 的读端
fd1[0]和 管道 2 的写端fd2[1],子进程关闭管道 1 的写端fd1[1]和 管道 2 的读端fd2[0],这样,管道 1 只能用于父进程写、子进程读;管道 2 只能用于父进程读、子进程写。管道是用环形队列实现的,数据从写端流入从读端流出,这就实现了父子进程之间的双向通信
如果不明白的话可以看看下面这张图

因此管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作
有名管道
匿名管道由于没有名字,只能用于父子进程间的通信。为了克服这个缺点,提出了有名管道,因为数据是先进先出的传输方式,因此也称做 FIFO。
在Linux中使用mkfifo 来创建有名管道
1 | mkfifo myPipe |
其中myPipe 是这个管道的名称
1 | echo "hello" > myPipe |
执行这行命令后,你会发现它就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。
于是,我们执行另外一个命令来读取这个有名管道里的数据
1 | cat < myPipe |
消息队列
管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流为此,消息传递机制(Linux 中称消息队列)因此而生。
A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程在需要的时候自行去消息队列中读取数据就可以了。同样的,B 进程要给 A 进程发送消息也是如此
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。对比一下管道机制:
- 消息队列允许一个或多个进程向它写入或读取消息
- 消息队列可以实现消息的「随机查询」,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。比有名管道的先进先出原则更有优势
- 对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。而对于管道来说,除非读进程已存在,否则先有写进程进行写入操作是没有意义的
- 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道随进程的创建而建立,随进程的结束而销毁
需要注意的是,消息队列对于交换较少数量的数据很有用,因为无需避免冲突。但是,由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,另一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入。
共享内存
共享内存就是允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存。而这个物理内存就会成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
原理
每个进程都有属于自己的进程控制块(PCB)和逻辑地址空间(Addr Space),并且都有一个与之对应的页表,这个页表负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,从而可以通过内存管理单元(MMU)对他们进行管理。两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存

跟消息队列不同,共享内存机制仅仅是在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可以作为常规内存访问,不需要借助内核。这样数据就不需要在进程之间来回拷贝。
因此这是一种最快的进程通信方式。
信号量和PV操作
对具有多CPU系统来说,息传递的性能是要优于共享内存的。因为消息队列无需避免冲突,而共享内存机制可能会发生冲突。因此如果多个进程同时修改同一个共享内存,先来的那个进程写的内容就会被后来的进行覆盖。
而且在多道批处理系统中,多个进程是可以并发执行的。然而由于系统的资源有限,进程的执行不是一贯到底的,它是走走停停,以不可预知的速度向前推进(异步性)。但我们又希望在某些时候多个进程能密切合作,按照某个特定的顺序依次执行,以实现一个共同的任务。
之前我没提过一个例子,是A、B两个进程一个负责写一个负责读,然而因为异步性发生了先读后写的操作,导致A阻塞。因此为了解决这个问题,我们就要使用进程的同步与互斥机制(如前面说过的信号量和PV操作)
进程的同步与互斥其实是一种对进程通信的保护机制,并不是用来传输进程之间真正通信的内容的,但是由于它们会传输信号量,所以也被纳入进程通信的范畴,称为低级通信
信号
信号和信号量是完全不同的两个概念
信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行用户、内核和进程都能生成和发送信号。
信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说我们可以通过键盘输入某些组合键给进程发送信号,比如常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是通过 kill 系列的命令给进程发送信号,比如 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其立即结束。
Socket
跨网络与不同主机上的进程进行通信一般就是使用Socket通信来做的(Socket 也能完成同主机上的进程通信)
Socket 起源于 Unix,原意是插座,在计算机通信领域,Socket 被翻译为套接字,它是计算机之间进行通信的一种约定或一种方式。通过 Socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
从计算机网络层面来说,Socket 套接字是网络通信的基石,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。
Socket 的本质其实是一个编程接口(API),是应用层与 TCP/IP 协议族通信的中间软件抽象层,它对 TCP/IP 进行了封装。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面。对用户来说,只要通过一组简单的 API 就可以实现网络的连接。
参考文献:
[]: https://veal98.gitee.io/cs-wiki/#/README?id=%e6%95%b0%e6%8d%ae%e5%ba%93