实验3 复杂PC问题——信号量与共享存储区

来自大学期间操作系统作业的实验报告

一、实验目的

  • 熟悉Linux下的多进程编程
  • 熟悉Linux的信号量与共享存储区的使用
  • 加深对生产者/消费者(PC)问题的理解

二、实验环境

  • Ubuntu Server 18.04 (LTS)

三、实验内容

  • 编写程序,构建父进程(左图)与子进程(右图)框架
    在这里插入图片描述

  • 对信号量操作进行再次封装,实现以下函数:

    • MySem newsem(int initVal)
      • 创建新的信号量,初值为initVal,完成后返回信号量的ID(typedef int MySem;)
    • void psem(MySem semID)
      • 对ID为semID的信号量做p
    • void vsem(MySem semID)
      • 对ID为semID的信号量做v
    • void freesem(MySem semID)
      • 注销ID为semID的信号量
  • 在init()中添加代码,请求用户输入仓库库存,然后申请并初始化共享存储区(仓库数组和变量in、out),以及所各种信号量

  • 在Pro()和Con()中添加代码,完成复杂PC问题(多生产者、多消费者)的模拟

  • 其中Pro()和Con()开始时在屏幕显示<进程类型(P或C)> <进程id> started.

  • 结束时Pro()显示:P <进程id> put an item to <仓库位置>.

  • Con()显示:C <进程id> got an item from <仓库位置>.

四、思考与实践

4.1 fork()的工作方式是怎样的?如何在父进程与子进程之间共享信息?

4.1.1 fork函数

fork函数用于创建一个与当前进程映像一样的子进程,所创建的子进程将复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,在内核中操作系统会重新为其申请一个子进程执行的位置。

fork系统调用会通过复制一个现有进程来创建一个全新的进程,新进程被存放在一个叫做任务队列的双向循环链表中,链表中的每一项都是类型为task_struct的进程控制块PCB的结构。

父子进程有什么区别呢?

  • 父进程设置了锁,子进程不继承。
  • 进程ID各不相同
  • 子进程的未决告警被清除
  • 子进程的未决信号集设置未空集

函数原型:

pid_t fork(void);

pid_t是一个宏定义,其实质是int被定义在#include <sys/types.h>头文件中。

头文件:

#include <unistd.h>
#include <sys/types.h>

返回值:

  • 在父进程中,fork返回新创建子进程的进程ID
  • 在子进程中,fork返回0
  • 如果出现错误,fork返回一个负值

在这里插入图片描述

每个进程都由独特换不相同的进程标识符(process ID),通过getpid()函数可获取当前进程的进程标识符,通过getppid()函数可获得父进程的进程标识符

4.1.2 fork深入

当进程调用fork后控制转入内核,内核将会做4件事:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容(数据空间、堆栈等)拷贝到子进程
  • 添加子进程到系统进程列表中
  • fork返回开始调度器调度

为什么fork会返回两次呢:

因为复制时会复制父进程的堆栈段,所以两个进程都停留在fork函数中等待返回,因此会返回两次,一个是在父进程中返回,一次是在子进程中返回,两次返回值是不一样的。

  • 在父进程中将返回新建子进程的进程ID
  • 在子进程中将返回0
  • 若出现错误则返回一个负数

因此可以通过fork的返回值来判断当前进程是子进程还是父进程

为什么pid在父子进程中不同呢:

其实就相当于链表,进程形成了链表,父进程的pid指向子进程的进程ID,因此子进程没有子进程,所以PID为0,这里的pid相当于链表中的指针

fork系统调用使用注意:

  • fork系统调用之后父进程和子进程是交替执行,父子进程是处于不同空间中的

  • fork系统调用的一次调用存在两次返回,此时二个进程处于独立的空间,各自执行自己的参数

4.1.3 如何在父进程与子进程之间共享信息

不同进程间主要有八种通信方式:

  • 无名管道( pipe ): 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
  • 高级管道(popen): 将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式
  • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信
  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
  • 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
  • 共享内存( shared memory ) : 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信
  • 套接字( socket ) : 套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信

4.2 如何在多个程序间获得同个信号量或共享存储区的ID,并正确使用信号量或共享存储区?

fork函数用于创建一个与当前进程映像一样的子进程,所创建的子进程将复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,其中就包括全局变量

我们只需要将信号量或共享存储区的ID设置在全局变量段,父子进程即可获得同个信号量或共享存储区的ID,例如实验中的:

int empty,full,mutex1,mutex2;

正确使用共享存储需要正确的资源分配机制,需要与与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信

正确使用信号量机制就在于正确的使用P操作或者V操作,正确的实现对于资源的抢占分配与释放

五、实验结果

5.1 什么是信号量

  • 信号量:信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量

  • 原语:原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的

  • 一对原语:wait(s)原语和signal(s)原语,可以把原语理解为我们自己写的函数,函数名分别为wait和signal,括号里的信号量s其实就是函数调用时传入的一个参数

  • waite、signal原语常称作P、V操作(来自荷兰语proberen和verhogen),因此,做题的时候经常把wait(s)、signal(s)两个操作分别写为P(S)、V(S)

  • 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步

5.2 信号量的工作原理

例子代码:

int S = 1;	//初始化整型信号量s,表示当前系统中可以使用的打印机数量

void wait(int S){	//wait原语,相当于“进入区”
	while(S <= 0);	//如果资源数不够,就一直循环等待
	S=S-1;			//如果资源数够,则占用一个资源	
}

void signal(int S){	//signal原语,相当于“退出区”
	S=S+1;			//使用完资源后,在退出区释放资源
}
进程P0:
...
wait(S);		//进入区,申请资源
使用打印机资源... //临界区,访问资源	
signal(S);	   //退出区,释放资源
...

由于信号量只能进行两种操作等待和发送信号,即P(S)和V(S),他们的行为是这样的:

  • P(S):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(S):如果有其他进程因等待S而被挂起,就让它恢复运行,如果没有进程因等待S而挂起,就给它加1

举个例子,就是两个进程共享信号量S,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行

5.3 Linux的信号量机制

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h

5.3.1 semget()函数

它的作用是创建一个新信号量或取得一个已有信号量,原型为:

int semget(key_t key, int num_sems, int sem_flags);
  • 第一个参数key:是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget()函数并提供一个键,再由系统生成一个相应的信号标识符(semget()函数的返回值),只有semget()函数才直接使用信号量键,所有其他的信号量函数使用由semget()函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作

  • 第二个参数num_sems:指定需要的信号量数目,它的值几乎总是1

  • 第三个参数sem_flags:是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误

  • semget()函数成功返回一个相应信号标识符(非零),失败返回-1

5.3.2 semop()函数

它的作用是改变信号量的值,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

sem_id是由semget()返回的信号量标识符,sembuf结构的定义如下:

struct sembuf{
    short sem_num; // 除非使用一组信号量,否则它为0
    short sem_op;  // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                   // 一个是+1,即V(发送信号)操作。
    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};
5.3.3 semctl()函数

该函数用来直接控制信号量信息,它的原型为:

int semctl(int sem_id, int sem_num, int command, ...);

如果有第四个参数,它通常是一个union semum结构,定义如下:

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个:

  • SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置

  • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符

5.4 Linux的共享存储区

相当于内存,可以任意读写和使用任意数据结构(当然,对指针要注意),需要进程互斥和同步的辅助来确保数据一致性

Linux 的共享存储区:

  • 创建或打开共享存储区(shmget):依据用户给出的整数值 key,创建新区或打开现有区,返回 一个共享存储区ID
  • 连接共享存储区(shmat):连接共享存储区到本进程的地址空间,可以指定虚拟地址或由系统分配,返回共享存储区首地址。父进程已连接的共享存储区可被 fork 创建的子进程继承
  • 拆除共享存储区连接(shmdt):拆除共享存储区与本进程地址空间的连接
  • 共享存储区控制(shmctl):对共享存储区进行控制。如:共享存储区的删除需要显式调用 shmctl(shmid, IPC_RMID, 0)
  • 头文件:
    • sys/types.h
    • sys/ipc.h
    • sys/shm.h
5.4.1 shmget()函数

原型:

int shmget(key_t key, int size, int flag);

格式:

shmid=shmget(key, size, flag);

功能:

创建一个关键字为 key,长度为 size 的共享存储区。其中,size 为存储区的字节数。key、 flag 与系统调用 msgget 相同

5.4.2 shmat()函数

原型:

int shmat(int id, char *addr, int flag);

格式:

virtaddr=shmat(id, addr, flag) 

功能:

从逻辑上将一个共享存储区附接到进程的虚拟地址空间上

参数:

  • id:为共享存储区的标识符
  • addr:用户要使用共享存储区附接的虚地址,若为 0,系统选择一个 适当的地址来附接该共享区
  • flag:规定对此区的读写权限,以及系统是否应对用户规定的地址做舍入操作:如果 flag 中设置了 shm_rnd 即表示操作系统在必要时舍去这个地址;如果设置了 shm_rdonly,表示只允许读操作
  • viraddr:是附接的虚地址
5.4.3 shmdt()函数

原型:

int shmdt(char *addr); 

功能:

把一个共享存储区从指定进程的虚地址空间断开,当调用成功,返回 0 值;不成功,返 回-1。addr 为系统调用 shmat 所返回的地址

5.4.4 shmctl()函数

原型:

int shmctl(int id, int cmd, struct shmid_ds *buf); 

功能:

对与共享存储区关联的各种参数进行操作,从而对共享存储区进行控制。调用成功返回 0,否则-1。其中,id 为被共享存储区的描述符

参数:

  • id:为被共享存储区的描述符
  • cmd:规定命令的类型。IPC_STAT 返回包含在指定的 shmid 相关数据结构中的状态信息,并且把 它放置在用户存储区中的*buf 指针所指的数据结构中。执行此命令的进程必须有读取允许权; IPC_SET 对于指定的 shmid,为它设置有效用户和小组标识和操作存取权;IPC_RMID 删除指定的 shmid 以及与它相关的共享存储区的数据结构;SHM_LOCK 在内存中锁定指定的共享存储区,必须 是超级用户才可以进行此项操作
  • buf:是一个用户级数据结构地址

5.5 源代码

这里使用的就是整数型信号量

#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <time.h>
#include <stdlib.h>
#include <sys/shm.h>
#define PRO 1
#define CON 0
#define P -1
#define V +1
typedef int MySem;
MySem empty,full,mutex1,mutex2;
int *buf;
MySem newsem(int intVal)
{
  int r,semID;
  semID=semget(0,1,IPC_CREAT|0666); 
  r=semctl(semID,0,SETVAL,intVal);
    return semID;
}
void psem(MySem semID)p
{
   struct sembuf s;
   s.sem_num=0;
   s.sem_op=P;
   s.sem_flg=0;
   int r=semop(semID,&s,1);
}
void vsem(MySem semID)
{
   struct sembuf s;
   s.sem_num=0;
   s.sem_op=V;
   s.sem_flg=0;
   int r=semop(semID,&s,1);
}
void freesem(MySem semID)
{
   int r;
   r=semctl(semID,0,IPC_RMID);
}
 
int init(int n)
{
  int shpid;
  shpid=shmget(0,sizeof(int)*(n+2),IPC_CREAT|0666);
  buf=(int *)shmat(shpid,0,0);
  empty=newsem(n);
  full=newsem(0); 
  mutex1=newsem(1);	  
  mutex2=newsem(1);   
  buf[n]=0;            
  buf[n+1]=0;         
  return shpid;                      
}
void pro(pid_t pid,int n)
{
  printf("<P> <%d> started\n",getpid());
  int index=buf[n];  
  psem(empty);  
  psem(mutex1);
  buf[n]=(buf[n]+1)%n;
  buf[ buf[n]]=1;
  printf("P <%d> put an item to <%d>\n",getpid(),index);
  vsem(mutex1);vsem(full);
  
}

void con(pid_t pid,int n)
{
  printf("<C> <%d> started\n",getpid());
  int index=buf[n+1];//out
  psem(full);
  psem(mutex2);
  buf[n+1]=(buf[n+1]+1)%n;
  buf[buf[n+1]]=0;
  printf("C <%d> got an item from <%d>\n",getpid(),index);
  vsem(mutex2);
  vsem(empty);
}
int main()
{
   int t,k,n;
   printf("请输入仓库库存n:\n");
   scanf("%d",&n);
   int shpid=init(n);
   k=rand()%1+1;
   pid_t pid;  
   while(1)
   {
     srand((unsigned)time(NULL));
     pid=fork();
     if(pid==0)   
     {                                                                                                     
       t=rand()%2; 
       if(t==PRO)
         pro(pid,n);
       else if(t==CON)
         con(pid,n);
       return 0; 
      }
     else  
       sleep(k);
    }
    int x1=shmdt(0);
	int x2=shmctl(shpid,IPC_RMID,0);
    return 0;
}
5.4.1 main函数
int main()
{
   int t,k,n;
   printf("请输入仓库库存n:\n");
   scanf("%d",&n);
   int shpid=init(n);
   k=rand()%1+1;
   pid_t pid;  
   while(1)
   {
     srand((unsigned)time(NULL));
     pid=fork();
     if(pid==0)   
     {                                                                                                     
       t=rand()%2; 
       if(t==PRO)
         pro(pid,n);
       else if(t==CON)
         con(pid,n);
       return 0; 
      }
     else  
       sleep(k);
    }
    int x1=shmdt(0);
	int x2=shmctl(shpid,IPC_RMID,0);
    return 0;
}

程序首先利用init函数返回了一个申请容量为仓库库存的共享存储区,返回了存储区id

int shpid=init(n);

设置父进程随机休眠的时间:

k=rand()%1+1;

然后定义了进程标识符,并且每次置随机数种子,然后建立一个新进程(子进程) ,返回子进程的进程ID 在子进程中返回0

注意:子进程与原进程(父进程)共享代码段,并拥有父进程的其他资源(数据、堆栈等)的一个副本

pid_t pid; 	//定义进程标示符
srand((unsigned)time(NULL));	//每次置随机数种子
pid=fork();	//建立一个新进程(子进程) ,返回子进程的进程ID 在子进程中返回0

然后如果是子进程,此时pid为0,进入子进程循环,随机选择子进程为生产者还是消费者,如果随机数为0则为生产者,反之生产者,传入pid数和仓库数量数

#define PRO 1
#define CON 0
if(pid==0)   //子线程  
{                                                                                                     
    t=rand()%2;//0,1 
    if(t==PRO)
        pro(pid,n);
    else if(t==CON)
        con(pid,n);
    return 0; //记得return 
}

如果进程结束,需要调用shmdt函数,将共享存储区与该进程的虚地址空间断开,然后删除指定的 shmid 以及与它相关的共享存储区的数据结构

int x1=shmdt(0);//断开已有的映射
int x2=shmctl(shpid,IPC_RMID,0);
5.4.2 init函数
int init(int n)
{
  int shpid;
  shpid=shmget(0,sizeof(int)*(n+2),IPC_CREAT|0666);//create 共享存储区+2 in out 
  buf=(int *)shmat(shpid,0,0);//将共享存储区映射到用户进程空间
  empty=newsem(n);//缓冲区单元格有n个,初始化标记为null,允许生产者进程一开始就连续执行k次 
  full=newsem(0);  //初始时没有满标记单元格,置初值full=0 
  mutex1=newsem(1);	   //生产者的互斥
  mutex2=newsem(1);   //消费者的互斥 
  buf[n]=0;           //缓冲区单元格in 
  buf[n+1]=0;         //缓冲区单元格out  
  return shpid;       //存储区id                
}

首先调用shmget创建一个关键字为0,长度为仓库容量+2的共享存储区,然后调用shmat将共享存储区映射到用户进程空间,然后分别初始化空缓冲区数目、满缓冲区数目、生产者关于缓冲区的互斥,消费者关于缓冲区互斥的信号量,然后建立输入与输出缓冲区

5.4.3 newsem函数
int newsem(int intVal)// 新建信号量 
{
    int r,semID;
    semID=semget(0,1,IPC_CREAT|0666); //创建新信号量集
    r=semctl(semID,0,SETVAL,intVal); //对指定信号量赋intVal值 返回值:如果成功,则为一个正数;如果失败,则为-1
    return semID;//获得的信号量的标识,用于此后的信号量操作 
}

就是根据申请的信号量的初始值,创建注册新的信号量,然后返回信号量的标识

5.4.4 psem函数
void psem(MySem semID)//对ID为semID的信号量做p
{
   	struct sembuf s;
   	s.sem_num=0;
   	s.sem_op=P;
   	s.sem_flg=0;
   	int r=semop(semID,&s,1);//对指定的信号量执行P操作
}

对指定的信号量做wite操作,执行p操作,相当于进入区,开始申请资源,sembuf的结构如之前所示

5.4.5 vsem函数
void vsem(MySem semID)//对ID为semID的信号量做v
{
		//unsigned short sem_num; 欲操作的信号量在信号量集中的编号
		//short sem_op; 信号量PV操作的增量(例如+1或-1)
		//short sem_flg; 额外选项标识(0表示无额外设置;IPC_NOWAIT表示不允许阻塞;
		//SEM_UNDO表示进程结束时恢复信号量 等)};
   	struct sembuf s;
   	s.sem_num=0;
   	s.sem_op=V;
   	s.sem_flg=0;
   	int r=semop(semID,&s,1);//对指定的信号量执行V操作
}

相当于对指定的信号量做signal操作,执行v操作,进入退出区,释放资源

5.4.6 freesem函数
void freesem(MySem semID)//注销ID为semID的信号量
{
   	int r;
   	r=semctl(semID,0,IPC_RMID);//IPC_RMID:注销(删除)信号量集,无需参数
}
5.4.7 pro函数
void pro(pid_t pid,int n)
{
  	printf("<P> <%d> started\n",getpid());//取得进程识别码 旧版 新_getpid();
  	int index=buf[n]; //buf[n]->in  用来标识in的位置   
  	psem(empty);  //同步,如果没有足够p值的话会放入队列中,系统进行维护
  	psem(mutex1);
  	buf[n]=(buf[n]+1)%n;
  	buf[ buf[n]]=1;//模拟存入,置1
  	printf("P <%d> put an item to <%d>\n",getpid(),index);
  	vsem(mutex1);//回调p函数,取出队首,
  	vsem(full);
}

buf的n位用来标识in的位置 ,即还可以填入数据的位置,从buf小到大,即按照buf[0]~buf[n-1]的顺序来生产

int index=buf[n]; //buf[n]->in  用来标识in的位置  

然后对空缓冲区信息量执行P操作,如果空缓冲区小于等于0,则进程挂起,反之执行下一步

psem(empty);

然后对生产者关于缓冲区的互斥量进行P操作,如果有其他生产者正在生产,则进程挂起,反之执行下一步

psem(mutex1);

然后buf的n位+1,表示下一次写入空缓冲区需要填入下一个位置,并对buf的之前取出的index位置执行放1

buf[n]=(buf[n]+1)%n;
buf[ buf[n]]=1;//模拟存入,置1

然后对之前的空缓冲区信息量与生产者关于缓冲区的互斥量进行V操作,释放资源

5.4.8 con函数
void con(pid_t pid,int n)
{
  printf("<C> <%d> started\n",getpid());
  int index=buf[n+1];//out
  psem(full);
  psem(mutex2);
  buf[n+1]=(buf[n+1]+1)%n;
  buf[buf[n+1]]=0;//模拟取出,置0
  printf("C <%d> got an item from <%d>\n",getpid(),index);
  vsem(mutex2);
  vsem(empty);
}

基本同上面的生产者函数,只不过将放入换成取出即可,不再赘述

5.5 实验截图

在这里插入图片描述

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值