Linux系统编程06---进程间通信

作者介绍

张伟伟,男,西安工程大学电子信息学院,2019级硕士研究生,张宏伟人工智能课题组。
微信公众号:可随时查阅,搜索—张二牛的笔记,内容会分类上传。
研究方向:机器视觉与人工智能。
电子邮件:[email protected]
电子邮件:[email protected]

学习目标

熟练使用pipe进行父子进程间通信
熟练使用pipe进行兄弟进程间通信

熟练使用fifo进行无血缘关系的进程间通信

使用mmap进行有血缘关系的进程间通信
使用mmap进行无血缘关系的进程间通信

1. 进程间通信(IPC)的相关概念(inter process communicateon)

Linux进程地址空间相互独立,每个进程各自有不同的用户地址空间,无法使用全局变量进行通信,数据不共享,进程与进程间要交换数据必须通过内核,在内核中开辟一块缓冲区,一个进程将数据写入内核,然后另一个进程从内核读走数据。
在这里插入图片描述
通信方式:

文件、=管道信号共享内存、消息队列、套接字、命名管道等

常用的通信方式

管道(使用最简单)
信号(开销最小)
共享映射区(无血缘关系)
本地套接字(最稳定)

2.管道(pipe函数)—管道内核缓冲区

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。
在这里插入图片描述

2.1 管道的特性

(1) 管道的本质是一块内核缓冲区
(2) 由两个文件描述符引用,一个表示读端,一个表示写端。
(3) 规定数据从管道的写端流入管道,从读端流出。
(4) 当两个进程都终结的时候,管道也自动消失。
(5) 管道的读端和写端默认都是阻塞的

2.2 管道的原理

管道的实质是内核缓冲区,内部使用环形队列实现。
默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
实际操作过程中缓冲区会根据数据压力做适当调整
只能在有血缘关系的进程中使用

2.3 管道的局限性

数据一旦被读走,便不在管道中存在,不可反复读取

数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道

只能在有血缘关系的进程间使用管道

3 创建管道—pipe函数(在fork之前)

	函数作用:
创建一个管道
	函数原型:
int pipe(int fd[2]);
	函数参数:
若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端
	返回值:
	成功返回0;
	失败返回-1,并设置errno值。

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信。
父子进程间具有相同的文件描述符,且指向同一个管道pipe
在这里插入图片描述
一个进程可以使用管道通信,自己读自己写。

3.1 实例:利用pipe父子进程间通信

父进程写,子进程读并且打印

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include<sys/types.h>
#include <unistd.h>
#include<wait.h>

int main()
{
    
    

	//创建管道,同于进程间通信
	//int pipe(int pipefd[2]);
	int fd[2];
	int ret = pipe(fd);
	if(ret < 0 )
	{
    
    
		perror("pipe erro");
	}	
	//创建进程
	pid_t pid = fork();
	if(pid < 0)		
	{
    
    
		perror("fork error");
	}
	else if(pid > 0)
	{
    
    
		close(fd[0]);
		sleep(5);
		write(fd[1],"hello world",strlen("hello world"));
		wait(NULL);
	}
	else
	{
    
    
		//关闭写端
		close(fd[1]);
		
		char buf[1024];
		memset(buf,0x00,sizeof(buf));
		int n = read(fd[0],buf,sizeof(buf)); //阻塞
		printf("read over,n = [%d],buf = [%s]", n , buf);
	}
	return 0;
}

3.2 实例:父子进程间通信, 实现ps aux | grep bash

使用execlp函数和dup2函数,ps为命令,aux为参数

在这里插入图片描述

3.3 管道的读写行为

	读操作
	有数据
read正常读,返回读出的字节数
	无数据
	写端全部关闭
read解除阻塞,返回0, 相当于读文件读到了尾部
	没有全部关闭
read阻塞
	写操作
	读端全部关闭
管道破裂,进程终止, 内核给当前进程发SIGPIPE信号
	读端没全部关闭
	缓冲区写满了
write阻塞
	缓冲区没有满
继续write

3.4 如何设置管道为非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参
考下列三个步骤进行:
第1步: int flags = fcntl(fd[0], F_GETFL, 0);2步: flag |= O_NONBLOCK;3步: fcntl(fd[0], F_SETFL, flags);
若是读端设置为非阻塞:
	写端没有关闭,管道中没有数据可读,则read返回-1;
	写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
	写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
	写端已经关闭,管道中没有数据可读,则read返回0

3.5 获取管道缓冲区大小

ulimit -a
	函数 (一般用不到)
long fpathconf(int fd, int name);
printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

4. FIFO

4.1 FIFO介绍

FIFO称为命名管道,以区分管道。pipe只能用于有血缘关系的进程间进行通信,而FIFO可以在非血缘关系的文件间进行通信。仅仅用来标识内核中一条通道,操作该描述符就可以了
在这里插入图片描述

4.2 mkfifo函数

方式1-使用命令 mkfifo
	命令格式: mkfifo 管道名
	例如:mkfifo myfifo
	
方式2-使用函数
	int mkfifo(const char *pathname, mode_t mode);
	参数说明和返回值可以查看man 3 mkfifo

4.3 FIFO完成两个进程通信的思路

进程A:
	创建一个fifo文件:myfifo
	调用open函数打开myfifo文件
	调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区)
	调用close函数关闭myfifo文件
进程B:
	调用open函数打开myfifo文件
	调用read函数读取文件内容(其实就是从内核中读取数据)
	打印显示读取的内容
	调用close函数关闭myfifo文件

》》》进程A比进程B先启动

4.3.1 fifo_write.c

//fifo完成两个进程间通信的测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    
    
	//创建fifo文件
	//int mkfifo(const char *pathname, mode_t mode);
	int ret = access("./myfifo", F_OK);
	if(ret!=0)
	{
    
    		
		ret = mkfifo("./myfifo", 0777);
		if(ret<0)
		{
    
    
			perror("mkfifo error");
			return -1;
		}
	}

	//打开文件
	int fd = open("./myfifo", O_RDWR);
	if(fd<0)
	{
    
    
		perror("open error");
		return -1;
	}

	//写fifo文件
	int i = 0;
	char buf[64];
	while(1)
	{
    
    
		memset(buf, 0x00, sizeof(buf));
		sprintf(buf, "%d:%s", i, "hello world");
		write(fd, buf, strlen(buf));
		sleep(1);

		i++;
	}

	//关闭文件
	close(fd);

	//getchar();

	return 0;
}

4.3.2 fifo_read.c

//fifo完成两个进程间通信的测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    
    
	//创建fifo文件
	//int mkfifo(const char *pathname, mode_t mode);
	//判断myfofo文件是否存在,若不存在则创建
	int ret = access("./myfifo", F_OK);
	if(ret!=0)
	{
    
    
		ret = mkfifo("./myfifo", 0777);
		if(ret<0)
		{
    
    
			perror("mkfifo error");
			return -1;
		}
	}

	//打开文件
	int fd = open("./myfifo", O_RDWR);
	if(fd<0)
	{
    
    
		perror("open error");
		return -1;
	}

	//读fifo文件
	int n;
	char buf[64];
	while(1)
	{
    
    
		memset(buf, 0x00, sizeof(buf));
		n = read(fd, buf, sizeof(buf));
		printf("n==[%d], buf==[%s]\n", n, buf);
	}

	//关闭文件
	close(fd);

	//getchar();

	return 0;
}

4.3.3 判断文件是否存在,然后再创建

代码补充:
int access 函数,判断文件是否存在
在这里插入图片描述

5 mmap完成两个进程通信的思路(读取内存)

存储映射I/O (Memory-mapped I/O)将磁盘与缓冲区相互映射。
可以通过mmap函数来实现。(直接操作内存较快),操作缓冲区,就相当于操作文件,使用memset,memcpy可以实现数读取。
在这里插入图片描述

5.1 mmap函数

函数作用:
建立存储映射区
	函数原型
   void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
	函数返回值:
	成功:返回创建的映射区首地址;
	失败:MAP_FAILED
	参数:	
	addr: 	指定映射的起始地址, 通常设为NULL, 由系统指定
	length:映射到内存的文件长度 使用lseek或者stat函数
	prot:	映射区的保护方式, 最常用的:
	读:PROT_READ
	写:PROT_WRITE
	读写:PROT_READ | PROT_WRITE

	flags:	映射区的特性, 可以是(重要)
	MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
	MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。

	fd:fd=open() 代表要映射的文件。
	offset:通常为0, 表示从文件头开始映射。以文件开始处的偏移量, 必须是4k的整数倍, 

5.1.1 使用mmap函数完成两个不相干进程间通信(read.c和write.c)

read.c

//使用mmap函数完成两个不相干进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main()
{
    
    
	//使用mmap函数建立共享映射区
	//void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
	int fd = open("./test.log", O_RDWR);
	if(fd<0)
	{
    
    
		perror("open error");
		return -1;
	}

	int len = lseek(fd, 0, SEEK_END);

	//建立共享映射区
	void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	if(addr==MAP_FAILED)
	{
    
    
		perror("mmap error");
		return -1;
	}

	char buf[64];
	memset(buf, 0x00, sizeof(buf));
	memcpy(buf, addr, 10);
	printf("buf=[%s]\n", buf);

	return 0;
}

write.c

//使用mmap函数完成两个不相干进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main()
{
    
    
	//使用mmap函数建立共享映射区
	//void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
	int fd = open("./test.log", O_RDWR);
	if(fd<0)
	{
    
    
		perror("open error");
		return -1;
	}

	int len = lseek(fd, 0, SEEK_END);

	//建立共享映射区
	void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	if(addr==MAP_FAILED)
	{
    
    
		perror("mmap error");
		return -1;
	}
	
	memcpy(addr, "0123456789", 10);

	return 0;
}

5.2 munmap(释放mmap内存缓冲区地址)

函数作用:
释放由mmap函数建立的存储映射区
函数原型:
int munmap(void *addr, size_t length);

返回值:
	成功:返回0
	失败:返回-1,设置errno值
函数参数:
	addr:调用mmap函数成功返回的映射区首地址
	length:映射区大小(mmap函数的第二个参数)

5.3 mmap注意事项

1)权限:创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
(2)当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
(3)映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
(4)特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
(5)munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
(6)文件偏移量必须为0或者4K的整数倍
(7)mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

5.4 使用总结

第一个参数写成NULL
第二个参数要映射的文件大小 >0
第三个参数: PROT_READ 、PROT_WRITE
第四个参数:MAP_SHARED 或者 MAP_PFIVATE
第五个参数:打开的文件对应的文件描述符
第六个参数:4K的整数倍

5.5 实例应用练习

练习1 使用mmap完成对文件的读写操作
练习2 使用mmap完成父进程间通信
在这里插入图片描述
思路:

	调用mma()函数创建存储映射区,返回映射区首地址ptr
	调用fork()函数创建子进程,子进程也拥有了映射区首地址
	父子进程可以通过映射区首地址指针ptr完成通信
	调用munmap()函数释放存储映射区

3.练习3:使用mmap完成没有血缘关系的进程间通信
思路:

两个进程都打开相同的文件,然后调用mmap函数建立存储映射区,这样连个进程共享一个存储映射区。
mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

6.综合实例

pipe_brother.c  
ret = pipe();
wpid = waitpid(-1, &status, WNOHANG);
dup2(fd[1], STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    
    
	int fd[2];
	int ret;
	pid_t pid;

	//创建一个管道
	ret = pipe(fd);
	if(ret<0)
	{
    
    
		perror("pipe error");
		return -1;
	}

	int i = 0;
	int n = 2;
	for(i=0; i<n; i++)
	{
    
    
		//创建子进程
		pid = fork();
		if(pid<0)
		{
    
    
			perror("fork error");
			return -1;
		}
		else if(pid==0)
		{
    
    
			break;
		}

	}

	if(i==n)
	{
    
    
		close(fd[0]);	
		close(fd[1]);	

		pid_t wpid;
		int status;

		while(1)
		{
    
    
			//等待回收子进程
			wpid = waitpid(-1, &status, WNOHANG);	
			if(wpid==0) //没有子进程退出
			{
    
    
				sleep(1);
				continue;
			}
			else if(wpid==-1) //已经没有子进程
			{
    
    
				printf("no child is living, wpid==[%d]\n", wpid);
				exit(0);
			}
			else if(wpid>0)
			{
    
    
				if(WIFEXITED(status)) //正常退出
				{
    
    
					printf("child normal exited, status==[%d]\n", WEXITSTATUS(status));
				}
				else if(WIFSIGNALED(status)) //被信号杀死
				{
    
    
					 printf("child killed by signo==[%d]\n", WTERMSIG(status));
				}
			}
			
		}
	}
	
	//第一个子进程
	if(i==0)
	{
    
    
		
		close(fd[0]);

		//将标准输出重定向到管道到写端
        dup2(fd[1], STDOUT_FILENO);
		execlp("ps", "ps", "aux", NULL);
		perror("execlp error");

		close(fd[1]);
	}

	//第二个子进程
	if(i==1)
	{
    
    
		printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
		close(fd[1]);

		//将标准输入重定向到管道到读端
		dup2(fd[0], STDIN_FILENO);
		execlp("grep", "grep", "--color", "bash", NULL);
		perror("execlp error");

		close(fd[0]);
	}

	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41858510/article/details/122081903