操作系统_Lab3_同步问题

本文详细介绍了操作系统实验,涉及进程同步问题,包括信号量机制、进程关系分析、实验流程及问题总结。实验通过任务1至任务5,分别探讨了信号量解决进程同步、线程调度、生产者-消费者问题、共享内存与管道通信的同步机制。通过对代码实现和实验结果的分析,深化了对操作系统中并发控制的理解。

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

实验目的

  1. 系统调用的进一步理解。
  2. 进程上下文切换。
  3. 同步的方法。

实验内容

task1

1.1 实验要求

通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”

要求:P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。

1.2 知识准备

1.2.1 信号量相关

信号量是一种特殊的变量,访问具有原子性。只允许对它进行两个操作:

  1. 等待信号量
    当信号量值为0时,程序等待;当信号量值大于0时,信号量减1,程序继续运行。
  2. 发送信号量
    将信号量值加1。
    信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
int sem_post(sem_t *sem)
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
int sem_post(sem_t *sem)
与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
sem_init函数
该函数用于创建信号量
int sem_init(sem_t *sem,int pshared,unsigned int value)
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。 pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.
sem_destroy函数
该函数用于对用完的信号量的清理
int sem_destroy(sem_t *sem)
成功时返回0,失败时返回-1.
sem_open函数
创建并初始化有名信号灯
参数:
name 信号灯的外部名字(不能为空,为空会出现段错误)
oflag 选择创建或打开一个现有的信号灯
mode 权限位
value 信号灯初始值
sem_t *sem sem_open(const char *name, int oflag, mode_t mode,unsinged int value)
成功时返回指向信号灯的指针,出错时为SEM_FAILED
sem_close函数
关闭有名信号灯。
一个进程终止时,内核还对其上仍然打开着的所有有名信号灯自动执行这样的信号灯关闭操作。不论该进程是自愿终止的还是非自愿终止的,这种自动关闭都会发生。但应注意的是关闭一个信号灯并没有将他从系统中删除。
int sem_close(const char *name)
若成功则返回0,否则返回-1。
sem_unlink函数
从系统中删除信号灯。有名信号灯用sem_unlink从系统中删除。每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生
int sem_unlink(const char *name)
成功则返回0,否则返回-1
sem_open与sem_init的区别

  1. 创建有名信号量必须指定一个与信号量相关链的文件名称,这个name通常是文件系统中的某个文件。
    基于内存的信号量不需要指定名称
  2. 有名信号量sem 是由sem_open分配内存并初始化成value值
    基于内存的信号量是由应用程序分配内存,有sem_init初始化成为value值。如果shared为1,则分配的信号量应该在共享内存中。
  3. sem_open不需要类似shared的参数,因为有名信号量总是可以在不同进程间共享的,而基于内存的信号量通过shared参数来决定是进程内还是进程间共享,并且必须指定相应的内存
  4. 基于内存的信号量不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号量的值,因此,对于一个给定的信号量,我们必须小心保证只调用sem_init一次,对于一个已经初始化过的信号量调用sem_init,结果是未定义的。
  5. 内存信号量通过sem_destroy删除信号量,有名信号量通过sem_unlink删除

1.2.2 进程关系

根据实验要求分析进程关系,绘制前趋图

由要求可知,进程必须是P1最先执行,之后P2、P3互斥执行,之后P4再执行。在本实验中,我们采取信号量完成这一实验。首先我们将题目要求拆分为两个部分:
1)P1执行后互斥执行P2,P3:故我们定义一个信号量t1_23,初始值为0,当P1进程执行完打印输出后,对t1_23进行signal操作,信号量值变为1。当执行P2和P3时,先对t1_23进行wait操作,维护前驱关系,同时保证P2和P3仅有一个进程可以执行,在P2,P3执行打印输出后,对t1_23进行signal操作,维护互斥关系。
2)P2、P3执行后执行P4:我们分别定义两个信号量P24和P34,初始值为0,P2进程执行完打印输出后对P24进行signal操作,P3进程执行完打印输出后对P34进行signal操作,在P4执行时先针对以上两个信号量进行wait操作,故P4会等待P2,P3都完成才进行,以维护P2、P3对于P4的前驱关系。

1.3 实验过程

  1. 程序流程图如下所示:
  2. task1_fork.c 代码实现:
      	#include <stdio.h>
		#include <stdlib.h>
		#include <unistd.h>
		#include <sys/ipc.h>
		#include <sys/types.h>
		#include <sys/sem.h>
		#include <pthread.h>
		#include <semaphore.h>
		#include <fcntl.h>
		
		int main()
		{
   
   
			pid_t p2,p3,p4; //创建子进程2,3,4,P1无需创建,进入主函数后的进程即为p1进程
			sem_t *t1_23,*t24,*t34;//创建信号量
	
		t1_23=sem_open("t1_23",O_CREAT,0666,0);//表示关系进程1执行完进程2,3中的一个可以执行
		t24=sem_open("t24",O_CREAT,0666,0);//表示关系进程2执行完进程4才可执行
		t34=sem_open("t34",O_CREAT,0666,0);//表示关系进程3执行完进程4才可执行
		
		p2=fork();//创建进程p2
		if(p2==0)
		{
   
   
			sem_wait(t1_23);//实现互斥
			printf("I am the process p2\n");
			sem_post(t1_23);
			sem_post(t24);//实现前驱
		}
		if(p2<0)
		{
   
   
			perror("error!");
		}
		if(p2>0)
		{
   
   
			p3=fork();//创建进程p3
			if(p3==0)
			{
   
   
				sem_wait(t1_23);//实现互斥
				printf("I am the process p3\n");
				sem_post(t1_23);
				sem_post(t34);//实现前驱
			}
			if(p3<0)
			{
   
   
				perror("error!");
			}
			if(p3>0)
			{
   
   
				p4=fork();//创建进程p4
				if(p4>0)
				{
   
   
					printf("I am the process p1\n");
					sem_post(t1_23);
				}
				if(p4==0)
				{
   
   
					sem_wait(t24);
					sem_wait(t34);
					printf("I am the process p4\n");
					sem_post(t24);
					sem_post(t34);
				}
				if(p4<0)
				{
   
   
					perror("error!");
				}
			}
			
		}
		sleep(1);
		sem_close(t1_23);
		sem_close(t24);
		sem_close(t34);
		sem_unlink("t1_23");
		sem_unlink("t24");
		sem_unlink("t34");
		return 0;
		}
  1. 编译运行该程序可得到符合要求的进程执行结果。(由于P2,P3是互斥,所以会有两种执行顺序)

1.4 问题总结

  1. 运行时出现进程提前结束,通过添加sleep()函数延迟结束可以解决这个问题。
  2. 运行结果多次测试仅有(P1->P3->P2->P4)一种情况,后发现是之前关闭了随机化导致,打开随机化后可以得到P2和P3先后是随机的。打开方式如下图:
  3. 直接gcc -o 无法编译该程序,需要添加-pthread,正确的编译命令为:
    gcc -o task1 task1_fork.c -lpthread

task2

2.1 实验要求

火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。

2.2 知识准备

2.2.1 pthread_yield()函数

作用:
主动释放CPU从而让另外一个线程运行
与sleep()的区别:
pthread_yield()函数可以使用另一个级别等于或高于当前线程的线程先运行。如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。
sleep()则函数是等待一定时间后等待CPU的调度,然后去获得CPU资源。

2.3 实验过程

  1. 程序task2.c代码,(全部代码,通过加入/取消注释完成不同测试)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
sem_t* Sem = NULL;

int ticketCount=1000;

void *back()
{
   
   
	int temp;
	int i;
	for(i=0;i<200;i++)
	{
   
   
		//sem_wait(Sem);
		int temp = ticketCount;
		printf("退票后的票数为:%d \n",ticketCount);
		//放弃CPU,强制切换到另外一个进程
		//pthread_yield();
		temp=temp+1;
		//pthread_yield();
		ticketCount=temp;
		//sem_post(Sem);
	}
}

void *sell()
{
   
   
	int temp;
	int i;
	for(i=0;i<200;i++)
	{
   
   
		//sem_wait(Sem);
		int temp = ticketCount;
		printf("售票后的票数为:%d \n",ticketCount);
		//放弃CPU,强制切换到另外一个进程
		//pthread_yield();
		temp=temp-1;
		//pthread_yield();
		ticketCount=temp;
		//sem_post(Sem);
	}

}

int main(int argc,char *argv[]){
   
   
	pthread_t p1,p2;
	printf("开始的票数为:%d \n",ticketCount);
	Sem = sem_open("sem", O_CREAT, 0666, 1);
    	pthread_create(&p1,NULL,sell,NULL);
    	pthread_create(&p2,NULL,back,NULL);
    	pthread_join(p1,NULL);
    	pthread_join(p2,NULL);
	sem_close(Sem);
	sem_unlink("sem");
    printf("最终的票数为: %d \n",ticketCount);
    return 0;
}
  1. 编译运行该程序,编译命令为gcc -o task2 task2.c -lpthread

  2. 测试不同情况运行结果:
    1) 不加pthread_yield();函数,测试“卖50张,退50张”的情况

    可见结果显示正确
    2) 不加pthread_yield();函数,测试“卖100张,退50张”的情况

    可见结果值不正确
    3) 不加pthread_yield();函数,测试“卖50张,退100张”的情况

    可见结果值不正确
    4)添加pthread_yield();函数在售票ticketCount值被写回前,测试“卖200张,退200张”的情况

    可见结果不正确且偏小
    5) 添加pthread_yield();函数在退票ticketCount值被写回前,测试“卖200张,退200张”的情况

    可见结果不正确且偏大
    6) 添加信号量机制,并添加pthread_yield();函数,测试不同买票张数
    a. 卖200张退200张:

    b. 卖80张退30张:

    可见结果均正确

  3. 结果分析
    在并发执行多进程时,当循环次数很大是,会产生进程间的切换,而多进程的切换可能导致在一个进程在对票数ticketCount进行操作后还未写回,另外一个进程就读取该数据。产生读取脏数据及覆盖的问题,进而导致结果的不正确。
    在测试中,第2、3种情况是因改变两个进程循环次数而得到不同的值,这个值没有一定的规律。第4种情况是在售票后未及时写回,售票进程会在之后一段时间出现覆盖性写入。故而售票量多,剩余票结果较小。第5种情况是在退票后未及时写回,退票进程会在之后一段时间出现覆盖性写入。故而退票量多,剩余票结果较大。第6种情况加入了信号量就可保证售票进程和退票进程的的原子性,避免了脏数据读取和覆盖性写入等问题,结果正确,可保证进程同步。

task3

3.1 实验要求

一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。

3.2 知识准备

3.2.1 生产者-消费者问题

所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
如果共享数据区为空的话,阻塞消费者继续消费数据;

3.3 实验过程

  1. 程序task3.c代码
#include <stdio.h>
#include 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值