【《现代操作系统 第4版》】6、进程间的通信之经典的IPC问题

本文深入探讨了哲学家就餐问题和读者-写者问题在操作系统中的解决方案。通过不同策略防止死锁,如随机等待、互斥访问和同步控制,确保资源的有效利用和并发执行的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哲学家就餐问题

1965年,Dijkstra提出并解决了一个他称之为哲学家就餐的同步问题。这个问题可以简单地描述如下∶五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉。由于通心粉很滑,所以需要两把叉子才能夹住。相邻两个盘子之间放有一把叉子。哲学家的动作包括思考和进餐,当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。问∶能为每一个哲学家写一段描述其行为的程序,且决不会死锁吗?
哲学家就餐

方案1

#define N 5		          		// 哲学家个数

void philosopher(int i) {    // 哲学家编号:0 - 4
	while(TRUE) {
		think();				  	// 哲学家在思考
		take_fork(i);               // 去拿左边的叉子
		take_fork((i + 1) % N);     // 去拿右边的叉子 
		eat();	                    // 哲学家在进餐
		put_fork(i);                // 放下左边的叉子
		put_fork((i + 1) % N);      // 放下右边的叉子
	}
}

如果是单线程,这段代码没有任何问题。但是如果是5个线程同时运行,假设都运行到了 P(fork[i]); ,所有的哲学家都拿到了左边的叉子,那么运行 P(fork[(i + 1) % N]); 时就会出现死锁。没有任何哲学家能够拿到右边的叉子。

方案2

为了避免死锁,拿不到右边的叉子时,就将左边的放下,这样就不会一直占有这个资源了。

#define N 5		          				// 哲学家个数

void philosopher(int i) {    			// 哲学家编号:0 - 4
	while(TRUE) {
		think();				  		 // 哲学家在思考
		take_fork(i);              		 // 去拿左边的叉子
		if (fork((i + 1) % N)) {		 // 右边的叉子还在吗?
			take_fork((i + 1) % N);     // 去拿右边的叉子 
			break;						// 两个叉子均到手
		} else {
			put_fork(i);                // 放下左边的叉子
			wait_some_time();			// 等待一会
		}
		
		eat();	                    	// 哲学家在进餐
	}
}

这样仍然存在问题,如果大家都是按同一个步调执行的话,大家同时拿到左边的叉子,发现拿不到右边的叉子,又都放下了左边的叉子。

方案3

如果哲学家在拿不到右边叉子时等待一段随机时间,而不是等待相同的时间,这样发生互锁的可能性不就很小了吗?

#define N 5		          				// 哲学家个数

void philosopher(int i) {    			// 哲学家编号:0 - 4
	while(TRUE) {
		think();				  		 // 哲学家在思考
		take_fork(i);              		 // 去拿左边的叉子
		if (fork((i + 1) % N)) {		 // 右边的叉子还在吗?
			take_fork((i + 1) % N);     // 去拿右边的叉子 
			break;						// 两个叉子均到手
		} else {
			put_fork(i);                // 放下左边的叉子
			wait_random_time();			// 等待随机时长
		}
		
		eat();	                    	// 哲学家在进餐
	}
}

但是这种方案存在概率性问题。并不能保证百分之百,甚至可能出现某个哲学家一直等待下去无法进餐的情况。

方案4

既然如此,将获取叉子、进餐、放下叉子放在临界区中,这样不就没有互锁了吗?

#define N 5		          			// 哲学家个数
semaphore mutex = 1; 				// 互斥信号量,初始值为1

void philosopher(int i) {    		// 哲学家编号:0 - 4
	while(TRUE) {
		think();				  	// 哲学家在思考
		P(mutex);					// 进入临界区
		take_fork(i);               // 去拿左边的叉子
		take_fork((i + 1) % N);     // 去拿右边的叉子 
		eat();	                    // 哲学家在进餐
		put_fork(i);                // 放下左边的叉子
		put_fork((i + 1) % N);      // 放下右边的叉子
		V(mutex);					// 退出临界区
	}
}

方案可行,但是每次只能有一个人进餐,而实际上最多是可以有两个人一起进餐的。

方案5

和其他方案将叉子作为临界资源不一样,该方案站着哲学家自身的角度来思考问题:
1、思考中。。。
2、进入饥饿状态
3、如果左邻或有右邻正在进餐,进餐进入阻塞状态,否则执行4
4、拿起两把叉子
5、进餐
6、放下左边的叉子,看看左邻现在是否能进餐(即是否为饥饿状态且两把叉子都在),若能则将其唤醒
7、放下右边的叉子,看看右邻现在是否能进餐(即是否为饥饿状态且两把叉子都在),若能则将其唤醒
8、跳到1循环

转换为代码需要考虑:
1、必须有数据结构来描述每个哲学家的当前状态(饥饿、思考、进餐)
2、该状态是一个临界资源,各个哲学家对它的访问应该互斥地进行
3、一个哲学家吃饱后,可能要唤醒其左邻右舍,两者之间存在同步关系

#define N 			5		    /* 哲学家个数*/
#define LEFT 		(i+N-1)%N 	/*i的左邻居编号*/
#define RIGHT 		(i+1)%N		/*i的右邻居编号*/
#define THINKING 	0			/*哲学家在思考*/
#define HUNGRY      1           /*哲学家试图拿起叉子*/
#define EATING      2		    /*哲学家进餐 */

typedef int semaphore;
int state[N];					/*数组用来跟踪记录每位哲学家的状态*/
semaphore mutex = 1;			/*互斥信号量,初始为1 */
semaphore s[N];					/*每个哲学家一个同步信号量,初始为0*/

void philosopher(int i)			/*i∶哲学家编号,从0到N-1*/
{
	while(TRUE){
		think();
		take_forks(i);			/*拿到两把叉子或被阻塞*/
		eat();
		put_forks(i);			/*把两把叉子放回原处并通知左邻右舍*/
	}	
}

void take_forks(int i)	/*i∶哲学家编号,从0到N-1*/
{
	P(&mutex);			/*进入临界区*/
	state[i] = HUNGRY;	/*记录哲学家i处于饥饿的状态*/
	test(i);			/*尝试获取2把叉子*/
	V(&mutex);			/*离开临界区*/
	P(&s[i);			/*如果得不到需要的叉子则阻塞*/
}

void put_forks(i)		 /*i∶哲学家编号,从0到N-1*/
{
	P(&mutex);			 /*进入临界区*/
	state[i] = THINKING; /*哲学家已经就餐完毕*/
	test(LEFT);			 /*检查左边的邻居现在能否进餐*/
	test(RIGHT);		 /*检查右边的邻居现在能否进餐*/
	V(&mutex);			 /*离开临界区*/
}

void test(i)		/*i∶哲学家编号,从0到N-1*/
{
	if( state[i] == HUNGRY &&  		/*i为自身或其他人*/
		state[LEFT]!= EATING && 
		state[RIGHT]!= EATING) 
	{
		state[i] = EATING;			/*拿到两把叉子*/
		V(&s[i);					/*通知第i个人可以吃饭了*/
	}
}

读者-写者问题

哲学家就餐问题对于互斥访问有限资源的竞争问题(如I/O设备)一类的建模过程十分有用。另一个著名的问题是读者-写者问题,它为数据库访问建立了一个模型。例如,设想一个飞机订票系统,其中有许多竞争的进程试图读写其中的数据。多个进程同时读数据库是可以接受的,但如果一个进程正在写数据库,则所有的其他进程都不能访问该数据库,即使读操作也不行。这里的问题是如何对读者和写者进行编程?

问题的约束:

  • 任何时候只能有一个线程可以操作共享变量
  • “读-读” 允许:同一时刻,允许有多个读者同时读
  • “读-写” 互斥:没有写者时读者才能读,没有读者时写者才能写
  • “写-写” 互斥:没有其他写者时写者才能写
  • 读者优先策略:只要有读者正在读状态,后来的读者都能直接进入。如读者持续不断进入,则写者就处于饥饿。

相反,也有写者优先策略:只要有写者就绪,写者应尽快执行写操作。如写者持续不断就绪,则读者就处于饥饿

用信号量解决读者-写者问题

用信号量描述每个约束

  • 信号量db:初始值为1,控制对数据库的互斥访问
  • 读者计数rc:初始值为0,正在读或者即将读的进程数目
  • 信号量mutex:初始值为1,控制对rc的互斥修改
typedef int semaphore;
semaphore mutex = 1; 		/*控制对rc的访问*/
semaphore db = 1;			/* 控制对数据库的访问*/
int rc = 0;					/*正在读或者即将读的进程数目*/

void reader(void)
	while(TRUE){
		P(&mutex);			/*获得对rc的互斥访问权*/	
		if(rC == 0) {		/*如果这是第一个读者*/
			P(&db);
		}
		rC++;				/*现在又多了一个读者*/
		V(&mutex);			/*释放对rc的互斥访问*/
		read_data_base();	/* 访问数据*/
		P(&mutex);			/*获取对rc的互斥访问*/
		rC--;				/*现在减少了一个读者*/
		if(rc == 0){ 		/*如果这是最后一个读者*/
			V(&db);
		}
		V(&mutex);			/*释放对rc的互斥访问*/
		use_data_read(); 	/*非临界区*/
	}
}

void writer(void) {
	while(TRUE){
		think_up_data();	/*非临界区*/
		P(&db);				/*获取互斥访问*/
		write_data_base();	/*更新数据*/
		V(&db); 			/*释放互斥访问*/
	}
}

对进程在进行读数据操作的前后( read_data_base();)必然需要使用信号量的PV操作来防止写进程进行写操作。如何来表示读者优先呢?代码中,是当第一个读进程要读取数据时才会进行 P($db),防止写进程进行写操作,然后 rc++。设想下,假设进程的顺序是读、读、写。当第一个读进程来时会进行 P($db),然后 rc++。当第二个读进程来时,由于第一个读进程已经执行 P($db) ,保证了写进程无法访问数据库,所以它后面的读进程可以直接进行读操作,而无需再执行 P($db) 。但是由于 rC 是一个共享变量,所以对 rC 的操作也需要保证互斥,防止多个读进程对其进行操作导致数据混乱。同理,当读进程的最后一个读进程时,执行 V($db),这样就可以保证之前的读操作都发生在写进程之前。

用管程解决读者-写者问题

用管程描述写者优先的情况。

读者的伪代码:

Database::Read() {
       Wait until no writers()
       read database;
       check out —— wake up waiting writers; 
}

等待的写进程,包括正在执行的写进程和等待队列中的写进程。当没有写进程后,读者才能进行读操作,然后唤醒正在等待的写进程。

写者的伪代码

Database::Write() {
       Wait until no readers/writers;
       write database;
       check out —— wake up waiting readers/writers; 
}

当没有读进程和写进程在执行时,写进程才能操作数据库。完成操作后唤醒正在等待的读进程或写进程。

管程

AR = 0;  	    // # of active readers
AW = 0;        // # of active writers
WR = 0; 	   // # of waiting readers
WW = 0;       // # of waiting writers
Lock lock;
Condition okToRead;
Condition okToWrite;

读者

Public Database::Read() {
  //Wait until no writers;
  StartRead(); 
  read database;
  //check out – wake up waiting writers; 
  DoneRead(); 
}

Private Database::StartRead() {
    lock.Acquire();
    while ((AW+WW) > 0) {	//存在正在执行的写进程或等待的进程时,读进程等待
        WR++;
        okToRead.wait(&lock);
        WR--;
    }
    AR++;
    lock.Release();
}

Private Database::DoneRead() {
    lock.Acquire();
    AR--;
    if (AR == 0 && WW > 0) {
        okToWrite.signal();
    }
    lock.Release();
}

写者

Public Database::Write() {
  //Wait until no readers/writers;
  StartWrite(); 
  write database;
  //check out-wake up waiting readers/writers; 
  DoneWrite(); 
}

Private Database::StartWrite() {
    lock.Acquire();
    while ((AW+AR) > 0)  {	//存在正在执行的写进程或读进程时,写进程等待
        WW++;
        okToWrite.wait(&lock);
        WW--;
    }
    AW++;
    lock.Release();
}

Private Database::DoneWrite() {
    lock.Acquire();
    AW--;
    if (WW > 0) {
        okToWrite.signal();
    }
    else if (WR > 0) {
        okToRead.broadcast();
    }
    lock.Release();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值