进程间通信方式-管道通信

进程间通信(Inter Process Communication,简称IPC)指的是进程之间的信息交换,进程间通信的方式有很多,比如管道通信、信号通信、共享内存、消息队列、信号量组、POSIX信号量等。

进程间通信可以达到数据传输、共享资源、控制进程等目的,方便用户对进程进行控制和管理。

1.管道通信 

Linux系统提供了一种通信方式,名字叫做管道通信,顾名思义,管道是单向的,比如水管、燃气管道等,换个说法就是管道是采用半双工通信的,也就是同一时刻只能完成发送数据或者接收数据。

就是进程A如果利用管道向进程B发送数据,则进程B就需要等待接收进程A发送的数据。

管道在Linux系统也属于文件的一种,Linux系统中管道文件分为两种:匿名管道(pipe)和命名管道(fifo)。 

1.1匿名管道

匿名管道的特点是没有名称,所以用户无法使用open来创建和打开,但是匿名管道进行数据读写的方式和普通文件一样,都是支持read()与write()操作的。

思考:匿名管道在Linux系统下属于文件,但是匿名管道没有名称,请问应该如何创建匿名管道以及对匿名管道进行访问?

回答:Linux系统提供了系统调用接口pipe()来创建匿名管道,用户可以通过man 2 pipe来查阅函数的使用规则。

可以看到pipe函数由一个名字叫做pipefd的参数,该参数是一个数组类型,用于存储对管道进行读写的文件描述符,pipefd[0]记录管道读取端的文件描述符,pipefd[1]记录管道写入端的文件描述符。

另外,用户把对数据写入到管道之后,数据是会在内核的缓冲区中进行暂存的,直到从管道中读取该数据为止。

注意:内核的缓冲区大小是固定的(linux系统下默认是4M大小),如果写入的速度快于读取的速度,则可能会发生缓冲区满的情况,如果读取的速度快于写入的速度,如果管道内部没有数据,则读取数据会出现阻塞等待的情况。

所谓的阻塞实际上就是系统将该进程挂起,等待资源就绪再继续调度的一种状态,这种阻塞的状态有利于系统中别的进程可高效地使用闲置CPU资源,提高系统的吞吐量。

思考:可以看到pipe函数调用成功可以得到创建成功的匿名管道关于读取端和写入端的文件描述符,这样两个进程之间就可以通过文件描述符对匿名管道进行访问,但由于匿名管道没有名称,请问匿名管道适合什么样的进程使用?以及进程中什么时候适合创建匿名管道?

回答:由于匿名管道没有名称,所以只适合在有亲缘关系的进程中使用,一般经常用于父子进程之间。

另外,如果父进程中创建了子进程,子进程会把父进程的代码段、数据段、堆栈段等完全复制一份,所以应该在复刻进程之前创建匿名管道,这样子进程就可以把匿名管道的文件描述符复制到自己的进程空间中,这样父进程和子进程就可以对管道进行读写访问。

练习:设计一个程序,实现在程序中创建一个子进程,然后父子进程通过匿名管道进行通信,要求子进程发送字符串“Hello Father,I am Child!”,要求父进程把子进程发送的数据输出到终端,然后父子进程退出。

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

int main(int argc, char const *argv[])
{
    // 1. 创建一条匿名管道 pipe  pipefd[0] 读操作  pipefd[1] 写操作
    int pipefd[2] = {0};

    int ret = pipe(pipefd);
    if (ret == -1)
    {
        fprintf(stderr, "pipe error, errno:%d,%s\n", errno, strerror(errno));
        exit(1); 
        // exit函数可以终止进程,并把进程的终止状态提供给该进程的父进程
    }

    // 2. 此时可以创建子进程  fork  子进程会拷贝父进程的数据段、代码段、堆栈段
    int child_pid = fork();

    // 3. 分析当前的进程空间是父进程 or 子进程 or 创建失败
    if (child_pid > 0)
    {
        // 说明是父进程,父进程需要从匿名管道中读取数据
        char recvbuf[128] = {0};
        read(pipefd[0], recvbuf, sizeof(recvbuf)); 
        // 父进程会阻塞
        printf("my is parent, read from pipe data is [%s]\n", recvbuf);

        wait(NULL); 
        // 父进程需要回收子进程的资源,也会阻塞
    }
    else if (child_pid == 0)
    {
        // 说明是子进程,子进程向匿名管道中写入字符串
        char sendbuf[128] = "my is child,hello parent";
        write(pipefd[1], sendbuf, strlen(sendbuf));
    }
    else
    {
        fprintf(stderr, "fork error, errno:%d,%s\n", errno, strerror(errno));
        exit(2); 
        // exit函数可以终止进程,并把进程的终止状态提供给该进程的父进程
    }

    return 0;
}

注意:匿名管道虽然可以在具有亲缘关系的进程中使用,但是匿名管道是不保证数据的原子性。

1.2命名管道

由于匿名管道只适合一对一、并且具有亲缘关系的进程间通信,导致局限性较大,所以Linux系统下还提供了另一种管道,名称叫做命名管道,也可以称为有名管道、具名管道等。

相比于匿名管道而言,命名管道有自己的文件名,可以被open,同样也支持read/write访问。当然,管道文件毕竟不是普通文件,由于管道的特性的原因,所以管道是不支持指定位置读取数据的,也就意味着不能对管道文件进行lseek操作。

当然,由于命名管道有文件名,所以没有亲缘关系的进程间可以通过命名管道进行通信,并且可以支持多路同时写入。

访问命名管道的前提是命名管道存在,所以Linux系统中提供了一个名字叫做mkfifo()的函数接口来创建命名管道。

可以看到mkfifo()函数有两个参数,第一个参数是const char *pathname,指的是创建的命名管道的文件路径。

注意:如果是新建的管道文件,则需保证创建路径位于Linux系统内,尤其是虚拟机中操作的时候,不可以将管道文件创建在共享文件夹中,因为共享文件夹是windows系统中的路径。

可以看到mkfifo()函数有两个参数,第一个参数是const char *pathname指的是文件所需的创建路径,第二个参数是mode_t mode,指的是创建的命名管道的权限,和open函数类似,权限需要使用八进制,并且分为用户本身、用户所在的组、其他用户。

练习:在 /tmp 目录下创建一条命名管道,命名管道的名称用户决定,然后设计两个程序,要求进程A获取当前系统时间(time-->ctime)并写入到命名管道,进程B从命名管道中读取数据并存储在一个名字叫做log.txt的文本中。

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#define FIFO_PATH	"/tmp/fifo"


int main(int argc, char const *argv[])
{
	//1.进程A创建一个有名管道
	int ret = mkfifo(FIFO_PATH,0777);
	if (ret == -1)
	{
		fprintf(stderr, "mkfifo error,errno:%d,%s\n",
						errno,strerror(errno));
		
		exit(1);
	}

	//2.进程A打开有名管道
	int fifo_fd = open(FIFO_PATH,O_RDWR);
	if (-1 == fifo_fd)
	{
		fprintf(stderr, "open error,errno:%d,%s\n",
						errno,strerror(errno));
		
		exit(2);
	}

	//3.进程A获取系统时间并转换为格式化字符串,写入到管道文件
	char *ptime;
	time_t tim;
	while(1)
	{
		tim = time(NULL);
		ptime = ctime(&tim);
		write(fifo_fd,ptime,strlen(ptime));
		sleep(1);
	}
	
	//4.关闭有名管道
	close(fifo_fd);

	return 0;
}
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#define FIFO_PATH	"/tmp/fifo"
#define LOG_PATH	"./log.txt"

int main(int argc, char const *argv[])
{
	//1.进程B创建一个log.txt
	int log_fd = open(LOG_PATH,O_RDWR|O_CREAT);
	if (-1 == log_fd)
	{
		fprintf(stderr, "open log.txt error,errno:%d,%s\n",
						errno,strerror(errno));
		
		exit(1);
	}

	//2.进程B打开有名管道
	int fifo_fd = open(FIFO_PATH,O_RDWR);
	if (-1 == fifo_fd)
	{
		fprintf(stderr, "open fifo error,errno:%d,%s\n",
						errno,strerror(errno));
		
		exit(2);
	}

	//3.进程B从管道文件读取时间并写入到log.txt
	char timebuf[256] = {0};
	while(1)
	{
		
		read(fifo_fd,timebuf,sizeof(timebuf));
		write(log_fd,timebuf,sizeof(timebuf));
		
		bzero(timebuf,sizeof(timebuf));

		sleep(1);
	}
	
	//4.关闭有名管道
	close(fifo_fd);
	close(log_fd);
	return 0;
}

思考:如果进程A向命名管道中写入了数据之后就关闭了命名管道,而进程B从命名管道中读取了进程A写入的部分数据之后就关闭了命名管道,请问下次打开命名管道之后是否可以继续读取上一次遗留的数据?  答案:

命名管道的 “残留数据” 是否保留,取决于管道是否被所有进程关闭

  • ✅ 若管道仍有进程未关闭读 / 写端 → 数据可残留,新进程打开后能继续读。
  • ❌ 若所有进程都关闭了管道 → 数据会被清空,下次打开是全新通道,无法读取历史残留。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值