进程与线程

本文深入探讨了进程和线程的概念,从进程的创建、终止、层次结构和多道程序设计模型,到线程的必要性、模型、实现方式以及如何使单线程代码多线程化。同时,讲解了进程间通信的机制,如竞争条件、临界区、信号量和互斥量,并介绍了调度算法在不同系统中的应用。文章最后讨论了经典的IPC问题,如哲学家就餐问题和读者-写者问题。
摘要由CSDN通过智能技术生成

一 、进程概念

在计算机上所有可以运行的软件,通常也包括操作系统,被组织成若干顺序进程,一个进程就是一个正在执行程序的实例,包括内存地址空间,程序技术器、寄存器和变量的当前值。

1.1 进程的创建:

有四种主要事件导致进程的创建,为1)系统初始化;2)执行了正在运行的进程所调用的进程创建系统调用;3)用户请求创建一个进程;4)一个批处理作业的初始化。
unix/linux中,fork系统调用,windows中,CreatProcess,创建进程后,父进程和子进程拥有各自不同的地址空间,在unix中,子进程的初始地址空间是父进程的一个副本,用于共享其创建者的其他资源。在windows中,从一开始父进程的地址空间和子进程的地址空间就是不同的。

1.2 进程的终止:

unix下为exit系统调用,windows下为ExitProcess
有下列四种情况会退出
1)正常退出(自愿的)
2)出错退出(自愿的)
3)严重错误(非自愿)
4)被其他进程杀死(非自愿)—unix下kill系统调用,windows下TerminateProcess函数

1.3 进程间的层次结构:

unix下具有进程层次的概念,即父进程和其创建的子进程构建成一个进程组的概念,例如,当用户从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员,每个进程可以分别捕获该信号,忽略该信号或采取默认的动作。
相反,windows中没有进程层次的概念,所有的进程都是地位相同的,但是在windwos中父进程可以得到子进程的一个特别的令牌(称为句柄),该进程可以用来控制子进程,同时父进程也有权把这个令牌传送给某个其他进程,这样就不存在进程层次了,在unix中,进程就不能剥夺其子女的“继承权”。
我爱你

1.4 多道程序设计模型:

CPU=1pn
其中p为一个进程等待I/O操作的时间与其停留在内存中的时间比,n为内存中的进程数目。实际的远远比这个复杂,但它对预测CPU的性能很有效。

二、 线程

2.1 为什么需要线程?原因主要有三:

1)并行实体共享同一地址空间和所有可用数数据,这种能力是多进程模型所没有的,它们具有不同的地址空间;
2)由于线程比进程更轻量级,所以它们比进程更容易创建也更容易撤销;
3)如果程序存在着大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。
4)针对多cpu系统,拥有多线程使得真正的并行拥有了实现的可能。
举例:
1) office word办公软件具有自动保存功能,至少具有两个线程,一个用于与用户编辑交互的,另一个用于自动保存的。
2)在需要处理海量数据的场合,通常的处理方式是,读进一块数据,对其进行处理,然后再写出数据。如果只能使用阻塞系统调用,那么在数据进入和数据输出时,会阻塞进程,使得cpu空转,利用效率不高。多线程提供了解决方案,可以使用一个输入线程,一个处理线程,一个输出线程构造。输入线程把数据读入到输入缓冲区中,处理线程从输入缓冲区中取出数据,处理数据,并把结果放到输出缓冲区中,输出线程把这些结果写到磁盘上,按照这种工作方式,输入、处理、输出可以全部同时进行。
构造服务器的三种方式:

2.2 经典的线程模型

进程有存放程序正文和数据以及其他资源的地址空间,这些资源中包括打开的文件、子进程、即将发生的报警、信号处理程序、账号信息等。
进程拥有一个执行的线程,在线程中有一个程序计数器用来记录着要执行哪一条指令,线程拥有寄存器,用来保存线程当前的工作变量,线程还拥有一个堆栈,用来记录执行历史。
多个线程共享同一个地址空间和其他资源,多个进程共享物理内存、磁盘、打印机和其他资源。
线程之间是没有保护的,原因是不可能,也没有必要,这是因为不同线程不像不同进程之间那样存在很大的独立性,所有的线程都有完全一样的地址空间,这意味着它们共享同样的全局变量。
如下图所示,每个线程有其自己的堆栈。
进程和线程中的内容
考虑下面几个问题:
这里写图片描述

2.3 POSIX线程

常见的一些Pthread函数调用
这里写图片描述
值得注意的是,在进程中没有类似Pthread_yield这种调用,因为假设进程间会有激烈的竞争性,并且每一个进程都希望获得它所能得到的所有的CPU时间。但是同一进程间的线程可以同时工作,程序员可以希望它们能互相给对方一些机会去运行。

2.4 在用户空间中实现线程

如下图所示两种实现方式,一种为在用户空间中实现,另一种为在内核空间中实现。
这里写图片描述
对于在用户空间中实现的:
从内核角度考虑,就是按照正常的方式(即单线程进程)管理,这种方法的优点有:
1)用户级线程包可以在不支持线程的操作系统上实现(这是最明显的优点),可以用函数库实现线程。
2)快捷、效率高效
当线程完成运行时,可以把线程的信息保存在线程表中,并且可以通过调用线程调度程序来选择另一个要运行的线程,由于保存信息及调用调度程序都只是在本地运行,所以启动它们比进行内核调用效率更高,同时不需要陷阱,不需要上下文切换,不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷。
3)允许每个进程有自己定制的调度算法。
缺点:
1)如何实现阻塞系统调用问题
这是因为假设在还没有任何击键之前,一个线程读取键盘,让该线程进行该系统调用时不可接受的,因为这会停止所有的线程。
使用线程的目标首先要允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他的线程。
可能的解决办法:
将系统调用全部改成非阻塞的;如果某个调用会阻塞就提前通知(首先进行select调用,只有在select选择为安全(即非阻塞)的才进行调用,否则切换为另一线程)。
类似的问题页面故障问题
由于并不是所有的程序均一次性存放在内存中的,如果某个程序调用或者跳转到了一条不在内存的指令上,就会发生页面故障。如果有一个线程引起页面故障,内核由于甚至不知道有线程的存在,通常会把整个进程阻塞知道磁盘I/O完成为止,尽管其他的线程是可以运行的。
2)如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU。
3)程序员通常在经常发生线程阻塞的应用的才有希望使用多个线程。

2.5 在内核中实现线程

这里写图片描述
优点:
首先,可以解决如何实现阻塞系统调用问题
当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程或者运行另一个进程中的线程,而在用户级线程中,运行时系统时钟运行自己进程中的线程,直到内核剥夺它的CPU为止。
其次,内核线程不需要任何新的、非阻塞系统调用
缺点:
这里写图片描述

2.6 混合实现

为了同时利用用户空间和内核空间线程优点,提出了调度程序激活机制。

2.7 使单线程代码多线程化

问题:
1)全局变量问题
由于在单线程中的有些变量为全局变量,但是在多线程中,只是对某个线程为全局的,并不是对整个程序都是全局的,此时可能会造成a线程中的全局变量被随意被其它线程进行修改。
解决办法:全面禁止全局变量;为每个线程赋予其私有的全局变量;引入新的库过程,以便创建、设置和读取这些线程范围的全局变量
2)堆栈的管理
当一个进程的堆栈溢出时,内核知识自动为该进程提供更多的堆栈,当一个进程有多个线程时,就必须有多个堆栈。如果内核不了解所有的堆栈,就不能使它们自动增长,直到造成堆栈出错。

三、进程间通信

需要解决的三个问题:
第一个问题,一个进程如何把信息传递给另一个;
第二个问题,确保两个或更多的进程在关键活动中不会出现交叉(如两个不同的进程为不同的客户试图抢夺飞机上的最后一个座位);
第三个问题,正确的顺序,如进程A产生数据进程B打印数据,那么在打印之前必须等待,直到A已经产生一些数据。
对于线程而言,第一个问题很好解决,因为线程共享一个地址空间,另外两个问题同样适用于线程,因此可以用同样的方法进行处理。

3.1 竞争条件

在一些操作系统中,协作的进程可能共享一些彼此都能读写的功用存储区,这个存储区是内存或是共享文件都不重要,这种两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。这种情况下大多是的测试用例运行结果较好,但在极少数情况下会发生一些无法解释的奇怪现象。

3.2 临界区

凡涉及共享内存、共享文件以及共享任何资源的情况都会引发竞争条件,从而造成错误,我们需要以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
我们把对共享内存进行访问的程序片段称为临界区域或临界区(critical section)。若能适当安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。
但是,为了使使用共享数据的并发进程能够正确和高效的进行协作,一般还需满足以下4个条件:
1)任何两个进程不能同时处于其临界区
2)不应对CPU的速度和数量做任何假设
3)临界区外运行的进程不得阻塞其他进程
4)不得使进程无期限等待进入临界区
如下图所示,进程A和进程B利用临界区技术
这里写图片描述

3.3 忙等待的互斥

1)屏蔽中断
使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断—–此方案并不好,原因:若一个进程屏蔽中断后不再打开中断将使整个系统可能因此而终止;而且如果是多处理器,屏蔽中断仅仅对执行disable指令的那个CPU有效,其他CPU仍将继续运行。
2)锁变量
一种软件解决方案,设置一个共享锁变量,初始值为0,当被某个进程拿到后,该线程将该锁设置为1,则其他进程无法获得该锁,只有等该进程退出临界区后,该锁才重新被赋值为0,其他进程才有可能获得该锁并进入临界区。
但有bug,如果当A进程恰好在将其设置为1之前,B进程被调度运行了,同时将其设置为1,当A进程再次能运行时,它不用再判断是否能拿到该锁,直接将锁变量设置为1,这样就同时有两个进程进入了临界区。
3)严格轮换法
连续测试一个变量直到某个值出现为止,称为忙等待,用于忙等待的锁称为自旋锁。由于这种方式浪费CPU时间,所以通常应该避免,只有在有理由认为等待时间是非常短的情形下,才使用忙等待。效率不高,并且违反了条件3),并不是一个很好的备选方案
这里写图片描述
4)Peterson解法
这里写图片描述
这里写图片描述
5)TSL指令

3.4 睡眠与唤醒

忙等待本质:当一个进程想进入临界区时,先检查是否允许进入,若不允许,则该进程将原地等待,知道允许为止。这种情况浪费CPU效率,并且有意想不到的结果,如优先级反转问题。解决此问题的最简单策略是sleep、wakeup策略。sleep将引起调用进程阻塞,直到另一个进程将其唤醒,wakeup用于唤醒进程。但同样可能会出现竞争条件问题。原因是未对相关变量的访问加以控制。
3.5 信号量(semaphore)
一个信号量的取值可以为0(表示没有保存下来的唤醒操作),或者为正值(表示有一个或多个唤醒操作)。
两个操作,up(增加一个唤醒的信号)、down(用掉一个保存的唤醒的信号)
为保证信号量能正确工作,最重要的是要采用一种不可分割的方式来实现它。用信号量解决生产者-消费者问题,该方案中用了三个信号量,分别为full、empty、mutex,信号量mutex用来实现互斥,用于保证任一时刻只有一个进程读写缓冲区和相关的变量。
信号量的另一种用途是用来实现同步。
3.6 互斥量(mutex)
互斥量是信号量的一个简化版本,仅仅适用于管理共享资源或一小段代码。由于互斥量在实现时既容易又有效,这使得互斥量在实现用户空间线程包时非常有用。是一个可以处于两态之一的变量:解锁和加锁。
互斥量使用两个过程,当一个线程(进程)需要访问临界区时,它调用mutex_lock,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock。
这里写图片描述
mutex_lock与enter_region代码相似,但有一个关键区别,在用户线程中,enter_region由于没有时钟停止运行时间过长的线程,结果是通过忙等待的方式来试图获得锁的线程将永远循环下去,绝不会得到锁,但mutex_lock在取锁失败时,调用thread_yield将CPU放弃给另一个线程,这样就没有忙等待。mutex_lock 代码运行速度非常快捷,适合实现在用户空间中的同步。
对于需要有至少一个字的内存共享空间的相关算法和信号量等,有两个解决方案:第一种共享数据结构,如信号量,第二种让进程与其他进程共享其部分地址空间。
Pthread中的互斥
这里写图片描述
除了互斥量之外,pthread提供了另一种同步机制,即条件变量。
互斥量在允许或阻塞对临界区的访问上很有用,条件变量则允许线程由于一些未达到的条件而阻塞。
这里写图片描述
条件变量和互斥量经常一起使用,这种模式用于让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量,最后另一个线程会向它发信号,使它可以继续执行。
由于条件变量不会存在内存中,如果将一个信号量传递给一个没有线程在等待的条件变量,那么这个信号就会丢失,程序员必须小心使用避免丢失信号。
3.7 管程
管程是为了避免使用信号量时的小心操作,一个管程是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包,进程可在任何需要的时候调用管程中的过程,但它们不能在管程之外声明的过程中直接访问管程内的数据结构。管程有一个很重要的特性,即任意时刻管程中只能够有一个活跃京城,这一特性使管程能有效的完成互斥。由编译器来安排互斥,降低了出错的可能性。提供了一种实现互斥的简便途径。
引入管程带来的两个问题:
问题一:如何使进程在无法继续运行时被阻塞?
解决的办法是使用条件变量以及相关的两个操作,wait、signal,主意和sleep、wakeup区别,sleep、wakeup会出现竞争条件是因为当一个进程想睡眠时另一个进程试图去唤醒它,但是wait、signal却不会出现这种现象,因为管程过程具有自动互斥功能。
问题二:分布式系统具有多个CPU
管程和信号量用来解决访问公共内存的一个或多个CPU之间的互斥问题,但对于分布式系统,这些原语将失效。结论:信号量太低级了,管程在少数几种编程语言之外又无法使用。
3.8 消息传递
一种进程间通信方式,使用两条原语send和receive,它们像信号量而不像管程,是系统调用而不是语言成分。
send(destination,&message),receive(source,&message)
设计要点:
1)如何确保消息发送成功与接收成功,通常加入消息确认机制
2)如何解决进程命名的问题,send和receive中指定的进程必须是没有二义性的,即身份认证问题
3)性能问题(将消息从一个进程复制到另一个进程通常比信号量操作和进入管程要慢)
3.9 屏障
用于进程组而不是用于双进程之间的通信
这里写图片描述

四、调度

调度指多个进程或线程同时竞争CPU,调度算法通常既适用于进程调度也适用于线程调度。
调度发生时间:
1)创建一个新进程之后,是调度父进程还是调度子进程;
2)在一个进程退出时必须做出调度决策;
3)当一个进程阻塞在I/O和信号量或由于其他原因阻塞时,必须选择另一个进程运行;
4)在一个I/O发生中断时,必须做出调度策略。
调度算法目标:
这里写图片描述
批处理系统举例:超市收银系统、图书管理系统等(属于非抢占式调度)
交互式系统举例:服务器、个人PC(抢占式调度)
实时系统举例:自动驾驶系统、机器人控制系统等
调度算法:
批处理系统中调度算法:
1)先来先服务
2)最短作业优先
3)最短剩余时间优先
交互式系统中的调度算法:
1)轮转式调度(使用相同的时间片)
2)优先级调度(按优先级进行调度,获得相同的时间片)
3)多级队列(设立优先级类,并且优先级不同获得的时间片不同,高优先级获得的时间片较短)
4)最短进程优先(需要估计出最短运行时间的进程,一般根据当前测量值和先前估计值,进行估计下一个估计值)
5)保证调度(具有性能保证,较难实现)
6)彩票调度
7)公平分享调度(指不同用户之间公平分享CPU时间)
实时系统中的调度
实时系统分为硬实时和软实时,调度系统的任务就是按照事件进行调度
事件可以分为周期性事件和非周期性事件,对于周期性时间,需满足这个条件才称系统为可调度的,即 i=1mCiPi1 ,其中 Pi 为事件 i 出现的概率,Ci为处理事件 i <script type="math/tex" id="MathJax-Element-558">i</script>所需要的时间。
用户级线程调度和内核级线程调度
这里写图片描述
区别:在于性能上,用户级线程的线程切换需要少量的机器指令,但内核级线程需要完整的上下文切换,修改内存映像、使高速缓存失效,这导致了若干数量级的延迟,另一方面,在使用内核级线程时,一旦线程阻塞在I/O上就不需要像在用户级线程那样将整个进程挂起。

五、经典的IPC问题

哲学家就餐问题

这里写图片描述

读者-写着问题

这里写图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值