此文针对非科班同窗来补充程序猿必备的基础知识。前端
获取计算机网络基础知识请点击这里node
获取计算机组成原理基础知识请点击这里mysql
好了,开撸操做系统!linux
这里先不讲操做系统
的概念了,由于文字太生硬了,咱们只须要看一个简单的例子:算法
console.log(1+1)
; 就能够在浏览器面板中看到2
,这其中发生了什么事情呢?(简单扫一眼)1+1
到显示器输出2
, 须要CPU
控制键盘(输入设备) ,将获取的1+1
指令放入内存1+1
的加法运算1+1
的加法运算,并得出结果2
2
返回给屏幕(输出设备)好了,这里问题是,若是没有操做系统,一个简单的1+1运算,你的js代码还须要考虑这些硬件的协调工做,好比你的代码要协调CPU资源何时读取你的代码,何时把进程切换到别的进程。。。这些脏活累活都是操做系统帮你屏蔽了,要不这代码可咋写啊。。。sql
很早之前看朴零大神的《深刻浅出NodeJS》的时候,讲到进程间通讯,有一句大概说,windows平台进程间通讯用的是管道,linux平台用的是domain socket,我一看就傻眼了,啥是进程间通讯?啥是管道?啥是domain socket?😭 看不懂啊.... 这些都是跟操做系统进程的知识相关)。数据库
啥也了不说了,兄弟,学习的小车已经粗发了!编程
预备知识: 什么是指令(更详细内容请看个人计算机组成原理文章)windows
好比说,以下图(简单扫一下便可):浏览器
a+b是一段程序代码,a+b在CPU看来并不能一步完成,能够翻译成以下:
// 意思是将内存的16号单元数据,放到A寄存器,
LOAD A, 16
// 意思是将内存的16号单元数据,放到B寄存器
LOAD B, 17
// 存器里的A,B数据相加,获得C
ADD C, A, B
复制代码
这里就能够看得出来,指令是CPU
能识别
和执行
的最基本命令。
假如说一个用户能够随意把服务器上的全部文件删光,这是很危险的。因此有些指令普通用户是不能使用的,只能是权限较高
的用户能使用。此时指令就分为了两种,以下图:
这就引出一个问题:CPU如何判断
当前是否能够执行特权指令
? 以下图: CPU一般有两种工做模式即:
内核态
和用户态
,而在PSW(这个不用管,就知道有一个寄存器的标志位0表示用户态,1表示核心态)中有一个二进制位控制这两种模式。
对于应用程序而言,有的程序能执行特权指令,有的程序只能执行非特权指令。因此操做系统里的程序又分为两种:
从下图,咱们先看看操做系统内核包含哪些
操做系统内核中跟硬件紧密相关的部分有:
硬件定时器
的(具体硬件怎么实现我也不太清楚,好像是靠硬件周期性的产生一个脉冲信号实现的)。时钟管理至关重要,好比咱们获取时间信息
,进程切换
等等都是要依靠时钟管理。不可被中断
的指令集合。原语有一个很是重要的特性,就是原子性(其运行一鼓作气,不可中断
)。暂时停止程序的执行
转而处理这个新的状况
的过程就叫作中断
。下面举一个例子:
第一个应用程序在用户态执行了一段时间后
接着操做系统切换到核心态,处理中断信号
中断的信号
是第一个程序的时间片(每一个程序不能一直执行,CPU会给每一个程序必定的执行时间,这段时间就是时间片)用完了,应该换第二个应用程序执行了第2个进程
后,操做系统会将CPU
的使用权
交换给第二个应用程序,接着第二个应用程序就在用户态
下开始执行。进程
2须要调用打印机资源
,这时会执行一个系统调用
(后面会讲系统调用,这里简单理解为须要操做系统进入核心态处理的函数),让操做系统进入核心态,去调用打印机资源此时进程2
由于要等待打印机启动,操做系统就不等待了(等到打印机准备好了,再回来执行程序2),直接切换到第三个应用程序
执行个中断信号
,操做系统又进入到核心态,发现这个中断是由于程序2
等待打印机资源,如今打印机准备好了,就切换到程序2
,切换到用户态
,把CPU给程序2继续执行。好了,如今能够给出一个结论,就是用户态、核心态之间的切换是怎么实现的?
而且中断时惟一途径
。举一个例子,什么是内中断和外中断:
接着说以前的范桶同窗,小时候不爱学习,每次学习着学习着忽然异想天开,回过神来已通过好好长一段时间,这是内部中断
。想着想着老师走过来,给了范捅一嘴巴,这是外部中断
。
官方解释以下:
程序非法操做
(好比你要拿的的数据的内存地址不是内存地址,是系统没法识别的地址),地址越界
(好比系统给你的程序分配了一些内存,可是你访问的时候超出了你应该访问的内存范围)、浮点溢出
(好比系统只能表示1.1到5.1的范围,你输入一个100, 超出了计算机能处理的范围),或者异常
,陷入trap
(是指应用程序请求系统调用形成的,什么是系统调用,后面小节会举例讲)。I/O中断
(由I/O控制器产生,用于发送信号通知操做完成等信号,好比进程须要请求打印机资源,打印机有一个启动准备的过程,准备好了就会给CPU一个I/O中断,告诉它已经准备好了)、时钟中断
(由处理器内部的计时器产生,容许操做系统以必定规程执行函数,操做系统每过大约15ms会进行一次线程调度,就是利用时钟中断来实现的)。为何须要系统调用?
好比你的程序须要读取文件信息
,可读取文件属于读取硬盘里的数
据,这个操做应该时CPU在内核态
去完成的,咱们的应用程序怎么让CPU去帮助咱们切换到内核态完成这个工做呢,这里就须要系统调用了
。
这里就引出系统调用的概念和做用。
应用程序经过系统调用请求操做系统的服务
。系统中的各类共享资源都由操做系通通一管理,所以在用户程序中,凡是与资源有关的操做
(如存储分配、I/O操做、文件管理等),都必须
经过系统调用的方式向操做系统提出服务请求,由操做系统代为完成。
如下内容简单看一下便可,系统调用的分类:
须要注意的是,库函数
和系统调用
容易混淆。
处于用户态
内核态
, 库函数中有很大部分是对系统调用的封装举个例子:好比windows
和linux
中,建立进程的系统调用方法是不同的。 但在node中的只须要调用相同函数方法就能够建立一个进程。例如
// 引入建立子进程的模块
const childProcess = require('child_process')
// 获取cpu的数量
const cpuNum = require('os').cpus().length
// 建立与cpu数量同样的子进程
for (let i = 0; i < cpuNum; ++i) {
childProcess.fork('./worker.js')
}
复制代码
单道程序
(是指全部进程一个一个排队执行,A进程执行时,CPU、内存、I/O设备全是A进程控制的,等A进程执行完了,才换B进程,而后对应的资源好比CPU、内存这些才能换B用)。多道程序
执行,就是同时看起来有多个程序在一块儿执行,那每一个程序执行都须要系统分配给它资源来执行,好比CPU
、内存
。记录目前程序运行的状态
。进程控制块
(PCB),用来描述进程的各类信息(好比代码段放在哪)。简要的说,进程就是具备独立功能的程序
在数据集合上运行的过程
。(强调动态性)
以下图,分别说明一下
进程标识符PID
至关于身份证。是在进程被建立时,操做系统会为该进程分配一个惟一的、不重复的ID,用于区分不一样的进程
。UID
用来表示这个进程所属的用户
是谁。内存的什么地方
。内存的什么地方
。分配获得的I/O设备
。在一个系统中,一般由数10、数百乃至数千个PCB
。为了对他们加以有效的管理,应该用适当的方式把这些PCB组织起来。这里介绍一种组织方式,相似数据结构里的链表。
进程是程序的一次执行。
在这个执行过程当中,有时进程正在被CPU处理
,有时又须要等待CPU服务
,可见,进程的 状态是会有各类变化。为了方便对各个进程的管理,操做系统须要将进程合理地划分为几种状态。
进程的三种基本状态:
进程的另外两种状态:
进程的状态并非一成不变的,在必定状况下会动态转换。
以上的这些进程状态的转换是如何实现的呢,这就要引出下一个角色了,叫`原语。
不可被中断
的原子操做。咱们举一个例子看看原语是怎么保证不可中断的。 原语采用
关中断指令
和开中断指令
实现。
为何须要进程间通讯呢?
由于进程是分配系统资源的单位
(包括内存地址空间),所以各进程拥有的内存地址空间相互独立。
由于两个进程的存储空间不能相互访问
,因此操做系统就提供的一个内存空间让彼此都能访问,这就是共享存储的原理。
其中,介绍一下基于存储区的共享。
共享存储区
,数据的形式、存放位置都是由进程控制,而不是操做系统。字符流
(注意不是字节流)的形式写入管道,当管道写满时,写进程的write()
系统调用将被阻塞,等待读进程将数据取走。当读进程将数据所有取走后,管道变空,此时读进程的read()
系统调用将被阻塞。读进程
最多只能有一个。进程间的数据交换以格式化的消息
为单位。进程经过操做系统提供的"发送消息/接收消息"
两个原语进行数据交换。
其中消息是什么意思呢?就好像你发QQ消息,消息头的来源是你,消息体是你发的内容。以下图:
接下来咱们介绍一种间接通讯
的方式(很像中介者模式或者发布订阅模式), 以下图:中介者是信箱,进程经过它来收发消息。
为何要引入线程呢?
同一时间作一件事情
(好比QQ打字聊天)没有多线程并发能力
,QQ可以的实用性就大大下降了。因此咱们须要线程
,也就是须要进程拥有可以并发
多个事件的能力。引入线程后带来的变化
同步。是指多个进程中发生的事件存在某种前后顺序。即某些进程的执行必须先于另外一些进程。
好比说进程A
须要从缓冲区读取进程B
产生的信息,当缓冲区为空时,进程B
由于读取不到信息而被阻塞。而当进程A
产生信息放入缓冲区时,进程B
才会被唤醒。概念如图1所示。
互斥。是指多个进程不容许同时使用同一资源。当某个进程使用某种资源的时候,其余进程必须等待。
好比进程B
须要访问打印机,但此时进程A
占有了打印机,进程B
会被阻塞,直到进程A
释放了打印机资源,进程B才能够继续执行。概念如图3所示。
信号量
主要是来解决进程的同步
和互斥
的,咱们前端须要了解,若是涉及到同步和互斥的关系(咱们编程大多数关于流程的逻辑问题,本质不就是同步和互斥吗?)
在操做系统中,经常使用P、V信号量
来实现进程间的同步
和互斥
,咱们简单了解一下一种经常使用的信号量,记录型信号量
来简单了解一下信号量本质是怎样的。(c语言来表示,会有备注)
/*记录型信号量的定义*/
typedef struct {
int value; // 剩余资源
Struct process *L // 等待队列
} semaphore
复制代码
意思是信号量的结构有两部分组成,一部分是剩余资源value
,好比目前有两台打印机空闲,那么剩余资源就是2,谁正在使用打印机,剩余资源就减1。
Struct process *L
意思是,好比2台打印机都有人在用,这时候你的要用打印机,此时会把这个打印机资源的请求放入阻塞队列,L就是阻塞队列的地址。
/*P 操做,也就是记录型信号量的请求资源操做*/
void wait (semaphore S) {
S.value--;
if (S.value < 0){
block (S.L);
}
}
复制代码
须要注意的是,若是剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把挂到信号量S的等待队列中。
/*V 操做,也就是记录型信号量的释放资源操做*/
void singal (semaphore S) {
S.value++;
if (S.value <= 0){
wakeup (S.L);
}
}
复制代码
释放资源后,若还有别的进程在等待这个资源,好比打印机资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为继续态。
为何要讲这个呢,主要是node的流的机制,本质就是生产者消费者问题,能够简单的看看这个问题如何解决。
如上图,
生产者
的主要做用是生成必定量的数据放到缓冲区中
,而后重复此过程
。与此同时,消费者也在缓冲区消耗这些数据
。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
这里咱们须要两个同步信号量和一个互斥信号量
// 互斥信号量,实现对缓冲区的互斥访问
semaphore mutex = 1;
// 同步信号量,表示目前还能够生产几个产品
semaphore empty = n;
// 同步信号量,表示目前能够消耗几个产品
semaphore full = 0;
复制代码
生产者代码以下
producer () {
while(1) {
// 生产一个产品
P(empty);
// 对缓冲区加锁
P(mutex);
这里的代码是生产一个产品
// 解锁
V(mutex);
// 产出一个产品
V(full);
}
}
复制代码
消费者代码以下
producer () {
while(1) {
// 消费一个产品
P(full);
// 对缓冲区加锁
P(mutex);
这里的代码是消费一个产品
// 解锁
V(mutex);
// 消费一个产品
V(empty);
}
}
复制代码
为何须要内存
内存是计算机其它硬件设备
与CPU沟通
的桥梁、中转站。程序执行前须要先放到内存中才能被CPU处理。
内存的存储单元编址
实现的。(存储单元通常是以字节为单位)let a = 1
这段代码。内存分配分为连续分配
和非连续分配
,连续分配是指用户进程分配的必须是一个连续的内存空间
。
这里咱们只讲连续分配中的动态分区分配
。
什么是动态分区分配呢,这种分配方式不会预先划份内存分区
,而是在进程装入内存时,根据进程的大小动态地
创建分区,并使分区的大小正好适合
进程的须要。(好比,某计算机内存大小64MB,系统区8MB,用户区56MB...,如今咱们有几个进程要装入内存,以下图)
空闲的区域
,后面该怎么分配(也就是说随着进程退出,会有不少空闲的内存区域出现)咱们讲一种较为简单的处理方法叫空闲分区表
法来解决这个问题。以下图,右侧的表格就是一个空闲分区表。
当不少个空闲分区都能知足需求时,应该选择哪一个分区进行分配呢,例以下图,分别有20MB
,10MB
,4MB
三个空闲分区块,如今进程5
须要4MB
空闲分区,改怎么分配呢?
咱们须要按照必定的动态分区分配算法,好比有首次适应算法
,指的是每次都从低地址开始查找,找到第一个能知足大小的空闲分区。还有好比最佳适应算法
,指的是从空闲分区表中找到最小的适合分配的分区块来知足需求。
连续分配缺点很明显
,大多数状况,须要分配的进程大小,不能跟空闲分区剩下的大小彻底同样,这样就产生不少很难利用的内存碎片
。
这里咱们介绍一种更好的空闲分区的分配方法,基本分页存储
。以下图
将内存空间分为
一个个大小相等
的分区(好比:每一个分区4KB
).每一个分区就是一个“页框”
。页框号从0
开始。
将用户进程的地址空间分为与页框大小相等的一个个区域,称为“页”
。每一个页也是从0
开始。
文件是什么?
文件就是一组有意义的信息/数据
集合。
计算机中存放了各类各样的文件,一个文件有哪些属性呢?文件内部的数据应该怎样组织起来?文件之间又该怎么组织起来?
不容许
有重名的文件。内部的名称
。存放的路径
,同时也是在硬盘里的位置(须要转换成物理硬盘上的地址)执行权限
,是否有删除文件权限,修改文件权限等等。以下图,文件主要分为有结构文件
和无结构文件
。
经过树状结构
组织的。例如windows
的文件间的组织关系以下:
接下来咱们详细的了解一下文件的逻辑结构
逻辑结构是指,在用户看来,文件内部的数据是如何组织起来的,而“物理结构”
是在操做系统看来,文件是如何保存在外存,好比硬盘
中的。
好比,“线性表”
就是一种逻辑结构,在用户看来,线性表就是一组有前后关系的元素序列,如:a,b,c,d,e....
“线性表”
这种逻辑结构能够用不一样的物理结构实现,好比:顺序表/链表
。顺序表
的各个元素在逻辑上相邻,在物理上也相邻:而链表
的各个元素在物理上能够是不相邻的。“随机访问”
,而“链表”
没法实现随机访问。接下来我了解一下有结构文件的三种逻辑结构
什么是顺序文件
指的是文件中的记录一个接一个地在逻辑上是顺序排列
,记录能够是定长
或变长
,各个记录在物理上能够顺序存储
或链式存储
串结构
和顺序结构
。关键字无关
,一般都是按照记录的时间决定记录的顺序。关键字排列
。这里须要注意的知识点是,顺序文件的存储方式和是否按关键字排列
,会影响数据是否支持随机存取
和是否能够快速按关键字找到对应记录
的功能。
能够看到,顺序文件按顺序存放对于查找是很是有帮助的,咱们在记录文件的时候也能够注意利用这一点。
对于可变长记录文件
,要找到第i
个记录,必须先顺序查找前i-1
个记录(也就是须要遍历一遍),可是不少场景中又必须使用可变长记录,如何解决这个问题呢?这就引出来立刻讲的索引文件
给这些变长的记录都用一张索引表来记录,一个索引表项包括了索引号
,长度
和指针
。
其中,能够将关键字做为索引号的内容,若是关键字自己的排列是有序的,那么还能够按照关键字进行折半查找。这里是关键,由于咱们平时用mysql数据库对某个字段假如索引,就是这个道理。
可是创建索引表的问题也很明显,首先若要删除/增长
一个记录,同时也要对索引表
操做,其次,若是增长一条记录才1KB
,可是索引表增长i一条记录可能有8KB
,以致于索引表的体积大大多于记录。存储空间的利用率就比较低。
索引顺序文件是索引文件
和顺序文件
思想的结合。索引顺序文件中,一样会为文件创建一张索引表,但不一样的是,并非每一个记录对应一个索引表项
,而是一组记录对应一个索引表项。
如上图,学生记录按照学生姓名的开头字母进行分组。每一个分组就是一个顺序文件,分组内的记录不须要按关键字排序
首先,咱们须要了解一下文件控制
块是什么。咱们假设目前在windows的D盘
,以下图
能够看到,目录自己就是一种有结构的文件
,记录了目录里的文件
和目录
的信息,好比名称和类型。而这些一条条的记录就是一个个的“文件控制块”(FCB)
。
文件目录的结构一般是树状的
,例如linux里/
是指根路径,/home
是根路径下的二级目录
不容易实现文件共享
,因此在树形目录结构的基础上,增长了一些指向同一节点的有向边(能够简单理解为引用关系,就跟js里的对象同样)每一个共享节点
设置一个共享计数器
,用于记录此时有多少个地方在共享该结点。只有共享计数器减为0
,才删除该节点。咱们这里介绍一种索引分配的方式:
索引分配容许文件离散地分配在各个磁盘块中,系统会为每一个文件创建一张索引表,索引表记录了文件各个逻辑块对应的物理块。索引表存放的磁盘块称为索引快。文件数据存放的磁盘块称为数据块。
如上图,假设某个新建立的文件'aaa'的数据依次存放磁盘块2->5->13>9。7号磁盘块做为’aaa‘的索引块,索引块保存了索引表的内容
上面咱们讲到了文件的逻辑分配,是站在用户视角的分配,物理分配是站在操做系统的角度的分配,分配的是实际的物理磁盘里的空间。
举个例子,咱们用户看到的文件,意识里都是顺序排列的,好比说,excel表有100行数据,用户看来这100行数据就是连续的。
在操做系统的视角,它能够把这100行数据分为一个个的数据块,好比跟磁盘块(假设磁盘块是1kb大小)同样都是1kb,拆分后的数据可使用索引表的形式分配(也就是咱们上面才讲了的,索引分配的方式,打散分配在实际的物理磁盘里)
首先,咱们了解一下磁盘分为目录区
和文件区
。
接着,咱们了解一下常见的两种文件存储空间的管理算法
,以下图,假如硬盘上空闲的数据块
是蓝色,非空闲的数据
块是橙色。
对分配连续的存储空间,能够采用空闲表法
(只讲这种较简单的方法)来分配
和回收
磁盘块。对于分配,能够采用首次适应,最佳适应等算法来决定要为文件分配哪一个区间。(空闲表表示以下)
首次适应
是指当要插入数据的时候,空闲表会依次检查空闲表中的表项,而后找到第一个知足条件
的空闲区间。
最佳适应算法
是指当要插入数据的时候,空闲表会依次检查空闲表中的表项,而后找到知足条件并且空闲块最小的空闲区间
。
再讲一种位示图法
以下图:
每个二进制位对应一个磁盘块,好比上面0表示空闲块,1表示已分配的块。并且咱们能够经过必定的公式,能够从示图表的横纵坐标推断出物理块的实际地址,也能够从物理块的实际地址推断出在表里的横纵坐标。
文件共享分为两种
注意,多个用户共享同一个文件
,意味着系统只有“一份”
文件数据。而且只要某个用户修改了该文件的数据,其余用户也能够看到文件的变化
。
软链接能够理解为windows
里的快捷方式
。
硬连接能够理解为js里的引用计数
,只有引用为0
的时候,才会真正删除这个文件。
操做系统须要保护文件的安全,通常有以下3种方式:
“口令”
(好比123),用户请求访问该文件时必须提供对应的口令。口令通常放在文件对应的FCB或者索引结点
上。"密码"
对文件进行加密,在访问文件时须要提供正确的“密码”
才能对文件进行正确的解密。访问控制列表
,该表中记录了各个用户能够对该文件执行哪些操做。咱们举一个实际案例,把上面的内容总结一下。
假如咱们当前的操做系统采用以下的文件系统管理方式:
那么咱们假设0号磁盘块就是装载位示图的磁盘块,用来管理哪些磁盘是空闲的哪些是正在使用的。
由于这里咱们已经使用了0号块,位示图的第一项就是1
接着咱们把磁盘块第二块用来放inode节点,也就是文件目录索引节点,意思是文件目录下存放的只有文件名和索引节点的位置,要知道文件的详细信息,就要靠着
假设2号磁盘块存放了咱们根目录信息,而自己目录其实也是一种特殊的文件,也在inode节点表里有本身的信息,1号磁盘块增长相似以下信息:
类型:目录
存放在:2号磁盘块
复制代码
2号磁盘块里面存放了一个你好.txt
文件,那么2号磁盘块会增长一行信息,相似
文件名:你好.txt
inode结点:2
复制代码
这就意味着,2号inode节点存放了你好.txt文件的具体磁盘块在哪,因此在1号磁盘块的2号inode节点增长
类型:txt
存放在:三、4号磁盘块
复制代码
为何要放到三、4号磁盘块呢,由于咱们有位示图,知道哪些磁盘块是空闲的就分配给它,一扫描发现三、4正空闲并且知足存放这个文件的条件,就分配出去了。
咱们在细分一下这里的三、4号磁盘块是什么意思,由于咱们物理磁盘块分配的方法是混合索引,其实三、4号磁盘块表示的形式以下:
直接索引:3号磁盘块
直接索引:4号磁盘块
复制代码
上面只涉及到直接索引,咱们其实还能够有1级索引,2级索引,这些索引指向的是一个索引表,咱们这里就不详细叙述了(以前交过索引块和索引表)。
到这里咱们们就基本明白了一个文件系统的基本运行原理。
什么是I/O设备
I/O就是输入输出
(Input/Output)的意思,I/O设备就是能够将数据输入到计算机,或者能够接收计算机输出数据的外部设备,属于计算机中的硬件部件。
CPU没法直接控制I/O设备的机械部件
,所以I/O设备还要有一个电子部件做为CPU
和I/O设备
机械部件之间的“中介”
,用于实现CPU对设备的控制。这个电子部件就是I/O控制器
。
控制寄存器
来存放命令和参数状态寄存器
,用来记录I/O设备是否空闲
或者忙碌
数据寄存器
。输出时,数据寄存器用于暂存CPU发来的数据
,以后再由控制器传送给设备。“地址”
。I/O控制器经过CPU提供的“地址”来判断CPU要读写的是哪一个寄存器这里咱们指讲一下目前比较先进的方式,通道控制方式。
通道能够理解为一种“弱鸡版CPU”
。通道能够识别并执行一系列通道指令。
通道最大的优势是极大的减小了CPU的干预频率
,I/O设备
完成任务,通道会向CPU发出中断
,不须要轮询来问I/O设备是否完成CPU下达的任务。
本文完结。
预告:后面会有数据结构入门知识(经常使用的数据结构以及在内存的存储形式,并对比其增删改查的时间复杂度)
注: 本文绝大多数资料来源于如下的学习视频资料