Linux 进程通信 2019.1.7(pipe,fifo,mmap,匿名映射)

ipc(进程间通信)的概念

interprocess communication:进程间通信——Linux通过内核给你提供了一块缓冲区,使得进程实现通信。

 

IPC通信的方式

  1. pipe 管道——最简单
  2. fifo 有名管道
  3. mmap 文件映射共享IO——速度最快(原理:在内存开辟一片缓冲区,把文件映射到内存上,你直接去操作内存就可以了)
  4. 本地socket 最稳定
  5. 信号 携带信息量最小的
  6. 共享内存 开辟一块内存区域,大家都能访问,一个进程退出之后,这块内存还会给你保留下来,后来者还可以继续使用
  7. 消息队列

 

 

管道——半双工通信

带fd的参数一般都是指文件描述符

一端读,一端写

一个进程写,一个进程读,他俩只能是父子进程

 

常见的通信方式

单工(广播)、半双工(对讲机)、全双工(打电话)

 

管道通信使用举例

管道函数:

int pipe(int pipefd[2]);

  1. pipefd 读写文件描述符 0 代表 1 代表
  2. 返回值 成功返回 0, 失败返回  -1.

调用完 相当于在内核开辟了一块缓冲区

 

父子进程间通信

#include<stdio.h>
#include<unistd.h>

int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();

	if (pid == 0) {
		//son
		sleep(3);
		write(fd[1], "hello", 5);
	}
	else if (pid > 0) {
		//father
		//现有缓冲区,然后才能读
		char buf[12] = { 0 };
		//阻塞等待,哪怕子进程sleep了,也要等
		int ret = read(fd[0], buf, sizeof(buf));
		//说明读到了
		if (ret > 0) {
			write(STDOUT_FILEND, buf, ret);
		}
	}
	return 0;
}

运行结果

 

父子进程实现pipe通信,实现ps aux |  grep bash 功能

由于父子进程都掌握着管道(pipe)的读写两端,因此有如下结构示意图

出现问题的测试代码:(我们希望子进程写入,父进程读取)

#include<stdio.h>
#include<unistd.h>

int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();

	if (pid == 0) {
		//son
		//son执行ps命令
		//1、先重定向
		dup2(fd[1], STDOUT_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("ps"."ps"."aux", NULL);
	}
	else if (pid > 0) {
		//father
		//1、先重定向
		dup2(fd[0], STDIN_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("grep"."grep"."bash", NULL);
	}
	return 0;
}

出现的问题:

子进程变成僵尸进程,父进程执行grep之后一直在等待输入。

 

分析原因:

grep命令的特性:等待标准输入,如果你输入的正确,则给你一个反馈。阻塞等待,因为grep一直认为还有输入,除非是输入端的进程放弃了写入的机会。grep阻塞等待示例,如图所示

 

上面代码出现问题,是因为虽然子进程死去了,但是父进程还掌握着管道写入端的句柄,所以grep一直认为还会有输入,虽然此时读写两端都是父进程掌握。所以我们需要在代码中关闭子进程的读端,以及父进程对于管道的写端

//关闭 读端
close(fd[0]);

//关闭 写端
close(fd[1]);

在使用管道时,应该把读写两端都规划好。这样数据的流向才稳定。

 

修正之后的代码:

#include<stdio.h>
#include<unistd.h>

int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();

	if (pid == 0) {
		//son
		//son执行ps命令
		//关闭 读端
		close(fd[0]);
		//1、先重定向
		dup2(fd[1], STDOUT_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("ps"."ps"."aux", NULL);
	}
	else if (pid > 0) {
		//father
		//关闭 写端
		close(fd[1]);
		//1、先重定向
		dup2(fd[0], STDIN_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("grep"."grep"."bash", NULL);
	}
	return 0;
}

运行结果

 

 

管道的读写行为分析

读管道

  • 写端全部关闭——read读到0,相当于读到文件末尾
  • 写端没有全部关闭
  1. 有数据——read读到数据
  2. 没有数据——read阻塞  fcntl 函数可以更改阻塞为非阻塞

写管道

  • 读端全部关闭——产生一个信号 SIGPIPE ,程序异常终止
  • 读端未全部关闭
  1. 管道已满——write阻塞
  2. 管道未满——write正常写入

 

 

测试代码

1、写端全部关闭——read读到0,相当于读到文件末尾

子进程关闭读端——子进程写入数据——子进程关闭写端——父进程关闭写端——父进程读取数据——读到0则说明读取完毕,输出“read over”

#include<stdio.h>
#include<unistd.h>

int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();

	if (pid == 0) {
		//son
		sleep(3);
		//关闭 读端
		close(fd[0]);
		//写入数据
		write(fd[1], "hello", 5);
		//关闭写端
		close(fd[1]);
		//关闭读写之后,先别去死
		while (1) {
			sleep(1);
		}
	}
	else if (pid > 0) {
		//father
		char buf[12] = { 0 };
		//关闭写端
		close(fd[1]);
		while (1) {
			//阻塞等待,读取动作
			int ret = read(fd[0], buf, sizeof(buf));
			//读取完毕
			if (ret == 0) {
				printf("read over\n");
			}
		}
	}
	return 0;
}

运行结果(正常退出):

 

2、读端全部关闭——产生一个信号 SIGPIPE ,程序异常终止

父进程关闭读写——子进程关闭读端——写入数据——子进程异常终止,产生信号SIGPIPE——父进程获取子进程的死亡信息

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

int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();

	if (pid == 0) {
		//son
		sleep(3);
		//关闭 读端
		close(fd[0]);
		//写入数据
		write(fd[1], "hello", 5);
		//关闭写端
		close(fd[1]);
		//关闭读写之后,先别去死
		while (1) {
			sleep(1);
		}
	}
	else if (pid > 0) {
		//father
		//关闭写端
		close(fd[1]);
		//关闭 读端
		close(fd[0]);
		//获取子进程死亡信息
		int status;
		wait(&status);
		if (WIFSIGNALED(status)) {
			printf("killed by %d\n", WTERMSIG(status));
		}
		while (1) {
			sleep(1);
		}
	}
	return 0;
}

运行结果:

 

 

 

实现兄弟间进程通信, ps aux | grep bash

 

 

 

 

管道的大小和优劣

使用命令查看当前系统中创建管道文件所对应的内核缓冲区大小

ulimit -a

函数fpathconf

 

优点

  • 简单

缺点

  • 只能有血缘关系之间的进程通信
  • 父子进程单方向通信,如果需要双向通信,需要创建多个管道。

 

 

 

FIFO通信

有名管道,实现无血缘关系进程的通信

  • 创建一个管道的伪文件
  1. mkfifo myfifo 命令创建
  2. 也可以用函数int mkfifo(const char *pathname, mode_t mode);
  • 内核会为fifo文件开辟一块缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写。
  • open函数的注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开。

 

mkfifo myfifo 命令创建出来的管道伪文件截图

 

写端示例

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>

int main(int argc, char *argv[]) {
	if (argc != 2) {
		//如果运行时没有输入文件名,则会报错
		printf("./a.out fifoname\n");
		return -1;
	}
	//当前目录中存在myfifo文件
	//打开fifo文件
	int fd = open(argv[1], O_WRONLY);
	//写
	char buf[256];
	int num = 1;
	while (1) {
		memset(buf, 0x00, sizeof(buf));
		sprintf(buf, "xiaoming%04d", num++);
		write(fd, buf, strlen(buf));
		sleep(1);
	}
	//关闭描述符
	close(fd);

	return 0;
}

 

读端示例

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>

int main(int argc, char *argv[]) {
	if (argc != 2) {
		//如果运行时没有输入文件名,则会报错
		printf("./a.out fifoname\n");
		return -1;
	}
	int fd = open(argv[1], O_RDONLY);

	char buf[256];
	int ret;
	while (1) {
		//循环读
		ret = read(fd, buf, sizeof(buf));
		if (ret > 0) {
			printf("read:%s\n", buf);
		}
	}

	//关闭描述符
	close(fd);
	return 0;
}

 

读端和写端中的open函数会阻塞,之后对端的open函数也打开了,他才会停止阻塞行为

 

 

 

 

mmap共享映射区

把文件中的某一段映射到内存上

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr 传地址,一般写NULL
  • length 映射区长度
  • prot 
  1. PROT_READ 可读
  2. PROT_WRITE 可写
  • flags
  1. MAP_SHARED 共享的,对内存的修改会影响到源文件
  2. MAP_PRIVATE 私有的
  • fd 文件描述符
  • offset 偏移量
  • 返回值
  1. 成功 返回 可用内存的首地址
  2. 失败 返回 MAP_FAILED

 

释放内存区

#include <sys/mman.h>
int munmap(void *addr, size_t length);
  • addr 传mmap返回值
  • length mmap创建的长度
  • 返回值

 

实例(MAP_SHARED的作用是,你修改了内存,会影响文件)

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>

int main(){
	int fd=open("men.txt", O_RDWR);
	//创建映射区
	char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

	if(mem==MAP_FAILED){
		perror("mmap err");
		return -1;
	}

	//拷贝数据
	strcpy(mem, "hello");

	//释放mmap
	munmap(mem, 8);
	close(fd);
	return 0;
}

运行结果

因此可以用mmap来修改文件内容。

 

mmap九问:

 

问题一的测试:(因此不能改,否则会释放失败

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>

int main(){
	int fd=open("men.txt", O_RDWR);
	//创建映射区
	char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

	if(mem==MAP_FAILED){
		perror("mmap err");
		return -1;
	}

	//拷贝数据
	strcpy(mem, "hello");
	mem++;
	//如果释放失败
	if (munmmap(mem, 8) < 0) {
		perror("munmmap err");
	}
	close(fd);
	return 0;
}

运行结果

 

问题二的测试:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>

int main(){
	int fd=open("men.txt", O_RDWR);
	//创建映射区
	char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

	if(mem==MAP_FAILED){
		perror("mmap err");
		return -1;
	}

	//拷贝数据
	strcpy(mem, "hellollllllllllll");

	//释放内存
	munmmap(mem, 8);
	close(fd);
	return 0;
}

文件的大小对映射区操作有影响,尽量避免。

 

char *mem=mmap(NULL, 8, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 1000000000);

offset必须是4k的整数倍。

 

没有。因为mmap之后,通道就打通了,文件就没用了。

 

int fd=open("men.txt", O_RDWR|O_TRUNC, 0664);//创建并截断文件

      bus error//总线错误

可以,但是文件大小不能为0.

 

不可以

 

不可以。SHARED的时候,映射区的权限要小于等于open文件的权限

 

 

 

mmap实现父子进程通信

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>

int main()
{
    // 先创建映射区
    int fd = open("mem.txt",O_RDWR);
    int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    //int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
    if(mem == MAP_FAILED){
        perror("mmap err");
        return -1;
    }
    // fork子进程
    pid_t pid = fork();

    // 父进程和子进程交替修改数据
    if(pid == 0 ){
        //son 
        *mem = 100;
        printf("child,*mem = %d\n",*mem);
        sleep(3);
        printf("child,*mem = %d\n",*mem);
    }
    else if(pid > 0){
        //parent
        sleep(1);
        printf("parent,*mem=%d\n",*mem);
        *mem = 1001;
        printf("parent,*mem=%d\n",*mem);
		//回收子进程
        wait(NULL);
    }

	//释放内存
    munmap(mem,4);
    close(fd);
    return 0;
}

运行结果

 

将上面的注释去掉,改成

//int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);

运行结果

父进程并没有读到子进程的数据,子进程也没有读到父进程改的数据。

因此,

 

 

匿名映射——避免打开文件的操作(上面的例子都有调用open函数)

 int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);

使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>

int main()
{
    int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);

    if(mem == MAP_FAILED){
        perror("mmap err");
        return -1;
    }

    pid_t pid = fork();

    if(pid == 0 ){
        //son 
        *mem = 101;
        printf("child,*mem=%d\n",*mem);
        sleep(3);
        printf("child,*mem=%d\n",*mem);
    }else if(pid > 0){
        //parent 
        sleep(1);
        printf("parent,*mem=%d\n",*mem);
        *mem = 10001;
        printf("parent,*mem=%d\n",*mem);
        wait(NULL);
    }

    munmap(mem,4);
    return 0;
}

运行结果

 

注意:有的Unix系统中没有MAP_ANON,ANONYMOUS这两个宏。此时该怎么办?

此时用这个

/dev/zero 是一个聚宝盆,无限大,用它做匿名映射,你想取多大都可以。

小技巧——快速把一个文件头几行的数据重定向到另一个文件中

 

 

mmap实现无血缘进程通信

写端

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>

typedef struct  _Student{
    int sid;
    char sname[20];
}Student;

int main(int argc,char *argv[])
{
    if(argc != 2){
        printf("./a.out filename\n");
        return -1;
    }
    
    // 1. open file 
    int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
	//结构体的大小
    int length = sizeof(Student);
	//将文件大小改变为参数length指定的大小,
	//如果原来的文件大小比参数length大,则超过的部分会被删除,
	//如果原来的文件大小比参数length小,则文件将被扩展
    ftruncate(fd,length);

    // 2. mmap
    Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    //如果不判断是否出错,会死的很难看
    if(stu == MAP_FAILED){
        perror("mmap err");
        return -1;
    }
    int num = 1;

    // 3. 修改内存数据
    while(1){
        stu->sid = num;
        sprintf(stu->sname,"xiaoming-%03d",num++);
        sleep(1);//相当于每隔1s修改一次映射区的内容
    }
    // 4. 释放映射区和关闭文件描述符
    munmap(stu,length);
    close(fd);

    return 0;
}

 

读端

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>

typedef struct _Student{
    int sid;
    char sname[20];
}Student;

int main(int argc,char *argv[])
{
    //open file 
    int fd = open(argv[1],O_RDWR);
    //mmap 
    int length = sizeof(Student);
    Student *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(stu == MAP_FAILED){
        perror("mmap err");
        return -1;
    }
    //read data 
    while(1){
        printf("sid=%d,sname=%s\n",stu->sid,stu->sname);
        sleep(1);
    }
    //close and munmap 
    munmap(stu,length);
    close(fd);
    return 0;
}

运行结果:

再开一个读的

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值