操作系统 实验二 进程与进程通信

操作系统 实验二 进程与进程通信

文章目录

  • 操作系统 实验二 进程与进程通信
    • 摘要
    • 算法思想和概要设计
      • 算法思想
        • PART1
        • PART2
          • 消息通信
          • 共享内存
      • 概要设计
        • PART1
        • PART2
          • 消息通信
          • 共享内存
    • 数据结构与变量说明
      • 消息缓冲区
      • 信号灯组
    • 源程序
      • PART1
        • 主程序
      • PART2
        • 消息通信
          • 消息缓冲区与预处理宏定义
          • Server 进程
          • Client 进程
        • 共享内存
          • 信号灯组与预处理宏定义
          • 信号灯组初始化函数
          • 信号灯semWait与semSignal函数
          • 共享内存重要函数
          • 主程序
    • 测试
      • PART1
      • PART2
        • 消息通信
        • 共享内存
    • 改进

摘要

在本试验 PART1 中设计了一个程序,该程序创建一个子进程,使父子进程合作,协调地完成收发信号、打印目录功能。在该程序中使用了进程的睡眠、进程图象改换、父进程等待子进程终止、信号的设置与传送(包括信号处理程序)、子进程的终止等有关进程的系统调用。在本试验 PART2 中,分别利用 UNIX 的消息通信机制、共享内存机制(用信号灯实施进程间的同步和互斥)实现两个进程间的数据通信与简单的信息处理。

算法思想和概要设计

算法思想

PART1

父进程设置信号 SIGUSR1 的信号处理函数,子进程继承父进程的信号处理方式。子进程创建后进入 10 s 10s 10s 的低优睡眠;父进程先打印相关信息,在发送软中断信号前先进入 1 s 1s 1s 的低优睡眠,防止在子进程进入睡眠前就发送信号。父进程完成睡眠后将软中断信号传送给子进程,唤醒低优睡眠中的子进程,触发自定义的信号处理程序。之后子进程正常运行,执行图像改换,列出当前路径下的目录。父进程等待子进程终止。

PART2

消息通信

本部分分为 Server 进程和 Client 进程两个进程。ServerClient 通过同一个关键字获得相同消息队列的标识符。之后 Client 通过消息通信向 Server 发送信息,内容为自己的进程号,并接收 Server 通过消息通信返回的信息。Server 保持监听消息队列,收到 Client 发送的消息后就打印出来,并将消息经过一定处理后返回 Client

共享内存

父进程创建 1 k b 1kb 1kb 大小的共享内存与同步信号灯组,得到共享内存标识符与信号灯组标识符。父进程把共享内存连接到当前进程的地址空间。由于本部分只有两个进程,所以任一信号灯的信号量不可能超过 1 1 1 ,因此信号灯组只需要两个信号灯,不需要互斥信号灯。信号灯组含读与写两个信号灯,写信号灯初始化为 1 1 1 ,读信号灯初始化为 0 0 0 。然后父进程创建子进程,子进程继承之前所述参数。之后父进程向共享内存中输入信息,子进程从共享内存中读出并处理信息,完成父子进程间的共享内存通信。

概要设计

以下将以流程图来展示对该程序分配与释放内存的过程概要设计。

PART1

操作系统 实验二 进程与进程通信_第1张图片

PART2

消息通信

操作系统 实验二 进程与进程通信_第2张图片

共享内存

操作系统 实验二 进程与进程通信_第3张图片

数据结构与变量说明

消息缓冲区

struct msgtype{
    long mtype;		// 消息类型
    int text;			// 消息正文
};

信号灯组

typedef	union semunion{
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux specific) */
}semunion;

源程序

PART1

主程序

#include 
#include 
#include 
#include 
#include 
#include 

int main(){
    int status;
    pid_t pid;
    void func();
    signal(SIGUSR1, func);				// 设置信号SIGUSR1的中断处理函数为func
    while((pid = fork()) == -1);	// 产生子进程
    if(pid){											// 父进程
        printf("It is the parent process.\n");
        printf("Parent: will send signal.\n");
        sleep(1);								// 向子进程传送信号前先睡眠一秒,保证信号到达时子进程正在低优睡眠中
        kill(pid, SIGUSR1);			// 父进程向子进程传送SIGUSR1软中断信号
        pid = wait(&status);		// 父进程等待子进程终止
        printf("Child process %d, status = %d.\n", pid, status);
    }
    else{													// 子进程
        sleep(10);
        printf("It is the child process.\n");
        printf("Child: signal is received.\n");
				execl("/bin/ls", "ls", "-l", (char*)0); // 进程图像改换,将子进程图像改换为ls命令
        printf("execl error\n");								// 图像改换失败
        exit(2);
    }
    printf("Parent process finish.\n");
}

void func(){		// 自己设置的信号SIGUSR1的中断处理函数
    printf("It is a signal processing function.\n");
    system("date");
}

PART2

消息通信

消息缓冲区与预处理宏定义
/* msgcom.h */
#include 
#include 
#include 
#include 
#define MSGKEY 5678

struct msgtype{
    long mtype;		// 消息类型
    int text;			// 消息正文
};
Server 进程
/* Server 进程 */
#include "msgcom.h"
void main()
{
	struct msgtype buf;		// 创建消息缓冲区
	int qid;
	if((qid=msgget(MSGKEY, IPC_CREAT|0666)) == -1)	// 获得消息队列标识符
		return(-1);
	while(1){
		msgrcv(qid, &buf, 512, 1, MSG_NOERROR);		// 接收消息队列消息存入消息缓冲区
		printf("Server receive a request from process %d\n", buf.text);	// 打印消息内容(对方进程的进程号)
		buf.mtype = buf.text;		// 处理消息,将消息类型赋值为消息内容
		msgsnd(qid, &buf, sizeof(int), 0);		// 将消息缓冲区的消息发送给消息队列
	}
}
Client 进程
/* Client 进程 */
#include "msgcom.h" 
void main()
{
	struct msgtype buf;		// 创建消息缓冲区
	int qid, pid;
	qid = msgget(MSGKEY, IPC_CREAT|0666);		// 获得消息队列标识符
	buf.mtype = 1;		// 赋消息类型为1
	buf.text = pid = getpid();	// 赋消息内容为本进程进程号
	msgsnd(qid, &buf, sizeof(buf.text), 0);	// 将消息缓冲区的消息发送给消息队列
	msgrcv(qid, &buf, 512, pid, MSG_NOERROR);	// 接收消息队列消息存入消息缓冲区
	printf("Request received a massags from server, type is:%d\n ", buf.mtype);	// 打印消息内容(本进程进程号)
}

共享内存

信号灯组与预处理宏定义
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define SEM_WRITE		0		// 写信号灯编号
#define	SEM_READ		1		// 读信号灯编号
#define SIZE			1024	// 共享内存容量
#define	P_OPERATION		-1		// P操作
#define V_OPERATION		1		// V操作

typedef	union semunion{
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux specific) */
}semunion;
信号灯组初始化函数
/*
func:对num个信号灯赋值
para:
	semid 信号灯组id
	sema_value[] 信号灯值缓冲区
	num 信号灯个数

*/
void init_sema_value(int semid,int *sema_value,int num){

	semunion semu;
	for(int i = 0; i < num; i++){
		semu.val = sema_value[i];
		semctl(semid, i, SETVAL, semu);
	}
}
信号灯semWait与semSignal函数
/*
func: semWait与semSignal操作封装
para:
	semid 信号灯组id
	whichnum 信号灯编号
	op 对信号灯的操作
*/
void operate(int semid, int whichnum, int op){
	struct sembuf opbuf;
	opbuf.sem_num = whichnum;
	opbuf.sem_op = op;
	opbuf.sem_flg = 0;
	semop(semid, &opbuf, 1);
}
共享内存重要函数
int shmget(key_t key, size_t size, int shmflg);

第一个参数,程序需要提供一个参数 k e y key key(非 0 0 0 整数),它有效地为共享内存段命名,shmget 函数成功时返回一个与 k e y key key 相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回 − 1 -1 1.

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用 shmget 函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget 函数的返回值),只有 shmget 函数才直接使用信号量键,所有其他的信号量函数使用由 semget 函数返回的信号量标识符。

第二个参数 s i z e size size 以字节为单位指定需要共享的内存容量

第三个参数 s h m f l g shmflg shmflg 是权限标志,它的作用与 open 函数的 m o d e mode mode 参数一样,如果要想在 k e y key key 标识的共享内存不存在时,创建它的话,可以与 I P C _ C R E A T IPC\_CREAT IPC_CREAT 做或操作。共享内存的权限标志与文件的读写权限一样,举例来说, 0644 0644 0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

void *shmat(int shm_id, const void *shm_addr, int shmflg);

第一次创建完共享内存时,它还不能被任何进程访问,shmat 函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

第一个参数 s h m _ i d shm\_id shm_id 是由 s h m g e t shmget shmget 函数返回的共享内存标识。
第二个参数 s h m _ a d d r shm\_addr shm_addr 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数 s h m _ f l g shm\_flg shm_flg 是一组标志位,通常为 0 0 0

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回 − 1 -1 1

int shmdt(const void *shmaddr);

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。参数 s h m a d d r shmaddr shmaddrshmat 函数返回的地址指针,调用成功时返回 0 0 0 ,失败时返回 − 1 -1 1

int shmctl(int shm_id, int command, struct shmid_ds *buf);

第一个参数 s h m _ i d shm\_id shm_idshmget 函数返回的共享内存标识符。

第二个参数 c o m m a n d command command 是要采取的操作,它可以取下面的三个值 :
I P C _ S T A T IPC\_STAT IPC_STAT:把 shmid_ds 结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
I P C _ S E T IPC\_SET IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为 shmid_ds 结构中给出的值
I P C _ R M I D IPC\_RMID IPC_RMID:删除共享内存段

第三个参数 b u f buf buf 是一个结构指针,它指向共享内存模式和访问权限的结构1

主程序
int main(void)
{
	int shmid, semid, sema_value[2];
	pid_t pid;
	char *addr = NULL;
	key_t Key;
	Key = ftok(".", 'm');		// 获得关键字,之后用于创建共享内存与信号灯组
	// 创建共享内存
	if((shmid=shmget(Key, SIZE, IPC_CREAT|0666)) < 0){
		perror("shmget");
		exit(-1);
	}
	// 创建信号灯组,含两个信号灯
	if((semid=semget(Key, 2, IPC_CREAT|0666|IPC_EXCL)) < 0){
		perror("semget");
		goto error1;
	}
	// 信号灯初始化
	sema_value[SEM_WRITE] = 1;
	sema_value[SEM_READ] = 0;
	init_sema_value(semid, sema_value, 2);
	// 映射共享内存
	if((addr=(char *)shmat(shmid, NULL, 0)) < 0){
		perror("shmat");
		goto error2;
	}
	if((pid=fork()) == 0){
		char *new, *old;
		while(1)		// 子进程循环
		{
			operate(semid, SEM_READ, P_OPERATION);	// 对读信号灯的semWait操作
			new = old = addr;
			while(*old){														// 删除信息中的所有空格
				if((*old) != ' '){
					(*new++) = (*old);
				}
				old++;
			}
			(*new)='\0';
			printf("Kid process %d is receiving : %s\n", getpid(), addr);
			operate(semid, SEM_WRITE, V_OPERATION);	// 对写信号灯的semSignal操作
		}
	}
	else
	{
		while(1)		// 父进程循环
		{
			operate(semid, SEM_WRITE, P_OPERATION);	// 对读信号灯的semWait操作
			printf("Parent process %d is sending :\n", getpid());
			fgets(addr, SIZE, stdin);
			operate(semid, SEM_READ, V_OPERATION);	// 对读信号灯的semWait操作
			if(strcmp(addr ,"quit\n")==0)						// 发现输入“quit”后发送软中断信号给子进程使子进程终止,父进程也推出循环
			{
				kill(pid, SIGUSR1);
				break;
			}
		}
	}

error2:
	semctl(semid, 0, IPC_RMID);		//删除信号灯集
	shmdt(addr);					//撤销共享内存映射
error1:
	shmctl(shmid, IPC_RMID, NULL);	//删除共享内存

	return 0;
}

测试

PART1

__过程描述与分析:__在以下两句打印出来之后,打印停止了 1 s 1s 1s ,之后立刻将剩余的所有输出全部打印完毕,子进程并没有完成等待 10 s 10s 10s ,且打印出了自定义中断处理程序中设置的内容,说明子进程成功被信号唤醒。

It is the parent process.
Parent: will send signal.

PART2

消息通信

过程描述与分析: Server 常开,Client 发送并接收一次消息后即关闭。进行多次实验,发现两边输出均为 Client 进程号,说明实验成功。

共享内存

可见父进程号为 41497 41497 41497 ,子进程号为 41978 41978 41978 。父进程发送消息,子进程将接受到的消息去掉空格后打印出来。测试结果完全正确,程序能正确运行。

改进

一开始(父进程发送信号前没有睡眠一秒) PART1 中遇到了一个问题:父进程发送的信号虽然能出发信号处理程序,但无法唤醒子进程的 10 s 10s 10s 低优睡眠;但若让父进程先睡眠 1 s 1s 1s 后就能正确唤醒子进程。与同学交流后发现大家都有这个现象。之后同学们在课程微信群中的讨论让问题逐渐明朗:原因是由于进程运行的不确定性,父进程发送软中断信号给子进程时,子进程可能还没有进入睡眠,也就不存在唤醒子进程的问题了,子进程之后还会照样睡眠十秒。所以父进程在发送信号给子进程前先睡眠一秒,使操作系统进行进程调度能保证会运行到一部分子进程代码,即让子进程进入睡眠。


  1. https://blog.csdn.net/ljianhui/article/details/10253345 ↩︎

你可能感兴趣的:(操作系统 实验二 进程与进程通信)