Linux进程间通信(一)

进程间通信

概念:进程是一个独立的资源分配单位,不同进程之间有关联,不能在一个进程中直接访问另一个进程的资源。

  • 进程和进程之间的资源是相互独立的,一个进程不能直接访问另外一个进程的资源,但是进程和进程之间不是相互独立的。

通信目的:

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

如何实现进程通信

要让两个不同的进程实现通信,前提条件是让它们看到同一份资源。所以要想办法让他们看到同一份资源,就需要采取一些手段,可以分为下面几种。

通信方式分类

1.管道

  • 匿名管道pipe
  • 命名管道

2.System V IPC

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

3.POSIX IPC

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

管道

概念:我们把一个进程连接到另一个进程的一个数据流称为一个“管道”。

管道的特点:

  • 数据只能从管道的一端写入,从另一端读出
  • 写入管道的数据遵循先入先出的原则
  • 管道所传达的数据是无格式的,这要求管道的读出方和写入方必须事先约定好数据的格式
  • 管道不是普通的文件,不属于某个文件系统,只存在于内存中
  • 管道读数据是一次性的,数据一旦被读走,它就从管道中抛弃,释放空间
  • 管道是一种特殊的文件类型,会在应用层打开两个文件描述符fd[0]对应的是写端,fd[1]对应的是读端
  • 管道只能服务于有血缘关系的两个进程

匿名管道

创建匿名管道-----pipe系统调用

int pipe(int pidefd[2]);

功能:创建无名管道

参数:pipefd:为int类型数组的首地址,其存放了管道的文件描述符pipefd[0]、pipefd[1]

当一个管道建立的时候,他会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

返回值:成功:0 失败:-1

  • 文件描述符就是操作系统为了高效管理已经打开文件所创建的一个索引(文件描述符在前面的文章介绍过)

匿名管道创建原理:

调用pipe函数后,OS会在fd_array数组中分配两个文件描述符给管道,一个是读,一个是写,并把这两个文件描述符放到用户传进来的数组中,fd[0]代表管道读端,fd[1]代表管道写端。这样一个管道就创建好了。

实例演示:

实例1:观察两个文件描述符的值

#include <stdio.h>
#include <unistd.h>
int main()
{
	int pipefd[2];
	int ret = pipe(pipefd);
	if (ret == -1){
	  // 管道创建失败
	  perror("make piep");
	  //用于退出进程
	  exit(-1);
	}
	// 成功返回0
	// pipefd[0] 代表读端
	// pipefd[1] 代表写端
	printf("fd[0]:%d, fd[1]:%d\n", pipefd[0], pipefd[1]);
	return0;
}

运行结果如下:

显然,pipefd这个数组里面放的是两个文件描述符,分别是3和4,因为0,1,2文件描述符在进程创建的时候会由系统自动创建。

实例2:尝试使用管道读写数据

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

int main()
{
	  int pipefd[2];
	  int ret = pipe(pipefd);
	  if (ret == -1){
	    // 管道创建失败
	    perror("make piep");
	    exit(-1);
	  }
	  char buf[64] = "hello world";
	  // 写数据
	  write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));
	  // 读数据
	  memset(buf,0,sizeof(buf));// 清空buf
	  ssize_t s = read(pipefd[0], buf, 11);
	  buf[s] = '\0';
	  printf("%s\n", buf);
	  return 0;
}//成功输出hello world

可以看见对管道的操作,实际上就是对两个读写文件的操作,本质就是对文件的操作和使用。

管道的本质

Linux下一切皆文件,看待管道,其实时可以像看待文件一样。且管道和文件使用方法是一致的。管道的生命周期随进程

父子进程通过匿名管道通信

原理:匿名管道是提供给有亲缘关系两个进程进行通信的。所以我们可以在创建管道之后通过fork函数创建子进程,这样父子进程就看到同一份资源,且父子进程都有这个管道的读写文件描述符。我们可以关闭父进程的读端,关闭子进程的写端,这样子进程往管道里面写数据,父进程往管道里面读数据,这样两个进程就可以实现通信了。
原理解读:

fork函数调用成功后,将为子进程申请PCB和用户内存空间,子进程是父进程的副本,在用户空间将复制父进程用户空间所有的数据(代码段、数据段、BBS、栈、堆,实际上是复制的父进程的虚拟空间的地址),子进程从父进程继承下列属性:有效用户、组号、进程组号、环境变量、信号处理方式设置、信号屏蔽集合、当前工作目录、根目录、文件模式掩码、文件大小限制和打开的文件描述符(特别注意:共享同一文件表项)。

共享同一文件表项就造成了一种现象,父子进程无论谁对文件进行操作,那么另外一个进程的文件表也会受到相同的影响。

从图中可以看出,虽然在子进程的表项中式复制了关于打开文件的信息,但是他们是共享文件表的,所以如果一个进程对文件指针进行移动,那么肯定会影响到另外的进程。

思考:这是不是和写时拷贝相违背了,为什么文件表就能共享了呢?

要知道在linux源码中,每个进程都存在一个PCB结构体,每个PCB中,存放了一个结构体指针指向一个我们理解为文件描述符的结构体struct file,而这个结构体里,才存了文件的id,值得注意的是,这个结构体里有一个指针才是指向真正文件的。文件系统存在于磁盘当中,对磁盘的操作操作系统不会拷贝一份文件给子进程,相反,像那些临时创建存放于堆区和栈区的数据,操作系统会采用写时拷贝,进行复制。
父子进程有用属于自己的PCB,只不过子进程PCB的内容全部都是复制父进程的虚拟地址,所以两个PCB的文件描述符表都是一样的,指向同一个文件表,由于文件系统存在于磁盘当中,所以对文件操作不会进行写时拷贝,所以彼此对文件的读写会相互影响,但是不影响各自关闭文件描述符,子进程关闭其中一个文件描述符,最多就是与文件表的连接断开,不会影响父进程与该文件的连接。

2.5

总结:父子进程共享文件表,对文件表进行的任何操作都会对父子进程造成相同的影响,与写时拷贝进行区分。

父子进程通过创建匿名管道通信具体过程如下

1.父进程创建管道(管道创建要在进程创建之前)

2.fork创建子进程(子进程继承父进程的管道文件描述符)

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

实例演示: 子进程每隔1秒往管道里面写数据,父进程每隔1秒往管道里读数据

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
  int pipefd[2];
  int ret = pipe(pipefd);
  if (ret == -1){
    // 管道创建失败
    perror("make piep");
    exit(-1);
  }
  pid_t id = fork();
  if (id < 0){
    perror("fork failed");
    exit(-1);
  }
  else if (id == 0){
    // child
    // 关闭读端
    close(pipefd[0]);
    const char* msg = "I am child...!\n";
    //int count = 0;
    // 写数据
    while (1){
      ssize_t s = write(pipefd[1], msg, strlen(msg));
      printf("child is sending message...\n");
      sleep(1);
    }
  }
  else{
    // parent
    close(pipefd[1]);
    char buf[64];
    while (1){
      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
      if (s > 0){
        buf[s] = '\0';// 字符串后放一个'\0'
        printf("father get message:%s", buf);
      }
      else if (s == 0){
        // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
        printf("father read end of file...\n ");
      }
      sleep(1);
    }
  }
  return 0;
}

运行结果如下:

匿名管道读写规则

读写规则总结:

  • 当没有数据可读时
    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

注意:O_NONBLOCK是非阻塞的标志位,指定管道对我们的操作要么成功,要么立刻返回错误,不被阻塞。

管道特点(了解)

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
    建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

  • 管道提供流式服务。也就是你想往管道里读写多少数据是根据自身来定的

  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

  • 半双工是指传输过程中同时只能向一个方向传输,一方的数据传输结束之后,另外一方再回应。双方传输数据是不可以同时进行的

  • 全双工是指两方能同时发送和接受数据。在这种情况下就没有拥堵的危险,数据的传输也就更快

命名管道

概念:无名管道,由于没有名字,所以只能用于亲缘关系的进程通信。为了克服这个缺点,提出了命名管道(FIFO)。

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

  • FIFO在文件系统(磁盘上)中作为一个特殊文件而存在,但是FIFO中的内容却存放在内存中。
  • 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
  • FIFO有名字,不相关的进程可以通过打开命名通道进行通信。

创建命名管道

1.通过命令创建命名管道

mkfifo filename

2.通过函数创建命名管道

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

功能:创建命名管道

参数:pathname:普通的路径名,也就是创建后FIFO的名字。

​ mode:文件的权限,与打开普通文件的open函数中的mode参数类似。

返回值:成功:0 (状态码) 失败:如果文件已经存在,则会出错返回-1

代码示例:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO "./fifo"
int main()
{
  umask(0);
  // 创建管道
  int ret = mkfifo(FIFO, 0666);
  if (ret == -1){
    perror("make fifo");
    exit(-1);
  }
}

运行结果如下:

上面说过,管道其实就是一种特殊的文件,管道文件大小是0,因为上面介绍过,管道文件的内容都存放在内存当中。

命名管道读写操作以及注意事项

一旦创建了一个FIFO,就可以用open打开它,常见的文件I/O都可以作用于FIFO文件。

FIFO严格的遵循先进先出的原则,对管道以及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾。

  • 一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道
  • 一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道

读写规则

读管道

  • 管道中有数据,read返回返回实际读到的字节数
  • 管道中无数据:(1)若管道写端被全部关闭,read返回0

​ (2)若写端没有全部关闭,read阻塞等待

写管道

  • 管道读端全部被关闭,进程异常终止
  • 管道读端没有全部关闭:(1)若管道已经满了。write阻塞

​ (2)若管道没满,write将数据写入,并返回实际写入的字节数

使用命名管道进行通信

接下来我会使用命名管道实现简单的版本聊天。

talkA.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//先读后写
//以只读的方式打开管道1
//以只写的方式打开管道2
define SIZE 1024
int main()
 {
    int fdr = -1;
    int fdw = -1;
    int ret = -1;
    char buf[SIZE];
    //以只读的方式打开管道1
    fdr = open("fifo1",O_RDONLY);
    if(-1==fdr)
    {
      perror("open");
      return 1;
    }
    printf("以只读的方式打开管道1....\n");
    //以只写的方式打开管道2
    fdw = open("fifo2",O_WRONLY);
    if(-1==fdw)
    {
      perror("open");
      return 1;
    }
    printf("以只写的方式打开管道2....\n");
    //循环读写
    while(1)
    {
      //读管道1
      memset(buf,0,SIZE);
      ret = read(fdr,buf,SIZE);
      if(ret<=0)
      {
        perror("read");
        break;
      }
      printf("read:%s\n",buf);
      //写管道2
      memset(buf,0,SIZE);
     fgets(buf,SIZE,stdin);
     //去掉最后一个换行符
     if('\n'==buf[strlen(buf)-1])
     buf[strlen(buf)-1]=0;
     //写管道
     ret = write(fdw,buf,strlen(buf));
     if(ret<=0)
     {
        perror("write");
        break;
     }
        printf("write ret:%d\n",ret);
     }
     //关闭文件描述符
    close(fdr);
    close(fdw); 
}                   

talkB.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//以只读的方式打开管道2
//以只写的方式打开管道1
#define SIZE 1024
int main()
  {
     int fdr = -1;
     int fdw = -1;
     int ret = -1;
     char buf[SIZE];
     //以只写的方式打开管道1
     fdw = open("fifo1",O_WRONLY);
     if(-1==fdw)
     {
        perror("open");
        return 1;
     }
     printf("以只写的方式打开管道1....\n");
     //以只读的方式打开管道2
     fdr = open("fifo2",O_RDONLY);
     if(-1==fdr)
     {
        perror("open");
        return 1;
     }
     printf("以只读的方式打开管道2....\n");
     //循环读写
     while(1)
     {
        //写管道1
        memset(buf,0,SIZE);
        fgets(buf,SIZE,stdin);
        //去掉最后一个换行符
        if('\n'==buf[strlen(buf)-1])
        buf[strlen(buf)-1]=0;
        //写管道
        ret = write(fdw,buf,strlen(buf));
        if(ret<=0)
        {
           perror("write");
           break;
        }
        printf("write ret:%d\n",ret);
        //读管道2
        memset(buf,0,SIZE);
        ret = read(fdr,buf,SIZE);
        if(ret<=0)
        {
           perror("read");
           break;
        }
           printf("read:%s\n",buf);
        }
       //关闭文件描述符
     close(fdr);
     close(fdw);
  }                             

运行结果如下:可以实现阻塞式的数据读取

当两个进程通信的时候,我们查看fifo的大小

可以发现,管道的大小没有发生变化。其实两个进程通信是在内存中进行的,并没有把数据写到管道中,因为管道只是一个符号性的文件。如果是在管道写数据,那么IO次数会很多,效率太低了。

posted @ 2022-10-24 21:33  一只少年AAA  阅读(394)  评论(0编辑  收藏  举报