进程之间的通信(管道详解)

发布于:2023-02-14 ⋅ 阅读:(818) ⋅ 点赞:(0)

目录

一、了解进程通信:

二、管道的概述:

三、无名管道的概述:

特点:

3.1 无名管道的创建与使用:

3.2无名管道的读写特点:

 3.3无名管道的案例:

四、有名管道的概述:

 与无名管道(pipe)不同:

4.1有名管道的创建:

4.2有名管道的示例:


一、了解进程通信:

系统中的进程都是独立的个体,拥有属于自己的用户空间,所以每个进程之间的数据是不共享的。在前面学习fork时,了解到父、子进程可以共享fork之前打开的文件描述符。那么现在我们思考一个问题:父进程现在想给子进程发送一个“hello world"字符串。可以采取哪种方式,我想大多数人会想到 借助文件传输这种办法:

  • 父、子进程指向同一个文件,先让父进程对文件进行write操作,子进程等待,让它sleep睡眠,保证父进程先运行进行数据的写入;
  • 父进程写完“hello world"后,文件偏移量会偏移到d的位置,故子进程在读取时需要先把文件偏移量移动到h才可以保证读到数据。
     
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<fcntl.h>

int main()
{
    int fd=open("a.txt",O_RDWR);//读写打开
    assert(fd!=-1);
    char buff[128]={0};
    pid_t pid=fork();
    
    if(pid==0)
    {
        sleep(1);
        lseek(fd,0,SEEK_SET);
        int n=read(fd,buff,20);
        printf("buff:%s\n",buff);
    }
    else
    {
       int n= write(fd,"Hello,world!",20);
    }
    close(fd);
    exit(0);
}

同时我们会发现这样存在一定的问题:

  • 无法准确保证子进程在父进程后面运行:子进程又不知道父进程的处理需要多少秒,所以无法确定睡眠的秒数,容易导致混论。
  • 速度效率极低:每次对文件进行操作,就需要和磁盘进行交互,写入时进行一次I/O操作,读出时进行一次I/O操作,只是一个读取过程就需要耗费大量的时间,所以效率低下。
  • 传送信息对象固定:只能在父子进程之间进行数据的传递,没有办法做到任意两个文件的信息传递。

为了解决上述的问题,达到进程间任一两个进程进行数据交互通讯的目的,科学家们研究出了进程间通讯(IPC)的几种方式

同一主机下

无名管道

有名管道 信号 消息队列 信号量 共享内存 存储映射

不同主机下

socket套接字

二、管道的概述:

每个进程的空间地址是独立的,因此进程与进程之间是不能相互访问的,要进行进程间通讯,必须通过内核,内核会开辟一段特殊的内存空间,进程可以在这块内存空间进行数据的交换。
管道是一个重要的通信机制,思想是:在内存中创建一个共享文件,从而使通信双方利用这个共享文件来传递信息,由于这种方式具有单向传递数据的特点(),所以称为管道,即在某一时刻只能一端读数据一端写数据。


根据使用方式和通信对象将它分为无名管道,有名管道(命名管道)两类

无论有名还是无名 , 写入管道的数据都在内存中

管道正常情况下默认为无名管道(匿名管道)

三、无名管道的概述:

无名管道是一种特殊类型的文件,在应用层体现打开的两个文件描述符。

无名管道的使用存在限制,它只能用于父,子进程之间,但是它却最常用,原因很简单:现在的项目都是由父进程创建子进程,替换为新代码来实现不同的操作。这样可以只创建一个进程,通过不断fork,execl进行不同功能的实现,所以无名管道最常用。
因为是父子之间,所以不用单独创建管道文件和打开文件,创建打开是一起的,故不用open操作,打开后就可以进行write,read等操作了。

无名管道没有文件名,存储在内核体和的一段内存中,通过借助这段内存完成进程间的通信,进程结束,管道也随之结束。

特点:

  1. 半双工,数据在同一时刻只能在一个方向上流动

  2. 数据只能从管道一端写入,从另一端读出

  3. 写入管道中的数据遵循先入先出的规则

  4. 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息

  5. 管道不是普通的文件,不属于某个文件系统,其只存在于内存中

  6. 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同

  7. 从管道读取数据是一次性操作,数据一旦被读取走,它就从管道中被抛弃,释放空间,以便写入更多的数据

  8. 管道没有名字,只能在具有公共祖先的进程(父子,兄弟,具有亲缘关系)之间使用

  9. 存在阻塞方式

3.1 无名管道的创建与使用:

#include<unistd.h>//头文件

int pipe(int pipefd[2]); //函数

 一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可以对管道进行操作,我们来具体看一下过程:

首先父进程pipe创建和打开管道,内核开辟一段内存,称为管道,用于通信,它有一个读端,一个写端,通过fd参数传给用户两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。此时管道的两端通过进程相互连接,fd[1]写端指向fd[0]读端,数据通过内核开辟的管道流动:

当父进程fork之后。子进程复制父进程的所有文件描述符,父进程有fd[1]->fd[0],子进程也有fd[1]->fds[0],都通过管道进行连接:

因为管道是半双工通信,所以只能一端读,一端写。那么根据需求,将多余的连接关闭,如果我们让父进程写,子进程读,那么需要父进程关闭fd[0]读端,子进程关闭fd[1]写端: 

所以注意:在使用无名管道时,必须事先确定谁发谁收的问题.

3.2无名管道的读写特点:

默认管道的读写两端都为阻塞模式。

阻塞模式下有两个特征:

  • 当读管道时,如果管道中没有数据,则会阻塞,直到管道另一端写入数据。

  • 当写管道时,如果管道中已经满了,则会阻塞,直到管道另一端读出数据(读出的数据会从管道中清除)。

设置非阻塞模式:

  1. 如果管道是空,默认用read函数从管道中读取数据是阻塞的。
  2. 调用write函数向管道里写数据,当缓冲区已满时write也会被阻塞
  3. 通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会受到SIGPIPE信号退出

查看管道的缓冲区函数:

 3.3无名管道的案例:

因为是用于父子进程,所以一个.c文件即可。我们实现:父子进程的数据交互,父进程给子进程发送信息,即在父进程种关闭读端,向管道中写入数据,子进程关闭写端,从管道中读取数据。

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

int main(int argc,char const *argv[])
{
	//创建一无名管道
	int fd[2];
	pipe(fd);
	//创建子进程
    //父进程发,子进程收
	pid_t pid = fork();
    assert(pid!=-1);
	if (pid == 0)//子进程
	{
		close(fd[1]);//关闭写端
        while(1)
        {
            char buff[128]={0};
            int accept=read(fd[0],buff,127);
            if(accept<=0)
            {
                break;
            }
            printf("son:%d success read %s\n",getpid(),buff);  
        }
          //通信结束,关闭读端   
        close (fd[0]);     
	}
	else //父进程
	{
		//父进程读端无意义(可以关闭)
        close(fd[0]);
        //写端写入数据
        while(1)
        {
        char buff[128]={0};
       printf("father:%d write data:",getpid());
         fgets(buff,127,stdin);
            write(fd[1],buff,strlen(buff)-1);
             if(strncmp(buff,"over",4)==0)
            {
                break;
             }
        }
            //通信结束,关闭写端
            close (fd[1]); 
            //等待子进程退出
            wait(NULL);
	}
	return 0;
}

根据运行过程可以看到,在子进程输出之前会打出father pid input: ,原因是它们是两个独立的进程,都在并发运行,即父进程运行自己的,子进程也在运行自己的,子进程是buff中有数据打印,比父进程慢一点,所以每次都会输出father pid input.
read,write一样会阻塞,父进程不输入,子进程就不会输出。
 C++网络通信中write和read的为什么会阻塞_

四、有名管道的概述:

 主要用于不相关的进程间通信

有名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之相关,以FIFO的文件形式存在于文件系统中,这样即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通过FIFO不相关的进程也能交换数据。

 与无名管道(pipe)不同:

  1. FIFO在文件系统中作为一个特殊的文件而存在,但FIFO中的内容却存放在内存中;(也就是说它的文件大小永远为0(占磁盘空间为0))
  2. 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便后续继续使用;
  3. FIFO有名字,不相关的进程可以通过打开有名管道进行通信(重要!!!)

4.1有名管道的创建:

通过命令创建有名管道

  1. mkfifo fifo;(mkfifo 管道名)
  2. eg创建fifo文件;
  3. fifo为抽象的形式文件,存在于内存中,查看文件大小为0

通过函数创建有名管道

#include<sys/types.h>
#include<sys/stat.h>

int mkfifo(const char*pathname,mode_t mode);

pathname -> 普通的路径名,也就是创建后FIFO的名字
mode ->文件权限
与打开普通文件的open()函数中的mode参数相同。(0666)

返回值:
    成功 :0
    失败: 如果文件已存在,则会出错且返回-1

一旦用mkfifo创建一个FIFO管道文件,就可以用open打开它,用read,write等可以对文件进行操作。

我们来看一下有名管道是如何实现进程间的通信,假设现在又A,B两个进程,A进程向管道文件写数据,B进程从管道文件读数据,那么就有下面这张图:

我们需要清楚以下这几个点:

  • 在磁盘上会有一块FIFO文件标识符,占据inode区域,inode结点会指向内核开辟的内存上的一片空间,两个进程对于磁盘上的文件是共享的进程之间通讯的所有数据都会在内存交互,而不会存储到FIFO文件,因为它只是一个标识,所以进程通讯结束后,可以通过ls -a查看文件详细信息,文件大小为0。
  • A,B两个进程通过文件inode区域可以访问到这一块内存区域,都指向内存上这一块空间的起始位置,就可以开始进行半双工通信了。
  • A,B进程只能一个写打开,一个读打开。不能以读写的方式打开,那样就会造成混乱,A写给B的,A自己读了,所以管道是一个半双工的通信机制,某一时刻只能A写B读(A-B);B写A读(B-A)。
  • 内核对管道这块的内存空间的管理是以循环方式管理,即写到末尾时会从头开始写,覆盖原来的数据;在读取数据时会将读过的数据删除,当读到末尾时,会从头开始读。故这一块区域是循环的使用的,类似循环队列。
  • 也正是因为这种工作方式:只有当有进程往管道中写数据,同时有进程从管道读数据这种情况下,管道才会有意义,如果一个进程是以只写或只读方式操作管道文件,那就没有意义,会阻塞,浪费空间。
     

4.2有名管道的示例:

使用一下管道进行两个进程间的通讯:有write.c,read.c两个进程:

  • write进程负责打开管道,循环获取用户从键盘上输入的信息,存储到buff,再将数据写入管道中,最后关闭管道。
  • read进程负责打开管道,循环从管道中读取数据,将数据输出到屏幕上。

那么我们代码如下:

write.c

# include<stdio.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(int argc,char *argv[])
{
    //创建有名管道(保存两个进程,识别相同目录)
    mkfifo("my_fifo",0666);
    //open以写的方式打开有名管道(阻塞 到 对方 以读的方式打开)
    int fd=open("my_fifo",O_WRONLY);
     assert(fd!=-1);
    if(fd<0)
    {
        perror("open");
        return 0;
    }
    printf("write.c open success\n");
    //循环写入数据
    while(1)
    {
        //获取键盘输入
        char buff[128]=" ";
        printf("input data:");
        fgets(buff,127,stdin);
        //发送数据
        write(fd,buff,strlen(buff)-1);
        //退出循环
          if(strncmp(buff,"over",4)==0)
        {
            break;
        }
    }
    close(fd);
    return 0;
}

 read.c

# include<stdio.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(int argc,char *argv[])
{
    //创建有名管道(保存两个进程,识别相同目录)
    mkfifo("my_fifo",0666);
    //open以读的方式打开有名管道
    int fd=open("my_fifo",O_RDONLY);
     assert(fd!=-1);
    if(fd<0)
    {
        perror("open");
        return 0;
    }
    printf("read.c open success\n");
    //循环读取数据
    while(1)
    {
        char buff[128]=" ";
        //接收数据
        int n=read(fd,buff,strlen(buff)-1);
          if(n==0)
        {
            break;
        }
        printf("read:%s\n",buff);
    }
        
    close(fd);
    return 0;
}

我们对代码进行下面几种方法的测试,分析出现的情况:

  1. 先运行write进程,read进程不运行出现的情况,出现的结果:

可以看到A进程阻塞,不能输出write进程的提示信息,这就表示open函数阻塞因为我们是以只写的方式打开管道,read进程不运行表示没有进程来读取管道数据,管道只写不读,就没有意义,所以会一直阻塞。

        2.先运行read进程,write进程不运行,会出现的情况:
                和(1)的情况是一样的,会阻塞,直到write进程运行才解除阻塞。

 通过这两个测试,我们直到任何一个进程先运行都不能正常运行,会出现阻塞,但我们要搞清楚一点,阻塞并不是说open函数会阻塞,而是操作的对象会阻塞,因为操作的是管道文件,只读/只写会导致无意义,阻塞,如果换成普通文件就不会阻塞。

   3.同时运行两个进程,出现的情况:

可以看到两个进程运行,一个对管道写,一个从管道读,可以立即输出提示信息,open不会阻塞,输入一个数据,读出一个数据,不会存在B进程read空读的问题,它会一直阻塞着,直到A进程往管道里面写入一个数据,它才读一个数据,而A进程的write操作,也不会一直的让你写,而是你读一个我写一个。所以read,write会阻塞,这样就解决了读写混乱的问题。close关闭管道文件不会阻塞。

注意:
我们可以总结出有名管道需要注意的点:

  • 以open方式打开管道文件会阻塞,直到有进程以另一种方式打开管道文件(读或写)。
  • 如果管道对应的内存空间没有数据,则read会阻塞,直到内存中有数据或写端关闭返回0。
  • 如果管道对应的内存空间已满,则write就会阻塞,直到内存中有空间或读端关闭返回0.
  • 一个为只读而打开一个管道的进程会阻塞直到另一个进程为只写打开该管道
  • 一个为只写而打开一个管道的进程会阻塞直到另一个进程为只读打开该管道

了解一个命令:

ulimit -a可以显示当前的各种用户进程限制,包括块大小,创建进程数等。

越努力越幸运!

本文含有隐藏内容,请 开通VIP 后查看