[整理] 进程与线程及调度

1、系统调用

在程序状态字(Program Status Word, PSW)寄存器中有一个二进制位控制CPU的两种工作模式(内核态和用户态)。

  • 在内核态运行时,CPU可以执行指令集中的每一条指令,操作系统在内核态下运行,从而可以访问整个硬件。
  • 用户程序在用户态下运行,仅允许执行整个指令集中的一个子集。
    一般而言,在用户态中有关I/O和内存保护的所有指令都是禁止的。

为了从操作系统中获得服务,用户程序使用系统调用(system call)陷入内核,TRAP(陷阱)指令把用户态切换成内核态,并启用OS,当有关工作完成之后,在系统调用后面的指令把控制权返回给用户程序。

  • 在Unix中,系统调用和系统调用所使用的库过程之间几乎是一一对应的关系。且进程之间存在层次关系,称为进程树,所有进程都属于以init(OS启动进程)为根的一棵树。
  • 在Windows中,我们通过API获得操作系统的服务,但大部分API和系统调用不存在对应关系。且不存在父进程和子进程的概念,进程创建之后,创建者和被创建者是平等的。

注意:
fork()创建一个原有进程的精确副本,子进程可以通过execve()替换不同的映像文件,从而执行不同的程序;
waitpid()用于挂起调用进程,直到其它(特定的)进程退出。
kill()供用户进程发送信号,若一个进程准备好捕捉一个特定的信号,那么,在信号到来时,运行一个信号处理程序,如果该进程没有准备好,那么信号的到来会杀死该进程。

2、进程

进程(process)本质上是正在执行的一个程序,是容纳运行一个程序所需要所有信息的容器。与一个进程相关的是进程的地址空间(address space)和进程表(process table)。进程的地址空间包括代码段、数据段、堆栈段。下面画出了进程的三种状态,以及状态之间的切换:

在多任务系统中,CPU使用某种调度算法在不同进程间来回快速切换,就好像这几个进程在并发执行一样。进程间的切换是通过中断(interrupt)来实现的。OS维护一张表格,即进程表,每个进程占用一个进程表项(也称进程控制块),该表项包含了进程状态的重要信息,包括程序计数器(保存了下一条指令的内存地址)、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未被中段过一样。

线程是把进程的两项功能——独立分配资源与被调度分派执行分离开来。进程作为系统资源分配和保护的独立单位,不需要频繁地切换。线程作为系统调度和分派的基本单位,能轻装运行,会被频繁地调度和切换。

3、线程(thread)

在传统的操作系统中,进程有一个地址空间和一个控制线程;而在多线程系统中,一个进程的地址空间中同时运行多个控制线程。

多线程的优点:
(1) 应用程序中往往同时发生着多种活动,其中某些活动随时间的推移会被阻塞。通过将这些应用程序分解成可以准并运行的多个顺序线程,程序设计模型会变得更加简单。
(2) 线程比进程更轻量级,更容易创建和撤销。
(3) 如果应用程序中存在大量的计算和I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快程序执行的速度。
(4) 在多CPU系统中,多线程可以实现真正的并行运算。

在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟。因此,线程也被称为轻量级进程(lightweight process)。与进程调度类似,CPU在线程之间快速切换,制造了线程并行运行的假象。不同的进程之间有独立的地址空间,而线程之间有完全一样的地址空间,这意味着它们共享同样的全局变量。

由于各个线程都可以访问进程地址空间的每一个内存地址,所以一个线程可以读、写,甚至清除另一个线程的堆栈。也就是说,线程之间是没有保护的。我们知道,不同的进程可能属于不同的用户所拥有,它们之间可能存在敌意。但用户创建多线程是为了它们之间的合作而不是彼此争斗。除了共享地址空间外,所有线程还共享同一个打开的文件集、子进程、报警以及相关信号等。但要注意的是,每个线程都有自己的堆栈、程序计数器、寄存器等信息,这些不是共享的。

与进程不同的是,无法利用时钟中断强制线程让出CPU,所以我们设法使线程行为“高尚”起来,并且随着时间的推移自动交出CPU,以便让其他线程有机会运行,这正是Pthread_yield系统调用的作用。

3.1 在用户空间中实现线程

有两种主要的方法实现线程包:在用户空间中和在内核中。前者是把整个线程包放在用户空间中,内核对线程包一无所知,仍按正常的单线程进程方式管理。

在用户空间实现线程,线程在一个运行时系统(即线程库)的顶部运行,这个运行时系统是一个管理线程的过程的集合,包括pthread_create、pthread_exit、pthread_join、pthread_yield等过程。每个进程需要其专门的线程表(thread table),用来跟踪该进程中的线程。线程表由运行时系统管理,当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重启该线程所需的信息,与内核在进程表中存放的进程的信息完全一样。

当某个线程做了一些会引起在本地阻塞的事情之后,例如等待进程中另一个线程完成某些工作,它调用一个运行时系统的过程,这个过程检查该线程是否必须进入阻塞状态。如果是,它在线程表中保持该线程的寄存器,并查看表中可运行的就绪线程,并把新线程的保存值重新装入机器的寄存器中。只要堆栈指针和程序计数器一被切换,新线程就又自动投入运行。

在用户空间中实现线程包的优点:
(1) 用户级线程包可以在不支持线程的操作系统上实现。
(2) 线程切换至少要比陷入内核要快一个数量级。在线程完成运行时,它调用thread_yield可以把该线程的信息保存在线程表中;进而,它可以调用线程调度程序来选择另一个要运行的线程。保存该线程状态的过程和调度程序都只是本地过程,所以启动它们比进行内核调用效率更高。另一方面,不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,这使得线程调度非常快捷。
(3) 允许每个进程有自己定制的调度算法。
(4) 具有较好的可扩展性,这是因为在内核空间中内核线程需要一些固定表格空间和堆栈空间,当内核线程的数量非常大,就会出现问题。

在用户空间中实现线程包的缺点:
(1) 如何实现阻塞系统调用而不影响其它线程。一种方法是使用非阻塞版本的系统调用;另一种方法是使用包装器(jacket 或 wrapper),在进行阻塞系统调用之前检查是否会引起阻塞,如果调用会被阻塞,有关的调用就不进行,代之以运行另一个线程。
(2) 在一个单独的进程内部,没有时钟中断,所以不能用轮转调度的方式调度线程。如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU。

3.2 在内核中实现线程包

在内核中实现线程,此时不再需要运行时系统。另外,每个进程中也没有线程表,相反,在内核中用来记录系统中所有线程的线程表。当一个线程阻塞时,内核可以根据其选择,可以运行同一个进程中的另一个线程,或者运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU为止。

内核级线程的缺点是:应用程序线程在用户态运行,而线程调度和管理在内核实现。在同一进程中,控制权从一个线程转移到另一个线程,需要用户态-内核态-用户态的模式切换,系统开销较大。

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

上面我们分别研究了用户级线程和内核级线程,如果能够将两者结合起来,可以实现混合式线程,如下图所示。

采用混合式线程,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用,在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。

4、进程调度机制

进程切换的代价是比较高的:首先必须从用户态切换到内核态,然后保存当前进程的状态以便以后重新装载,接着通过调度程序(scheduler)选定一个新进程,之后将新进程的内存映像重新装入,最后新进程开始运行。可见,选择一个好的进程调度算法(scheduling algorithm)非常重要。通常我们将进程划分两类:I/O密集型(IO-bound)和计算密集型(computer-bound),更多的进程倾向为I/O-bound,这是因为CPU的技术进步速度远快于磁盘。

何时调度:
(1) 在创建一个新进程之后,需要决定是运行父进程还是运行子进程。
(2) 在一个进程退出时必须做出调度决策,从就绪进程队列中选择一个进程运行。
(3) 当一个进程被阻塞在I/O或信号量或其他原因时,必须选择另外一个进程运行。
(4) 在一个I/O中断发生时,必须做出调度决策。如果中断来自I/O设备,当该设备完成工作后,被阻塞等待该I/O的进程就可以成为就绪进程,等待调度。

根据如何处理时钟中断,可以把调度算法分为两类:
(1) 抢占式:进程运行某个固定时段的最大值,在该时刻结束时被挂起,并由调度程序挑选另一个进程运行(根据优先级)。
(2) 非抢占式:进程运行直至被阻塞(阻塞在I/O上或等待另一个进程),或者直到该进程自动释放CPU。

在不同类型的系统中,调度策略也不相同,下面介绍三种环境下的调度机制:

4.1 批处理系统中的调度

批处理系统主要应用商业领域或科研单位等大型机构,批处理系统的三个主要指标为:吞吐量(单位时间完成的作业数量);周转时间(平均完成每个作业所需的时间);CPU利用率。如下面所示,假如有4个作业需要处理,每个作业需要的CPU单位时间标记在括号里,这两种调度方法的吞吐量是一样的,都需要20个CPU单位时间。但周转时间不一样,第一个的周转时间为(8+12+16+20)/4=14;第二个的周转时间为(4+8+12+20)/4=11。

----A(8)-----B(4)-----C(4)-----D(4)-----

----D(4)-----B(4)-----C(4)-----A(8)-----

(1) 先来先服务(非抢占式):使用一个队列记录所有就绪进程,从队列头部选择一个要运行的进程;要添加一个新的作业或阻塞一个进程,只要把该作业或进程追加到队尾即可。先来先服务对于I/O-bond进程来说,CPU利用率很低。
(2) 最短作业优先(非抢占式):如果各个作业的CPU时间是可预期的,将进程按照它们各自所需要的CPU时间排序,优先执行占用CPU时间短的作业,以达到最短的作业周转时间。
(3) 最短剩余时间优先(抢占式):最短作业优先的抢占式版本,调度程序总是选择剩余时间最短的那个进程运行。当然,有关的运行时间必须提前掌握。

4.2 交互式系统中的调度

交互式系统在个人计算机、服务器等系统中比较常见,它对响应时间和均衡性要求比较高。
(1) 轮转调度:每个进程被分配一个时间段,称为时间片(quantum),即允许该进程在该时间段中运行。如果在时间片结束时刻该进程还在运行,则将剥夺CPU并分配给另一个进程。如果该进程在时间片结束前阻塞或结束,则CPU立即切换。注意:时间片设置太短会导致过多的进程切换,降低CPU效率;而设得太长又可能引起对短的交互请求的响应时间变长。
(2) 优先级调度:轮转调度隐含一个假设,即所有进程同等重要。而优先级调度给每个进程赋予一个优先级,并允许优先级高的进程先运行。为了防止高优先级进程无休止地运行下去,调度程序可以在每个时钟中断降低当前进程的优先级。例如I/O-bound进程可以被赋予较高的优先级,以改善对用户的交互性。
(3) 多级队列:
(4) 最短进程优先:从当前进程中选择需要CPU时间最短的进程运行,以获得最短的响应时间。不过,这里需要某种算法来推测各个进程的运行时间。
(5) 保证调度:假设每个进程需要的CPU时间是可预期的,系统跟踪各个进程自创建以来已使用的CPU时间,然后计算出各个进程真正获得的CPU时间和应获得的CPU时间之比,调度程序优先让这个比率值比较低的进程运行。
(6) 彩票调度:系统向将进程发放彩票,调度程序随机抽出一张彩票,拥有该彩票的进程获得资源。各个进程拥有的彩票份额与它们获得的CPU时间大致成正比。
(7) 公平分享调度:考虑进程的拥有者,做到让不同用户的进程获得大致相等的CPU时间。

4.3 实时系统中的调度

实时系统是一种时间起着主导作用的系统,它对截止时间的要求比较高。

两者的区别总结

进程 线程
根本区别 操作系统资源分配的基本单位 任务调度和执行的基本单位
开销方面 有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销; 同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境 操作系统中能同时运行多个进程(程序) 同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配 系统在运行的时候会为每个进程分配不同的内存空间 除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
包含关系 没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

通俗解释

假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。
背后的含义就是,单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。
这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。
这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual['mjuːtʃuəl] exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。

这种做法叫做"信号量"(Semaphore ['semə.fɔr]),用来保证多个线程不会互相冲突。不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

posted @ 2020-12-06 12:29  哆啦梦乐园  阅读(389)  评论(0编辑  收藏  举报