Linux应用开发基础 进程间通信篇


简介:这篇文章偏基础,适合初识IPC,里面有几种通信方法的详细函数介绍,每种通信方式通过代码实例解析能更好的理解每种通信方式,文章有点长,还请耐心阅读,一定会对大家有帮助。
关键字:IPC、管道、消息队列、共享内存、信号、信号量(我们下次讲,信号量和我们的其他几种功能不同)、单工、双工。

1.进程间通信简介(IPC)

1.1 简介

什么是IPC呢?为什么会出现IPC呢?这其实就是顺理成章的事情。拿企业举个例子,企业中某一个车间负责整车的某一个部分,这个车间就好比如一个进程,根据任务的不同就有了不同的车间(不同功能的进程)。这些车间总要有沟通吧,不同车间生产的东西跟我们车间生产的东西配合是否正常,这就出现了一些沟通方式。
关于进程的基本知识在之前一篇文章中讲到过,想了解的可以看看
Linux应用开发基础 进程篇

1.2 通信方式

1.管道:无名管道、有名管道。(ps aux | grep “bash”)中间的“|”就相当于一个管道,半双工。
2.消息队列:内核链表,半双工。
3. 信号:系统开销小,单工。
4. 共享内存:全双工。
5. 内存映射:全双工。
6. 套接字:全双工。

1.3 单工、双工

1.3.1 单工

数据传输是单向的。通信双方中,一方固定为发送端,另一方则固定为接收端,信息只能沿一个方向传输。

1.3.2 双工

1.半双工
数据可以沿两个方向传送,但在同一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信。
2.全双工
数据通信允许数据同时在两个方向上传输,相当于两个单工通信方式的结合。它要求发送设备和接收设备都有独立的接收和发送能力。

2. 通信方式

2.1 管道

在内核缓冲区,不占用磁盘,数据只能读一次不能保存。
在这里插入图片描述

2.1.1 无名管道

2.1.1.1特点:

只能通过fork创建通讯。
大小为4096,通过ulimit -a 或者 fpathconf函数查看。
环形队列(FIFO)。
读端和写端对应两个进程。
进程退出管道自动销毁。
管道默认是阻塞的。

2.1.1.2 创建
int pipe(int fd[2])
/*
fd[0]: 读端文件描述符;
fd[1]: 写端文件描述符;
return: 成功0,错误-1。
2.1.1.2.1 实例

无名管道实现 ps aux | grep bash
插个函数介绍

int dup2(int oldfd, int newfd);
/*
文件描述符重定向
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	int pfd;
	pid_t pid;
	char buf[128]= {0};
	char readbuf[128] = {0};
	int fd[2];
	pfd = pipe(fd);
	if(pfd == -1)
	{
		perror("pipe");
		exit(1);
	}
	long size = fpathconf(fd[0], _PC_PIPE_BUF);  //返回fd[0]管道缓冲长度的限制值
	printf("the length of pipe buffer is %ld\n", size);
	printf("fd[0]: %d, fd[1]: %d\n", fd[0], fd[1]);
	pid = fork();
	if(pid == -1)
	{
		perror("fork");
		exit(1);
	}
	if(pid == 0)
	{
		close(fd[0]);
		dup2(fd[1],STDOUT_FILENO);   //重定向标准输出到fd[1]
		system("ps ajx");
		close(fd[1]);
	}
	else if(pid>0)
	{
		close(fd[1]);
		dup2(fd[0],STDIN_FILENO);    //重定向标准输入到fd[0]
		system("grep bash");
		close(fd[0]);
	}
	return 0;
}

在这里插入图片描述

2.1.1.3 管道读写行为
2.1.1.3.1 读操作

1.有数据
read(fd[1]) 正常读,返回读出的字节数
2.无数据
写端被全部关闭,read返回0,相当于读文件到了尾部
没有全部关闭,read阻塞

2.1.1.3.2 写操作

1.读端全部关闭
管道破裂,进程被终止
内核发送SIGPIPE,默认处理动作
2.读端没有全部关闭
缓冲满了,write阻塞
缓冲没满,继续写,直到写满阻塞。

2.1.2 有名管道

无名管道只能亲缘进程间通讯,所以就出现了有名管道。
特点:
管道中有一个伪文件,大小为0 ls -l
其他和无名管道一样
在这里插入图片描述
就是通过这样一个伪文件来通信。

2.1.2.1 创建
int mkfifo(const char filename, mode_t mode); 
/*
filename: 伪问文件名
mode: 文件访问权限
return: 成功0 失败-1
2.1.2.2 实例

同样实现ps ajx | grep bash

2.1.2.2.1 pipe_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char** argv)
{
	if(argc != 3)
	{
		printf("parameter amount anomaly\n");
		exit(1);
	}
	int fiforet;
	int pfd;
	fiforet = mkfifo("mkfifo", 0777);
	pfd = open("mkfifo", O_RDWR);
	if(pfd<0)
	{
		perror("mkfifo");
		exit(1);
	}
	dup2(pfd,STDOUT_FILENO);
	close(pfd);
	execlp(argv[1], argv[1], argv[2],NULL);   //execl函数族,相当于一个新进程,具体的在开头推荐的进程篇中有详细讲解
	return 0;
}

2.1.2.2.2 pipe_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char** argv)
{
	if(argc != 3)
	{
		printf("parameter amount anomaly\n");
		exit(1);
	}
	int fiforet;
	int pfd;
	pfd = open("mkfifo", O_RDWR);
	if(pfd<0)
	{
		perror("mkfifo");
		exit(1);
	}
	dup2(pfd, STDIN_FILENO);
	execlp(argv[1], argv[1], argv[2],NULL);  
	return 0;
}

这里出现了一个问题,终端输出之后,read读取没有退出,grep bash 一直在处于S+状态,搞了好久没解决,如果有知道的请解释一下,如何解决。
在这里插入图片描述

2.2 消息队列

消息对列是内核的链表,存放在内核中。
ipcs 查看各种关于IPC相关的信息
在这里插入图片描述

2.2.1 特点

1.消息是面向记录的,其中消息具有特定的格式和优先级。
2.消息队列独立于发送和接收进程,进程终止,消息队列仍然存在。
3.消息队列可以实现随机查询,不一定要按照先进先出的顺序,可以按照消息类型读取。

2.2.2 函数介绍

2.2.2.1 msgget函数
int msgget(key_t key, int msgflg); 
//创建或打开消息队列, 
参数: 
key:和消息队列关联的key值 
msgflg:是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或
操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被
忽略,而只返回一个标识符。 
返回值:成功返回队列ID,失败则返回‐1
2.2.2.2 msgsnd函数
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 
//读取消息,成功返回消息数据的长度,失败返回‐1 
参数: 
msgid:消息队列的ID 
msgp:指向消息的指针,常用结构体msgbuf如下: 
struct msgbuf 
{ 
long mtype;  //消息类型 
char mtext[N]; //消息正文 
} 
size:发送的消息正文你的字节数 
flag: 
	IPC_NOWAIT 消息没有发送完成函数也会立即返回 
    0:知道发送完成函数才返回 
 返回值: 
         成功:0 
         失败:‐1 
2.2.2.3 msgrcv函数
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); 
//从一个消息队列中获取消息 
参数: 
         msgid:消息队列的ID 
         msgp:要接收消息的缓冲区 
         size:要接收的消息的字节数 
        	msgtype: 具有优先级性质
                   0:接收消息队列中第一个消息 
                   大于0:接收消息队列中第一个类型为msgtyp的消息 
                   小于0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。
         flag:        
                  0:若无消息函数一直阻塞 
                  IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG。 
返回值: 
         成功:接收到的消息i长度 
         出错:‐1 
2.2.2.4 msgctl函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf); 
//控制消息队列,成功返回0,失败返回‐1 
参数: 
        msqid:消息队列的队列ID 
        cmd: 
                 IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆
盖msgid_ds的值。 
                 IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值 
                 IPC_RMID:删除消息队列 
     
         buf:是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构 
          
返回值: 
        成功:0 
        失败:‐1 
2.2.2.5 ftok函数
key_t ftok( char * fname, int id ) 
//系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。 
参数: 
fname就时你指定的文件名(该文件必须是存在而且可以访问的)。 
id是子序号, 虽然为int,但是只有8个比特被使用(0255)。 
返回值: 
当成功执行的时候,一个key_t值将会被返回,否则 ‐1 被返回。

2.2.3 实例

2.2.3.1 msg_server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>

typedef struct msq{
	long mtype;     
	char mtext[128];
} msg;
int main()
{	
	msg sendmsg, recvmsg;
	key_t key;
	int msgid;
	pid_t pid;
	//1.build IPC
	key = ftok("a.c", 2);   //获取IPC通信key值
	if(key==-1)
	{
		perror("ftok");
		exit(1);
	}
	printf("the key is %d\n", key);
	//2.build msg_queue
	msgid = msgget(key, IPC_CREAT|0755);  
	if(msgid==-1)
	{
		perror("msgget");
		exit(1);
	}
	system("ipcs -q");
	//3.fork
	pid = fork();
	if(pid==-1)
	{
		perror("fork");
		exit(1);
	}
	sendmsg.mtype = 200;   //消息类型200,server读,client写
	//3.1.child process 
	if(pid==0)
	{
		//msgrcv
		while(1)
		{
			memset(recvmsg.mtext,0,128);
			if(msgrcv(msgid, (void*)&recvmsg, 128, 100, 0) == -1)   //server从消息类型100中读
			{
				perror("msgrcv");
				exit(1);
			}
			printf("THE SERVER RECEIVES SUCCESSFULLY, %s\n", recvmsg.mtext);
		}
	}
	//3.2.father process
	else if(pid>0)
	{

		while(1)
		{
			memset(sendmsg.mtext, 0, 128);
			//msgsnd
			printf("PLEASE INPUT YOUR MESSAGE:\n");
			if(fgets(sendmsg.mtext, 128, stdin) == NULL)
			{
				printf("the msg-text fails to require from stdin\n");
				exit(1);
			}
			if(msgsnd(msgid, (void*)&sendmsg, strlen(sendmsg.mtext), 0)==-1)
			{
				perror("msgsnd");
				exit(1);
			}
		}
	}
	return 0;
}

2.2.3.2 msg_client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>

typedef struct msq{
	long mtype;
	char mtext[128];
} msg;
int main()
{	
	msg sendmsg, recvmsg;
	key_t key;
	int msgid;
	pid_t pid;
	//1.build IPC
	key = ftok("a.c", 2);
	if(key==-1)
	{
		perror("ftok");
		exit(1);
	}
	printf("the key is %d\n", key);
	//2.build msg_queue
	msgid = msgget(key, IPC_CREAT|0755);
	if(msgid==-1)
	{
		perror("msgget");
		exit(1);
	}
	system("ipcs -q");
	//3.fork
	pid = fork();
	if(pid==-1)
	{
		perror("fork");
		exit(1);
	}
	sendmsg.mtype = 100;  //消息类型100,client写,server读
	//3.1.child process 
	if(pid==0)
	{
		//msgrcv
		while(1)
		{
			memset(recvmsg.mtext,0,128);
			if(msgrcv(msgid, (void*)&recvmsg, 128, 200, 0) == -1)   //client从200中读
			{
				printf("cuole\n");
				perror("msgrcv");
				exit(1);
			}
			printf("THE SERVER RECEIVES SUCCESSFULLY, %s\n", recvmsg.mtext);
		}
	}
	//3.2.father process
	else if(pid>0)
	{

		while(1)
		{
			memset(sendmsg.mtext,0,128);
			//memset(sendmsg.mtext, 0, 128);
			//msgsnd
			printf("PLEASE INPUT YOUR MESSAGE:\n");
			if(fgets(sendmsg.mtext, 128, stdin) == NULL)
			{
				printf("the msg-text fails to require from stdin\n");
				exit(1);
			}
			if(msgsnd(msgid, (void*)&sendmsg, strlen(sendmsg.mtext), 0)==-1)
			{
				perror("msgsnd");
				exit(1);
				
			}
		}
	}
	return 0;
}

终端输出
在这里插入图片描述

2.3 共享内存

2.3.1 特点

1.共享内存(shared memory)是进程间共享和交换数据最快捷的方式,无需通过系统调用与内核交互,但是缺点很明显,多个进程同时操作一个数数据,如果不通过有效的管理,会发生数据紊乱,问题很严重。
2.消息队列的的数据能被多次读取。

2.3.2 函数介绍

2.3.2.1 shmget函数
1.int shmget(key_t key, size_t size, int shmflg); 
//用来获取或创建共享内存 
参数: 
key:IPC_PRIVATE 或 ftok的返回值 
size:共享内存区大小 
shmflg:同open函数的权限位,也可以用8进制表示法 
返回值: 
成功:共享内存段标识符‐‐‐ID‐‐‐文件描述符 
出错:‐1 
2.3.2.2 shmat函数
2.void *shmat(int shm_id, const void *shm_addr, int shmflg); 
//把共享内存连接映射到当前进程的地址空间 
参数: 
shm_id:ID号 
shm_addr:映射到的地址,NULL为系统自动完成的映射 
shmflg: 
SHM_RDONLY共享内存只读 
默认是0,表示共享内存可读写 
返回值: 
成功:映射后的地址 
失败:NULL 
2.3.2.3 shmdt函数
3.int shmdt(const void *shmaddr); 
//将进程里的地址映射删除 
参数: 
shmid:要操作的共享内存标识符 
返回值:  
成功:0 
出错:‐1 
2.3.2.4 shmctl函数
4.int shmctl(int shm_id, int command, struct shmid_ds *buf); 
//删除共享内存对象 
参数: 
shmid:要操作的共享内存标识符 
cmd : 
IPC_STAT  (获取对象属性)‐‐‐  实现了命令ipcs ‐m 
IPC_SET (设置对象属性) 
IPC_RMID (删除对象)   ‐‐‐实现了命令ipcrm ‐m 
buf :指定IPC_STAT/IPC_SET时用以保存/设置属性 
返回值: 
成功:0 
出错:‐1 

2.3.3 实例

2.3.3.1 shm_wirte.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
	int shmid;
	key_t key;
	char *shmp;
	key = ftok("a.c", 1);
	if(key ==-1)
	{
		perror("ftok");
		exit(1);
	}
	printf("the key is %d\n", key);
	shmid = shmget(key,128,IPC_CREAT|0777);
	if(shmid==-1)
	{
		perror("shmget");
		exit(1);
	}
	printf("shmid:%d\n", shmid);
	shmp = (char*)shmat(shmid, NULL, 0);
	while(1)
	{
		printf("PLEASE INPUT\n");
		fgets(shmp, 128, stdin);
		if(!strcmp(shmp, "exit\n")) break; 
	}
	shmdt(shmp);   
	printf("write port has exited\n");
	return 0;
}

2.3.3.2 shm_read.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
	key_t key;
	int shmid;
	char *shmp;
	key = ftok("a.c", 1);
	if(key<0)
	{
		perror("ftok");
		exit(1);
	}
	printf("the key is %d\n", key);
	shmid = shmget(key, 128, IPC_CREAT|0777);
	if(shmid<0)
	{
		perror("shmget");
		exit(1);
	}
	printf("shmid:%d\n", shmid);
	shmp = (char*)shmat(shmid, NULL, 0);
	while(1)
	{
		if(strlen(shmp)==0) continue;
		if(!strcmp(shmp,"exit\n")) break;
		printf("the shmp content is %s, length: %ld\n", shmp, strlen(shmp));
		memset(shmp,0,128);
	}
	shmdt(shmp);
	shmctl(shmid, IPC_RMID,NULL);
	printf("read port has exited\n");
	return 0;
}

终端输出
这里存在一个问题,为什么输入的长度比输出大1,因为fgets函数的性质,fgets函数将回车符也写入到了输入数据中,所以才会如此。
在这里插入图片描述
这里可能会下面这种功能上的bug,大家想想当一个共享内存有多个进程进行读写操作的时候,这些进程没有进行先后顺序的管理会发生什么,下面这种就是例子,写端还没有完全写进内存,数据就已经开始读了,这中情况不是我们想要的结果,所以我们要限制他们,写完之后才能重新读,读完之后才能重新写。
这篇文章会写解决方案:
Linux信号量PV操作
在这里插入图片描述

2.4 信号

2.4.1 特点

1.只能由内核发送信号,用户程序不能发送信号,用户程序可以使用一些函数通过告知内核发送什么信号。
2.小信息,系统开销小,单工。
通过 kill -l 查看内核能够发送多少种信号

2.4.2 函数介绍

信号的发送(发送信号进程):kill、raise、alarm
信号的处理(自定义信号处理方式) :signal
SIGKILL, SIGSTOP这两个信号进程捕获不到,这是强制执行的。

2.4.2.1 kill函数
#include<signal.h> 
#include<sys/types.h> 
函数原型:int kill(pid_t pid, int sig); 
参数: 
函数传入值:pid 
正数:要接收信号的进程的进程号 
0:信号被发送到所有和pid进程在同一个进程组的进程 ‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外) 
sig:信号 
函数返回值:成功  0  出错  ‐1  
2.4.2.1.1 实例
2.4.2.1.1.1 kill.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char **argv)
{
	if(argc!=3)
	{
		printf("argument amount anomaly\n");
		exit(1);
	}
	int pid = atoi(argv[2]);
	int sig = atoi(argv[1]);	
	int ret = kill(pid, sig);
	if(ret == -1)
	{
		perror("kill");
		exit(1);
	}
	printf("the %d process is killed\n", pid);
	return 0;
}

2.4.2.1.1.2 demo.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	printf("the pid of process is %d\n",getpid());
	pause();
	return 0;
}

终端输出
在这里插入图片描述
demo.c中使用了pause()函数但是进程状态不是字面意思上的暂停状态,而是睡眠状态。
在这里插入图片描述

2.4.2.2 raise函数

相当于kill(getpid(),signame) 给自己发送信号。

所需头文件: 
#include<signal.h> 
#include<sys/types.h> 
函数原型: 
int  raise(int sig); 
参数: 
函数传入值:sig:信号 
函数返回值: 
成功  0  出错  ‐1 
2.4.2.2.1 实例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
	printf("the pid of process is %d\n",getpid());
	raise(SIGKILL);
	while(1);
	return 0;
}

终端输出
在这里插入图片描述

2.4.2.3 alarm函数
所需头文件#include  <unistd.h> 
函数原型 unsigned int  alarm(unsigned int seconds) 
参数: 
返回值: 
出错:‐1 
seconds:指定秒数 
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则 返回上一个闹钟时间的剩余时间,否则返回0
2.4.2.3.1 实例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
	printf("the pid of process is %d\n",getpid());
	alarm(5);
	for(int i=0;i<10;i++)
	{
		printf("-------%d-------\n",i);
		sleep(1);
	}
	return 0;
}

终端输出
在这里插入图片描述

2.4.2.4 signal函数
所需头文件  #include  <signal.h>   
函数原型  void (*signal(int signum, void (*handler)(int)))(int);   
函数传入值   
           signum:指定信号    
           handler  
                   SIG_IGN:忽略该信号。 
                   SIG_DFL:采用系统默认方式处理信号 
                   自定义的信号处理函数指针   
函数返回值   
           成功:设置之前的信号处理方式     
           出错:‐1 
 
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函
数;这个函数的返回值是一个函数指针。 
2.4.2.4.1 实例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>


void func(int signum)
{
	printf("hello world, signum:%d\n",signum);
}

int main()
{
	pid_t pid;
	pid = fork();
	signal(SIGCHLD,func);
	if(pid<0)
	{
		perror("fork");
		exit(1);
	}
	if(pid==0)
	{
		int i = 0;
		while(i<5)
		{
			sleep(1);
			printf("-----%d------\n",i++);
		}
	}
	else if(pid>0)
	{
		printf("the child pid:%d, the father pid:%d\n",pid,getpid());
		wait(NULL);
	}
}

终端输出
在这里插入图片描述

3.总结

这篇文章是对进程间常见的几种通信方式的介绍,管道、消息队列、共享内存、信号、信号量,信号量会在另一篇文章中通过解决共享内存出现的bug让大家对信号量能有更深刻的认识,文章写太长,容易感到枯燥,希望这些知识对大家有帮助。
文章如有错误请各位指出,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值