操作系统定义 是控制和管理计算机系统的硬件和软件资源,合理的组织计算机工作流程以及方便用户使用,是一种系统软件

操作系统作用 设置操作系统的目的就是提高计算机系统的效率,增强系统的处理能力,充分发挥系统的利用率,方便用户使用

硬件基础---中断和通道

中断 指CPU 在收到外部中断信号后,停止原来工作,转去处理该中断事件,完毕后回到原来断点继续工作。

中断过程:中断请求,中断响应,中断点(暂停当前任务并保存现场),中断处理例程,中断返回(恢复中断点的现场并继续原有任务)

通道( 又称为I/O 处理机)  实际上是一台功能单一、结构简单的I/O 处理机,它单独与CPU ,并直接控制外部设备,与内存进行数据传输。

有自己的I/O 处理器,指令,可编程实现各种复杂的I/O 处理

多道程序设计技术的概念   在计算机内存中同时存放几道相互独立的程序,它们在管理程序的控制下相互穿插地运行,共享CPU 和外设等资源。采用多道程序设计技术的批处理系统称为多道批处理系统

操作系统是一种  系统软件 ,在操作系统中采用多道程序设计方式能提高CPU 和外部设备的   利用效率 。一般来说,为了实现多道程序设计,计算机需要有   更大的内存

三种操作系统基本类型批处理系统、分时系统、实时系统

分时系统:多个用户分时(按时间划分轮流)的使用同一计算机的系统称为为分时系统。

分时系统中,为使多个用户能够同时与系统交互,最关键的问题是  能在一短的时间内,使所有用户程序都能运行;

分时特点:

– 同时性或多路性:多用户同时操作、使用计算机

– 独占性:各终端用户感觉到自己独占了计算机;

– 及时性:用户的请求能在较短时间内相应;

– 交互性:用户能与计算机进行人——机对话。

响应时间为用户发出一条指令到系统处理完这条指令并做出回答所需要的时间

实时操作系统主要用于过程控制、事务处理等有实时要求的领域,其主要特征是 实时性可靠性

在设计分时操作系统时,首先要考虑的是  交互性和响应时间 ;在设计实时操作系统时,首先要考虑的是  实时性和可靠性 ;在设计批处理系统时,首先要考虑的是  周转时间和系统吞吐量

目前的操作系统,通常具有分时、实时和批处理功能,又称作通用操作系统

现代操作系统主要特征

并发性

单处理机、多道程序 处理时,宏观上并发,微观上 交替执行。并发指的是进程,操作系统是一个并发系统。

共享性

多个进程共享有限的计算机系统资源,系统合理分配,资源在一个时间段内 交替被多个进程所用。

虚拟性

一个 物理实体映射为若干个 对应的逻辑实体(分时或分空间)。虚拟是操作系统 管理 系统资源的 重要手段,可提高资源利用率。

异步性

异步性也称不确定性,指进程的执行顺序和执行时间及执行 结果的不确定性:A程序执行结果不确定,不可再现  B多道程序设计环境下, 程序按异步方式运行。

 

为什么需要中断,异常,系统调用?

中断:因为IO设备的时候,比如说键盘,你如果敲了之后CPU在干别的事,没空理你,那么,用户看到没有反应就会觉得非常不好用,所以这个时候就要中断停下手头的事情,然后给出响应

异常:因为除数为0这种错误都是运行到了之后才能知道,这个时候再弄下去就会有问题了,所以就有了异常来报错

系统调用:银行这种安全的地方需要一定的保护机制,然后又要让用户访问,这个时候就需要系统内核层面的调用来,因为内核是最安全的

 

系统调用:源头:应用程序主动向操作系统发出的服务请求      响应方式:异步      处理机制:持续的,对用户透明

异常:  源头:非法指令或者其他原因导致当前指令执行失败  响应方式:同步      处理机制:杀死或者重新执行命令

中断:  源头:来自硬件设备的处理请求             响应方式:同步或者异步  处理机制:等待和持续

 

系统调用和函数调用的区别?

系统调用有个堆栈转换

函数调用就是用简单的堆栈

系统调用的堆栈和函数调用的堆栈是不同的堆栈,因为如果是一样的,函数堆栈就可以很简单的修改里面的值,这是不安全的

系统调用比函数调用更安全,但是系统调用的开销比函数调用大

原因:因为牵扯到的有几点

1,引导机制       2,建立堆栈      3,验证参数(验证传过来的参数到底符不符合)    4,内核态映射用户态的地址空间    4,内核态独立地址空间

 https://blog.csdn.net/qq_39823627/article/details/78736650  用户态与内核态的详细讲解

 

 用户态和内核态的操作流程

用户态也称用户运行态     内核运行态   ->也就是用户运行的时候的状态,和内核运行时候的状态

一个简单的例子:用户的时候使用read读取文件,然后就用经过编译 汇编  链接 三个过程后,转到内核态调用read的函数

 

内存管理方式

重定位   分段  分页    虚拟存储

 

内碎片和外碎片

内碎片:内碎片是分配的时候,用户申请内存,但是我动态分配,它无法利用的地址就是内碎片,因为计算机只能分配2的次方的内存地址,但是如果用户申请511,那么实际会分配512,那么有1个字节无法使用

 

外碎片:就是经过多次分配,剩下空闲分区无法再分配给其他人,也就成了碎片

 

连续内存分配

1,最先分配    找到第一个可以分配的地址

2,最佳分配  找到正好大于的

3,最差匹配     找到最大的来分配

优缺点,emmm  不写了

连续内存的碎片整理

1,紧凑  (如果没有指针啥引用的话,可以动态重定位就可以去把所有空闲空间合在一起)

2,分区兑换(通过抢占并回收处于等待状态进程的分区,增大可用内存空间)

 

 

  伙伴系统

一种平衡各种分配方式优劣的一种分配方式

运作原理:开始是一整块2^k,如果申请的大小是 2^(k-1)<=x<=2^k ,那么就把这个块分配给它,如果不是,就把当前块给劈开,分成两半,如果还是不满足,还劈,也就是说

内碎片缩小到最多是当前块的大小,效率也不会低,是一种综合的做法,这个用法在Linux内核中用到

 

非连续存储

非连续存储的发明主要是因为每次申请连续存储太难了,一言不合就会产生内外碎片,这个时候就想出了不连续的做法,方法有段式,页式,段页式,基本思想的话:我不连续,但是我也不是每个单元都不连续。单位不是一个字节,而是一个段或者一个页,段就是分配的比较大的块,页就是分配的比较小的块,段内和页内都还是连续的,这样也尽量避免了复杂性,逻辑地址是连续的,但是其实中间会有一个段表 页表与

物理地址做了一个映射,实现了逻辑上连续

段式存储,页式存储

段式存储里面的类型都相同或者相似的,分配这么大空间,一段是段号,一个是段内偏移,这个挺好理解的,页的话也一样,就是一个页号,一个页内偏移

页表

实现逻辑地址和物理地址映射的一个东西,但是呢也带来一些问题,因为页表采用的是,一对一的映射,当64位系统的时候,那么页表可能会非常大,那个时候一个页就分配很大的空间,非常不划算。

还有就是用页表会访问比连续存储慢,连续存储直接加上偏移就能得到实际的物理地址,但是这个的话要先去访问页表,然后才能去访问物理地址,速度上慢了一点

解决办法:

1,多级页表,也就是有个类似目录一样的玩意儿,然后去寻找,好处就是如果全部排满倒是花费的内存比普通页表多,但是大部分情况来说都是用不满的,所以我们开始只用建立一层目录,然后动态创建即可

但是普通的话像是一维数组必须先创建完空间,所以实现了页表过大的优化

2,快表:为了优化页表时间上过慢的一个东西,在CPU寄存器中存了一个小一个点表,存储的都是最近访问的,因为是CPU内,所以访问起来特别快,相当于缓存

 

段页式存储

就是利用了两个的优点然后建立出来的一个东西,因为段式里面相同类型,所以在内存保护上比较完善,页的话因为块比较小,比较灵活,利用效率比较高

然后存储就是   第一段  段号    第二段   页号    第三段   页内偏移

 

段页式存储的内存共享

进程的结构  :共享段,堆数据段 ,代码段

其实就是利用共享段,他们实现共享就是两个进程指向的页表是一样的,所以他们使用的物理地址也是一样的,然后实现了共享

 

虚拟内存

虚拟内存是为了跟进时代,要运行的程序越来越大,然后内存不够使用,程序可能都放不下内存中然后发明的一种技术,最开始前期有两种方法解决这个问题,覆盖和交换

覆盖:代表是一个程序就已经大于内存了,思路:将程序划分成很多模块,然后划分成会可能同时运行的和不会同时运行的,不会同时运行的就共享一块内存区域,到了需要执行的块时就进行块的一个置换。在外存和内存中置换一个模块

缺点:这些划分模块的工作全都是程序员做的事,极大的增加了编程复杂度

交换:运行的程序可以装入内存,但是因为内存中有其他进程,所有装不下,这个时候就进行一个置换,将一些进程都放入外存中,然后当前运行就可以放入内存中了

缺点:每次都直接把一个进程的空间转移,开销太大

基于上面两个然后结合了优点,弱化了缺点研究出了虚拟内存,虚拟内存把分模块的工作交给了操作系统,然后把进程转移改成了进程页的转移

大概流程

1,去访问页表

2,发现页表中标记在外存中,报缺页异常

3,然后去寻找外存中的页面

4,转移到内存中

5,将页表项修改

6,重新执行访问命令

 

页面置换算法

也就是虚拟内存中缺页的时候选择内存中哪一块被置换出去的一个算法,有很多,最优算法,FIFO  LRU

最优算法一般是用其他算法跑完,然后最优算法再去跑一遍做比较的

最优算法思想:比较每个的下一次访问时间,然后选择最长的置换

FIFO:选择呆在内存块中最久的那个

LRU:比较上一次的访问时间,最早的被置换

LRU结构有链表和栈两种形式,复杂度差不多

 

 

时钟置换算法

是FIFO和LRU的一个折中算法,FIFO是在链表中没有做任何处理,直接找到第一个,开销较小,缺页次数最大,时钟置换算法是开销中等,缺页次数中等,LRU是少的,但是实现起来开销比较大

工作机制:FIFO  顺序遍历,找到按时间呆在最久的那个

     LRU    链表排的是按上一次访问时间的排序,头是最近访问的,尾是离得最久的,然后找去插入进去,缺页的话找到最后一个被置换,然后放入表头

     Clock   一个循环链表,直接遍历,链表上打了标记,一个访问位,在缺页的时候如果当前访问位是1打0,然后到下一个找,找到0即为置换,标记1

在最坏的情况 三种算法效率一样,即每次都是没见过的

FIFO有delady现象,就是改大了内存存储页的空间,但是缺页次数还更多了

最不常用算法:使用访问次数做比较,缺点,如果一个访问次数很大了,后期就算不访问也不会被置换

 

全面置换算法

因为局部性置换算法不能很好的控制访存的差异,可能你这一段你加一页,你的中断会大幅度减少,有些段就弄几页就够了,是动态的

工作集和常驻集

工作集 是理想情况内存中存在的页

常驻集 是现实情况内存中存在的页

常驻集>=工作集  这个时候的中断较少

否则就是抖动问题,会发生很多中断,也就是进程里面分配的物理页不够,原因是因为运行进程越来越多,然后物理页减少

 

工作集置换算法:首先定窗口的大小,然后如果一个页过了窗口大小还没被访问就置换出去

缺页率置换算法:两个缺页中断中,内存中在这中间的没被访问就置换出去

 

 

进程

进程其实就是一个程序去对数据的一次动态执行,(进程由PCB   程序    数据三部分组成),进程空间内包含有自己的堆,代码区,数据,栈等,进程控制块是在内核中负责操作的

进程状态有 创建   就绪    运行    等待     结束五种状态

创建就是还没分配完资源时处于的状态

就绪就是分配完了资源,只要等待运行就可以了

运行就是正在运行的进程

等待就是IO设备的申请,系统的一些服务之类,这个时候进程是不能运行的,只能处于等待状态

结束就是销毁进程

其中有指向的有      创建->就绪         就绪->运行           运行到->就绪(时间片用完)      运行->等待(IO事件)        等待->就绪(等待的事件发生了)

为了节省内存空间,开了挂起状态,有就绪挂起和等待挂起,和就绪和等待的区别也就是位置是处于外存中,腾出空间给其他进程,普通到挂起的条件有可能是普通的优先级更高之类的

挂起到普通就是优先级比当前的低

 

线程

因为进程之间有隔离性,而且共享消息起来开销太大,所以又提出了线程的概念,线程就是为了实现并发执行

线程是在进程内部的,进程中含有多个线程,同一个进程的线程可以互相共享空间,也就是他们互相共享代码区,数据,堆这些资源,然后还有自身的栈和寄存器资源

线程提出之后,进程成了分配CPU资源的单位,线程成了调度的单位

线程的优点:

一个进程可以同时存在多个线程

各个线程之间可以并发执行

y各个线程可以共享资源

线程缺点:

一个线程崩溃可能导致进程内所有线程崩溃,原因线程之间共享资源

线程和进程的区别:

进程是资源分配单位,线程是CPU调度单位

进程含有完整的资源,线程只有寄存器和栈

线程的时间开销和空间开销比进程少(因为线程资源要求少,因为线程间资源共享,切换不需要内核通信)

用户线程:用户线程也就是在进程内用户自己进行了一些操作,在进程内存储线程TCB,但是内核是完全不知晓的

好处:不依赖内核,在用户空间实现线程由线程库函数维护,进程内的线程切换速度快(因为不牵扯内核,用户态)

坏处:如果线程堵塞,进程就堵塞      不支持抢占      线程分配的时间片太小

总结:坏处主要都基于内核不知道,好处页式基于内核不知道

内核线程:把TCB放入内核中控制,然后和进程产生映射关系,这样的话线程就和内核是一对一的关系,时间片   抢占什么的都不是问题

缺点:牵扯内核,开销较大

 

 

进程切换:

进程切换的话也就是把CPU资源让给别人,然后自身进入等待状态,在这里要把自己上下文保存下来,俗称保存现场,然后下次轮到自己的时候恢复线程然后继续执行

进程创建:

使用fork拷贝当前进程,当前进程的内存空间,缓存,还有程序代码段等等都会拷贝过来,这个也就是子进程,

exec代表改变进程运行的程序,并且可以带有一些参数啥的,这样就能把进程内容覆盖掉,成了一个新的进程

父进程可以通过wait(),使用过该方法后父进程会等待子进程的返回值

1,有子进程,进行等待

2,没有,直接退出

3,有很多僵尸进程,然后随便选一个值

子进程通过exit()返回值,使用该方法,就会给一个值给父进程,等待父进程处理,如果父进程一直不处理就会变成僵尸进程

 

 

调度:

也就是进程之间切换怎么选择的问题,有几个算法

1,先来先服务        简单,但是平均周转时间一般太高

2,短进程优先        平均周转时间是最低的,但是可能长进程会饥饿,一直得不到资源       利用过去的访问时间来进行猜测预估,得出结果

3,高响应比      (s+t)/s     算出响应时间        

 

时间片轮转

就是给每个进程分配一个时间片,一般设置时10ms,这个能保证每个进程不会太饥饿,但是平均周转时间相对较大,并且会有额外的进程切换的开销

多级反馈队列

也就是有很多个就绪队列,然后不同的队列使用的调度算法不一样,使用比较灵活

 

实时调度

给每个任务定义了一个开始时间,和截止时间,要在这个区间将这个任务完成

分为了硬时限和软时限

硬时限如果不行,错过会导致严重后果,必须在最坏情况都要完成

软事先不能满足的话降低要求,尽量满足要求就行

 

同步问题:

现实生活中例子:

冰箱买面包

 

A想吃面包

去冰箱找  

发现没有 B想吃面包

去买    去冰箱找

      发现没有

      去买

 

这样就买了重复的面包,如果在计算机内,弄出很多重复的事情将不可估量,然后就考虑做法

这里就牵扯到了计算机的同步,把这些操作都定义为原子操作,那么那个地方就是临界区,同时只能有一个人访问

 

 

临界区的实现有三种

1,禁用中断    也就是直接用硬件禁用中断掉,那么无法产生进程切换, 从而达到只有一个访问,但是可能导致其他进程进入饥饿,系统也会停止下来

2,软件操作    使用一些共享变量

3,高级抽象   化为原子操作

 

信号量

代表资源的数量,有两个操作  P,V

P :代表我申请一个资源,资源-1,如果<0,那么处于等待状态

V:代表释放一个资源,资源+1,如果<=0,代表有人申请了但是处于等待状态,那么我就去唤醒它

int num=3;

P(){
    num--;
    if(num<0) {
         add 当前进入等待队列
        block  堵塞在这等待有资源
    }                
}

V (){
     num++;
     if(num<=0){
         remove  从队列中删除
         wakeup   唤醒一个线程
    }
}    

 

 

经典问题: 生产者-消费者

要求:一个桶子可以装n件物品,生产者不断的在制作东西,消费者不断在拿东西,但是对桶子的操作只能同一时间做一个,如果桶子满了,那么生产者需要等待,如果桶子是空的,那么消费者需要等待

 

int x=1;    //代表互斥信号量,同一时间只能一个人操作桶
int y=0;    //代表现在桶里的东西数量    
int z=n;   // 代表现在桶里剩余空间

product(){   //生产者
       P(z);
       P(x);
        制作东西  
      V(x);
      V(y);      
}

custmer(){
      P(y);
      P(x) ;
      拿东西
      V(x); 
      V(z);
}

 

管程:

管程也是用于临界区的一种方法,它的操作过程就是,考虑到了当前线程因为一些原因等待时候的情况,这个时候就先放弃临界区的锁,给别的进程先执行,在里面设置有等待队列

Wait():将自己加入等待队列种,然后唤醒一个等待者或者释放管程的互斥访问

Signal():  将等待队列中的一个线程唤醒

管程形式的生产者-消费者问题

Lock lock
int count=0    
int   x,y    //代表东西数和空间数


product(){
     lock-Acquire() ;
     while(count==n){
            notFull.wait(&lock)  //把自己加入等待队列
      }   
       Add c to the buffer;
      count++
       notEmpty.Signal();
      lock->release
}        

cutomer(){
     lock->Acquire()
     while(count==0)
           notEmpty.Wait(&lock);
      remove c from  buffer
      count--
     notFull.Signal
     lock->Release()    
}

 

 

哲学家就餐

要求:哲学家就餐也就是n个人n把叉围成一圈,每个人左右两边分别有把叉,然后哲学家每个都能做两件事,思考和就餐,就餐必须拿自己左右两边的叉子才能就餐,怎么有序执行下去

这里明显叉子就是互斥信号量

 

信号量做法

int cha[5]    //5把叉子

while(1){
      think()//哲学家在思考
      P(cha[i]);
      P(cha[i+1])
      eat()    //就餐
      V(cha[i]);    
      V(cha[i+1])
}        

//如果每个哲学家同时动手拿左边叉子,然后都得不到右边,又无法释放资源,那么处于死锁


while(1){
      think()//哲学家在思考
     P(flag) //  临界区
     P(cha[i]);
     P(cha[i+1])
      eat()    //就餐
      V(cha[i]);    
      V(cha[i+1])
      V(flag);
}        
//这种方法虽然可以,但是加了临界区之后,明明5个人可以同时两个用餐,但是这样变成了一次只能有一个人



while(1){
      think()//哲学家在思考
       if(i%2==0){ 
           P(cha[i]);
           P(cha[i+1])
       }
       else{
            P(cha[i+1]);
            P(cha[i]);
       }
     eat()    //就餐
      V(cha[i]);    
      V(cha[i+1])
}                        
因为第一种方案的死锁只是在都拿左边的时候才会出现,所以只要控制不同人拿的方向不一样就不会出现这种情况了    

 

 

读者-写者

要求:读者能够同时有多个进行访问,写只能一个人进行写,读写不能同时进行

 

 

 

 

 

 

 

 

死锁概念:

死锁就是进程都处于在申请资源的状态,并且自己也持有对方要的资源

 

死锁四个必要条件:四个同时成立才会发生死锁

1,互斥:这些资源必须是互斥的,同时只能有一个能够访问

2,请求并保持:自己有资源而且处于申请资源的状态

3,非抢占:只能进程自己运行完自己主动释放资源不能够抢夺

4,循环等待:构成一个环  A->B->C->A   申请的资源都在对方这里

 

死锁处理

死锁预防:  构成死锁的四个必要条件,只要把其中一个破坏,那么就会预防掉死锁,所以有四种办法

死锁避免:如果当前分配资源会发生死锁,那么申请的资源不会得到分配,然后可以通过银行家算法得出安全序列

死锁检测和恢复

 

 

银行家算法:银行家算法顾名思义就是银行家(操作系统)贷款(资源),给顾客(进程) ,进程用完资源就得快速归还

银行家算法有四个数组,Max(资源的最大需要量)   need(还需要多少)    Avail(当前分配了多少)     P(还剩多少资源)   

判断安全:去找need,看是否当前资源能够满足,能够满足的话就给它,然后一次顺序找可以分配资源的进程,如果找了一圈发现当前资源不能够满足它了,那么就结束,代表当前状态处于不安全,可能发生死锁

如果所有进程能够满足完,那么代表当前是一个安全序列

 

进程通信: 有直接通信和间接通信两种

直接通信: 管道  共享内存

间接通信:信号  消息队列

 

信号:过程进程注册时候把自己有的信号函数注册在内核以便识别,然后处理器或者别的进程发送信号,这个时候从内核取出信号函数进行判断

管道:一般用于父进程建立子进程,然后父子通信,一边发,一边收,创建的时候有参数是一个两位数组,过程大概是shell建立一个管道,然后把进程放入管道发方,再把进程放到收方

消息队列:消息队列支持多个进行发送,然后发送到消息队列里面,以字节为单位,然后收方直接去消息队列中去取,消息队列是内核操作的

共享内存:如果是线程的话,那么就是天然的共享内存,进程的话有专门设置一个共享区,然后因为共享内存可能发生两个进程一起在写的情况,所以一般要搭配同步来使用

 

文件系统 和  IO 下次写把,挂机了