【操作系统】知识整理

操作系统基础

  • 并行与并发

并行性是指两个或多个事件在同一时刻发生;

并发性是指两个或多个事件在同一时间间隔内发生。

  • 引入进程

在操作系统中引入进程的目的,就是为了使多个程序能并发执行

  • 引入线程

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的OS中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位

操作系统的功能

  • 处理机管理功能
    • 进程控制
    • 进程同步
    • 进程通信
    • 调度
  • 存储器管理功能
    • 内存分配
    • 内存保护
    • 地址映射
    • 内存扩充
  • 设备管理功能
    • 缓冲管理
    • 设备处理
    • 设备分配
  • 文件管理功能
    • 文件存储空间管理
    • 目录管理
    • 文件读写管理

进程

在多道程序环境下,程序的执行属于并发执行,此时它们将失去其封闭性,并具有间断性及不可再现性的特征。这决定了通常的程序是不能参与并发执行的,因为程序执行的结果是不可再现的。这样,程序的运行也就失去了意义。为使程序能并发执行,且为了对并发执行的程序加以描述和控制,人们引入了“进程”的概念。

  1. 结构特征
      通常的程序是不能并发执行的。为使程序(含数据)能独立运行,应为之配置一进程控制块,即PCB(Process Control Block);而由程序段、相关的数据段和PCB三部分便构成了进程实体。在早期的UNIX版本中,把这三部分总称为“进程映像”。

  2. 动态性
      进程的实质是进程实体的一次执行过程,因此,动态性是进程的最基本的特征。动态性还表现在:“它由创建而产生,由调度而执行,由撤消而消亡”。可见,进程实体有一定的生命期,而程序则只是一组有序指令的集合,并存放于某种介质上,其本身并不具有运动的含义,因而是静态的。

  3. 并发性
      这是指多个进程实体同存于内存中,且能在一段时间内同时运行。并发性是进程的重要特征,同时也成为OS的重要特征。引入进程的目的也正是为了使其进程实体能和其它进程实体并发执行;而程序(没有建立PCB)是不能并发执行的。

  4. 独立性
      在传统的OS中,独立性是指进程实体是一个能独立运行、独立分配资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。

  5. 异步性
      这是指进程按各自独立的、 不可预知的速度向前推进,或说进程实体按异步方式运行。

进程状态转换

PCB

PCB中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。

当OS要调度某进程执行时,要从该进程的PCB中查出其现行状态及优先级;在调度到某进程后,要根据其PCB中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其PCB中的程序和数据的内存始址,找到其程序和数据;

进程在执行过程中,当需要和与之合作的进程实现同步、通信或访问文件时,也都需要访问PCB;当进程由于某种原因而暂停执行时,又须将其断点的处理机环境保存在PCB中。

在进程的整个生命期中,系统总是通过PCB对进程进行控制的,亦即,系统是根据进程的PCB而不是任何别的什么而感知到该进程的存在的。所以说,PCB是进程存在的惟一标志

因为PCB经常被系统访问,尤其是被运行频率很高的进程及分派程序访问,故PCB应常驻内存。系统将所有的PCB组织成若干个链表(或队列),存放在操作系统中专门开辟的PCB区内。

在Linux系统中用task_struct数据结构来描述每个进程的进程控制块,在Windows操作系统中则使用一个执行体进程块(EPROCESS)来表示进程对象的基本属性。

进程间通信

1.共享存储器系统
(1) 基于共享数据结构的通信方式。
(2) 基于共享存储区的通信方式。

2.消息传递系统

消息传递系统(Message passing system)是当前应用最为广泛的一种进程间的通信机制。在该机制中,进程间的数据交换是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。

3.管道通信

所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。

4.客户机-服务器系统通信

  1. 套接字(Socket) 套接字起源于20世纪70年代加州大学伯克利分校版本的UNIX(即BSD Unix),是UNIX 操作系统下的网络通信接口。一开始,套接字被设计用在同一台主机上多个应用程序之间的通信(即进程间的通信),主要是为了解决多对进程同时通信时端口和物理线路的多路复用问题。随着计算机网络技术的发展以及UNIX 操作系统的广泛使用,套接字已逐渐成为最流行的网络通信程序接口之一。

  2. 远程过程调用(远程方法调用) 远程过程(函数)调用RPC(Remote Procedure Call),是一个通信协议,用于通过网络连接的系统。该协议允许运行于一台主机(本地)系统上的进程调用另一台主机(远程)系统上的进程,而对程序员表现为常规的过程调用,无需额外地为此编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称做远程方法调用。

多进程的相互影响

多个进程同时在存在于内存时,可能进程A的写入会修改进程B的代码。

解决的办法:多进程的地址空间分离,内存管理。

多进程的合作

进程同步:合理的推进顺序

例子:

司机
while(true){
	启动车辆
	正常运行
	到站乘车
}
售票员
while(true){
	关门
	售票
	开门
}

合作:

  1. 司机启动车辆之前必须等待售票员关门。
  2. 售票员开门必须等待司机到站乘车。

生产者——消费者实例

共享数据

#define BUFFER_SIZE 10
typedef struct {...} item;
item buffer[BUFFER_SIZE];
int in = out = counter = 0;

生产者进程

while(true){
	while(conuter == BUFFER_SIZE);//满则等待
	
	buffer[in] = item;
	in = (in +1) % BUFFER_SIZE;
	conuter++;	//发信号让消费者走
}

消费者进程

while(true){
	while(counter == 0);	//空则等待
	
	item = buffer[out];
	out = (out+1)%BUFFER_SIZE;
	counter--;	//发信号让生产者走
}
  • 让进程走走停停来保证多进程合作的合理有序。 —— 即进程同步。
  • 其中停是关键

问题:只发信号不能解决全部问题

  1. 缓冲区满以后生产者P1生产一个item放入,它会sleep。
  2. 又一个生产者P2生产了一个item放入,会sleep。
  3. 消费者C执行1次循环,counter == BUFFER_SIZE-1,会发送信号给P1,唤醒P1。
  4. 消费者C再执行1次循环,counter == BUFFER_SIZE-2,不会发送信号给P2,从而不会唤醒P2。

在之后,无论怎么循环P2永远不会被唤醒。原因:单纯的依靠conuter来作为判断是不够的,counter仅仅表示缓冲区的个数,不足以表达有几个生产者,我们还需要知道有多少个进程在sleep。

信号量

我们引入一个新的变量,来记录与sleep和weekup相关的进程数量

(1) 缓冲区满,P1执行,P1 sleep,sem = -1
(2) P2执行,P2 sleep, sem = -2
(3) C执行1次循环,wakeup P1, sem = -1
(4) C再执行1次循环,wakeup P2 ,sem = 0
(5) C再执行1次循环,sem = 1
(6) P3执行, P2继续执行,sem = 0

sem=-1,表示缓冲区满,没有资源了,反倒还有生产者在等待,这时生产者需要sleep,取绝对值同时也表示有几个生产者在睡眠。

sem=1,表示空闲缓冲区未满,现有一个资源(缓冲区),生产者可以占用资源进行生产。

struct semaphore
{
	int value;//记录资源个数
	PCB * queue;//记录等待在该信号量上的进程
}

P(semaphore s);		//消费资源
V(semaphore s);		//产生资源

P(semaphore s)
{
	s.value--;		//申请资源
	if(s.value < 0)	//<0表示没有资源,反而亏欠资源
		sleep(s.queue);
}

V(semaphore s)
{
	s.value++;		//释放资源
    if(s.value <= 0)//+1之后还是<=0,说明有进程sleep,应当唤醒
        weakup(s.queue);

}

使用信号量解决生产者——消费者问题

int fd = open("buffer.txt",O_RDWR);
write(fd,0,sizeof(int));	//写in
write(fd,0,sizeod(int));	//写out

semaphore full = 0;
semaphore empty = BUFFER_SIZE;//初始都未空
semaphore mutex = 1;

Producer(item)
{
    P(empty);
    P(mutex);
    读取in;将item写入到in的位置上
    V(mutex);
    V(full);
}

Consumer(item)
{
    P(full);
    P(mutex);
    读入out,从文件中的out位置读出item,打印item
    V(mutex);
    V(empty);
}
  • 生产者:当缓冲区满的时候会停,这时empty = 0.即P(empty)会使生产者进入sleep。
    • 而对应的empty增加是在消费者那里。即有V(empty)。
  • 消费者:当缓冲区空的时候停下来,这是full=0,即P(full)会使消费者进入sleep。
    • 而对应的full增加是在生产者那里,即有V(full)。
  • 然后向文件写内容的时候,要互斥的进行,即定义一个互斥信号量mutex=1。即只有一个资源。

新的问题

与调度有关的共享数据不做保护可能会发生语义错误。

错误是由于多个进程并发操作共享数据引起的,并且错误和调度顺序有关,难于发现和调试。

例如上面的变量empty,我们某个进程在写empty时,应当阻止其他进程访问empty。

临界区

临界区是一次只允许一个进程进入的该程序的那一段代码

而我们要解决问题的核心则是进入临界区之前的代码和退出临界区之后的代码。

临界区代码的保护原则

  • 互斥进入: 如果一个进程在临界区中执行,则其他进程不允许进入
    • 这些进程间的约束关系称为互斥(mutual exclusion)
    • 这保证了是临界区,这是临界区的基本原则
  • 有空让进: 若干进程要求进入空闲临界区时,应尽快使一进程进入临界区。
  • 有限等待: 从进程发出进入请求到允许进入,不能无限等待

一些尝试

轮换法

问题: P0完成后不能接着再次进入,尽管进程P1不在临界区…(不满足有空让进)

标记法:

问题:当执行了P0第一句指令后就执行P1第一句指令,再执行P0第二句和P1第二句指令,这样的顺序会导致无限等待。

进入临界区Peterson算法

  • 结合了标记和轮转两种思想

算法正确性:

  • 满足互斥进入: 如果两个进程都进入,则flag[0]=flag[1]=true,turn01,矛盾!
  • 满足有空让进: 如果进程P1不在临界区,则flag[1]=false,或者turn=0,都P0能进入!
  • 满足有限等待: P0要求进入,flag[0]=true;后面的P1不可能一直进入,因为P1执行一次就会让turn=0。

面包店算法

  • 适用于多个进程,结合了标记和轮转两种思想

如何轮转: 每个进程都获得一个序号,序号最小的进入

如何标记: 进程离开时序号为0,不为0的序号即标记

面包店: 每个进入商店的客户都获得一个号码,号码最小的先得到服务;号码相同时,名字靠前的先服务。

正确性:

  • 互斥进入: Pi在临界区内,Pk试图进入,一定有(num[i], i)<(num[k],k),Pk循环等待。
  • 有空让进: 如果没有进程在临界区中,最小序号的进程一定能够进入。
  • 有限等待: 离开临界区的进程再次进入一定排在最后(FIFO),所以任一个想进入进程至多等n个进程

硬件方法:关中断

进入临界区前关闭中断,可以放防止别的进程干扰,执行完临界区的代码再打开中断。

缺点:在多核CPU是无效。仅能关闭某个CPU的核心,不能防止其他CPU执行进程影响结果。

硬件方法:硬件原子指令

//这个函数是原子指令,要么全部执行,要么全部都不执行。
boolean TestAndSet(boolean &x)
{
	boolean rv = x;
	x = true;
	return rv;
}

----------

剩余区
while(TestAndSet(&lock));	//临界前
临界区
look = false;				//临界后
剩余区

如果已经锁上,TestAndSet返回True,循环一直执行,如果没有被锁上,则返回false,同时lock被置为true,表示已经上锁,当前进程就可顺利执行临界区的代码。

总结:用临界区保护信号量,用信号量实现同步。

线程

线程的引入:如果说,在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销使OS具有更好的并发性

进程与线程的比较

  • 调度

在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。

而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位

  • 并发性

在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。

  • 拥有资源

进程都可以拥有资源,是系统中拥有资源的一个基本单位。一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,已打开的文件、I/O设备等

  • 系统开销

在创建或撤消进程时,操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地,线程的切换则仅需保存和设置少量寄存器内容,不涉及存储器管理方面的操作,所以就切换代价而言,进程也是远高于线程的。

  • 支持多处理机

进程只能运行在一个处理机上,

同一进程中的线程可以运行在多个处理机上.

  • 独立性

进程之间的独立性很高.而进程中的线程之间独立性相对较弱,线程之间要共享进程的资源.

线程的实现方式

1.内核支持线程

无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等也是依靠内核,在内核空间实现的。此外,在内核空间还为每一个内核支持线程设置了一个线程控制块,内核是根据该控制块而感知某线程的存在,并对其加以控制

优点:
(1)在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;
(2) 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;
(3) 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;
(4) 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。

缺点:

对于用户的线程切换而言,其模式切换的开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。

2.用户级线程

用户级线程ULT(User Level Threads)仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。

优点:
(1) 线程切换不需要转换到内核空间,对一个进程而言,其所有线程的管理数据结构均在该进程的用户空间中,管理线程切换的线程库也在用户地址空间运行。因此,进程不必切换到内核方式来做线程管理,从而节省了模式切换的开销,也节省了内核的宝贵资源。
(2) 调度算法可以是进程专用的。在不干扰操作系统调度的情况下,不同的进程可以根据自身需要,选择不同的调度算法对自己的线程进行管理和调度,而与操作系统的低级调度算法是无关的。
(3) 用户级线程的实现与操作系统平台无关,因为对于线程管理的代码是在用户程序内的,属于用户程序的一部分,所有的应用程序都可以对之进行共享。因此,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。

缺点:
(1) 系统调用的阻塞问题。在基于进程机制的操作系统中,大多数系统调用将阻塞进程,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。

(2) 在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点。内核每次分配给一个进程的仅有一个CPU,因此进程中仅有一个线程能执行,在该线程放弃CPU之前,其它线程只能等待。

死锁

原因

产生死锁的原因可归结为如下两点:

(1) 竞争资源。当系统中供多个进程共享的资源如打印机、公用队列等,其数目不足以满足诸进程的需要时,引起诸进程对资源的竞争而产生死锁。

(2) 进程间推进顺序非法。进程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。

产生死锁的必要条件

(1) 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。

(2) 不可抢占条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

(3) 请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

(4) 循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,…,Pn}中的P0正在等待一个P1占用的资源; P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

解决方法

(1) 预防死锁。这是一种较简单和直观的事先预防的方法。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。

  • 可在进程执行前,一次性的申请所有需要的资源
    • 缺点1:需要预知未来,编程困难。
    • 缺点2:许多资源分配很长时间之后才使用,资源利用率低。
  • 或者对资源类型进行排序,资源申请必须按序进行,不会出现环路等待
    • 缺点:仍然会造成资源浪费。

(2) 避免死锁。该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。这种方法只需事先施加较弱的限制条件,便可获得较高的资源利用率及系统吞吐量。目前在较完善的系统中常用此方法来避免发生死锁。

  • 检测每个资源请求,如果造成死锁就决绝。

由Dijkstra提出的银行家算法,通过寻找安全序列而避免死锁。

对于上述进程,Allocation是已经分配的资源,Need是需要的资源,Available是空闲的资源。

则有一个安全序列是:P1,P3,P2,P4,P0

但是算法效率很低,尤其是每次申请资源都进行这样一次检查的话,额外开销巨大。

银行家算法可以使用于定时检测或者是发现资源利用率低的时候再检测。

(3) 检测死锁+恢复。这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,而是允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源; 然后,采取适当措施,从系统中将已发生的死锁清除掉。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤消或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

  • 恢复很不容易,进程造成的改变很难恢复。

(4) 忽略死锁

实际上,许多通用操作系统,如PC机上安装的Windows和Linux,都采用了死锁忽略的方式:

  • 这种机器上死锁忽略的代价最小,且出现死锁的概率比其他机器低。
  • 死锁的出现是不确定的,这种机器可以通过重启来解决,引用它重启造成的影响小。
  • 死锁预防会让编程变的困难,且代价可能过大。
  • 有趣的是大多数非专门的操作系统都用它,如 UNIX,Linux,Windows。
posted @ 2021-03-17 00:04  Colourso  阅读(288)  评论(0编辑  收藏  举报