Files
912-notes/thu_os/eisenberg.md
2019-09-22 21:05:26 +08:00

8.3 KiB
Raw Blame History

多进程互斥问题的软件实现方法

在前面进程管理3同步互斥中,已经就两个进程情况下互斥问题的软件实现方法进行了讨论,最终介绍了两种算法,即Peterson算法和Dekkers算法,这两种算法的基本思路是相同的,但是Dekkers算法更容易推广到多进程的情况。因此,以下我们将尝试以Dekkers算法为基础,实现多进程的互斥访问。

一些简单尝试

首先还是先来回顾一下Dekkers算法,它的代码描述如下:

flag[i] = true;
while(flag[j] == true){ 
    if(turn != i){ 
       flag[i] = false 
       while(turn != i); 
       flag[i] = true 
    }  
} 
critical_section
turn = j
flag[i] = false;

在两个进程的情况下,该算法是通过设置一个turn标志位来表示当前应该由哪个进程进入临界区,每个进程都拥有一个flag[i]标志位,表示当前进程是否想要进入临界区。

因此,可以简明的将Dekkers算法推广到多进程的情况下,即为每一个进程都设置一个flag标志位来表示该进程是否想要进入临界区,继续沿用turn标志位表示获得了进入临界区权限的进程。在进入临界区之前,每个进程都需要首先判断所有其他进程的flag标志,保证没有其他进程想要进入临界区时,当前进程才进入。在进程退出临界区时,将turn标志设置为当前进程的下一个进程。这样,就形成了下面的代码:

//code for process pid
flag[pid] = true;
while(1){
	for(idx = pid; idx != pid; idx = (idx + 1) % n)
		if(flag[idx] == true) break;
	if(idx = pid) break;
	if(turn != pid){
		flag[pid] = false;
		while(turn != pid);
		flag[pid] = true;
	}
}
critical_section
turn = (pid + 1) % n;
flag[pid] = false;

和双进程的Dekkers算法比较,该版本的主要区别只是在while循环处,需要判断所有其他进程的标志位,一旦有一个进程的标志位为true,就会进入循环内部。

乍一看这个算法好像是挺好的,但它还具有致命性的缺陷。考虑一种情况,同时有多个进程请求进入临界区,但是当前turn所指向的那个进程并不想要进入临界区。那些想要进入临界区的进程都会因为互相判断到对方而进入循环体,由于它们都不拥有turn,它们都将陷入等待turn的循环,但是拥有turn的进程却并不进入临界区,因此也无法释放turn标志。可见,这种实现方法不满足空闲则入原则。

另一种尝试

然后看了看向勇老师的网课,他说所有可能进入临界区的进程是排成了一个环。想要进入临界区的进程只需要等待介于turn和自己之间的进程。进程退出临界区时,将turn设置为下一个请求进入的进程。整体的流程如下图所示:

nproc_mutex

因此,我就根据这个简单的描述修改了上面第一种尝试的代码,形成了下面的代码:

//code for process pid
flag[pid] = true;
while(1){
	for(idx = turn; idx != pid; idx = (idx + 1) % n)
		if(flag[idx] = true) break;
	if(idx == pid) break;
	if(turn != pid){
		flag[pid] = false;
		while(turn != pid);
		flag[pid] = true;
	}
}
critical_section
for(idx = pid + 1; idx != pid; idx = (idx + 1) % n)
	if(flag[idx] == true) break;
turn = idx;
flag[pid] = false;

看起来这种实现方法似乎规避了第一次尝试的不足,因为总存在第一个离turn最近的请求进程,它只需要等待turn进程即可,不可能出现多个想要进入临界区的进程互相等待的情况,并且如果turn进程并没有请求进入临界区,离turn最近的进程可以直接通过循环进入临界区,满足了空闲则入原则。

但是通过深入的分析,我们可以构造出一种特殊情况。当前拥有turn的进程没有请求进入临界区,距离turn为1的进程也没有请求临界区距离turn为3的进程发出了请求因此它进入for循环判断在turn和它之间的进程是否还有发出请求的。然后这个循环还没有结束,刚刚判断完了距离turn为1的进程就发生了进程的调度。此时距离turn为1的进程发出了进入临界区的请求这样这两个进程就同时进入临界区了就违背了忙则等待原则。

eisenberg算法

eisenberg算法是在上面算法的基础上的又一次改进,它的伪代码如下:

repeat {
	/* announce that we need the resource */
	flags[i] := WAITING;

	/* scan processes from the one with the turn up to ourselves. */
	/* repeat if necessary until the scan finds all processes idle */
CYCLE1:
	index := turn;
	while (index != i) {
		if (flags[index] != IDLE) index := turn;
		else index := (index+1) mod n;
	}

	/* now tentatively claim the resource */
	flags[i] := ACTIVE;
CYCLE2:
	/* find the first active process besides ourselves, if any */
	index := 0;
	while ((index < n) && ((index = i) || (flags[index] != ACTIVE))) {
		index := index+1;
	}

	/* if there were no other active processes, AND if we have the turn
	   or else whoever has it is idle, then proceed.  Otherwise, repeat
	   the whole sequence. */
        } until ((index >= n) && ((turn = i) || (flags[turn] = IDLE)));

        /* Start of CRITICAL SECTION */

	/* claim the turn and proceed */
	turn := i;

        /* Critical Section Code of the Process */

        /* End of CRITICAL SECTION */

        /* find a process which is not IDLE */
	/* (if there are no others, we will find ourselves) */

	index := (turn+1) mod n;
	while (flags[index] = IDLE) {
		index := (index+1) mod n;
	}

	/* give the turn to someone that needs it, or keep it */
	turn := index;

	/* we're finished now */
	flags[i] := IDLE;

	/* REMAINDER Section */

可以看得到,这个算法也太复杂了,下面直接给出对该算法的分析。

eisenberg算法中,进程被分成了三个状态,分别为IDLE, WAITING, ACTIVEIDLE表示进程没有请求进入临界区,WAITING表示发出了请求,在等待进入临界区,ACTIVE表示可能进入了临界区,也可能还没有。进入区主要是分成两个部分,分别是CYCLE1CYCLE2

CYCLE1中的操作与我们第二次尝试的for循环一致,即判断从turn到当前进程之间是否存在请求进程,若存在,则不断循环CYCLE1,直至不存在这样的进程,将当前进程标记为ACTIVE。可以看出,所有标记为ACTIVE的进程,在循环中都判断自己是距离turn最近的请求进程,但是由于进程的调度,这样的进程仍然有多个,而这正是导致我们第二次尝试失败的原因。

CYCLE2中,对所有ACTIVE的进程做进一步的判断,主要操作就是判断除了当前进程以外,是否还存在其他ACTIVE的进程。需要注意的是,这里是对所有其他进程做判断,而不仅限于判断从turn到当前进程。可以看到,CYCLE2对应了我们的第一次尝试。

进入临界区的条件是,当前进程是唯一的ACTIVE进程,并且当前进程获得了turn,或者获得turn的进程处于IDLE状态。否则,退回到CYCLE1重新进行上面的判断。

首先证明,eisenberg算法是满足忙则等待原则的,这是通过CYCLE2来实现的。在第一次尝试中已经说明过,对所有进程做遍历并且保证当前进程是唯一的请求进程或者ACTIVE进程是一种更强的互斥条件,它甚至会导致空闲则入原则的不满足。

下面说明一定会有进程可以进入临界区,即空闲则入原则是满足的。设一次循环中,所有ACTIVE状态的进程中,距离turn最近的进程为pid_min,距离turn最远的进程为pid_max,这样ACTIVE状态的进程至多有pid_max - turn个。在CYCLE2中,由于同时存在多个ACTIVE进程,所有这些进程都会重新进入WAITING状态并且退回到CYCLE1。第二次循环,根据CYCLE1的性质,这些进程中只有pid_min可以通过CYCLE1进入ACTIVE状态,此时进入ACTIVE状态的进程至多只有pid_min - turn个。这样,在每次循环以后,可以进入ACTIVE状态的进程数量是递减的,最终只会有一个进程可以通过CYCLE1进入ACTIVE状态,并且获得临界区的访问权限。