操作系统——进程通信

目录

一、管道通信

二、共享内存

1. 创建共享内存

2. 连接共享内存

3. 分离共享内存

4. 释放共享内存

5. 共享内存通信

三、消息队列

1. 创建消息队列

2. 放置消息

3. 读取消息

4. 删除消息队列

5. 消息队列通信

四、总结


一、管道通信

借助管道可以实现进程之间的通信,一个进程在管道中写入信息,另一个进程就可以从管道中读取进程写入的信息。

c语言中unistd.h库中就包含了管道函数,其原型如下

int pipe(int p[2])

该函数调用时需要传入一个整型数组p,这个数组长度只有2,是用于存储管道的写入端和读取端,p[0]是打开读取端,p[1]是打开写入端。调用函数后会根据数组p创建一个管道,如果调用成功,则返回0,如果失败则返回-1。

下面我们创建一个使用管道通信的简易代码。

#include<iostream>
#include<unistd.h>
using namespace std;
int main(){
	int filedes[2];
	char buffer[80];
	if(pipe(filedes) < 0){
		cout << "管道创建失败";
		exit(0);
	}
	if(fork() > 0){
		char s[] = "Hello!\n";
		write(filedes[1], s, sizeof(s));
	}
	else{
		read(filedes[0], buffer, 80);
		cout << buffer;
	}
}

上面代码首先定义一个数组filedes存储管道输入和输出端,再定义一个字符数组用于存储从管道中读取到的数据。接着就使用pipe函数传入filedes数组创建管道,如果函数返回值小于0说明创建失败,打印管道创建失败,结束程序。

管道成功创建后使用fork函数创建子进程,创建子进程后,会有两个进程运行下面的代码,同时该函数会在父进程返回子进程的PID,在子进程返回0,根据返回值的不同来区分是父进程还是子进程。

如果是父进程,则在管道中写入数据,使用write实现,write函数传入要写入的目标文件或设备、要写入数据的缓冲区指针以及要写入的字节数,write函数就会在缓冲区寻找对于地址的数据,写入给定字节数的数据到要写入的文件中,管道就是一种特殊的文件,我们需要在写入端进行写入,也就是filedes[1],写入数据指针就是字符数组变量名。

如果是子进程,就使用read函数,read函数与write函数类似,传入参数:管道的读取端,存储读取数据的缓冲区指针和读取长度,然后输出读取到的数据,getpid函数用于获取当前的进程ID。

在linux系统中运行上述代码,测试结果。

我使用的是红帽系统,在命令行页面输入gedit命令就可以打开gedit文本编辑器,在里面我们就可以写入各种代码。

 直接使用gedit命令打开的是一个未命名的空白文件,如果在gedit命令后面加上文件名就可以打开对应的文件,如果该文件不存在就会新建一个。

写完代码后ctrl+s保存,然后在命令行页面输入g++ -o目标文件名 原文件名.cpp对代码文件进行编译(c语言文件是gcc),比如g++ -ocx cx.cpp编译cx.cpp文件,生成一个可执行文件cx,接着输入./可执行文件名运行,比如./cx。

输出结果:

现在我们再加大难度,父进程创建两个子进程,两个子进程在管道中写入数据,父进程从管道中读取数据,并输出写入这个数据的子进程号。

#include<iostream>
#include<unistd.h>
using namespace std;
int main(){
	int filedes[2];
	char buffer[80];
	pid_t child1, child2;
	if(pipe(filedes) < 0){
		cout << "管道创建失败" << endl;
		exit(0);
	}
	child1 = fork();
	if(child1 < 0){
		cout << "进程创建失败" << endl;
		exit(0);
	}
	// 子进程1
	if(child1 == 0){
		char s[] = "child 1 is sending a message!\n";
		write(filedes[1], s, sizeof(s));
	}
	else{
		child2 = fork();
		// 子进程2
		if(child2 == 0){
			char s[] = "child 2 is sending a message!\n";
			write(filedes[1], s, sizeof(s));
		}
		else{
			read(filedes[0], buffer, sizeof(buffer));
			cout << "来自子进程" << child1 << ":" << buffer;
			read(filedes[0], buffer, sizeof(buffer));
			cout << "来自子进程" << child2 << ":" << buffer;
		}
	}
}
		

上面的代码首先创建管道,然后创建子进程1,对于子进程1,往管道中写入数据"child 1 is sending a message!\n",父进程则再创建一个子进程2,再接着判断是子进程2还是父进程,子进程2往通道中写入数据"child 2 is sending a message!\n",父进程则读取管道中的数据。

执行结果:

但是这个代码可能有点问题,虽然在创建子进程是先创建子进程1再创建子进程2的,但我不太确定是否始终是进程1先执行,进程2后执行,跟系统的进程调度有关,可能需要进程同步或者其他方法来解决。

二、共享内存

在linux系统中,共享内存也是一种进程间通信的方式,共享内存允许多个进程访问,一个进程在共享内存中写入数据,另一个进程从共享内存中读取数据实现进程之间通信。

1. 创建共享内存

shmget函数可以创建/获取共享内存,它的原型如下:

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

key是唯一标识共享内存的关键字(键值)。

size是共享内存大小(字节),内核会把这个数值向上舍入取最近的虚拟内存帧的大小。

shmflag是标志参数,指定选项及其权限位。可以是IPC_CREAT(如果共享内存不存在则创建)、IPC_EXCL(如果共享内存存在则报错)等标志。

调用该函数会返回key对应的共享内存的ID,失败的话返回-1.

2. 连接共享内存

在创建共享内存后,为了便于使用共享内存,我们可以把内存变量和共享内存连接起来。

使用shmat函数可以实现,shmat函数的原型如下:

void *shmat(int shmid,const void *shmaddr,int shmflag);

shmid是由shmget函数产生的共享内存ID;

shmaddr是任意类型的指针变量;

shmflg是共享内存标志参数,含义同上。

连接共享内存实际上就是将共享内存的首地址赋值给变量,这样就可以直接使用这个指针变量在共享内存地址中写入和读取数据了。

3. 分离共享内存

在使用完共享内存后需要将变量与共享内存分离,防止后续误访问。

使用shmdt函数就可以实现将变量与共享内存分离,其原型如下:

int shmdt(char *shmaddr);

shmaddr是内存指针变量;

该函数调用成功时,返回0值,调用不成功,返回-1,addr是共享内存的首地址,也就是系统调用shmat所返回的地址。

分离共享内存实际上就是将变量shmaddr中存储的共享内存地址清除。

4. 释放共享内存

在使用完共享内存后,我们需要将共享内存删除,使用shmctl函数可以实现。

shmctl是用于控制共享内存的函数,它可以根据cmd参数选择对共享内存执行相应的操作,如获取状态、删除等。

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

其中:调用成功时返回0,否则返回-1。shmid为被共享存储区的标识符。cmd规定操作的类型。规定如下:

IPC_STAT: 返回包含在制定的shmid相关数据结构中的状态信息,并且把它放置在用户存储区中的*buf指针所指的数据结构中。执行此命令的进程必须有读取允许权。

IPC_SET:  对于指定的shmid,为它设置有效用户和小组标识和操作存取权。

IPC_RMID  删除制定的shmid以及与它相关的共享存储区的数据结构。

SHM_LOCK: 在内存中锁定指定的共享存储区,必须是超级用户才可以进行此操作。

BUF 是一个shmid_ds结构类型的指针,shmid_ds是用于描述共享内存信息的结构体。

5. 共享内存通信

使用上面的函数我们就可以实现一个使用共享内存实现进程之间通信的例子。

#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<cstring>
#define KEY 2002
#define SIZE 1024
using namespace std;
int main(){
	// 共享地址ID
	int shmid;
	// 共享地址变量
	char* shmadd;
	// shmid_ds结构的指针
	struct shmid_ds* buf;
	// 创建共享地址
	shmid = shmget(KEY, SIZE, IPC_CREAT);
	// 创建子进程	
	pid_t child = fork();
	// 子进程
	if(child == 0){
		// 将共享地址连接到变量shmad
		shmadd = (char*) shmat(shmid, NULL, 0);
		// 复制要写入的字符串数据到共享内存中,会覆盖原有的字符串
		strcpy(shmadd, "THis is a child process!\n");
		// 将指针变量与共享地址分离		
		shmdt(shmadd);
		return 0;
	}

	else{
		int* status;
		// 等待子进程执行完毕
		waitpid(child, status, 0);
		// 获取共享内存状态
		shmctl(shmid, IPC_STAT, buf);
		// 显示共享内存状态
		cout << "shm_segsz = " << buf->shm_segsz << endl;
		cout << "shm_cpid = " << buf->shm_cpid << endl;
		cout << "shm_lpid = " << buf->shm_lpid << endl;
		shmadd = (char*) shmat(shmid, NULL, 0);
		// 显示共享内存内容		
		cout << shmadd;
		shmdt(shmadd);
		// 删除共享内存
		shmctl(shmid, IPC_RMID, NULL);
	}
}		

 上面代码首先使用shmget函数创建地址,由于是创建地址,使用shmflag为IPC_CREAT,接着创建子进程。

对于子进程,首先获取共享内存的地址(获取的是字符类型的指针),也就是连接共享内存,然后在这个地址里写入数据(由于获取的是字符类型的指针,写入数据也是字符,可以使用strcpy复制要写入的数据到共享内存中(从首地址开始一个个字符复制,会覆盖原有的数据)),写完后再使用shmdt分离变量和共享地址。

对于父进程,先使用waitpid函数等待子进程执行完毕,然后可以获取共享内存的状态,使用shmctl传入IPC_STAT参数获取状态,接着连接共享内存,从共享内存中读取数据,由于是字符类型的指针,共享内存中存放的是字符/字符串,直接输出数据,最后分离共享内存和变量并删除共享内存。

运行结果:

在写实验3进程同步的时候发现strcpy的解释说错了,把它错认为strcat了,于是回来修改了一下。同时也发现了上面代码其实有个小问题,是使用strcpy导致的问题,strcpy它会将新数据覆盖在原数据上,但如果新数据长度没有新数据长,剩下的字符仍会保留,也就是会剩下一些脏数据。

比如在下面这个例子里,首先往进程中写入字符串"sxiazjkl",接着再写入"hello",而"hello"是没有"sxiazjkl"长的,因此共享地址中会剩下一些"sxiazjkl"字符串的部分字符,我们使用指针进行访问就会输出原本属于"sxiazjkl"字符串的字符k,同时还有一个空字符'\0',这个字符其实是strcpy复制完后加上的字符串终止字符。

不过这个也只是个小问题,上面也说了strcpy复制后会在字符串末尾加上 '\0' 结束符,从上面的截图中我们也能看到直接输出字符串的话只会输出“hello”,而不会输出脏数据,因为字符串输出遇到\0就会自动结束,如果你不乱用指针的话其实根本不会遇到这个问题。

三、消息队列

消息队列也是进程通信的一种方式。

1. 创建消息队列

使用msgget函数创建一个新的消息队列或访问一个消息队列,其原型如下:

int msgget(key_t key,int msgflag);

key与msgflag的意义与创建共享内存函数shmget是一样的,msgflag一般有两种,IPC_CREAT创建消息队列,IPC_EXCL—检查消息队列是否存在,创建消息队列使用IPC_CREAT,获取消息队列使用IPC_EXCL,与共享内存都是一样的。返回值也是成功是返回消息队列标识符,不过失败返回-1.

2. 放置消息

消息队列的写入和读取数据与共享内存不太一样,共享内存无论读还是写都是直接访问内存,而消息队列需要使用两个函数分别来读和写。

msgsnd函数用于在消息队列中放置一个消息。其原型如下:

Int msgsnd(int msgid,struct msgbuf *msgf,size_t nbytes,int flag);

其中msgid是消息队列的标识符,由msgget函数返回。

msgf是一个msgbuf结构的指针,msgbuf结构的定义如下:

struct msgbuf{
    long mtype; /*消息的类型,用来区分不同类型的消息*/
    char mtext[1]; /*消息的数据*/
};

其中消息的数据的大小可以自己定义。

nbytes是消息的大小,flag是选项标志位:可以是0(不使用标志),也可以是IPC-NOWAIT(当消息队列满时不等待,返回错误信息。)

3. 读取消息

使用msgrcv函数来从消息队列中读取消息,其原型如下:

int msgrcv(int msgid,struct msgbuf *msgp,size_t nbytes,long type,int flag);

msgp是存放消息的msgbuf结构体,nbyte是信息数据的长度。

type是接受消息的类型,type =0返回队列内第一项信息;type>0返回队列内第一项与mtype相同的信息type<0返回队列内第一项mtype小于或等于mtype绝对值的信息。

flag是选项标志,可以取0(等待消息到来,进程挂起)或IPC-NOWAIT(没有消息不等待,返回错误)

4. 删除消息队列

使用msgctl函数对消息队列进行操作,其原型如下

int msgctl(int msgid,int cmd,struct msgid_ds *buf);

msgctl()提供了几种方式来控制信息队列的运作。参数msgid为欲处理的信息队列识别代码,参数cmd为欲控制的操作,有下列几种数值:

IPC__STAT:把信息队列的msqid—ds结构数据复制到参数buf.

IPC__SET:将参数buf所指的msqid—ds结构中的msqid.uid、msg_perm.gtd、msg_perm.mode和msg_qbytes参数复制到信息队列的msqid—ds结构内。

IPC__RMID:删除信息队列和其数据结构。

5. 消息队列通信

根据上面的函数编写消息通信的代码

#include<iostream>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>
#define KEY 2002
#define TEXT_SIZE 64
using namespace std;
struct msgbuffer{
	long mtype;
	char mtext[TEXT_SIZE];
};
int main(){
	// 消息队列ID
	int msgqueid;
	struct msgbuffer *msgp = new msgbuffer;
	struct msgbuffer *msgr = new msgbuffer;
	// 获取消息对联ID
	msgqueid = msgget(KEY, IPC_CREAT);
	// 创建子进程
	pid_t child = fork();
	// 子进程
	if(child == 0){
		msgp->mtype = 1;
		// 往msgp结构体中写入数据		
		strcpy(msgp->mtext, "This is a child process(queue)!\n");
		// 将msgp类型的数据写入消息队列		
		msgsnd(msgqueid, msgp, TEXT_SIZE, 0);
		return 0;
	}
	else{
		int* status;
		// 等待子进程执行完毕
		// waitpid(child, status, 0);
		// 读取消息队列中的消息
		msgrcv(msgqueid, msgr, TEXT_SIZE, 0, 0);		
		// 输出消息
		cout << "parent process receive msg from chile process: " << msgr->mtext;
		// 删除消息队列
		msgctl(msgqueid, IPC_RMID, NULL);
	}
}

上述代码首先定义了一个msgbuffer类型的结构体指针msgp,这个结构体用于存储消息传入消息队列以及存储从消息队列中读取的数据。

接着是创建一个消息队列并获取消息队列的id,然后创建一个子进程。

对于子进程,设置消息类型为1(可以自己任意选择),接着在msgp中存储数据(mtext),同样是使用strcpy函数将两个字符串连接(如果字符串为空,就相当于直接写入,否则就是连接到原有字符串的后面),再使用msgend函数将msgp中的数据写入消息队列,大小是宏定义的(64)。

对于父进程,先等待子进程写完数据,然后使用msgrcv读取数据到msgr中,接着输出msgr中的消息,最后然后消息队列,结束通信。

四、总结

        父进程创建子进程后,父进程和子进程都会继续执行下面的代码,为了区分父子进程,让父子进程执行不同的操作,可以根据fork函数返回的值判断,fork函数会分别在父进程和子进程返回子进程号和0。

        往管道中写入数据使用的函数write和读取数据的read函数传入的参数都是,要写入/读取的文件或设备、写入数据/读取数据 存储的缓冲区地址以及写入/读取的长度,写入数据时,打开写入端filedes[1],往里面写入数据,写入的数据的地址是存储数据变量的指针,如果是字符数组的话,那就是字符数组的变量名,因为字符数组变量名就是字符在缓冲区存放的首地址,如果是整型的话就要用*符号来取指针,读取数据就是打开读取端filedes[0],读取数组存放的缓冲区地址就是我们定义的存储数据的变量的地址,同样使用字符数组的变量名,一般我们不能预先知道管道里有多少的数据,定义一个足够大范围的字符数组用来读取数据,读取的长度就是我们定义字符数组的长度。

        管道中的数据是先进先出的,读取数据时会先读取最先写入的数据,再读取就是依次向后读取,如果管道中没有数据,进程读取时一般会阻塞,等待其他进程往其中写入数据。但是子进程写入数据时不一定是哪个进程先执行,可能需要进程同步或者信号量机制来解决。

        共享内存和消息队列两种通信的方式是类似的,都是使用一个容器来存放数据,一个进程写入,一个进程读取,以次来实现进程之间的通信,不同的是二者使用的容器,共享内存方式使用的是内存,进程只要获取内存地址就能进行读和写操作,而消息队列方式使用的是队列,读和写需要分别使用对应的函数来操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值