汇总:Android小白成长之路_知识体系汇总【持续更新中…】
进程简介
进程的概念
进程是正在运行的程序实例,也就是程序的一次执行过程,包括了程序的输入、输出和当前的程序状态。进程是传统计算机进行资源分配的基本单位。
进程与程序的区别
- 程序是指令和数据的有序集合,而进程是程序在cpu上的一次执行,前者是静态的概念,后者是动态的概念。
- 从生命周期来看,程序只要不删除就一直存在,是相对长期的,而进程只是从执行开始到结束的那一段过程,是相对短暂的。
- 同一个程序可以执行多次,然后得到多个进程。一个进程在一个时间点只能有一个程序在执行。
- 一个程序运行了两次,会启动两个进程。
进程的创建
进程在执行过程中可能创建多个进程,正在创建新进程的进程成为父进程,被创建的新进程成为子进程。子进程被创建后又可以创建新的子进程,最终形成进程树。
大多数操作系统采用进程的唯一标识符(pid)对进程进行区分,每个pid都是唯一的,可以用作索引。
子进程可以从操作系统中直接获得资源,也可以只从父进程那里获得资源。子进程的地址空间有两种可能:
- 子进程复制父进程的地址空间,它具有同样的程序和数据。
- 子进程自己加载了另一个程序。
在UNIX中,通过系统调用fork()方法可以创建新的进程,新进程的地址空间复制了原来进程的地址空间,这样可以让父进程和子进程之间的通信变得简单。这两个进程都会继续执行fork()之后的指令,只是fork()的返回值不一样,子进程返回0,父进程返回子进程的标识符(pid)。
如果子进程调用exec()架子啊二进制文件到内存中并开始执行,将会用新的程序取代原来复制的地址空间。
进程的终止
一个进程会终止,通常原因为:
-
正常退出(自愿):多数进程都是由于完成了任务而正常终止的。
-
出错退出(自愿):进程引起的错误,通常是程序中的错误导致,
-
严重错误(非自愿):进程发现严重的错误,无法继续执行下去。
-
被其他进程杀死(非资源):某个进程通过执行一个系统调用通知操作系统杀死另一个进程。不过必须获取授权。
当进程是执行完任务然后调用exit()请求操作系统退出自己时,进程可以返回状态值到父进程,然后所有的进程资源,如物理和虚拟内存、打开文件和I/O缓冲区等,都会由操作系统释放。
有些系统不允许子进程在父进程已终止的情况下存在,对于这类系统,如果一个进程终止了,那么它的所有子进程也应该终止,这种现象称为级联终止。
进程的状态
进程有三种基本状态:
-
就绪状态:当进程已分配到除CPU时间片以外的所有必要资源,说明它已经准备好可以运行了,只需要CPU时间片一到,就可以立即运行。
-
执行状态:当前进程正占有CPU时间片,正在运行中。
-
阻塞状态:本来正在运行的进程,由于等待某个事件(例如输入事件)的发生而无法继续执行时,就会处于阻塞状态,放弃CPU时间片。
进程有四种可能的转换关系:
- 就绪转执行:处于就绪状态的进程获取到CPU时间片,就转换成了执行状态。
- 执行转就绪:由于某些原因(例如执行时间过长时间片用完了),调度程序把CPU让给了另外的进程执行,之前执行的进程就只能进入就绪状态
- 执行转阻塞:执行的进程需要等待某个事件输入才能继续执行,因此进入了阻塞状态等待事件输入完成。
- 阻塞转就绪:阻塞的进程等到了所需要的资源或者事件输入,可以再次执行了,进入了就绪状态。
操作系统维护着一张表格,即进程表,每个进程占用一个进程表项,这些表项也成为进程控制块。包含了进程状态的重要信息,包括了程序计数器、堆栈指针、内存分配情况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程下次重新进入运行态时能继续之前的工作,就像没停止过一样。
进程间通信
竞争条件
两个或多个进程读写某些共享数据时,因为时序的问题,可能其中一个进程的读写会被另一个进程的读写影响到,导致其中一个进程的读写出现错误。
例如,有个共享数据值为1,A进程先对其进行读,读到的是1,然后A进程对其进行+1操作变成了2,保存在自己的局部变量中,刚好这个时候A进程的CPU时间片用完了,它还没来得及把这个结果2写回去。B进程占有CPU时间片,也刚好对这个共享数据值1进行读取,+2操作后数据变成了3保存在自己的局部变量中,并且把3写回到共享数据值中去。此时B进程执行完了,A进程得到CPU时间片开始继续执行,把它之前改为的2进行写入共享数据值中,就导致覆盖掉了B进程写的结果3,最终共享数据值就是3,B进程的操作就等于失效了。
临界区
在某些时候进程可能需要访问共享资源,或进行一些会导致竞争的条件,这些都是由进程中某个程序片段进行的,因此把这些负责进行以上操作的程序片段称为临界区。如果能设计出方法避免两个进程不同时进入这个临界区,就能避免竞争条件,也就是要两个或多个进程互斥。
虽然要求避免竞争条件,但是也不能忽略了别的问题,还需要保证进程能正确执行并且性能不能太差。因此解决的方案应该包括以下条件:
- 同一时间不能有两个进程进入临界区。
- 这个方案需要适合所有CPU,无论是CPU的速度还是CPU的数量。
- 为了进入临界区而阻塞等待的进程不能也阻塞别的进程执行。
- 等待进入临界区的进程不会形成永远等待的情况。
互斥方案
-
屏蔽中断:在某个进程进入临界区后,立即屏蔽所有中断,在离开临界区之后再打开中断。屏蔽中断时,时钟中断也会被屏蔽,因此CPU无法进行进程切换,也就不会有别的进程会进入临界区。
- 优点:容易实现
- 缺点:
- 因为是给用户控制的,如果用户屏蔽中断后不再打开,那么整个操作系统就停止了。因此这只适合于系统进程,不让用户操作。
- 对于多CPU系统,屏蔽中断只是屏蔽那一个CPU而已,别的CPU依然执行,并且也能进入临界区。
-
锁变量:这种属于软件层次的解决方案,设定一个共享锁变量值为0,在一个进程进入临界区后,把这个变量设置为1,代表着已经有进程占用这个临界区。别的进程想进入临界区时,先判断这个锁变量值,如果为1则禁止进入,直到在临界区的进程退出,这时候把这个锁变量值设置为0。
- 优点:实现相对简单,并且不会导致系统终止。
- 缺点:
- 这种方案也有纰漏,假设第一个进程刚检测完锁变量值为0还没来得及改时,另一个进程也来检测发现锁变量为0,然后它们就会一起进入了临界区,虽然概率小,但是不排除有这种可能性。
-
严格轮换法:
//进程0 while(true){ while(turn != 0); //自循环 critical_region(); //临界区 turn = 1; //退出临界区后把turn改为1 other_region(); //继续执行临界区之后的任务 } //进程1 while(true){ while(turn != 1); //自循环 critical_region(); //临界区 turn = 0; //退出临界区后把turn改为1 other_region(); //继续执行临界区之后的任务 }
引入一个变量turn,初始值为0,数值代表着当前可以进入临界区的进程。进程0到达临界区,用循环检测turn是否等于0,发现其等于0,则进入临界区。此时进程1也想进入临界区,因此检测turn的值,发现是0而不是1,只能不停的循环检测,直到这个数值变为1。这种方式成为忙等待。
- 优点:真正在流程上实现了互斥,避免了所有的竞争条件
- 缺点:
- 当一个进程比另一个进程慢很多的情况下,会消耗大量的时间在等待上面。例如,进程0执行离开临界区后,把turn的值改为1,开始执行后面的流程,此时进程1就开始进入临界区,如果进程1很快出了临界区,把turn改回0,此时进程0开始新一轮任务,判断turn为0,又开始进入临界区,然后在短时间内离开临界区,并把turn改为1。在这个过程中,如果进程1还在执行临界区后面的任务,还没开始过新一轮循环,那它就不会把turn的改为0值,如果此时进程0又开始新的一轮循环,会发现turn还是1,就开始循环等待,如果进程1需要很久才开始下一轮循环,那么进程0就得一直等待。
-
Peterson解法:
#define TRUE 1 #define FALSE 0 @define N 2 //进程的数量 int turn; //当前可以进入临界区的进程的编号 Int interested[N]; //进程数组,值为1(TRUE)代表想进入临界区,或者已经在里面了 void enter_region(int process){ int other; //另一个进程 turn = process; //理论上轮到process进入临界区了(但不一定真的可以了,因为可能会别的进程被覆盖) interested[process] = TURE; //process进程想进入临界区 other = 1 - process; //另一个进程 while(turn == process && interested[other] == TRUE); //如果当前轮到process了,但另一个进程还在占用临界区,就继续等待 } void leave_region(int process){ interested[process] = FALSE; //进程process离开临界区 }
创建两个任务,各个进程在想进入临界区时,先调用enter_region(int process),离开时要调用leave_region(int process)。一开始,进程0进入enter_region(0),process为0,turn也是0,interested[0]为TRUE,other为1,此时interested[1]为默认值FALSE,因此进程0不需要最后的循环等待,直接可以进入临界区。此时如果进程1再想进临界区,调用enter_region(1),却发现interested[0]为TRUE,所以它会在最后一步循环等待,直到interested[0]为FALSE。
如果进程0和进程1是几乎同时进去的,都同时改变了turn,但是后进去的会覆盖先进去的,假设进程1后进去,则turn为1,此时进程0执行到while语句,发现turn为1,自己为0,turn == process 不成立,所以不需要循环就直接进了临界区。此时进程1才到while语句,但是两个条件都成立,所以进程1还是得等待进程0退出临界区自己才能进去临界区,因此这个方法可以解决竞争条件,
-
TSL指令:TSL是需要硬件支持的一种方案,全称为测试并加锁(Test and set lock),通常用法为:
TSL RX,LOCK
它将一个内存字LOCK读取到寄存器RX中,然后再该内存地址上存一个非零值,该指令结束之前其他CPU均不允许访问该内存字,执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束前访问内存。
enter_region: TSL REGISTER,LOCK |复制锁原来的值到寄存器并将锁设为1 CMP REGISTER,#0 |判断锁是否是0 JNE enter_region |若不是0.说明锁已经被设置,因此继续循环 RET |返回调用者,进入临界区 leave_region: MOVE LOCK,#0 |把锁的值改为0 RET |返回
假设进程0先执行,LOCK原来为0,现将lOCK的值0复制到寄存器中,然后把LOCK的值改为1,然后判断寄存器中LOCK原来的值是否是0,如果是的话说明还没有进程进入临界区,所以进程0可以进入临界区。此时进程1也调用enter_region,判断发现LOCK的值为1,说明有进程在临界区,所以进程1只能继续循环,直到进程0调用leave_region把LOCK的值改为0。
-
XCHG指令:可代替TSL,它原子性地交换了两个位置的内容
enter_region: MOVE REGISTER,1 |在寄存器中放一个1 XCHG REGISTER,LOCK |交换寄存器和锁变量的值 CMP REGISTER,#0 |判断锁是否是0 JNE enter_region |若不是0.说明锁已经被设置,因此继续循环 RET |返回调用者,进入临界区 leave_region: MOVE LOCK,#0 |把锁的值改为0 RET |返回
-
忙等待缺陷:
- 浪费CPU时间
- 可能引起预想不到的结果:当H进程优先级较高,L进程优先级较低。如果L处于临界区中,而H准备就绪,进入忙等待,但由于H就绪时L无法被调度,因为优先级没有H高,H优先执行,但H又在等待L离开临界区,所以L永远无法离开临界区,H永远忙等待,这种情况称为优先级翻转问题
睡眠与唤醒
-
原语:指由若干条指令组成的程序段,用来实现某个特定的功能,在执行过程中不可被打断。
-
sleep:一个将引起进程阻塞的系统调用,即被挂起。直到另一个进程将其唤醒
-
wakeup:唤醒指定的进程
生产者-消费者问题
两个进程共享一个公共的固定大小的缓冲区,其中一个是生产者,将信息放入缓冲区,另一个是消费者,从缓冲区中取出信息。当缓冲区已满,此时生产者还想放入一个新的数据项,其解决方法是让生产者睡眠,等待消费者把缓冲区的数据项取出再唤醒生产者。如果缓冲区为空,消费者想要取数据项却无数据项可取,消费者就睡眠,直到生产者放入数据项再将其唤醒。
#define N 100 //缓冲区大小
int count = 0; //缓冲区现有数据项数目
void producer(){
int data;
while(TRUE){
data = produce_data(); //生产数据
if(count == N) sleep(); //如果缓冲区已满,挂起
insert_data(data); //如果未满,放入数据项
count++; //缓冲区数目加一
if(count == 1) wakeup(consumer); //如果缓冲区数目为一,说明不为空,唤醒消费者
}
}
void consumer(){
int data;
while(TRUE){
if(count == 0) sleep(); //如果缓冲区无数据,挂起
data = pop_data(); //如果有数据,取出数据
count--; //缓冲区数目减一
if(count == N - 1) wakeup(producer); //如果缓冲区有空位,唤醒生产者
consume_data(data); //消费数据项
}
}
由于没有对count的访问加限制,可能出现的竞争条件:当缓冲区为空时,消费者读取count值发现是0,此时调度程序暂停消费者并启动生产者,生产者向缓冲区存放一个数据项,count加一,由于刚刚count为0,所以生产者认为刚刚消费者一定在睡眠,因此调用wakeup唤醒消费者,但实际上消费者此时并没有进入睡眠,因此这个wakeup信号被忽略而丢失了,而之后消费者真正进入睡眠,生产者却不会再去唤醒它,最后缓冲区满了,生产者和消费者都永远进入了睡眠。
用信号量解决生产者-消费者问题
-
引入三个信号量:
- full:记录缓冲区已经放入的数据项数目,初始值为0
- empty:记录缓冲区还可以放入的数据项数目,初始值为缓冲区大小
- mutex:二元信号量,用来确保生产者和消费者不会同时进入缓冲区,初始值为1
-
两种操作:
- down(P)操作:对一信号量执行down操作,则是检查其值是否大于0,若大于0,则将其减一并继续,若该值为0,则进程将睡眠,而且此时down操作并未结束。检查数值和修改变量值以及可能发生的睡眠操作均作为单一的。不可分割的原子操作完成,保证一旦一个信号量操作开始,则在该操作完成或阻塞前,其他进程都不能访问该信号量。
- up(V)操作:对信号量的值加一,如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中的一个并允许该进程完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量仍旧为0,但在其上睡眠的进程却少了一个。
#define N 100 //缓冲区大小
typedef int semaphore;
semaphore mutex = 1; //控制对临界区的访问
semaphore full = 0; //计数缓冲区的满槽数目
semaphore empty = N; //计数缓冲区的空槽数目
void producer(){
int data;
while(TRUE){
data = produce_data(); //生产数据
down(&empty); //将空槽数目减一
down(&mutex); //进入临界区
insert_data(data); //将数据放到缓冲区中
up(&mutex); //退出临界区
up(&full); //将满槽的数目加一
}
}
void consumer(){
int data;
while(TRUE){
down(&full); //将满槽数目减一
down(&mutex); //进入临界区
data = pop_data(); //从缓冲区取出数据
up(&mutex); //离开临界区
up(&empty); //将空槽数目加一
consume_data(data); //处理数据
}
}
信号量的另一种用途是用于实现同步,信号量full和empty用于保证某种事件的顺序发生或不发生。
调度
当计算机系统是多到程序设计系统时,通常会有多个进程同时竞争CPU,如果只有一个CPU可用,那么必须选择可用运行的进程,这个选择工作由调度程序完成,调度程序使用的算法成为调度算法。
-
在不同的系统中,调度程序的优化是不同的,主要划分三种环境:
- 批处理
- 交互式
- 实时
-
调度算法的共同目标:
- 公平:给每个进程公平的CPU份额
- 策略强制执行:保证规定的策略被执行
- 平衡:保持系统的所有部分都忙碌
-
批处理系统的目标:
- 提高吞吐量:每小时最大作业数
- 减小周转时间:从提交到终止间的最小时间
- 提高CPU利用率:保持CPU始终忙碌
-
交互式系统的目标:
- 减少响应时间:快速响应请求
- 均衡性:满足用户的期望
-
实时系统的目标:
- 满足截止时间:避免丢失数据
- 可预测性:在多媒体系统中避免品质降低
-
批处理系统的调度:
- 先来先服务(非抢占式):进程按照它们请求CPU的顺序使用CPU
- 最短作业优先(非抢占式):当有多个进程处于就绪态时,选择运行时间最短的进程执行
- 最短剩余时间优先(抢占式):是最短作业优先的抢占式版本,调用程序总数选择剩余运行时间最短的那个进程运行,当一个新的作业到达时,用其整个时间同当前进程的剩余时间做比较,如果新的进程比当前进程所需要更少的时间,则挂起当前进程,运行新的进程
-
交互式系统的调度:
- 轮转调度:每个进程分配一个CPU时间片,如果时间片结束该进程还在运行,则将剥夺CPU并且分配给下一个进程,如果该进程在时间片结束前阻塞或者结束,则CPU立刻切换。
- 优先级调度:每个进程被赋予一个优先级,优先级越高的可运行进程先运行。为了防止高优先级进程永远运行下去,调度程序可能在每个时钟中断时降低当前进程的优先级,直到其低于其他可运行进程,则进行进程切换。另一种方法是,给每个进程赋予一个允许运行的最大时间片,用完这个时间片后,让出CPU。
- 多级队列:设立优先级分类,最高优先级的进程运行1个时间片,次高优先级的进程运行2个时间片,再次一级运行4个时间片,以此类推。当一个进程用完分配的时间片后,就会被移到下一类。
- 最短进程优先:根据进程过去的行为进行推测,并执行估计运行时间最短的那一个。(老化技术)
- 保证调度:
- 向用户做出明确的性能保证,如果系统有n个进程运行,那么每个进程将获得CPU处理能力的1/n。系统应该记录和计算各个进程在一定时间内,已经使用的CPU时间和应该分配到的CPU时间,二者之比较小者优先级更高;
- 为了实现所做的保证,系统必须跟踪各个进程自创建以来已使用了多少CPU时间。然后它计算各个进程应获得的CPU时间,即自创建以来的时间除以n。由于各个进程实际获得的CPU时间是已知的,所以很容易计算出真正获得的CPU时间和应获得的CPU时间之比。比率为0.5说明一个进程只获得了应得时间的一半,而比率为2.0则说明它获得了应得时间的2倍。于是该算法随后转向比率最低的进程,直到该进程的比率超过它的最接近竞争者为止。
- 彩票调度:为进程提供各种资源(如CPU时间)的彩票,一旦需要进行调度决策时,就随机抽出一张彩票,拥有该彩票的进程获得该资源。更重要的进程拥有额外的彩票以便其有更高的概率被执行,彩票也可以交换
- 公平分享调度:如果用户1启动9个进程而用户2启动一个进程,使用轮转或相同优先级调度算法,那么用户1将得到90%的CPU时间,而用户2只得到10%的CPU时间。为了避免这种情况发生,某些系统在调度处理之前会考虑用户角度,在公平共享调度策略中,一个进程能够分配到的时间与登录的系统用户数以及拥有该进程用户开辟进程数的多少有关。如果两个用户都得到获得50%CPU的保证,那么无论一个用户有多少个进程存在,每个用户都会得到应有的CPU份额,这里公平的定义有很多可能的情况,取决于我们从什么方面考虑。
-
实时系统的调度:
-
实时系统是一种时间起着主导作用的系统,可以分为硬实时和软实时
-
硬实时:必须满足绝对的截止时间
-
软实时:虽然不希望超过截止时间,但可以容忍偶尔发生。
-
调度程序的任务就是按照满足所有截止时间的要求调度进程
-
周期性事件:以规则的时间间隔发生
-
非周期性事件:发生时间不可预知
-
如果有m个周期事件,事件i以周期Pi发生,并需要Ci秒CPU时间处理一个事件,那么可以处理负载的条件为:
∑ i = 1 m C i P i ≤ 1 \sum_{i=1}^m\frac{C_i}{P_i}\leq1 i=1∑mPiCi≤1
满足以上条件的实时系统成为可调度的,这意味着它实际上能够被实现。实时系统的调度算法可以是静态或动态的,前者在系统开始运行之前作出调度决策,后者在运行过程中进行调度决策,只有在可以提前掌握所完成的工作以及必须满足的截止时间等全部信息时,静态调度才能工作,而动态调度算法不需要这些限制。
-
-
调度机制和调度策略分离:将调度算法以某种形式参数化,而参数可以由用户进程填写。