1.进程同步的概念
在操作系统中,进程同步是一种用于协调多个进程之间执行顺序和共享资源的机制。它确保了在并发执行的进程之间的正确性和一致性。
1.1进程同步中的两种制约关系
间接相互制约关系 系统资源共享:互斥地访问、系统统一分配。 这种关系下,进程之间并不直接依赖于对方的执行顺序,而是通过对共享资源的访问进行协调。例如打印机、磁带机这样的系统资源,
直接相互制约关系:进程间合作,需要直接合作或依赖对方的执行顺序来实现同步。这种关系下,进程的执行顺序是关键因素,一个进程必须在另一个进程执行完毕或达到某个特定状态之后才能进行下一步操作。例如,进程A需要将数据传递给进程B进行处理,那么进程B一定要在进程A之后执行,否则数据可能还没有准备好。在这种情况下,进程A和进程B之间需要通过条件变量或其他通信机制进行等待和通知,以确保正确的执行顺序和数据的正确传递。
总结来说,间接相互制约关系是通过对系统资源的互斥访问来实现同步,而直接相互制约关系是通过进程之间的合作和对执行顺序的依赖来实现同步。两者的区别在于同步的实现机制和进程之间的依赖方式。
1.2临界资源(critical resource):
一段时间仅允许一个进程访问的资源。
临界资源可能是硬件,也可能是软件:变量,数据,表格,队列等。
(注意cpu不是临界资源)
解释:
cpu一次只允许一个进程的使用,但这是进程活动的前提,不属于临界资源。而临界资源指的是一段时间内只为一个进程而服务,而不能强制转让,如打印机的使用,必须打印完整文件,才可以算作是打印结束,而cpu不符合这个特性
并发进程对临界资源的访问必须做某种限制,否则就可能出现与时间有关的错误。
1.3临界区问题
临界区:在每个程序中,访问临界资源的那段程序
临界区问题:是指多个进程同时访问共享资源时可能引发的竞态条件(Race Condition)和数据不一致性的情况。为了避免这种问题,需要使用同步机制来保护临界区,确保同一时间只有一个进程可以进入临界区执行。
常见的解决临界区问题的方法是使用互斥锁(Mutex)或信号量(Semaphore)。当进程需要进入临界区时,首先尝试获取互斥锁或信号量,如果锁或信号量已经被其他进程占用,则当前进程会被阻塞,直到锁或信号量被释放。只有获取到锁或信号量的进程才能进入临界区执行,执行完毕后释放锁或信号量,让其他进程可以获取并进入临界区。
另外,还可以使用条件变量(Condition Variable)来解决临界区问题。条件变量可以用于进程之间的等待和通知机制。当一个进程在临界区内执行时,如果某个条件不满足,该进程可以调用条件变量的等待操作,将自己阻塞。而在另一个进程执行完临界区操作后,可以通过条件变量的通知操作唤醒等待的进程,使其重新检查条件并继续执行。
但是,解决临界区的问题都需要满足以下四条准则:
- 空闲让进:如资源空闲则允许一个请求进入。
- 忙则等待:表明临界资源正在被访问,其他资源必须等待其的完成。
- 有限等待:要求访问临界资源的进程必须在有限的时间内进入临界区。
- 让权等待:如果进程不能进入临界区时,应立即释放处理机,以免陷入“忙等”。
2.软件同步机制
经典的Peterson解决方案
int turn;
boolean flag[2];
do{
flag[i]=true;
turn=j;
while(flag[i]&&trun==j);
临界区;
flag[i]==FALSE;
剩余区;
}while(ture)
int turn;
:turn
是一个整数变量,用于表示谁有权限进入临界区。它的值可以是0或1,分别代表两个进程的标识符。
boolean flag[2];
:flag
是一个布尔数组,用于表示进程的状态。flag[0]
和flag[1]
分别表示进程0和进程1的状态。当一个进程想要进入临界区时,它将自己的flag
设置为true
,表示它有意进入临界区。
do{...}while(true)
:这是一个无限循环,表示进程将一直执行以下操作。
flag[i]=true;
:进程i将自己的flag
设置为true
,表示它有意进入临界区。
turn=j;
:进程i将turn
设置为j,表示进程j有权限进入临界区。
while(flag[i]&&turn==j);
:这是一个忙等待循环,进程i在此处等待直到轮到它进入临界区。循环条件检查进程j是否还在临界区内,并且轮到进程i进入临界区(turn
的值为j)。只有当进程j退出临界区且轮到进程i时,循环才会结束。
临界区;
:这里表示进程进入临界区,执行需要互斥访问的代码。
flag[i]=false;
:进程i将自己的flag
设置为false
,表示它不再有意进入临界区。
剩余区;
:这里表示进程执行剩余的代码区域,不需要互斥访问的代码。
通过使用flag
数组和turn
变量,Peterson解决方案满足了临界区问题的四个准则:互斥性、进程进入和退出临界区的有限等待、进程不得干扰其他进程和有限资源的利用。它确保了两个进程交替进入临界区执行,并避免了竞态条件和数据不一致性的问题。
3.硬件同步机制
虽然软件方法可以解决诸多问题,但有一定的难度,并且具有局限性,列入解决让权等待问题,所以得应用硬件来解决。实际上,其硬件则是设置相应的“锁’,锁开则进入,锁关则等待。
3.1.关中断
关中断:实现互斥最简单的方法。 进入锁测试之前关闭中断,直到完成锁测试并且关上锁以后再打开中断。进程进入临界区执行期间,计算机系统不会响应中断,也不会引发调度,就不会引起线程或者进程的切换。
缺点:影响系统效率、不适合多CPU、会导致严重后果
3.2.利用Test-and-Set指令实现互斥(专用机器指令)
使用Test-and-Set指令可以实现互斥,确保在并发程序中的临界区只有一个进程或线程能够进入。Test-and-Set指令是一种原子操作,它将一个内存位置设置为一个特定的值,并返回该位置之前的值。
以下是使用Test-and-Set指令实现互斥的一种典型方法,称为Test-and-Set锁:
# 初始化锁
lock = False
# 进入临界区前获取锁
while TestAndSet(lock) == True:
pass # 自旋等待锁释放
# 临界区代码
# ...
# 退出临界区后释放锁
lock = False
3.3.利用swap指令实现线程的互斥
-
定义一个共享的变量,用于表示线程的状态,比如可以使用一个整数变量来表示线程是否处于互斥状态。
-
在每个线程的代码中,使用swap指令来交换共享变量的值。当一个线程要进入临界区时,首先检查共享变量的值,如果发现其他线程已经进入临界区,则使用swap指令将自己的状态与共享变量的值进行交换,然后进入等待状态。
-
当一个线程退出临界区时,使用swap指令将共享变量的值恢复为原来的状态,这样其他线程就可以检测到临界区已经空闲,然后进入临界区。
4.信号量机制
4.1整形型号量
-
wait():当一个线程希望访问一个被整形型号量保护的共享资源时,它会调用wait()操作。如果整形型号量的值大于等于1,则wait()操作会将整形型号量的值减1,并允许线程继续执行。如果整形型号量的值为0,则wait()操作会将线程置入等待状态,直到有其他线程调用signal()操作来增加整形型号量的值。
-
signal():当一个线程使用完共享资源后,它会调用signal()操作来释放资源并通知其他等待的线程。signal()操作会将整形型号量的值加1,并唤醒一个或多个等待的线程,使它们有机会继续执行。
wait 与 singal 都是原子操作,因此在执行过程中是不可终断的。
wait(S): { while (S <= 0); S := S - 1; }
signal(S): { S := S + 1; }
这段代码描述了wait()和signal()的操作过程:wait()操作会在S的值小于等于0时进入循环等待,直到S的值大于0,然后将S的值减1;signal()操作则是将S的值加1--->用于通知下个阻塞区的进程,资源已经空闲。
问题:整形信号量不能解决”让权等待“的问题,会使进程处于“忙等”的现象
4.2记录型信号量
为解决“忙等”,除了需要一个代表数据资源的value(整形变量),还应该增加一个进程链表指针list。用于链接所有的等待进程(等待链)。
tupedef struct {
int value;
struct Process_Control_Block *list;
} semaphore
相应的其wait()和signal()可描述为
wait(semaphore *S)
{
S->value--;
if(S->value<0)block(S->list);
}
signal(semaphore *S)
{
S->value++;
if(S->value<=0)walkup(S->list);
}
-
wait():意味着进程申请一个单位的该资源,value--,如果value<0,表示该资源分配完毕,进行自我阻塞block()。
-
signal():意味着释放一个该单位的资源,value++,如果自增之后value还是<=0,则表示该进程被阻塞了,需要被唤醒wakeup;
4.3.AND信号量
前两者针对的是多个并发进程共享一个临界资源。但是有些场合进程需要两个或者多个资源。那么则需要and信号量的帮助。
AND同步机制的基本思想是:
将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未分配给进程,其它所有可能为之分配的资源,也不分配给它。
实现时在wait操作中,增加一个“AND”条件,故称AND同步,或同时wait操作(Swait)。
Swait(S1, S2, …, Sn) //P原语;
{
if(S1 >=1 && S2 >= 1 && … && Sn >= 1)
{ //满足资源要求时的处理;
for (i = 1; i <= n; ++i) {
--Si;
break;
}
}
else
{ //
//如果某些资源不够时,进程会将自己转为阻塞状态,
//并将自己插入到第一个小于1的信号量Si的阻塞队列中,等待资源释放。
}
Ssignal(S1, S2, …, Sn)
{
for (i = 1; i <= n; ++i)
{
++Si; //释放占用的资源;
//将与Si相关的所有阻塞进程移出到就绪队列
}
}
4.4.信号量集
在And信号量的基础上,为了保证安全,会设置一个下限值,当低于下限值时,则资源不予分配
信号量集是指同时需要多种资源、每种占用的数目不同、且可分配的资源还存在一个临界值时的信号量处理。 一般信号量集的基本思路就是在AND型信号量的基础上进行扩充,在一次原语操作中完成所有的资源申请。
在信号量集中,进程对信号量Si的下限值为ti,占用值为di(表示资源的申请量,即Si=Si-di)。
对应的P、V原语格式为: Swait(S1, t1, d1; ...; Sn, tn, dn); Ssignal(S1, d1; ...; Sn, dn);
在上述P、V原语中,
Swait用于等待资源的申请,参数包括信号量Si、测试值ti和占用值di。当资源满足测试值ti时,进程可以成功获取资源,并占用资源量为di。如果资源不满足测试值ti,则进程会被阻塞或等待。
Ssignal用于释放资源,参数包括信号量Si和释放的资源量di。通过调用Ssignal,进程可以释放之前占用的资源量,并将资源分配给其他等待资源的进程。
5.信号量的运用
-
进程同步:信号量可以用来实现进程之间的同步,确保多个进程按照一定的顺序执行,避免竞争条件和死锁。
-
互斥访问:通过信号量可以实现对共享资源的互斥访问,即同一时刻只有一个进程可以访问共享资源,避免数据不一致和冲突。
-
缓冲区管理:在生产者-消费者问题中,可以使用信号量来管理缓冲区的访问,确保生产者和消费者之间的数据传递和处理顺利进行。
-
任务调度:信号量可以用于实现任务调度和控制,确保不同任务之间的执行顺序和资源分配。
-
线程同步:在多线程编程中,信号量也可以用于线程之间的同步和互斥访问共享资源。
6.经典的进程同步问题
-
生产者-消费者问题:生产者进程负责往共享的有限缓冲区中生产数据,消费者进程负责从缓冲区中消费数据。需要确保生产者和消费者之间的同步,避免生产者往满的缓冲区中写入数据或消费者从空的缓冲区中读取数据。
-
读者-写者问题:多个读者进程可以同时读取共享数据,但写者进程在写入数据时必须独占访问,且当有写者进程在写入时,不能有其他读者或写者进程访问共享数据。
-
哲学家就餐问题:五个哲学家围坐在圆桌前,每个哲学家需要交替地思考和进餐。每个哲学家需要两支筷子才能进餐,但每次只能拿起一支筷子。需要确保哲学家之间的协作,避免死锁和饥饿的发生。
-
同步传送带问题:多个生产者和消费者在一个环形的传送带上进行数据的生产和消费。需要确保生产者和消费者之间的同步,避免数据的丢失和混乱。
后续会对经典的进程同步问题作详细的讲解,尽情期待。