进程与线程

进程与线程

进程

进程的理解

现代计算机中,经常会在同一时间中做许多事,比如我们可以一边登陆 team 上着网课,一边开着微信与女朋友聊天,还可以使用 Chrome 访问各种商城的官网查看黑色星期五的活动信息,等等。只要内存够用,我们可以无限进行多任务并行。这些正在运行的软件就是我们创建的一个个的进程,他们之间是相互独立的。

但是为什么是伪并行呢,因为严格意义上来说,对于单CPU的电脑来说,在某一瞬间,CPU只能运行一个进程,但是他可以非常快速的在多个进程之间切换,堪称时间管理的鼻祖,因此在1秒中内,它可能运行多个进程,给人们产生并行的错觉。这种快速切换也称为多道程序设计

进程的出生到灭亡

进程的创建有4种方式,

1、系统初始化:启动操作系统时,通常会创建一些进程,一些是前台进程,与用户进行交互,例如我们的整个桌面,一些是后台进程,例如邮件系统,通知系统,这些进程停留在后台运行,也称为守护进程(daemon)大部分时间都在睡眠,只有来新通知了才会被唤醒。

其余三种是正在运行的程序执行了创建进程的系统调用,用户请求创建新进程,以及在批处理系统中一个批处理作业的初始化。

这几种方式都是因为一个已存在的进程执行了一个用于创建新进程的系统调用,在UNIX中,只有一个系统调用——fork 来创建新进程。他会创建一个与调用进程相同的 副本,(不同的地址空间,不可写的内存区可以共享)也就是子进程,这两个进程拥有相同的内存映像,环境字符串和同样的打开文件。子进程的叛逆性较强,通常接下来他会调用 execve 或一个类似的系统调用以修改内存映像并运行一个新的程序,不和他爹父进程一样。

进程的层次机构:一个父进程可以创建多个子进程,一个父进程和他的多个子进程组成一个进程组,而一个子进程只能有一个父进程。这种结构类似树,而UNIX中,系统发出的信号可以一层一层在所有进程中传递。例如在UNIX在启动过程中,一个叫做 init 的进程出现在启动映像中,他读入一个说明终端数量的文件,然后为每个终端创建一个登陆进程(用来等待用户登陆),用户登陆成功之后,该登陆进程就启动一个新进程 shell来接受命令,所接受的命令会启动更多进程,以此类推,所有的进程都属于 init 为根的那一棵树。

进程的状态:进程有三种状态分别是,就绪(可以运行,但是因为其他进程正在运行而暂时停止),阻塞(出问题了,除非某种外部事件发生,不然没办法运行),运行(正常,正在占用CPU);

因为CPU在某时刻只能运行一个进程,加上很多进程都有运行的需求,以及运行过程中会发生各种各样的问题,进程在活着的时候就不停的在这三种状态来回切换。

一个进程在运行过程中会遇到数不清次的中断,那么如何在中断后的下一次运行时还能保持正常呢?重点是,保存中断时刻进程的信息,包括。。。。

系统中那么那么多的进程,操作系统老师维护这一张 进程表 来进行管理,表中包含了进程状态的重要信息,下图为进程表中的保存的一些字段。

多道程序设计模型

假设一个进程在生命中只有20%的时间被CPU执行,如果提高进程的数量,那么就可以提高 CPU的利用率,可以用公式 1 - p^n 表示,p为等待时间占比,n为进程数量,如下图所示,如果进程 80% 的时间都在等待,那么进程数量在10个以上时,CPU利用率才能达到80%。

虽然这种预测只是大致的,但是我们可以用来预测 CPU 的性能,假设我们电脑现在内存有8G,我们日常指使用微信,team,Chrome,他们各占2g 内存,加上操作系统运行过程中占2g内存,我们的8g就用完了,加上这些进程80%的时间都在等待,此时CPU利用率 大约是 1 - 0.8^3 = 49%。现在我们觉得电脑有卡,并且CPU买亏了,于是花了半个月的生活费买了一块8g内存条,现在不加操作系统可以运行7个占用2g内存的进程了。还按80%的等待时间算,我们CPU的利用率以及已经是1 - 0.8^7 = 79%了,提高了30%的利用率,嗯,感觉不错。这时如果有大哥追求完美,想尽量的榨干 CPU,他又花了半个月生活费买了一条8g内存条,这时候的CPU利用率为91%,只增加了12%的利用率,性价比明显降低。

线程

线程的理解

举个栗子,在微信的运行过程中,我们在查看朋友圈的过程中,微信的消息接收功能还是正常运行的,展示朋友圈和消息接收就是两个独立的线程,他们运行在微信这个进程当中,负责各自的任务。

线程的特点

为什么我们需要在一个进程中再创建线程呢,这是因为大部分应用的功能不是单一的,在其运行过程中同时发生着许多活动,通过将应用分解成许多准并行的线程,分而治之,会简化程序设计模型。

与进程非常不同的一点是:线程之间是 共享地址空间的,这意味着他们可以便利的进行合作,并且创建线程更加轻量级,创建时间比一个进程要快10~100倍;还有一个优点是性能方面的,如果存在大量的I/O处理以及计算,90%的时间花在I/O交互中,拥有多个线程意味着活动可以重叠进行(也就是并行),加快执行速度,如果大量的活动都是 CPU 密集型(大部分时间都占用CPU,而CPU在一个时刻只能被一个任务占用)的,就获得不了性能提升。

一个进程中的多个线程拥有其自己的信息,包括程序计数器,寄存器,堆栈,以及状态,每个线程会调用不同的过程,拥有各自不同的执行历史,用其自己的堆栈记录。

线程的使用

操作系统进行资源管理的单位是进程,IEEE定义了线程包 pthread,大部分的 UNIX 系统都支持该标准,所有 pthread 线程都含有一个标识符,一组寄存器(包括程序计数器)和一组属性(堆栈大小,调度参数等)。

创建一个新线程需要调用 Pthread_create,返回新线程的标识符, Pthread_exit 用于在线程完成任务后结束线程, Pthread_join 调用来等待别的特定线程的终止。 Pthread_yield 用于当自己执行时间过长,希望让其他线程运行而主动停止自己,让出 CPU。 Pthread_attr_init 和 Pthread_attr_destory 分别用于初始化和结束线程的属性结构。

线程的实现地点

在用户空间实现

线程在操作系统的用户空间的进程中实现时,每个进程需要有其专有的线程表,存放了每个线程的程序计数器,堆栈指针,寄存器,状态等。线程表由运行时系统管理维护。

在用户空间中线程的切换十分快捷,在一个线程执行 Pthread_yield 让出控制权时,保存该线程的状态以及调度下一个线程执行都只是本地过程,执行它们不用陷入内核和上下文切换,也不需要对高速缓存进行刷新。此外,不同的线程可以执行不同的调度算法,例如具有垃圾调度算法的线程就不用担心在不合适的时间被停止。相比于内核空间的线程(受限于内核中内存大小,如果线程数量很大,就会出现问题),用户空间的线程具有良好的扩展性。

但是这样也存在一些问题,首先是如何实现阻塞系统调用,因为我们不能让一个线程的调用阻塞系统,因为这样会停止其他所有正常运行的线程。一种方案是,改变系统调用,使得全部系统调用都是非阻塞的,但是这样很麻烦,也会影响其他用户程序。另一种方案是,在线程的某个调用会导致阻塞,就提前通知,由系统来审核,只有在安全情况下才进行线程要求的调用,不然就放一边,先运行别的线程,下次该线程取得控制权后在进行检测。系统周围从事检查的这类代码成为包装器(wrapper)。类似的问题是缺页中断问题,因为不是所有的程序都一次性放在内存中,当一个线程调用跳转到了一个不在内存上存储的指令时,CPU就要去访问别的地方(可能是磁盘),然后进行I/O操作,这会吧整个进程阻塞起来,指导磁盘I/O完成。

在内核空间

内核支持和管理线程时,无需使用运行时系统了,内核中有用来记录系统中所有线程的线程表,保存的信息和用户空间中每个进程的线程表一样。

内核中实现线程的一个优点是,内核中的线程的所有阻塞系统调用都以系统调用的形式实现,这样当一个线程阻塞时,内核可以选择运行其他线程(自己进程或者其他进程的)。而在用户空间中,运行时系统始终运行自己进程中的线程,直到内核剥夺他的CPU为止。

由于在内核中创建和撤销一个线程的代价较大,某些系统在线程被撤销时,将其标记为不可运行状态,稍后在需要创建新线程时就重新启动某个旧线程,节省一部分开销。

调度程序激活机制

即使内核级线程在某些点优于用户级线程,但是内核级线程的速度慢,科学家提出了调度程序激活机制来解决用户级线程的阻塞调用问题。

其基本思路是,内核给每个进程安排一些虚拟处理器,运行时系统分配线程到处理器上,当内核了解到一个线程被阻塞后,内核通知该进程的运行时系统,并在堆栈中传递一些线程编号和事件描述,然后运行时系统就重新调度其线程。通常,运行时系统把该线程标记为阻塞状态,然后再其线程表中取出另一个线程启动。当内核知道原来的线程又可以运行时,再一次通知运行时系统,运行时系统再进行判断是否立即重启线程。

在这个过程中,内核通过在一个地址上启动运行时系统,从而传送一些信息,这个机制称为上行调用,这违反分层系统内在结构的概念,通常 底层提供高层可调用的服务,而不调用高层中的服务。

进程间通信

进程之间经常要进行通信,正确通信需要解决三个问题,首先是进程如何把信息传递给另外一个进程,第二是通信过程中如何避免碰撞(例如两个进程对同一资源进行读写),第三是如何确保顺序。这三个问题也同样适用于线程间通信。

进程冲突

临界区

为了避免进程间的冲突,我们找到一些方法组织多个进程同时读写共享的数据。引入临界区的概念,我们将那些访问共享内存的程序片段称为临界区,在一个进程进入临界区后,就不允许别的进程进入临界区,这样在一定程度上可以保证避免冲突。

一个好的解决方案应满足以下几点:

  • 任何两个进程不能同时处于临界区
  • 临界区外的进程不能阻塞其他进程
  • 不能让进程无限期等待进入临界区
  • 不应对CPU的速度和数量做任何假设

忙等待的互斥

首先介绍忙等待的解决方案,忙等待是指临界区外的进程不断检查是否可以进入。

屏蔽中断

我们可能在一个进程进入临界区后,立即屏蔽所有中断(CPU装聋),在进程将要离开时再打开中断,允许其他进程来访问共享内存。

信任问题,将屏蔽中断的权利交给用户不安全(如果进入后再也不打开中断就GG了),但是在操作系统本身是一项很有用的技术。

多核处理器中,屏蔽中断不会屏蔽所有的CPU,其他进程可能会使用没有别屏蔽的CPU操作共享资源。

使用锁变量

这是一种软件层面的方案,在进程中设置一个锁变量,当一个进程想进入临界区时,查看锁变量的值,如果是1(代表有进程以及进入)就等待直到值变为0。进程进入临界区后再将锁变量设置为1。

但是其实这个方案啥也没解决,当一个进程刚进入临界区后,还没来得及更改锁变量为 1,另外一个变量一看锁变量还是 0,一下子就进来了,这样临界区里面就有两个进程了,Boom。

严格轮换法

这个方案借用了轮换的帮助,对共享资源的操作是轮流的。定义一个锁变量 turn,turn 为 0 时代表进程 0 可以进入操作,进程 0 操作完之后,再将 turn 改为 1,然后退出。这个过程中进程 1 一直在检查 turn的值,发现变为 1 之后就进入,完成后再改为 0,让进程 0 进入。

这种方法中,等待的进程连续测试一个变量直至一个值出现为止,称为忙等待。用于忙等待的锁,称为自旋锁

但是,当进程 0 在临界区完成操作后将 turn 置为 1,并退出临界区,此时按理说应该进程1 进去临界区表演了,但是进程 1 还在忙别的,等了好久才进入临界区。这不是耽误事嘛,该你了你还在忙别的。

因此,当两个进程的速度相差甚远时,轮流进入并不是一个好办法。

Peterson解法

这种耽误事的情况荷兰科学家 Perterson在 1965 年解决了,最早提出了不严格轮换的互斥算法。他将一个 interested 数组结合了进去,这个数组中存储了进程们是否想进入临界区。

例如一开始没有进程希望进入临界区,现在进程 0 想进入临界区,它将 interested[0] 设置为 true 并且 把 turn 设置为 0,只有当 interested[1] = false(别人没兴趣进来) 并且 turn = 0 时,进程0才能进入成功;进程 0 离开时将 interested[0] 设置为 false。

当两个进程 0 和 1都想进入时,它们将分别设置 interested[0] = true, interested[1]=true, 并将自己的进程号 存入 turn,假设进程 0 先存了 0 ,然后就进去了,接着进程 1 有把 1 存入 trun,此时因为 interested[0] = true ,他就无法进入,只能在外面一直等待,直到进程 0 退出(退出前设置 interested[0] = false)进程 1 才能进入。

TSL指令

现在我们考虑硬件层面的指令,在一些多处理器的 计算机中,会有一条指令 TSL RX,LOCK 称为测试并加锁,它将一个内存字 LOCK 读到寄存器中,然后在该内存地址上存入一个非零值。

执行 TSL 指令的CPU将 锁住总线,禁止其他 CPU在指令结束前访问内存。而前面讲到的屏蔽中断,并不能屏蔽别的处理器。

一个可替代 TSL 的指令是 XCHG,所有的 Intel x86 CPU在底层同步中使用该指令,本质上和 TSL 一样,它原子性的交换两个位置内容,例如一个寄存器与一个存储字。

睡眠与唤醒

以上方案虽然不错,但还是没有解决忙等待的问题,为了避免资源浪费,我们可以先让等待者进行睡眠(调用 sleep),先将他挂起,直到另一个进程将其唤醒(调用 wakeup)。

举一个生产者- 消费者问题,也称为有界缓冲区问题,两个进程共享一个缓冲区,一个是生产者,负责将消息放入缓冲区,另一个是消费者,负责将取出消息。它们两个可以同时再缓冲区运行,当缓冲区存在的消息达到上限之后,生产者就会睡眠,等消费者从中取出一个或多个消息后再将其唤醒;同样的,如果缓冲区一个消息也没有了,就将消费者睡眠,等到生产者生产几个消息后再唤醒他。

这种方式在一定程度上解决了忙等待的问题,现在如果不需要进程的话,他就可以先睡一会了。但是现在考虑一种情况。现在缓冲区消息数量为 0,消费者准备睡眠,此时生产者启动,发现现在是 0,他推测消费者正在睡觉,于是生产一个消息之后,调用 wakeup想要唤醒消费者,但是消费者此时并未睡眠(准备睡,但是还没闭眼),因此唤醒失败,过了一会消费者就睡了,而生产者稍后将缓冲区填埋后自己也睡了,这样两个进程都将永远睡眠下去。

解决这个问题,可以引入一个唤醒等待位,当一个 wakeup 信号发送给一个清醒进程时,将该位置 1,随后当线程准备睡眠时,如果这个位置是 1,就取消睡眠,保持清醒。

互斥量

如果不需要信号量的计数功能,可以使用他的一个简化版本,称为互斥量,可以用来管理共享资源,或者一小段代码。

快速用户区互斥量 futex

在进程进行一些同步操作时,需要陷入内核完成,在开始时和退出时都要陷入内核去检查是否有进程在里面或者是等待。当并行进程的数量增加,有效的同步和锁机制对性能而言非常重要。在竞争激烈,等待时间较短时,使用自旋锁会很快,这时阻塞等待进程,仅当锁被释放时让内核解除阻塞会很有效。而竞争的激烈程度是难以预测的,如果刚开始竞争并不激烈,那么频繁的内核切换将花销很大,我们需要减少不必要的内核陷入。

Futex 实现了基本的锁,并且除非在迫不得已的情况下才会陷入内核,这样做改善了性能。Futex 包含内核服务和一个用户库,内核服务提供一个等待队列,允许多个进程在一个锁上等待(将进程放到等待队列中需要系统调用),这些进程均被阻塞。因此,在没有竞争时,futex 完全在用户空间工作。而这些进程共享一个锁变量,线程通过减少并检验来夺取锁,如果发现锁被人持有,就使用一个系统调用将线程放在内核的等待队列上。而进程使用完锁后会通过原子操作“增加并检验”来释放锁,并检查结果,看是否仍有进程阻塞在系统队列上,如果有就会通知内核可以对队列中的进程接触阻塞。

总的来说,在没有锁竞争时,完全不需要陷入内核。

pthread 中的互斥量

pthread (POSIX的线程标准)提供了许多可以用来同步线程的函数。其基本机制是用一个可以被锁定和解锁的互斥量来保护每个临界区。线程如果想进入临界区,首先要尝试锁住相关互斥量,如果发现互斥量已被加锁,就被阻塞,直到该互斥量解锁。与互斥量相关的函数有 pthread_mutex_init (创建互斥量) pthread_mutex_destory(销毁), pthread_mutex_lock(加锁,失败的话阻塞调用者)pthread_mutex_trylock( 尝试锁住互斥量,失败就返回错误码)

除了互斥量之外,pthread 还提供了 条件变量,用于线程由于一些条件未达到而被阻塞,pthread_cond_wait 用于阻塞线程以等待另一个信号,pthread_cond_signal 和pthread_cond_broadcast 分别向另一个或多个线程发送信号来唤醒。

互斥量和条件变量经常一起使用,互斥量用于锁住或解锁一个互斥量,而条件变量用于使线程阻塞以等待信号以及发送信号唤醒线程。

管程

即使有了信号量和互斥量,还是不能完全保证进程间通信完全不冲突,在一些情况下,会发生死锁。

一个管程是由过程,变量,数据结构等组成的一个集合。进程可以在任何需要的时候调用管程,但不能之间访问管程内部。管程的一个重要特性是在任意时刻,只接受一个活跃进程,这样可以有效的保证互斥。

典型的处理方法是,当一个进程调用管程后,管程的前几行代码检查是否有活跃进程正在管程内部,有的话调用线程被挂起,如果没有就进入。但是只解决互斥问题并不足够,还要保证管程中的进程在无法运行后被阻塞,解决的方法是引入条件变量 以及相关的两个操作 wait 和 signal,当一个管程过程发现它无法继续运行时,就在 某个条件变量上执行 wait 操作将自身阻塞,然后将一个在管程之外等待的进程放入管程中。而那个进程可以在完成任务后,调用 signal 通知另一个阻塞进程,signal 语句可以简单的放在管程过程的最后一条语句。

wait / signal 和 sleep / wakeup 虽然很像,但是有一个关键的区别,后者之所以失败是因为一个进程唤醒另一个还未睡眠的进程,导致信号丢失,而管程的互斥性保证这一点不会发生。

但是管程只是一个编程概念,是编程语言的组成部分,编译器知道它们的特殊性,因此可以使用不同的方法来处理对管程的调用,写管程的人只用将所有临界区过程转化为管程过程即可,无需担心别的。管程的应用是较为局限的,因为大多数编程语言都没有管程。

与管程和信号量有关的另一个问题是,这些机制都是用来访问公共内存的一个或多个 CPU 上的互斥问题的,而现在企业级应用都是具有多个 CPU 分布式系统,每个 CPU 有自己的私有内存,它们之间通过局域网连接,那么这些原语将失效。可以得出结论,信号量太低级了,而管程只能用于少数几个编程语言,这些原语均为提供机器间的通信方法,所以需要其他手段。

消息传递

上面提到的其他手段就是消息传递,使用两条原语 send (dest, &msg) 和 recieve(src, &msg)。前一个调用向一个给定的目标发送消息,后一个负责从一个指定的源接收消息。

调度

当计算机是多道程序设计系统时,通常会有多个进程或进程同时竞争CPU,选择去执行哪一个进程叫做调度

进程行为

几乎所有进程的 I/O 请求和计算都是交替发生的,如下图所示,将多数时间用于计算的称为计算密集型,用于I/O操作的称为 I/O密集型。随着 CPU和内存速度差异的增加,未来对 I/O密集型进程的调度似乎更为重要。

如果运行I/O密集型进程,应让其尽快得到计算机会,以便发出磁盘请求并保持磁盘始终忙碌。

何时调度

第一,当进程完成工作退出时。

第二,当一个进程阻塞在I/O和信号量上,或由于其他原因阻塞时。

第三,在一个I/O中断发生时。

调度算法

如果硬件时钟提供一定时间间隔的中断,可以在每个时钟中断时进行调度。

根据如何处理时钟中断,可以将调度算法分为抢占式调度算法和非抢占式调度算法,前者挑选一个进程,并设定一个进程运行的最大时间,如果超过这个时间进程还在运行就将其挂起,挑选另一个进程运行。后者挑选一个进程运行,直至该进程被阻塞(阻塞在I/O上或等待另一个进程),或者直到进程自动释放 CPU,在时钟中断发生时不会进行调度,

不同的环境需要不同的调度算法,这里的环境可以划分为3类:

批处理系统

批处理系统中不会有很多时效性要求高的用户请求,非抢占式算法通常是可以接受的。设计调度算法主要需要考虑系统的吞吐量(单位时间作业数),周转时间(批处理任务从提交到终止的时间),CPU利用率。

先来先服务

按照进程请求CPU的先后顺序来决定运行次序,这种方法简单易行,也具有一定的公平性。但是,这种方式有一个很大的缺点,现在假设有一个计算密集型的进程1,他运行1s然后去读1次磁盘(花费1s),接着很多I/O密集型的进程运行了,它们每个需要读1000次硬盘;过了1s之后,进程1又开始使用CPU运行1s,紧跟着I/O密集型的进程再运行。这样一来,每个I/O密集型进程需要花费1000s才能完成操作。此时如果有一个抢占式调度,每10ms抢占计算密集型的CPU,那么I/O进程将在10s内完成。

这就好比一堆人吃饼,每个人先拿一个,吃完了再拿,大多数人拿饼1s,吃饼1分钟,而有个哥们拿饼1s,吃饼也1s,吃完就拿,吃完就拿,吃的比其他人多的多。

最短作业时间

适用于运行时间可以预知的另一个非抢占式的批处理调度算法。当队列中有多个进程等待运行时,调度程序选择预计运行时间最短的作业来运行。

这种方法的优点是可以减小平均等待时间,假设现在有4个作业,分别用时8,4,4,4。如果按照先来先服务运行,它们的周转时间分别为8,12,16,20,平均为14分钟。如果这种方法,将次序变为4,4,4,8,它们的周转时间将变为4,8,12,20,平均为11分钟。

这种方法的局限是参加比较的所以进程应当是已经准备好的,同时可以运行的情况下。

最短剩余作业时间

这种方法是最短作业时间的抢占式版本,将当前运行的进程剩余时间也拿来比较,如果存在某个等待的进程其运行时间比当前运行的进程剩余时间还短,就把当前进程挂起,运行该进程。

交互式系统

交互式系统经常会有多个用户发送请求,为避免进程霸占CPU太久而影响其他进程使用,抢占式调度是必须的,在设计算法时要考虑响应时间,以及均衡性(尽量满足用户的期望)

轮转调度

最古老,简单,公平的调度算法,给每个进程分配一个时间段,称为时间片,允许进程在该时间段中运行,如果在该时间段内进程完成或者阻塞,CPU立即进行切换,如果超过时间段还未结束,就剥夺CPU分配给另一个进程。调度程序所做的就是维护一张可运行进程表,当一个进程用完他的时间片后就被移动到队尾。

而进程的切换,也叫上下文切换是需要时间的,假设时间片为4ms,上下文切换为1ms,则CPU 20%的时间浪费在管理开销上。因此,适当的增加时间片长度会提高CPU效率。但是如果时间片太长,一些短的交互请求的等待时间会增加,通常20-50ms是一个比较合理的折中。

优先级调度

轮转调度的假设是每个进程的优先级相同,但是现实中总有一些比较重要的进程需要先被执行。根据情况的不同,进程执行过程中可以采用抢占式或者非抢占式的方法进行调度,可以将进程根据优先级分为几类,在每一类上采用轮转法分配时间片。

多级队列

为CPU密集型的进程设置较长的时间片比频繁给他们分配很短的时间片更为高效。而另一方面,长时间的时间片又会影响响应时间。解决的方法是设立优先级类,属于最高优先级类的运行1个时间片,一次类推,优先级越低,时间片越长。进程运行一次就降低一个优先级。

举个例子,假设一个进程需要计算100个时间片,起初被分配1个时间片,第二次被分配2个,然后4个,8个,16个,32个,最后一次他只使用64个时间片中的37个即可完成作业。该进程需要7次交换,远远低于轮转调度的100次。而且,随着运行频度变慢,可以为短的交互进程让出CPU。

最短进程优先

类似批处理系统中的最短进程优先调度,降低进程的平均调度时间,这里的问题是如何估计进程的运行时间,一种办法是根据进程过去的行为进行推测,执行预计时间最短的那个。

通过测量当前值和先前估计值进行加权平均而得到下一个估计值的 aT0 +(1-a)T1 ,这种技术称为老化。适用于许多预测值必须基于先前值的情况。

保证调度

这种算法,保证n个进程运行的情况下,每个进程获得CPU处理能力的 1/n。系统必须跟踪每一个进程自创建以来已经使用CPU的时间与CPU运行时间的比值,根据这个比值与1/n的差距来调度进程。

彩票调度

上述保证调度虽然挺好,但是难以实现,可以采用更为简单的彩票调度来实现。每个进程拥有一些彩票,CPU在做调度决策时,随机抽取彩票,拥有该彩票的进程就获得CPU资源。这样在很长的一段时间内,进程得到CPU的概率是几乎一样的。

公平分享调度

当我们把进程的归属(用户)也考虑进去时,如果一个系统有两个用户,每个用户都保证有50%的CPU时间,用户1启动9个进程,用户2启动1个进程,我们就不能简单的使用以上方法。

线程调度

用户级线程:内核并不知道有线程的存在,还是对进程进行调度,而进程内部的线程调度程序来对线程进行调度,可以是以上任何一种。

内核级线程:内核选择特定的线程运行,不是必须考虑该线程属于哪一个进程,

区别:

1、性能,用户级线程切换需要少量的机器指令,内核级线程切换时需要完整的上下文切换,修改内存映像,使高速缓存失效。但是好处是,内核级线程阻塞时不需要像用户级线程一样将整个进程挂起。

2、用户级线程可以定制线程调度程序,选择更适合应用的调度策略。

经典的IPC问题

哲学家就餐问题

1965年,Dijkstra提出并解决了这个问题。哲学家的生活只有吃饭和思考两个问题,现在5个哲学家坐在圆桌上,每个人面前有一盘通心粉,相邻连个盘子间放着叉子,每次只能拿一个叉子,拿起两个叉子后才可以吃饭,吃完饭把叉子放下继续思考。现在需要写出一段描写哲学家行为的程序,并且要求不造成死锁。哲学家问题对于互斥访问有限资源的竞争问题十分有用,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值