什么是进程,进程是现代分时操作系统的工作单元。
一些知识点
多道程序的设计目标是,无论何时都有进程执行,从而最大化CPU利用率。(CPU-IO执行周期中,等待IO很耗时)
分时系统的目的是在进程之间快速切换CPU,以便用户在程序运行时能与其交互。为了满足这些目标,进程调度器(process scheduler)选择一个可用进程到CPU上执行。
单处理器系统不会有几个正在运行的进程;如果有,则其它进程需要等待CPU空闲才能重新调度
进程有五个状态:
进程控制块。操作系统用PCB表示进程
它包含许多与某个特定进程相关的信息:
选择特定的算法对特定的进程有利,而我们需要一些准则去量化有利程度:
追求是:
最大化——CPU使用率、吞吐量
最小化——周转时间、等待时间、响应时间
进程在进入系统时,会被加入作业队列
,这个队列包括所有进程
驻留在内存中的、就绪的、等待运行的进程就保存在就绪队列
上,这个队列通常用链表实现;其头结点有两个指针,用于指向链表的第一个和最后一个PCB块;每个PCB块还包括一个指针,指向就绪队列的下一个PCB。
等待特定I/O设备的进程列表,称为设备队列
,每个设备都有自己的设备队列
进程调度通常用队列图
来表示:
进程在整个生命周期中,会在各种调度队列之间迁移。操作系统为了调度必须按一定的方式从这些队列中选择进程。进程选择通过适当调度器或调度程序执行。
通常对于批处理系统,提交的进程多于可以立即执行的。这些进程会被保存到大容量存储设备(通常是磁盘)的缓冲池,一般以后执行。
调度程序是一个模块,用来将CPU控制交给由短期调度程序选择的进程。这个功能包括:
调度程序停止一个进程而启动另一个进程所需的时间称为调度延迟
切换CPU到另一个进程需要保存当前进程状态和恢复另一个进程的状态,这个切换任务称为上下文切换
选择特定的算法对特定的进程有利,而我们需要一些准则去量化有利程度:
追求是:
CPU空闲时,调度程序调度就绪队列队首的进程,并且后来的进程的PCB会被链接到队列尾部
缺点:
当CPU空闲时,它会被赋给具有最短CPU执行的进程,如果进程的CPU执行长度相等,则按照先到先服务处理
更恰当的表示为,最短下次CPU执行算法
,这是因为调度取决于进程的下次CPU执行的长度,而不是其总长度
可以是抢占式和非抢占式的:
每一个进程都有一个优先级与其关联,而具有最高优先级的进程会分配到CPU,如果优先级相等,则按照先到先服务处理
轮转算法是专门为分时系统
设计的。它类似先来先服务调度,但是增加了抢占以切换线程。
调度过程:
在进程容易分组的情况下,可以采用这种算法
原理:
前台进程
(或交互进程)和后台进程
(或批处理进程),因为它们对响应时间有不同的要求,而且前台进程往往有更高的优先级多级队列调度算法的优点是开销低,但是不够灵活,因为进程一旦分配到了一个单独队列,就不能移动到其它队列,而多级反馈队列调度可以
多级反馈队列算法允许进程在队列之间迁移
思想:
unix就使用此调度算法
非对称多处理
对称多处理(SMP)
如果一个进程需要转移到另一个处理器上执行,那么原来的处理器缓存应该设为无效,第二个处理器缓存应该重新填充。正因如此,代价很高,大多数SMP系统试图避免将一个进程从原来的处理器转移到另一个处理器
软亲和性
:当操作系统试图保持进程在同一个处理器上运行
硬亲和性
:允许某个进程运行在某个子处理器子集上
许多操作系统提供软硬结合的方式
Load Balance设法将负载平均分配到SMP系统的所有处理器。
对于只有公共的就绪队列而言没有意义,这里只针对私有就绪队列而言。
负载平衡通常有两种方法:
传统:SMP系统有多个物理处理器,以便允许多个线程并行执行
近来:多个处理器放置在同一物理芯片上,从而产生多核处理器,如图所示:
多核的SMP系统速度更快,功耗更低
如果一个线程停顿而等待内存,该核可以切换到另外一个线程
一般来说,处理器核的多线程有两种办法:粗粒度
和细粒度
:
软实时系统:不保证调度关键实时进程,但是会保证优先于非关键进程
硬实时系统:一个任务要在它的deadline之前完成,否则就是没有完成
提供抢占的、基于优先级的调度程序仅保证软实时功能
硬实时系统应该保证实时任务在截止期限内得到服务
事件延迟
:从事件发生到事件得到服务的时间
中断延迟
:从CPU收到中断到中断处理程序开始的时间
调度延迟
:从停止一个进程到启动另一个进程所需的时间
冲突阶段
:
操作系统最重要的功能是:当一个实时进程需要CPU时,立即响应。因此实时操作系统的调度程序应支持抢占的基于优先级的算法。
Windows有32个不同的优先级,其中16-31是高级别。
调度进程是周期性的,也就是说它们需要CPU。一旦周期性获得CPU,它具有固定的处理时间t、CPU应处理的截止期限d和周期p。
三者的关系:0 <= t <= d <= p
周期的任务速率:1/p
调度程序可以利用这些特性,根据进程的d或1/p要求分配优先级。这种调度比较奇葩的地方是它要求进程向调度器公布其deadline要求,然后使用准入控制算法。
调度程序只做两件事之一:
它承认进程,保证进程完成;
不能保证任务在deadline之前得以服务,就拒绝请求
此调度算法采用抢占的、静态优先级的策略,调度周期性任务
更频繁地需要CPU的任务应该分配更高的优先级
单调速率调度假定:对于每次CPU执行,周期性进程的处理时间是相同的,也就是说CPU执行长度相同
单调速率调度有一个限制:CPU的利用率是有限的,并不可能完全最大化占用CPU资源。
N个进程的最差CPU利用率:N(2^(1/N)-1)
根据截止期限动态分配优先级。
截止期限越早,优先级越高。
唯一的要求:进程在变成可运行时,应宣布它的截止期限。理论上CPU占用率可以是100%,但是由于上下文切换和中断,这是不可能的。
很简单,谁要用的多,谁就占用得多
例如:
A:10,B:60,C:30
那么A占 10/(10+60+30) =10%,B占60%,C占30%
这里只讨论实时线程调度有关的POSIX API,有两个API关于实时线程调度:
SCHED_FIFO:
SCHED_RR
SCHED_OTHER
前两个的相同点:
Windows/Solaris:内核线程调度
Linux:任务调度
参考:linux进程/线程调度策略(SCHED_OTHER,SCHED_FIFO,SCHED_RR)
当所有任务都采用分时调度策略时(SCHED_OTHER):
当所有任务都采用FIFO调度策略时(SCHED_FIFO):
当所有任务都采用RR调度策略(SCHED_RR)时:
系统中既有分时调度,又有时间片轮转调度和先进先出调度:
参考:Windows线程调度策略(超详细)
Windows采用基于优先级的、抢占调度算法来调度线程,调度程序
确保具有最高优先级的线程总是在运行
用于处理调度的Windows内核部分称为调度程序(dispatcher)
调度程序采用32级的优先级方案,以此来确定线程执行顺序:
调度程序为每个调度优先级采用一个队列;从高到低检查队列,直到一个线程可以执行。如果没有找到则调度程序会执行空闲线程(idle thread)的特别线程。
进程间通讯的7种方式
通信主要有两种形式:共享内存和消息传递
这两种方式并不相互排斥,可以在同一OS上同时实现
命名
两个进程需要发送和接受信息,那么它们之间要有通信链路。通信链路的逻辑实现和操作send()/receive()
- 直接或间接的通信
- 同步或异步的通信
- 自动或显示的缓冲
同步
消息传递可以是阻塞或者非阻塞,也称为同步和异步
- 阻塞发送/阻塞接收
- 非阻塞发送/非阻塞接收
不同组合的都有可能。当发和收都是阻塞的,则发和收直接会有一个交会(rendezvous),类似接力棒接力的那个瞬间
缓存
进程交换信息总是驻留在临时队列中,队列简单实现有三种:
- 0容量:发送者阻塞,直到接收者接收消息
- 有限容量:队列满,发送者阻塞,直到接收者接收消息,腾出空间,发送者才能继续发送消息
- 无限容量:发送者从不阻塞
接下来讲述几种进程间通信方式
共享内存方法要求,通信进程共享一些变量。进程通过使用这些共享变量来交换信息。
对于此方法,提供通信的任务交给了编程人员,而共享内存由操作系统提供。
生产者—消费者问题(有缓冲区、无缓冲区)
不恰当的例子:JVM的堆区
消息传递方法允许进程交换信息。提供通信的责任可能在于OS本身。
不恰当的例子:Golang 的 chan
RPC是一种分布式通信。
一个进程(或线程)调用一个远程应用(不在当前主机上)的过程(方法,函数)
例如Dubbo、gRPC、Spring Cloud
套接字定义为通信的端点。一对应用程序之间的连接由一对套接字组成,通信的两端各有一个Socket
管道提供了一个相对简单的进程间的相互通信
线程是进程的一个执行路径,现代操作系统进程拥有多个线程,拥有多个线程的好处是提升了CPU的利用率。
多线程的优点有如下四大类:
创建一个用户线程就要创建一个内核线程,开销极大
多个用户线程映射到一个内核线程。单核CPU
如果这个内核线程阻塞,那么全部gg。
多路复用多个用户级线程到同样数量或者更少数量的内核线程。
当一个线程阻塞系统调用时,内核可以调度另一个线程执行
多对多的一个变种,允许某个用户线程绑定到一个内核线程。几乎没人用了
这里可以看一下Golang的GMP如何设计的,理解一下为什么Golang的并发性能为什么那么强
以及,Java的多线程
进程在执行该区时可以修改公共变量、更新一个表、写一个文件等、该系统的重要特征是:当一个进程在临界区内执行时,其它进程不允许在它的临界区内执行。也就是说没有两个进程可以同时在临界区内执行。
临界区问题:设计一个协议以便协作进程
进入区:在进入临界区前,每个进程应请求许可才能进入临界区
退出区:临界区退出后的区域
剩余区,退出区后剩余代码的区域
临界区问题的解决方案应满足如下三个要求:
解决临界区问题常用的两个方法:
抢占式内核:允许处于内核模式的进程被抢占
非抢占式内核:不允许处于内核模式的进程被抢占。处于内核模式的进程会一直执行,直到主动退出内核模式、阻塞或者自愿放弃CPU资源
现代操作系统提供特殊指令,用于检测和修改字的内容,或者用于原子地交换两个字
test_and_set()
:声明互斥compare_and_swap()
:CAS,无锁安全访问,但是会造成忙等待(自旋锁)略
功能类似互斥锁,但是它提供了更高级的方法,以便进程能够同步活动
一个信号量S是个整型变量,它除了初始化外只能通过两个标准原子操作wait()
和signal()
。
操作系统通常区分:
一些点:
信号量的初值可以为可用资源数量,wait()
减少信号量的计数,signal()
增加信号量的计数
信号量操作应原子执行
信号量的正确使用不依赖信号量链表的特定排队策略
假设有三个优先级为L 优先级继承协议允许进程L临时继承进程H的优先级,从而防止进程M抢占执行。当进程L用完资源R时,它将放弃继承自H的优先级,以采用原来的优先级。由于进程H优先级比进程M高,因此进程H将执行。 http://c.biancheng.net/view/1234.html 管程结构确保每次只有一个进程在管程内处于活动状态 然而,如到目前为止所定义的管程结构,在处理某些同步问题时,还不够强大。为此,我们需要定义附加的同步机制;这些可由条件(condition)结构来提供。 当程序员需要编写定制的同步方案时,他可定义一个或多个类型为 condition 的变量: 对于条件变量,只有操作 wait() 和 signal() 可以调用。操作 操作 传统上,如互斥锁、信号量、管程等技术用于解决竞争和死锁,但是随着处理器核的增加,设计多线程应用程序并且避免竞争条件和死锁变得越来越困难。 事务内存:原子数据库理论,提供了一种进程同步的策略 内存事务:为一个内存读写操作的序列是原子的。如果事务中的所有操作都完成了事务就提交;否则就该终止并回滚。 软件事务内存(Software Transactional Memory):STM通过在事务块中插入检测代码来工作。代码由编译期插入,通过检查哪些语句并发运行和哪些地方需要特定的低级加锁,来管理每个事务。JVM就是这么干的 硬件事务内存(Hardware Transactional Memory):使用硬件告诉缓存层次结构和告诉缓存一致性协议,对设计驻留在单独处理器的高速缓存汇总的共享数据进行管理和解决冲突。 JVM vs. STM Clustering JVMs with Software Transactional Memory Support OpenMP(Open Multi-Processing)是一套支持跨平台共享内存方式的多线程并发的编程API,使用C,C++和Fortran语言,可以在大多数的处理器体系和操作系统中运行(WIki) OpenMP是一个跨平台的多线程实现,主线程(顺序的执行指令)生成一系列的子线程,并将任务划分给这些子线程进行执行。这些子线程并行的运行,由运行时环境将线程分配给不同的处理器。 其实就是类似Fork/Join的东西 函数式语言不维护状态,也就是说一旦一个变量被定义和赋予了一个值,它的值是不可变的,即它不能被修改。由于不可变,使用不需要关心竞争条件和死锁等问题。 举例 Erlang:Erlang编程语言由瑞典公司爱立信在20世纪80年代后期开发,最初用于实现容错电信系统 Scala:函数式和面向对象编程的混合,支持纯函数式和指令式编程 当一组进程内的每个进程都在等待一个事件,而这一时间只能由当前这一组进程的另一个进程引起,那么这租进程就处于死锁状态 必要条件: 死锁的处理方法: 忽视死锁是Linux和Windows的做法。而防止死锁的任务就交给程序开发人员管程
condition x, y;
x.wait();
意味着调用这一操作的进程会被挂起,直到另一进程调用 x.signal();
x.signal()
重新恢复正好一个挂起进程。如果没有挂起进程,那么操作 signal() 就没有作用,即x的状态如同没有执行任何操作。这一操作与信号量的操作 signal()
不同,后者始终影响信号量的状态。软件同步——多线程
事务内存
OpenMP
函数式编程语言
6 死锁