《现代操作系统》第二章学习笔记

来源:互联网 发布:python应用 编辑:程序博客网 时间:2024/05/16 19:56

一个进程就是一个正在执行程序的实例,包括程序计数器,寄存器和变量的当前值.从概念上说,每个进程拥有它自己的虚拟CPU.实际上真正的CPU在个进程之间来回切换。如果一个程序运行了两遍,则算作两个进程。

四个主要事件导致进程的创建

1.      系统初始化。

2.      执行了正在运行的进程所调用的进程创建系统调用。

3.      用户请求创建一个新进程。

4.      一个批处理作业的初始化。

在Unix系统中,只有一个系统调用可以用来创建新进程:fork。在Unix和windows中,进程创建之后,父进程和子进程有各自不同的地址空间,如果其中某个进程在其他地址空间中修改了一个字,这个修改对其他进程而言是不可见的。

进程的终止:

1.    正常退出

2.    出错退出

3.    严重错误

4.    被其他进程杀死

进程只有一个父进程,但可以有零个、一个、两个甚至多个子进程。

进程的状态:

在shell命令中:cat chapter1 chapter2 chapter3|grep tree

第一个进程运行cat,将三个文件链接并输出。第二个进程运行grep,它从输入中选择所有包含单词tree的行。根据这两个进程的相对速度(这取决于这两个程序的相对复杂度和各自所分配到的CPU时间),可能发生这种情况:grep准备就绪可以运行,但输入还没有完成。于是必须阻塞grep,直到输入到来。

三种状态:

1.    运行态(该时刻进程实际占用CPU)

2.    就绪态(可运行,但因为其他进程正在运行而暂时停止)

3.    阻塞态(除非某种外部事件发生,否则进程不能运行)

人们需要多线程的原因:1.在许多应用中同时发生着多种活动。其中某些活动随着时间的推移会被阻塞。通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。2.由于线程比进程更轻量级,所以他们比进程更容易创建,也更容易撤销。在许多系统中,创建一个线程较创建一个进程要快10~100倍。3.涉及性能方面的讨论:拥有多个线程允许这些活动彼此重叠运行,从而会加快应用程序执行的速度。

线程有:程序计数器,寄存器,堆栈、状态。

程序计数器用来记录接着要执行哪一条指令;寄存器用来保存线程当前的工作变量;堆栈用来记录执行历史,其中每一帧保存了一个已调用的但是还没有从中返回的过程。同一进程中的不同线程有完全一样的地址空间,共享相同的全局变量。由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读、写或者甚至清除某一个线程的堆栈。线程之间是没有保护的,原因:(1)不可能,(2)也没有必要。这和不同进程是有差别的,不同进程可能从属不同的用户,彼此之间可能有敌意。而一个进程总是由某个用户所拥有,该用户创建多个线程是为了他们之间的合作而不是斗争。资源管理的单位是进程。

每个线程都有自己的堆栈。

进程间通信:

两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,成为竞争条件。

我们把共享内存进行访问的程序片段称作临界区域,如果我们能够适当的安排,使得两个进程不可能同时处于临界区中,就能避免竞争条件。

尽管这样的要求避免了竞争条件,但它还不能保证使用共享数据的并发进程能够正确和高效的进行写作。对于一个好的解决方案,需要满足以下四个条件:

1.    任何两个进程不能同时处于其临界区。

2.    不应对CPU的速度和数量做任何假设。

3.    临界区外进行的进程不得阻塞其他进程。

4.    不得使进程无限期等待进入临界区。

忙等待的互斥:

1.   屏蔽中断

2.   所变量

3.   严格轮换法

4.   Peterson解法

5.   TSL指令

睡眠与唤醒

生产者-消费者问题

信号量

互斥量(信号量的特殊情况),仅仅适用于管理共享资源或一小段代码。

调度:选择下一个要运行的进程。

进程的行为:

1.CPU密集型进程:具有较长时间的CPU集中使用和较小频度的I/O等待。

2.I/O密集型进程:具有较短时间的CPU几种使用和频繁的I/O等待。

有必要之处,随着CPU变得越来越快,更多的进程倾向为I/O密集型。这种现象之所以是因为CPU的改进比磁盘的改进快得多。

何时调度:

1.     在创建一个新进程之后,需要决定是运行父进程还是运行子进程。

2.     在一个进程退出时必须做出调度决策。

3.     当一个进程阻塞在I/O和信号量上后由于其他原因阻塞时,必须选择另一个进程运行。

4.     在一个I/O中断发生时,必须做出调度决策。

调度算法分类:

1.   批处理

2.   交互式

3.   实时

调度算法的目标:

所有系统:公平,策略强制执行,平衡(保持系统的所有部分都忙碌)

批处理系统:吞吐量(每小时最大作业数),周转时间(从提交到终止间的最小时间),CPU利用率(保持CPU始终忙碌)

交互式系统:相应时间,均衡性(满足用户的期望)

实时系统:满足截止时间(避免丢失数据),可预测性

批处理系统中的调度:

1.   先来先服务(队列)

2.   最短作业优先:适用于运行时间可以预知的另一个非抢占式的批处理调度算法。

3.   最短剩余时间优先(最短作业优先的抢占版本)

交互式系统中的调度

1.   轮转调度(最古老,最简单,最公平且使用最广的算法)

每个进程被分配一个时间段,成为时间片,即允许该进程在该时间段中运行。如果在时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程。如果该进程在时间片结束前阻塞或结束,则CPU立即切换。调度程序只需要维护一张可运行进程列表。当一个进程用完他的时间片后,被移到队列的末尾。

2.   优先级调度

轮转调度做了一个所有的进程同等重要的假设。但是在实际情况下,每个进程被赋予一个优先级,允许优先级最高的可运行进程先运行。

优先级可以被静态赋予或动态赋予。

动态赋予时,例如使I/O密集型进程获得较好服务的一种简单算法是,将其优先级设为1/f,f位该进程在上一时间片中所占的部分。

3.   多级队列

4.   最短进程优先

根据进程过去的行为进行推测,并执行估计运行时间最短的那个。

5.   保证调度

6.   彩票调度

7.   公平分享调度

实时系统中的调度

线程调度

当若干进程都有多个线程时,就存在两个层次的秉性:进程和线程。在这样的系统中调度处理有本质差别,这取决于所支持的是用户级线程还是内核级线程。

用户级线程:可能的序列为:A1,A2,A3,A1,A2,A3

不可能为A1,B1,A2,B2,A3,B4

内核级线程可能为:A1,A2,A3,A1,A2,A3;也可能为A1,B1,A2,B2,A3,B4

用户级线程和内核级线程之间的差别在于性能。用户级线程的线程切换需要少量的机器指令,而内核级线程需要完整的上下文切换,修改内存映像,使高速缓存失效,这导致了若干数量级的延迟。另一方面,在使用内核级线程时,一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起。

哲学家就餐问题:对于互斥访问有限资源的竞争问题一类的建模过程十分有用。

#define N 5

#define LEFT (i+N-1)%N//i的左邻居编号

#define RIGHT (i+1)%N//i的右邻居编号

#define THINKING 0//哲学家在思考

#define HUNGRY 1//哲学家试图拿起叉子

#define EATING 2//哲学家进餐

typedef int semaphore;//信号量是一种特殊的整型数据

int state[N];//数组用来跟踪记录每位哲学家的状态

semaphore mutex = 1;//临界区的互斥

semaphore s[N];//每个哲学家一个信号量

void philosopher(inti)//i:哲学家编号,从0N-1

{

       while (true)

       {

              //无限循环

       }

}

void take_forks(inti)

{

       down(&mutex);//进入临界区

       state[i] = HUNGRY;

       test(i);//尝试获取两把叉子

       up(&mutex);//离开临界区

       down(&s[i]);//如果得不到需要的叉子阻塞

}

void put_forks(inti)

{

       down(&mutex);//进入临界区

       state[i] = THINKING;//就餐完毕

       test(LEFT);//检查左边的邻居现在可以吃么

       test(RIGHT);//检查右边的邻居现在可以吃么

       up(&mutex);//离开临界区

}

void test(inti)

{

       if (state[i] ==HUNGRY&&state[LEFT] !=EATING&&state[RIGHT] !=EATING)

       {

              state[i] = EATING;

              up(&s[i]);

       }

}

使用了一个信号量数组,每个信号量对应一位哲学家,这样在所需要的叉子被占用时,想进餐的哲学家就被阻塞。每个进程将过程philosopher作为主代码运行,而其他过程take_forks,put_forks和test只是普通的过程,而非单独的进程。

读者-写者问题:为数据库访问建立了一个模型。

多个进程同时读数据库是可以接受的,但如果一个进程正在写数据库,则所有的其他进程都不能访问该数据库即使读也不可以。

第一个读者对信号量db执行down操作。随后的读者只是递增一个计数器rc.当读者离开时,他们递减这个计数器,而最后一个读者则对信号量执行up,这样就允许一个被阻塞的写者可以访问该数据库。

这种策略的结果就是,如果有一个稳定的读者流存在,那么这些读者将在到达后被允许进入。而写者就始终被挂起,直到没有读者为止。为了避免这种情形,在一个读者到达,且一个写者在等待时,读者在写者之后被挂起,而不是立即允许进入。但这也是有缺点的,并发度和效率比较低。

 

0 0