Linux进程间通信

进程间通信的概念

进程间通信(IPC)全称 Interprocess communication ,进程通信就是在不同进程间传播或者交换信息
 

进程间通信的目的

数据传输 :一个进程需要将它的数据发送给另一个进程
资源共享 :多个进程之间共享同样的资源。
通知事件 :一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如子进程终止时要通知父进程)。
进程控制 :有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变


进程间通信的本质

让不同进程看到同一块内存,(同一块资源)

在之前的 Linux进程概念中我们谈到 :进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰。,这在一定程度上加大了进程通信的难度。所以,我们通常借助第三方资源来进行数据通信。

进程间通信的分类

管道

匿名管道 pipe
命名管道

System V IPC

System V 消息队列
System V 共享内存
System V 信号量

POSIX IPC

消息队列
共享内存
信号量
互斥量
条件变量
读写锁

管道

什么是管道

我们把从一个进程连接到另一个进程的一个数据流称为管道,我们比较常见的  " | "就是典型的管道。

示例:

who | wc -l# 作用 统计我们当前使用云服务器上的登录用户个数。

Linux进程间通信_第1张图片
 匿名管道

匿名管道的原理

 让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

Linux进程间通信_第2张图片
        pipe函数

Linux进程间通信_第3张图片

参数解释:

pipefd[0] :表示管道读端的文件描述符;pipefd[1] : 表示管道写端的文件描述符       

 匿名管道使用步骤

1. 创建管道(父进程);

Linux进程间通信_第4张图片

2. 创建子进程;Linux进程间通信_第5张图片

3.父进程关闭读端,子进程关闭写端

Linux进程间通信_第6张图片

 管道读写规则

  1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要明确父子进程谁读谁写,然后关闭相应的读写端。
  2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取,(往往需要调用fflush函数刷新缓冲区来观察效果)。

管道的特点

1、管道内部自带同步与互斥机制。(不用担心安全问题)

  • 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
  • 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。为了保护管道资源,内核会对管道操作进行同步与互斥。

2、管道的生命周期随进程。

管道依赖文件系统,只要当打开管道的所有进程退出后,管道文件才会退出

3、管道提供的是流式服务。

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

管道提供的服务是管道文件中有多少就获取多少,(写多少读多少),这种服务属于流式服务

4、管道是半双工通信的。

我们之前提到,管道只能提供单向服务,只能单独“ 读 ” 或者单独 “ 写 ” ,不能同时 “ 读写 ” 。这种通信叫做 半双工通信,与之相对的是全双工通信,即 可以同时读写,这样虽然效率好像高了,但是也要求对临界资源有其他的保护手段。

管道的四种特殊情况

  1.  写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。

上述两种情况内核中可以通过调节同步与互斥来调节这种读写不均的问题。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。

没有数据可以读了,写段关闭后,读端结束读写逻辑去做之后的代码
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
 为什么这种情况不会像第二种一样呢? ---读端已经关闭了,没有操作可以读了,os只能停止写端了,这种很明显是程序异常退出,那这种异常是什么呢。我们通过这个实验来观察

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0){ //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
	return 0;
}
Linux进程间通信_第7张图片

13号信号对应的是 SIGPIPE  信号

我们可以通过  man 7 signal  去查看信号的具体解释

Linux进程间通信_第8张图片

管道的大小

通过上面几种情况的分析,我们不难得出管道是有大小限制的,我们可以通过以下两种方式查看管道大小:

1. ulimit  -a       Linux进程间通信_第9张图片

2.  man  7 pipe

Linux进程间通信_第10张图片

命名管道

命名管道的原理

     如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。   

使用命令创建命名管道

mkfifo fifo

Linux进程间通信_第11张图片  

使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。

while : ; do echo "hello";sleep 1;done > fifo


cat < fifo

Linux进程间通信_第12张图片
 这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,

Linux进程间通信_第13张图片

在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。

 创建一个命名管道

我们通过使用函数 mkfifo函数创建命名管道

函数介绍:

Linux进程间通信_第14张图片

参数解释:

path name:要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

mode:     建命名管道文件的默认权限 

示例:

#include 
#include 
#include 

#define FILE_NAME "myfifo"

int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}

	//create success...

	return 0;
}

    命名管道的打开规则

        1、如果当前打开操作是为读而打开FIFO时。

                O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
                O_NONBLOCK enable:立刻返回成功。
        2、如果当前打开操作是为写而打开FIFO时。

                O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
                O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

 

用命名管道实现serve&client通信

server端

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

#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道


int main()
{
	umask(0); //将文件默认掩码设置为0
	if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
		perror("mkfifo");
		return 1;
	}
	int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 2;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		//从命名管道当中读取信息
		ssize_t s = read(fd, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s] = '\0'; //手动设置'\0',便于输出
			printf("client# %s\n", msg); //输出客户端发来的信息
		}
		else if (s == 0){
			printf("client quit!\n");
			break;
		}
		else{
			printf("read error!\n");
			break;
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

client端

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

#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道


int main()
{
	int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
	if (fd < 0){
		perror("open");
		return 1;
	}
	char msg[128];
	while (1){
		msg[0] = '\0'; //每次读之前将msg清空
		printf("Please Enter# "); //提示客户端输入
		fflush(stdout);
		//从客户端的标准输入流读取信息
		ssize_t s = read(0, msg, sizeof(msg)-1);
		if (s > 0){
			msg[s - 1] = '\0';
			//将信息写入命名管道
			write(fd, msg, strlen(msg));
		}
	}
	close(fd); //通信完毕,关闭命名管道文件
	return 0;
}

 

        命名管道和匿名管道的区别        

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,由open函数打开。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

你可能感兴趣的:(Linux,linux)