操作系统是管理计算机硬件和软件资源的计算机程序。比如管理配置内存、决定资源供需顺序、控制输入输出设备,提供用户交互等。
操作系统并不局限于存在在计算机,手机也是有操作系统的。常见的操作系统有:Android、iOS、Windos、Linux、MacOS。
我们不可能直接操作计算机硬件,而且像以前的计算机中编写的程序并不是可以在任何计算机上可以运行,所以就需要一个通用的环境,提出了操作系统。有了操作系统,普通人也可以方便使用。
1.无操作系统的时代
2.批处理系统的时代:
多道程序设计。后来才提出的,此时的操作系统可以称为:多道批处理系统。即在内存中可同时存在若干道作业,说白了就是让批处理系统可以一次性处理多个作业,但其实是快速的穿插运行,使得看起来可以处理多个作业,因此CPU的利用率显著地提高了。
当提到单处理器时实际上指的是并发性,提到多处理器指的是并行性。
共享性:操作系统的资源可以供给多个并发的程序共同使用。共享性可分为:互斥共享(当资源被程序A占用时,其他程序就只能等待程序A使用完释放后才可以使用),同时访问(某种资源在一段时间内并发地被多个程序访问,可以宏观地看该资源可以被同时访问,因为很快)
虚拟性:把一个物理实体转变为若干个逻辑实体,物理实体是真实存在的,比如硬件设备,逻辑实体是虚拟存在的,虚拟的技术主要有时分复用技术和空分复用技术。
异步性:在多道程序设计的环境下,允许多个进程并发执行,进程在使用资源时可能需要等待或放弃。比如上面的互斥共享。进程的执行并不是一下子对程序的代码执行到底,而是以走走停停的形式推进。比如进程A释放资源,进程B,C目前在抢夺资源,此时B或者C谁能抢到是不知道的,这也体现异步性。
操作系统的五大功能:进程管理、存储管理、作业管理、文件管理、设备管理。
进程(Process):是系统进行资源分配和调度的基本单位。进程作为程序独立运行的载体,保证了程序正常执行。我们的桌面应用当点击启动时就会变成进程。
为什么需要进程?早期没操作系统,那么资源就只能分配给当前运行的程序;当有了操作系统,引入多道程序设计后,进程就出现,来合理隔离每个程序占用的资源。
进程在内存是一段连续的内存空间,这段空间称为进程控制块(PCB,一个数据结构),每一个进程一定有一个PCB,而进程控制块的构成如下:
解释:
而上面可以总结分成四个种:进程标识符、处理机状态、进程调度信息、进程控制信息。
因为PCB是操作系统进行调度经常被读取的信息,所以PCB是常驻内存的,系统中有专门开辟一块PCB区域来存储所有进程的PCB。
线程(Thread):是系统进行资源分配和调度的最小单位。进程是由系统分配,线程是由CPU分配,线程包含在进程中的,是进程中实际运行工作的单位,比如计算一个数据,是利用线程来进行计算。一个进程至少有一个线程,每当启动程序就先创建进程再创建一个线程。
一个进程可以并发多个线程,每个线程执行不同的任务。进程中的线程共享进程的资源,比如一个线程计算出的结果可以保存再进程中某个区域,然后另一个线程可以从进程中拿去到这个结果。
总结进程和线程的区别:
也称为进程的生命周期。
1.就绪状态
2.运行(执行)状态:
3.阻塞状态:
4.创建状态:创建进程时拥有PCB但其他资源尚未就绪的状态称为创建状态。
5.终止状态:进程结束由系统清理或者归还PCB的状态称为终止状态。
进程状态转换图:
引入生产者-消费者问题:有一群生产者进程在生产产品,并将这些产品提供给消费者进程进行消费,生产者进程和消费者进程可以并发执行,在两者之间设置了一个具有n可缓冲区的缓冲池,生产者进程需要将所生产的产品放到一个缓冲区中,消费者进程可以从缓冲区取走产品消费。
当生产者生产一个产品,缓冲区就+1,当消费者拿取一个产品,缓冲区就-1。
来到计算机,一般一个操作缓冲需要三个步骤:
register = count; // 先从缓冲区拿取产品数
register = register + 1; // 然后生产,对其加1
count = register; // 把更新的值归还到缓冲区
但是在计算机中这样是有问题的,因为计算机中是并发执行的,比如:
可以代码:
/*
* 刚开始仓库数为0
* 现在要生产10次,消费10次,那么理想中最终仓库数应该为0
* 使用多线程来并发模仿一个生产,一个消费
*/
class Store {
private int num = 0;
public void producer() {
int times = 10;
while(times-- != 0) {
try {
this.num = this.num + 1;
System.out.println("生产后,产品数为:" + this.num);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void comsumer() {
int times = 10;
while(times-- != 0) {
try {
this.num = this.num - 1;
System.out.println("消费后,产品数为:" + this.num);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getNum() {
return num;
}
}
/*
* 消费者
*/
class Comsumer implements Runnable {
private Store store;
Comsumer(Store store) {
this.store = store;
}
@Override
public void run() {
store.comsumer();
}
}
/*
* 生产者
*/
class Producer implements Runnable {
private Store store;
Producer(Store store) {
this.store = store;
}
@Override
public void run() {
store.producer();
}
}
public class Main {
public static void main(String[] args) {
Store store = new Store();
Producer producer = new Producer(store);
Comsumer comsumer = new Comsumer(store);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(comsumer);
t1.start();
t2.start();
// join 等待线程执行完才执行下面
try {
t1.join();
t2.join();
System.out.println("最终仓库剩余数:" + store.getNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果不唯一:我的量设置小了,可以设置大点。
最终仓库剩余数:3
最终仓库剩余数:5
最终仓库剩余数:0
最终仓库剩余数:-1
上面是我运行几次的结果,错的也有对的也有,这就是线程不同步的表现。
以上的根源问题就是:彼此间没有通信。比如现在仓库为空,生产者没有去通知消费者我目前正在生产,你先别消费,等我生产完再来消费。
线程同步:这个应该是我们听得最多的,进程也有同步那么线程更是有同步,因为进程的线程共享进程资源,像上面的代码案例就是一个线程不同步的表现(异步)。
线程同步的方法:(待详讲)
临界资源:指的是一些虽作为共享资源却又无法同时被多个线程共同访问的共享资源。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放该共享资源才可重新竞争使 用共享资源。像上面的仓库就是一个临界资源。
进程同步:对竞争资源在多进程间进行使用有序的协调,使得并发执行的多个进程之间可以有效使用资源和相互合作。
为了对临界资源进行约束,提出了进程/线程间同步的四个原则:
进程同步的方法:(待详讲)
进程类型:
进程ID:唯一标识,ID是一个非负数,最大值由操作系统限定。ID为0的进程为idle进程,是系统创建的第一个进程。ID为1的进程为init进程,是0号进程的子进程,完成系统初始化。init进程是所有用户进程的祖先进程。
linux进程状态:
命令:
ps
ps -aux // 打印进程的详细信息
ps -u root // 查看用户root的进程
ps -aux | grep 'xxx' // 查看特定进程
ps -ef --forest // 打印进程的父子关系
ps -aux --sort=-pcpu // 按照cpu的频率排序
ps -aux --sort=-pmen // 按照内存排序
进程调度指的是计算机通过决策决定哪个就绪进程可以获得CPU使用权。主要有两个步骤:
为了实现这两个步骤就要了解三种机制:
要是进程调度时,旧进程还没执行完,此时也有两种进程调度方法:
非抢占式的调度:处理器一旦分配给某个进程,就让该进程一直执行下去。调度程序不以任何原因抢占正在被使用的处理器,直到进程完成工作或因为IO阻塞才会让出处理器。
抢占式的调度:允许调度程序以一定的策略暂停当前正在运行的进程,然后保留旧进程的上下文信息,分配处理器给新的进程。
进程调度的算法:
除了时间片轮转调度算法属于抢占式的调度,其他三个属于非抢占式的调度。
看道题目:
解:
其实可以发现,如果没有多个进程在等待时,使用什么算法都是一样的。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的根源就是:
只要满足以下四个必要条件就一定发生死锁:
如何预防死锁?只需要把上面四个条件中**除了互斥条件(这个破坏不了,因为有些资源根本就不能同时访问,比如打印机)**任意一个破坏即可:
如何避免死锁?提出了银行家算法:以银行借贷系统分配策略为基础的算法,客户申请的贷款是有限的,每次申请需声明最大资金量,银行家在能够满足贷款时,都应该给用户贷款,客户在使用贷款后,能够及时归还贷款。
一般有三个数据结构:已分配资源表,可分配资源表,所需资源表。
来看例子,假设三张表的数据如下:
根据两张表,求个还需分配资源表:
下面就来操作:
可能有时候遇到资源分配不了的情况,一般会等待或者强行杀掉进程,极端的话就是蓝屏。
动态分区分配相关数据结构:
动态分区的分配算法:
一共有以下4种情况:
对于第一种和第二种其实都是一样的:
对于第三种:
对于第四种:
页内碎片/内碎片/内部碎片:内部碎片是已经被分配出去的内存空间(能明确指出属于哪个进程)大于请求所需的内存空间,不能被利用的内存空间就是内部碎片。
页外碎片/外碎片/外部碎片:外部碎片是指还没有分配出去的内存空间(不属于任何进程),但是由于大小而无法分配给申请内存空间的新进程的内存空闲块。
看图:
单一连续分配算法可能产生内碎片。
固定分区分配算法可能产生内碎片和外碎片。
动态分区分配算法可能产生外碎片。
上面是分配内存和回收内存,现在我们需要知道操作系统是如何管理进程的空间,比如内存满了又来一个新程序要怎么办等问题。有三种方法:页式存储管理,段式存储管理,段页式存储管理。
首先需要知道页面这个单位还有块。
页和块的大小相等,只是为了区别计算机内存和进程而提出块和页。
页式存储管理:将进程逻辑空间等分成若干大小的页面(等分划分),并编号;相应的把物理内存空间分成与页面相同大小的物理块,并编号;以页面为单位把进程空间装进物理内存中分散的物理块。
在这里就有页表这个概念,页表记录进程逻辑空间与内存物理空间的映射。如图:
在现代计算机中,可以支持非常大的逻辑地址空间,所以页表就变得非常大,要占用非常大的内存空间,就提出了多级页表,由顶级页表(首页)与其他页表(二级页表、三级页表等)的映射,而且是按需拿取,比如现在需要某一页,但该页没有在内存中,所以通过顶级页表的映射关系把该页加载到内存中,其他不用的就不加载。所以相对来说节省空间,当然这是一种时间换空间的算法,所以比较耗时。
页式存储管理如果有一段连续的逻辑分布在多个页面中,将大大降低执行效率,所以提出了段式存储管理。
段式存储管理:将进程逻辑空间划分成若干段(非等分)并编号,每段都定义了一组逻辑信息,例如主函数MAIN、子程序段X、子函数Y等。段的长度由逻辑信息组的长度(连续逻辑)决定。
段由段号和段内偏移地址组成,也有段表,存储段号、物理地址的起始地址(基址)和段长的表,如下:
段式存储和页式存储的比较:
分页可以有效提高内存利用率(虽然说存在内碎片),分段可以更好满足用户需求,两者结合,形成段页式存储管理。
段页式存储管理:先将逻辑空间按段式管理分成若干段,再把段内空间按页式管理等分成若干页。
段页式的地址结构:段号、段内页号、页内地址(通过块号找到内存的物理地址)。
如下图,基址是每一段的起始地址,表可能还有其他内容,这里忽略。
有些进程实际需要的内存很大,超过物理内存的容量,而且多道程序设计(一个内存运行多个进程),使得每个进程可用物理内存更加稀缺,但是不可能无限增加物理内存,物理内存总有不够的时候,所以提出虚拟内存。
虚拟内存的实现是基于局部性原理的。局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。
所以程序运行时,无需全部装入内存,装载部分即可,如果访问页不在内存,则发出缺页中断,发起页面置换算法。从用户层面看,程序拥有很大的空间,即是虚拟内存。虚拟内存实际是对物理内存的补充,速度接近于内存,成本接近于辅存。
虚拟内存:把程序使用的内存划分,将部分暂时不使用的内存放置在辅存。
上面提到置换算法,在虚拟内存中有三种置换算法:(这些置换算法也在组成原理的高速缓存中也有,但缺了OPT,当时说的是字块,现在说的是页面,其实都是一样的)
看题:
解:
大型游戏中,有时候走近一个场景发现场景的东西才慢慢加载出来,这就是触发了置换策略;黑暗之魂中打BOSS前的雨门,当我们走进雨门就触发置换策略,把BOSS场景的东西加载出来。
在组成原理中说到高速缓存和主存的置换:
现在,说到主存和辅存的置换:
替换策略发生在Cache-主存层次、主存-辅存层次,Cache-主存层次的替换策略主要是为了解决速度问题,主存-辅存层次主要是为了解决容量问题。
Buddy内存分配算法:在Linux中为了努力让内存分配与相邻内存合并能快速进行(解决外碎片),算法基于计算机处理二进制的优势具有极高的效率,以向上取整为2的幂大小作为内存分配原则,比如一个进程占70k,那么就会分配一个128k的内存给它。Buddy翻译过来是“伙伴”,具体指的是内存的“伙伴”,一片连续内存的“伙伴”是相邻的另一片大小一样的连续内存,看图理解:
Buddy内存的分配,会创建一系列空闲块链表,每一种都是2的幂次方。
来看下Buddy内存分配的例子:
Buddy内存分配算法把外碎片变成内碎片。
交换空间(Swap)是磁盘的一个分区,Linux物理内存满时,会把一些内存交换至Swap空间,Swap空间是初始化系统时配置的。可以在Linux中查看,使用top命令:
交换空间的作用:
交换空间跟我们前面说到虚拟空间类似,下面是它们的比较:
无结构文件:也称为流式文件,文件内容长度以字节为单位,比如exe文件、dll文件、so文件等。
有结构文件:文件内容由定长记录和可变长记录组成,定长记录存储文件格式、文件描述等结构化数据项,可变长记录存储文件具体内容。
有以下几种形式实现有结构文件:
辅存分配方式指的是如何在辅存(磁盘)上存储数据。
1.连续分配:顺序读取文件内容非常容易,速度很快,对存储要求高,要求满足容量的连续存储空间。如图:
2.链接分配:链接分配可以将文件存储在离散的盘块中,需要额外的存储空间存储文件的盘块链接顺序。
3.索引分配:把文件的所有盘块集中存储(索引),读取某个文件时,将文件索引读取进内存即可。每个文件拥有一个索引块,记录所有盘块信息。索引分配方式支持直接访问盘块,文件较大时,索引分配方式具有明显优势。所以现在常用这个来分配辅存。
存储空间管理就是磁盘的空间管理,比如记录磁盘哪里是空闲的、哪里是已经存放东西的。
1.空闲表:空闲盘区的分配与内存分配类似,比如首次适应算法、循环适应算法等,回收过程也与内存回收类似。如图:
2.空闲链表:空闲链表把所有空闲盘区组成一个空闲链表,每个链表节点存储空闲盘块和空闲的数目。分配和回收同上。
3.位示图:位示图维护成本很低,位示图可以非常容易找到空闲盘块,位示图使用0/1比特位,占用空间很小。主要使用这个,如图:
目录树:任何文件或目录都只有唯一路径。
需要知道:在Linux中,一切皆为文件(进程也是文件,进程以数字命名,存放在/proc目录下)。
绝对路径:从根目录开始的路径,比如:/home/aa/b 这个就是绝对路径。
相对路径:相对于当前的文件路径,比如:…/aa(…/ 返回上一级目录),/aa/bb(当前目录中的aa文件夹中的bb文件)
关于文件操作的一些指令:
文件类型:
使用指令:ll,来查看文件类型:第一个字母指的是文件类型,后面的到菜鸟教程看。
文件系统概述:当格式化U盘时就会出现以下几种格式化模式。
EXT文件系统,长下面这样:
每一个Block Group长下面这样:
Inode Table:存放文件Inode的地方每一个文件(目录)都有一个Inode,是每一个文件(目录)的索引节点。
Inode:存放的内容包含:索引节点编号、文件类型、文件权限、文件物理地址、文件长度、文件连接计数、文件存取时间、文件状态、访问计数等。文件名不是存放在Inode节点上的,而是存放在目录的Inode节点,为了列出目录文件的时候无需加载文件的Inode。
Inode bitmap:Inode的位示图,记录已分配的Inode和未分配的Inode。
Data block:存放文件内容的地方,(中文被翻译成块)每个block都有唯一的编号,文件的block记录在文件的Inode上。
Block bitmap:功能与Inode bitmap类似,记录Data block的使用情况。
Superblock:记录整个文件系统相关信息的地方,Block和Inode的使用情况、时间信息、控制信息等。
一些相关指令:(了解就行)
把它导下来:dumpe2fs /dev/sda1 > 文件名.log,然后使用vim查看
这里说一个vim中的查询,输入 /,然后加上你要搜索的字符,比如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PKGQkfCY-1587525418135)(概述/78.png)]
广义的IO设备:指的是对CPU而言,凡是对CPU进行数据输入的都是输入设备,对CPU而言,凡是CPU进行数据输出的都是输出设备(比如CPU处理完的数据放到内存)。可以有以下几种分类:按使用特性分类、按设备的共享属性分类、按信息交换的单位分类、按传输速率分类。如图:
组成原理中提到CPU与IO设备的速率不匹配,所以采用了存储层次(主存-辅存层次、主存-高速缓存层次)。而IO设备的缓冲区也是为了解决这个问题,减少CPU处理IO请求的频率,提高CPU与IO设备之间的并行性。
然而该缓冲区只属于特定的IO进程(专用缓冲区),所以当这样的IO进程比较多时,对内存的消耗也很大。因此操作系统划出可供多个进程使用的公共缓冲区,称之为缓冲池。
当需要用到IO缓冲区时,就从缓存池取出,当使用完就把缓冲区归还到缓冲池。达到了多个进程共同使用缓冲区的要求,也减少了内存的消耗。
SPOOLing技术:是关于慢速字符设备如何与计算机主机交换信息的一种技术(还是跟上面一样,解决CPU与IO设备速度不匹配的问题),利用高速共享设备将低速的独享设备模拟为高速的共享设备,逻辑上,系统为每一个用户都分配了一台独立的高速独享设备,也称为虚拟设备技术。
作业:在输入、输出之间增加了排队转储环节(输入井、输出井),SPOOLing负责输入(出)井与低速设备之间的调度,逻辑上,进程直接与高速设备交互,减少了进程的等待时间。