管理计算机硬件和软件资源的系统软件
方便性、有效性、可扩充性、开放性
提高系统资源的利用率,提高系统的吞吐量
最基本的特征,互为存在的条件:并发、共享
中断机制的作用:为了在多道批处理系统中让用户进行交互;
分析:外设和CPU交替空闲和忙碌,CPU和外设利用效率低
从单道批处理系统对CPU的利用情况可看出,作业运行过程中若发生IO请求,高速的CPU要等待低速的I/O操作完成,导致CPU资源利用率和系统吞吐量降低。
内存中存放多道程序,当某道程序因某种原因如执行I/O操作时而不能继续运行放弃CPU时,操作系统便调度另一程序运行,这样CPU就尽量忙碌,达到提高系统效率的目的。
分析:程序A要通过操作系统的调度进行磁盘操作,B则进行磁带操作。当程序A执行I/O请求时,A放弃了CPU,操作系统接着调度B,B开始占用CPU(红宽线),此时程序A的磁盘操作也在同时进行。
结论:A,B两道程序相互穿插运行,使CPU和外设都尽量忙碌。
系统给程序员(应用程序)提供的唯一接口,可获得OS的服务。在用户态发生,核心态处理
为了更好地描述和控制程序并发执行,实现操作系统的并发性和共享性(进程是动态的,程序是静态的)
是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位(没有引入线程时 )
用于描述和控制进程运行的通用数据结构,记录进程当前状态和控制进程运行的全部信息,是进程存在的唯一标识。
区别与联系:
有一群生产者进程在生产产品,并将这些产品提供给消费者进程进行消费,生产者进程和消费者进程可以并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程需要将所生产的产品放到缓冲区中(+1操作),消费者进程可以从缓冲区取走产品消费(-1操作)。
(感觉有点微服务的意思了)产生问题:当两者并发执行时可能出差错,导致预期的结果与真实的结果不相符:当执行生产者+1和消费者-1操作之后,缓冲区的值从10变为了11。
由于前两点原因,因此需要保持线程间的同步,即一个线程消费(或生产)完,其他线程才能进行竞争CPU,获得消费(或生产)的机会。对于这一点,可以使用条件变量进行线程间的同步:生产者线程在product之前,需要wait直至获取自己所需的信号量之后,才会进行product的操作;同样,对于消费者线程,在consume之前需要wait直到没有线程在访问共享区(缓冲区),再进行consume的操作,之后再解锁并唤醒其他可用阻塞线程。
有5个哲学家,他们的生活方式是交替的思考和进餐,哲学家们共同使用一张圆桌,分别坐在5张椅子上,圆桌上有5只碗和5只筷子。平时哲学家们只进行思考,饥饿时则试图取靠近他们的左右两只筷子,只有两只筷子都被拿到的时候才能进餐,否则等待,进餐完毕后,放下左右筷子进行思考。
这会导致以下的问题,筷子就相当于临界资源:
临界资源指的是一些虽作为共享资源却又无法同时被多个线程共同访问的共享资源。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放该共享资源才可重新竞争使用共享资源。
当某一个哲学家能够同时拿起左右两只叉子时,才让他拿,这样就能够保证不会因为每个科学家都只拿了一只叉子而导致死锁。
为了保证能够同时拿起,我们需要对拿叉子这一步骤进行加锁,保证哲学家能够同时拿起一双叉子,而不会拿了一边后另一边被人抢走
class DiningPhilosophers {
public:
DiningPhilosophers()
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
//对拿叉子进行这一流程进行加锁,保证其能同时拿起一双,而不会被其他人抢走
_lock.lock();
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
_lock.unlock();
//拿起左右叉子
pickLeftFork();
pickRightFork();
eat(); //吃饭
//放下左右叉子
putLeftFork();
putRightFork();
//解锁,让其他人获取叉子
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
}
private:
mutex _lock;
mutex _fork[5];
};
如果要保证至少有一个哲学家能够进餐,那么我们可以采用最简单粗暴的方法,限制人数,只要同时进餐的哲学家不超过四人时,即使在最坏情况下,也至少有一个哲学家能够拿到多出来的那一个叉子。
我们需要用到一个计数器来表示当前就餐的人数,为了保证线程安全我们需要用到一个互斥锁和一个条件变量对其进行保护
class DiningPhilosophers {
public:
DiningPhilosophers()
:_count(0)
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
unique_lock<mutex> lock(_mtx);
_cond.wait(lock, [this]()->bool{
return _count < 4;
}); //当就餐人数不超过四人的时候允许拿叉子
++_count;
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
--_count;
_cond.notify_one(); //就餐完成,让下一个人进来就餐
}
private:
mutex _fork[5];
mutex _mtx;
condition_variable _cond;
int _count;
};
由于餐桌是一个如下图的圆环,如果我们此时规定奇数位的哲学家先拿左边的叉子,再拿右边的叉子。而偶数位的哲学家先拿右边的再拿左边的,此时竞争情况如下图所示
此时2号和3号哲学家争抢3号叉子,4号和5号哲学家争抢5号叉子,1号没有竞争对手,直接获取叉子1。
可以看到,在第一轮中所有哲学家先去争抢奇数叉子,抢到偶数叉子后再去争抢偶数叉子,这样就能够保证至少有一个科学家能够获得两只叉子
class DiningPhilosophers {
public:
DiningPhilosophers()
{}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
//如果是奇数则先抢左后抢右
if(philosopher & 1)
{
_fork[philosopher].lock();
_fork[(philosopher + 1) % 5].lock();
pickLeftFork();
pickRightFork();
}
//如果是偶数则先抢右后抢左
else
{
_fork[(philosopher + 1) % 5].lock();
_fork[philosopher].lock();
pickRightFork();
pickLeftFork();
}
eat(); //吃饭
putLeftFork(); //放下叉子
putRightFork();
_fork[philosopher].unlock();
_fork[(philosopher + 1) % 5].unlock();
}
private:
mutex _fork[5];
};
管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
对竞争资源在多进程间进行使用次序的协调,使得并发执行的多个进程之间可以有效使用资源和相互合作。
使用fork系统调用无参数,fork会返回两次,分别返回子进程id和0,返回子进程id的是父进程,返回0的是子进程。
如果创建失败,返回-1
子进程一般继承父进程:用户信息、权限、目录信息、信号信息、环境表、共享存储段和资源限制。 (86条消息) 子进程和父进程的关系和示例_xujiali5172923的博客-CSDN博客 【Linux 进程】fork父子进程间共享数据分析 - 我得去图书馆了 - 博客园 (cnblogs.com) 进程——父子进程共享 - _程序兔 - 博客园 (cnblogs.com) (1)父子进程 子进程通过父进程创建,子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程什么时候结束。 当子进程退出的时候,内核会释放子进程所有资源,包括打开的文件,占用的内存等。但是依然会保留部分信息(进程id,退出状态,运行时间),直到父进程通过wait/waitpid来调用获取子进程状态信息后才释放 (86条消息) 面试中常被问到的(18)父子进程,孤儿进程及僵尸进程_HT . WANG的博客-CSDN博客
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程,孤儿进程将被init进程(1号进程)托管,由init进程负责完成状态收集工作
通常情况下,子进程退出后,父进程会使用 wait
或 waitpid
函数进行回收子进程的资源,并获得子进程的终止状态。(如果父进程在子进程结束之前退出,则子进程由init接管。init将会以父进程身份对僵尸状态的子进程进行处理)
但是,如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程对孤儿进程完成状态收集工作。
而如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程,如下图所示:
Linux系统僵尸进程详解 - 良许Linux - 博客园 (cnblogs.com)
在某种程度上,多进程是共同使用物理内存的,但是由于操作系统的进程管理,进程间的内存空间是独立的,因此进程默认是不能访问进程空间之外的内存空间的。
域套接字是一种高级的进程间通信的方法,可以用于同一机器进程间通信。
套接字(socket):为网络通信中使用的术语。
Unix系统提供的域套接字提供了网络套接字类似的功能,如Nfinx、uWSGI等。
服务端和客户端分别使用Unix域套接字的过程:
自旋锁是一种多线程同步的变量,使用自旋锁的线程会反复检查锁变量是否可用,自旋锁不会让出CPU,是一种忙等待状态,即死循环等待锁被释放,自旋锁的效率远高于互斥锁。特点:避免了进程或者线程上下文切换的开销,但是不适合在单核CPU使用。
常见的例子:分布式锁设计
是一种特殊的自旋锁,允许多个读操作同时访问资源以提高读性能,但是对写操作是互斥的,即对多读少写的操作效率提升很显著。
是一种相对比较复杂的线程同步方法,条件变量允许线程睡眠,直到满足某种条件,当满足条件时,可以给该线程信号通知唤醒。
(86条消息) 带你了解Docker背后的守护进程_董哥的黑板报的博客-CSDN博客
处理机是什么?
简单来说,处理机指的是硬件,它包含cpu在内(早期CPU由运算器和控制器组成,称为中央处理机),而内核是操作系统中的概念,是操作系统的核心,是属于软件部分。
是对处理机进行分配,即从就绪队列中按照定的算法(公平、高效)选择一个进程并将处理机分配给它运行,以实现进程并发地执行。
按一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立PCB),以使它(们)获得竟争处理机的权利。
高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。作业调入时会建立相应的PCB,作业调出时才撤销PCB。高级调度主要是指调入的问题,因为只有调入的时机需要操作系统来确定,但调出的时机必然是作业运行结束才调出。
为了使内存中的内存不至于太多,有时需要把某些进程从内存中调到外存。在内存使用情况紧张时,将一些暂时不能运行的进程从内存中对换到外存中等待。当内存有足够的空闲空间时,再将合适的进程重新换入内存。
主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次。
(86条消息) 三级调度: 高级调度、中级调度、低级调度彭嘭嘭的博客-CSDN博客高级调度中级调度低级调度高级调度(作业调度)、低级调度(进程调度)、中级调度 - 简书 (jianshu.com)
非抢占式调度:只能由当前运行的进程主动放弃CPU;处理器一旦分配给某个进程,就让该进程一直使用下去; 调度程序不以任何原因抢占正在被使用的处理器; 调度程序不以任何原因抢占正在被使用的处理器; 抢占式调度:可由操作系统剥夺当前进程的CPU使用权。允许调度程序以一定的策略暂停当前运行的进程; 保存好旧进程的上下文信息,分配处理器给新进程;
image-20210826162907842
为了提高进程调度的效率,将就绪进程按照一定的方式排成队列,以便调度程序可以最快找到就绪进程。
调度程序以一定的策略,选择就绪进程,将CPU资源分配给它。
存当前进程的上下文信息,装入被委派执行进程的运行上下文
单位时间内cpu完成作业的数目
作业的完成时间-提交时间
进程与等待处理机的时间之和
从提交到第一次开始运行的时间
算法原理:按照作业(进程)到达的先后次序来进行调度,谁先来,谁就先被调度。 缺点:忽略了作业的运行时间
算法原理:以作业的长短来计算优先级,作业越短优先级越高,作业长短以所要求的运行时间来衡量。 缺点:必须预先知道作业的运行时间、对长作业不利,未考虑作业的紧迫程度。
例题:
解:“作业被调度进入运行后不再退出"意为非抢占式调用,job2到来时也得等待job1执行完
①job1最先达到,运行60分钟,此时job2-6已经全部提交,此时从job2-6中挑选运行时间最短的,那么顺序依次为1→5→6→3→4→2
标准流程如图(要写出作业号、提交时间、运行时间、开始时刻、完成时刻、周转时间):
②周转时间=完成时间-提交时间
平均周转时间=1/n *(N1+N2+……+Nn)
(n为作业过程总数,N1、N2为周转时间)
算法原理:FCFS、SJF两种算法都不能反映进程的紧迫程度。而优先级调度算法是外部赋予进程相应的优先级,来体现出进程的紧迫程度,紧迫性进程优先运行
(如何确定优先级:
1、利用某一范围内的一个整数,优先数
2、响应比的大小,谁响应比大,谁优先级就大——高响应比优先调度算法)
响应比=作业周转时间/作业处理时间=(作业处理时间+作业等待时间)/作业处理时间=1+(作业等待时间/作业处理时间)
适合系统:分时系统
算法原理:基于时间片的轮转,非常公平,就绪队列中的每一个进程每次仅仅运行一个时间片,并且每个进程是轮流运行。
首先按照FCFS策略把就绪进程排成一个就绪队列,设置时间片,从第一个进程开始分配处理机,第一个进程的时间片执行完后,再从就绪队列中新的队首进程开始。若进程已经运行完,注意此时第一个进程就已经不在就绪队列的队首,而是从就绪队列中删除。若未执行完只是时间片完了,则是调度程序把它送往末尾去了。
算法原理(调度机制):
设置多个就绪队列,每个队列赋予不同的优先级,第一个队列优先级最高,并且首先调度最高优先级,也就是第一个队列里面的所有进程,仅当第一个队列空闲时,才开始调度第二个队列中的进程运行。优先级越高的队列,时间片越小。
先来先服务算法:按照在就绪队列中的先后顺序执行。 短进程优先调度算法:优先选择就绪队列中估计运行时间最短的进程,不利于长作业进程的执行。 高优先权优先调度算法:进程附带优先权,优先选择权重高的进程,可以使得紧迫的任务优先处理。 时间片轮转调度算法:按照FIFO的原则排列就绪进程,每次从队列头部取出待执行进程,分配一个时间片执行,是相对公平的调度算法,但是不能保证就是响应用户。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
破坏四个必要条件的中一个或多个。
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。 第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程
检查当前资源剩余是否可以满足某个进程的最大需求;如果可以,就把该进程加入安全序列,等待进程允许完成,回收所有资源;重复1,2,直到当前没有线程等待资源;
(85条消息) 银行家算法详解(C语言)Sparky*的博客-CSDN博客银行家算法数据结构
死锁检测算法,资源剥夺法,撤销进程法(终止进程法),进程回退法;
存储管理为了确保计算机有足够的内存处理数据;确保程序可以从可用内存中获取一部分内存使用;确保程序可以归还使用后的内存以供其他程序使用。
> [(85条消息) 什么是高地址,什么是低地址,举举例说明?_高地址和低地址_方园几里的博客-CSDN博客](https://blog.csdn.net/CSDNmilu/article/details/123095988)
这一部分是内存分配过程的详细介绍,可以简单看一下
把主存中可分配的用户区域预先划分成若干个连续的分区,每个连续区的大小可以相同,也可以不同。但是,一旦划分好分区之后,主存中分区的个数就固定了,且每个分区的大小也固定不变。这是一种静态分区法。
在固定分区方式管理下,每个分区用来装入一个作业。由于主存中有多个分区,就可同时在每个分区中装入一个作业。所以,这种存储管理方式适用于多道程序系统。
了管理主存空间的使用,必须设置一张“主存分配表”(分区说明表),以说明各分区的分配情况。主存分配表中应指出各分区的起始地址和长度,并为每个分区设一个标志位。当标志位为“0”时,表示对应的分区是空闲分区,当标志位为非“0”时,表示对应的分区已被某作业占用。空闲分区可以用来装作业。
当作业队列中有作业要装入主存时,存储管理可采用“顺序分配算法”进行主存空间的分配。
顺序查看主存分配表,找到一个标志为“0”的并且长度大于或等于欲装入作业的地址空间长度的分区,则把此分区分配给该作业,相应表目的标志位改成作业名的标识;若找不到一个这样的空闲分区,则该作业暂时不能装入主存。
主存空间的释放很简单。某作业执行结束后必须归还所占的分区,这时存储管理根据作业名查看主存分配表,找到相应的表目后,把其中的标志位重新置成“0”即可。
固定分区管理方式下作业的地址转换常采用静态重定位技术。
(74条消息) 静态重定位和动态重定位阿肆_Maggie的博客-CSDN博客静态重定位
固定分区管理方式下只考虑判断其物理地址即可。常采用“界限寄存器对”法。
If 下限地址<=物理地址<=上限地址
Then 继续
Else 产生“越界中断” ,转越界中断的处理子程序
采用覆盖技术
覆盖技术是指一个程序的若干程序段和几个程序的某些部分共享一个存储空间。
优点:实现简单,无外部碎片
缺点:
解决办法:采用可变分区存储管理
内存管理的可变分区模式,又称变长分区模式、动态分区分配模式。这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。
与固定分区的区别就是:动态的划分分区。
把一个新作业装入内存时,需要按照一定的可变分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。
在可变分区分配方式中,当有很多空闲分区都满足需求时,应该使用哪个分区进行分配?
这里介绍三种可变分区分配算法
算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区。
实现步骤:
空闲区地址由低到高排序
=>1.顺序查找各个空闲区,把第一个找到能容纳申请要求的内存区分配给申请者.(若空闲区比作业长度大,则分割该空闲区。一部分分配给作业一部分空闲。)
=>2.调整相应的空闲分区表和已分配分区表。
评价:性能一般但实现比较简单直接,易于释放时合并相邻空间分区。比较容易的满足大作业的需要。完成一次分配平均需要的搜索次数较大,影响了工作效率。
尽可能地利用存储器中低地址的空闲区,而尽量保存高地址的空闲区。
算法思想:由于可变分区分配是一种连