为什么需要进程间通信

使用进程间通信来实现数据共享,也就是说进程间通信的目的,就是为了实现进程间数据共享的。

进程间通信的原理

尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。

既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的转发。

因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数据的转发,达到数据共享的目的。

Linux提供的“进程通信”方式

				1)信号
						信号其实也是进程间通信的一种,只不过信号是非精确通信,而本章讲的IPC是精确通信。
					所谓精确通信,就是能告诉你详细信息,而信号这种非精确通信,只能通知某件事情发生了,但是无法
					告诉详细信息。
						
						
				2)本章的进程间通信
					(a)管道
							· 无名管道
							· 有名管道
								
							OS在进程之间建立一个“管道”,通过这个管道来实现进程间数据的交换。
								
					(b)system V IPC
							· 消息队列:通过消息队列来通信
							· 共享内存:通过共享内存来通信
							· 信号量:借助通信来实现资源的保护(一种加锁机制)
							

				3)域套接字
						

无名管道

无名管道的通信原理

具体来说就是,内核会开辟一个空间,通信的进程通过共享这个空间,从而实现通信。

无名管道API

#include <unistd.h>
int pipe(int pipefd[2]);

它的功能就是创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符关联起来。

为什么无名管道只能用于亲缘进程之间通信

由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,从而实现通信。

另外:读管道时,如果没有数据的话,读操作会休眠(阻塞)

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int main(void)
{
	int ret = 0;
	//[0]:读文件描述符
	//[1]:写文件描述符
	int pipefd[2] = {0};//用于存放管道的读写文件描述符
	
	ret = pipe(pipefd);
	if(ret == -1) print_err("pipe fail");

	ret = fork();
	if(ret > 0)
	{	
		//signal(SIGPIPE, SIG_IGN);
		//close(pipefd[0]);	
		while(1)
		{
			write(pipefd[1], "hello", 5);				
			sleep(1);
		}
	}
	else if(ret == 0)
	{
//		close(pipefd[1]);
//  	close(pipefd[0]);
		while(1)
		{
			char buf[30] = {0};
			bzero(buf, sizeof(buf));
			read(pipefd[0], buf, sizeof(buf));
			printf("child, recv data:%s\n", buf);
		}	
	}

	return 0;
}

父子进程双向通信Demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>

void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int main(void)
{
	int ret = 0;
	//[0]:读文件描述符
	//[1]:写文件描述符
	int pipefd1[2] = {0};//用于存放管道的读写文件描述符
	int pipefd2[2] = {0};//用于存放管道的读写文件描述符
	
	ret = pipe(pipefd1);
	if(ret == -1) print_err("pipe fail");
	ret = pipe(pipefd2);
	if(ret == -1) print_err("pipe fail");

	ret = fork();
	if(ret > 0)
	{	
		close(pipefd1[0]);
		close(pipefd2[1]);
		char buf[30] = {0};
		while(1)
		{
			write(pipefd1[1], "hello", 5);				
			sleep(1);

			bzero(buf, sizeof(buf));
			read(pipefd2[0], buf, sizeof(buf));
			printf("parent, recv data:%s\n", buf);
		}
	}
	else if(ret == 0)
	{
		close(pipefd1[1]);
		close(pipefd2[0]);
		char buf[30] = {0};
		while(1)
		{
			sleep(1);	
			write(pipefd2[1], "world", 5);

			bzero(buf, sizeof(buf));
			read(pipefd1[0], buf, sizeof(buf));
			printf("child, recv data:%s\n", buf);
		}	
	}

	return 0;
}

有名管道

不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以读写文件的形式来操作的。

因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道一样,必须在通过继承的方式才能获取到文件描述符。

所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。

demo1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>


#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"

void print_err(char *estr)
{	
	perror(estr);
	exit(-1);
}	

int creat_open_fifo(char *fifoname, int open_mode)
{	
	int ret = -1;
	int fd = -1;	
	
	ret = mkfifo(fifoname, 0664);
	//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
	if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");	
	
	fd = open(fifoname, open_mode);
	if(fd == -1) print_err("open fail");

	return fd;
}

void signal_fun(int signo)
{
	//unlink();
	remove(FIFONAME1);
	remove(FIFONAME2);
	exit(-1);
}
		
int main(void)
{
	char buf[100] = {0};
	int ret = -1;
	int fd1 = -1;
	int fd2 = -1;


	fd1 = creat_open_fifo(FIFONAME1, O_WRONLY);
	fd2 = creat_open_fifo(FIFONAME2, O_RDONLY);

	ret = fork();
	if(ret > 0)
	{
		signal(SIGINT, signal_fun);
		while(1)
		{
			bzero(buf, sizeof(buf));
			scanf("%s", buf);
			write(fd1, buf, sizeof(buf));	
		}
	}
	else if(ret == 0)
	{
		while(1)
		{
			bzero(buf, sizeof(buf));
			read(fd2, buf, sizeof(buf));
			printf("%s\n", buf);
		}
	}

	return 0;
}	

demo2

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>


#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"

void print_err(char *estr)
{	
	perror(estr);
	exit(-1);
}	

int creat_open_fifo(char *fifoname, int open_mode)
{	
	int ret = -1;
	int fd = -1;	
	
	ret = mkfifo(fifoname, 0664);
	//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
	if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");	
	
	fd = open(fifoname, open_mode);
	if(fd == -1) print_err("open fail");

	return fd;
}

void signal_fun(int signo)
{
	//unlink();
	remove(FIFONAME1);
	remove(FIFONAME2);
	exit(-1);
}
		
int main(void)
{
	char buf[100] = {0};
	int ret = -1;
	int fd1 = -1;
	int fd2 = -1;


	fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
	fd2 = creat_open_fifo(FIFONAME2, O_WRONLY);


	ret = fork();
	if(ret > 0)
	{
		signal(SIGINT, signal_fun);
		while(1)
		{
			bzero(buf, sizeof(buf));
			read(fd1, buf, sizeof(buf));
			printf("recv:%s\n", buf);
		}
	}
	else if(ret == 0)
	{
		while(1)
		{	
			bzero(buf, sizeof(buf));
			scanf("%s", buf);
			write(fd2, buf, sizeof(buf));
			
		}
	}

	return 0;
}	

消息队列

消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为了消息队列。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。

收发数据的过程

					1)发送消息
					
						(a)进程先封装一个消息包
						
									这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文
								写到结构体的成员中。
									struct msgbuf
									{
												long mtype;         /* 放消息编号,必须> 0 */
												char mtext[msgsz];  /* 消息内容(消息正文) */
									};	
								
						(b)调用相应的API发送消息
						
									调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列,消息包
								(存放消息的结构体变量)会被作为一个链表节点插入链表。
								
								
					2)接收消息
					
						调用API接收消息时,必须传递两个重要的信息,
						(a)消息队列标识符
								
						(b)你要接收消息的编号
						
								有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号的消息,
							如此就收到了别人所发送的信息。
					
						
					“消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公告牌上
				去取对应编号的消息,如此,发送者和接受者之间就实现了通信。

使用demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <signal.h>

#define MSG_FILE "./msgfile"

#define MSG_SIZE 1024

struct msgbuf
{
	long mtype;         /* 放消息编号,必须 > 0 */
	char mtext[MSG_SIZE];  /* 消息内容(消息正文) */
};									


void print_err(char *estr)
{
	perror(estr);
	exit(-1);
}

int creat_or_get_msgque(void)
{
	int msgid = -1;
	key_t key = -1;
	int fd = 0;

	/* 创建一个消息队列的专用文件,ftok会用到这个文件的路径名 */
	fd = open(MSG_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	/* 利用存在的文件路径名和8位整形数,计算出key */
	key = ftok(MSG_FILE, 'a');
	if(key == -1) print_err("ftok fail");

	/* 利用key创建、或者获取消息队列 */
	msgid = msgget(key, 0664|IPC_CREAT);
	if(msgid == -1) print_err("msgget fail");

	return msgid;
}	

int msgid = -1;
void signal_fun(int signo)
{
	msgctl(msgid, IPC_RMID, NULL);
	remove(MSG_FILE);	
	
	exit(-1);
}

int main(int argc, char **argv)
{	
	int ret = -1;
	long recv_msgtype = 0;
	
	if(argc !=  2)
	{
		printf("./a.out recv_msgtype\n");
		exit(-1);
	}
	recv_msgtype = atol(argv[1]);
	
	
	msgid = creat_or_get_msgque();

	ret = fork();
	if(ret > 0) //发送消息
	{
		signal(SIGINT, signal_fun);
		struct msgbuf msg_buf = {0};
		while(1)
		{
			bzero(&msg_buf, sizeof(msg_buf));
			/* 封装消息包 */
			scanf("%s", msg_buf.mtext);
			printf("input snd_msgtype:\n");
			scanf("%ld", &msg_buf.mtype);

			/* 发送消息包 */
			msgsnd(msgid, &msg_buf, MSG_SIZE, 0);	
		}
	}
	else if(ret == 0)//接收消息
	{
		struct msgbuf msg_buf = {0};
		int ret = 0;
		while(1)
		{
			bzero(&msg_buf, sizeof(msg_buf));
			ret = msgrcv(msgid, &msg_buf, MSG_SIZE, recv_msgtype, 0);
			if(ret > 0) 
			{
				printf("%s\n", msg_buf.mtext);
			}	
		}
	}
	
		
	return 0;
}

使用方式

root@snappyjack-VirtualBox:/home/morty# ./a.out 1
aaaaa
input snd_msgtype:
2
xxxxxxx

另一个窗口

root@snappyjack-VirtualBox:/home/morty# ./a.out 2
aaaaa
xxxxxxx
input snd_msgtype:
1

共享内存

共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。

不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。

对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,效率会非常的低。

共享内存demo

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>



#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;	



void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");

	//write(fd, &shmid, sizeof(shmid));
}

char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};

void signal_fun(int signo)
{
	shmdt(shmaddr);
	shmctl(shmid, IPC_RMID, NULL);
	remove("./fifo");
	remove(SHM_FILE);
	
	exit(-1);	
}

int get_peer_PID(void)
{
	int ret = -1;
	int fifofd = -1;

	/* 创建有名管道文件 */
        ret = mkfifo("./fifo", 0664); 
        if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
	
	/* 以只读方式打开管道 */
	fifofd = open("./fifo", O_RDONLY);
        if(fifofd == -1) print_err("open fifo fail");

	/* 读管道,获取“读共享内存进程”的PID */
        int peer_pid;
        ret = read(fifofd, &peer_pid, sizeof(peer_pid));
        if(ret == -1) print_err("read fifo fail");

	return peer_pid; 
}

int main(void)
{
	int peer_pid = -1;

	/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
	signal(SIGINT, signal_fun);


	/* 使用有名管道获取读共享内存进程的PID */
	peer_pid = get_peer_PID();
	
	
	/* 创建、或者获取共享内存 */
	create_or_get_shm();
	
	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	
	
	while(1)
	{
		memcpy(shmaddr, buf, sizeof(buf));
		kill(peer_pid, SIGUSR1);
		sleep(1);
		
	}
	
	return 0;
}

另一个窗口运行

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>


#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;	



void print_err(char *estr)
{
	perror(estr);	
	exit(-1);
}

void create_or_get_shm(void)
{
	int fd = 0;
	key_t key = -1;	

	fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
	if(fd == -1) print_err("open fail");
	
	key = ftok(SHM_FILE, 'b');
	if(key == -1) print_err("ftok fail");
	
	shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
	if(shmid == -1) print_err("shmget fail");
	
	//read(fd, &shmid, sizeof(shmid));
}

void signal_fun(int signo)
{
	if(SIGINT == signo)
	{
		shmdt(shmaddr);
		shmctl(shmid, IPC_RMID, NULL);
		remove("./fifo");
		remove(SHM_FILE);

		exit(-1);
	}
	else if(SIGUSR1 == signo)
	{
		
	}
}

void snd_self_PID(void)
{
	int ret = -1;
	int fifofd = -1;

	/* 创建有名管道文件 */
	mkfifo("./fifo", 0664); 
	if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
	
	/* 以只写方式打开文件 */
	fifofd = open("./fifo", O_WRONLY);
	if(fifofd == -1) print_err("open fifo fail");
	
	/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
	int pid = getpid();
	ret = write(fifofd, &pid, sizeof(pid));//发送PID
	if(ret == -1) print_err("write fifo fail");
}

int main(void)
{

	/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
	signal(SIGUSR1, signal_fun);
	signal(SIGINT, signal_fun);

	/* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
	snd_self_PID();	


	/* 创建、或者获取共享内存 */
	create_or_get_shm();


	//建立映射
	shmaddr = shmat(shmid, NULL, 0);
	if(shmaddr == (void *)-1) print_err("shmat fail");	
	
	while(1)
	{
		pause();
		printf("%s\n", (char *)shmaddr);
		bzero(shmaddr, SHM_SIZE);
	}
	
	return 0;
}

直接运行就完事了