栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【Linux操作系统】--进程间通信(一)匿名管道

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Linux操作系统】--进程间通信(一)匿名管道

目录

进程间通信介绍

进程间通信目的

进程间通信发展

管道

什么是管道

站在文件描述符角度-深度理解管道

匿名管道

建立信道

开始操作-管道基本特性

 总结:

命名管道


进程间通信介绍

有时候进程之间可能会存在特定的协同工作的场景!那么进程之间的协同工作,就是进程之间的通信。进程的通信就是一个进程要把自己的数据交付给另一个进程,让其进行处理。因为进程是具有独立性的,如果要进行通信,那么通信双方一定是通过某种介质来进行通信。比如我跟你通信是通过某信进行交流。所以进程间通信的介质时操作系统,操作系统要设计通信方式。

因为进程是具有独立性的!并且交互数据,成本很高。一个进程是看不到另一个进程的资源,所以必须得先看到一份公共的资源,这里的资源就是一段内存,这个公共资源是属于操作系统的。

所以进程间通信的前提本质:其实是由OS参与,提供一份所有通信进程都能看到的公共资源。这段内存提供公共资源的方式可能以文件方式提供,也可能以队列方式提供,也可能提供的就是原始的内存块。这也是通信方式很多种的原因。

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

进程间通信发展
  • 管道
  • System V进程间通信
  • POSIX进程间通信

管道

什么是管道

当创建了父子进程,因为父子进程是独立的两个进程,进程的PCB块是描述进程的控制块,其中也包括了文件操作符数组。当我们要向文件写入信息,要传入一个文件操作符,通过这个文件操作符fd,来查找对应的文件。就比如下图的文件属性结构体它的文件操作符是3,如果要向3这个文案金写入内容,那么就通过PCB里面的文件指针找到文件操作符数组,再通过文件操作符数组最硬的3找到对应的文件属性。

因为进程具有独立性,所以父子进程是两个独立的进程。因为是独立的进程,所以子进程要将父进程的所有内容拷贝一份,其中也包括文件操作符数组。但是文件属性结构体并不属于进程,而属于操作系统的,因为操作系统要向磁盘读取文件,就要把文件的信息都读入操作系统,这些属性放在一个结构体中,struct file和进程只是有关系,但并不属于进程。所以文件属性结构体不用复制两份。

当对文件进行写操作时,比如说write(3,"hello world");找到文件结构体后,在文件属性中找到对应的写操作,将进程缓冲区的“hello world”写到OS缓冲区中,然后找到磁盘驱动对应的写操作。进而将OS缓冲区的“hello world”写进磁盘文件中。

当父子进程都是指向同一块文件结构体,这就是操作系统参与,让不同的进程看到统一个内容,操作系统就起到了媒介作用。当父进程将“hello world”写进OS的内核缓冲区中,不刷新磁盘,不调用底层磁盘驱动的读写方法,将“hello world保留在缓冲区中,那么另一个进程子进程就可以通过它的文件描述符找到对应的同一个struct file,找到同一个缓冲区的数据。此时就做到了将一个进程的数据交给下一个进程,这就叫做让不同进程看到同一份资源,这种基于文件的通信方式叫做管道。

站在文件描述符角度-深度理解管道

 第一步:父进程创建管道。创建管道用到的系统结构是pipe,它的参数是一个输出型参数:我们想通过这个参数读取到打开的两个fd。通过传递一个数组,数组会发生降维,传递一个数组就是传递一个指针,最后pipe将数据写入数组中,我们就能拿到对应的文件描述符fd。

pipe的返回值,文档告诉我们,如果返回0创建fd成功,如果失败返回-1.

 第二步:父进程fork出子进程。分别以读方式和写方式打开一个管道,实际上这里的管道可以看作内核的缓冲区。在同一个进程中,文件可以被打开两次。就像管道,即可以读文件,又可以写文件,但我们通常只读或只写,不会两个都做。

管道是一个只能单向通信的通信信道,想要双向通信就建立两个管道。

第三步:如果子进程写,父进程读。子进程将读文件fd[0]关闭,父进程将写文件fd[1]关闭。

匿名管道

建立信道

第一步:创建管道

因为管道是对两个进程进行通信,所以数组有两个描述符。我们将它初始化为0。当管道创建完毕,我们可以知道这两个描述符是3和4,因为0,1,2是stdin,stdout,stderr,所以第一个最小空文件描述符是3,从3开始。

那么创建好的数组后,pipefd[0]=3,pipefd[1]=4。规定0下标代表读,1下标代表写。

#include 
#include 

int main()
{
  int pipefd[2]={0};
  if(pipe(pipefd)!=0)//如果返回的不是0,创建失败,打印错误信息,然后返回错误码
  {
    perror("pipe error!");
    return 1;
  }

  printf("pipe[0]:%dn",pipefd[0]);
  printf("pipe[1]:%dn",pipefd[1]);
  return 0;
}

#写一个makefile文件
[wjy@VM-24-9-centos pipe]$ cat makefile
pipe_process:pipe_process.c
	gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
	rm -f pipe_process

#运行结果
[wjy@VM-24-9-centos pipe]$ make
gcc -o pipe_process pipe_process.c -std=c99
[wjy@VM-24-9-centos pipe]$ ./pipe_process 
pipe[0]:3
pipe[1]:4

第二步:创建父子进程,并让父进程读,子进程写。

创建了父子进程,那么就创建好了双向通信的信道。当我子进程写,关闭读pipefd[0]文件;当父进程读,关闭写pipefd[1]文件,这样才建立好管道,可以进行通信了。

[wjy@VM-24-9-centos pipe]$ cat pipe_process.c
#include 
#include //exit的头文件
#include 

int main()
{
  int pipefd[2]={0};
  if(pipe(pipefd)!=0)//如果返回的不是0,创建失败,打印错误信息,然后返回错误码
  {
    perror("pipe error!");
    return 1;
  }

  printf("pipe[0]:%dn",pipefd[0]);
  printf("pipe[1]:%dn",pipefd[1]);

  //父进程读,子进程写。
  if(fork()==0)
  {
    //子进程写,所以关闭读pipefd[0]文件
    close(pipefd[0]);
    
  }

  //父进程读,所以关闭写pipefd[1]文件
  close(pipefd[1]);
  return 0;
}

开始操作-管道基本特性

特性1:

当我们要将内容写入子进程,因为pipefd数组都是文件描述符,所以我们进行读写还可以用系统调用接口read和write

子进程进行写入,每隔一秒写入一次。父进程进行读操作时候,将文件读入buffer缓冲区,然后再进行打印。这里可以发现,父进程是不断读的,子进程是间隔写的,也就是说父进程读的块,子进程写的慢。

然后运行程序打印结果,成功输出,这就是子进程将数据通过管道发送给父进程。

if(fork()==0)//子进程
  {
    close(pipefd[0]);
    
    const char* msg="hello world!";
    while(1)
    {
      write(pipefd[1],msg,strlen(msg));//这里的msg不需要+1【strlen(msg+1)】
      sleep(1);
    }
  }

//父进程
  close(pipefd[1]);
  while(1)
  {
    char buffer[64]={0};
    ssize_t s=read(pipefd[0],buffer,sizeof(buffer));//read的返回值为0,通常情况下文件读取结束,在这里代表子进程关闭文件描述符了。
    //ssize_t 是一个有符号整数
    if(s==0)//读取结束
      break;
    else if(s>0)//读取成功
    {
      buffer[s]=0;//读取的字符串以0结尾,遇到0结束。
      printf("child say#%sn",buffer);
    }
    else //s<0读取失败,直接退出
      break;
  }
  return 0;
}

#运行结果
[wjy@VM-24-9-centos pipe]$ ./pipe_process 
pipe[0]:3
pipe[1]:4
child say#hello world!
child say#hello world!
child say#hello world!
child say#hello world!
ichild say#hello world!
child say#hello world!
^Z

当我们让子进程不休眠,一直不断的写入;父进程会休眠1秒,间接性的读取数据,每隔一秒读一次数据,我们再看一下结果。

父进程read向buffer缓冲区中少写一个字符,因为子进程不断写入,父进程每次读取都会写满整个缓冲区,这样的话没有地方放下面设置的结尾字符,所以要留出一个空间给到。

if(fork()==0)
  {
    close(pipefd[0]);
    
    const char* msg="hello world!";
    while(1)
    {
      write(pipefd[1],msg,strlen(msg));//这里的msg不需要+1【strlen(msg+1)】
 //     sleep(1);
    }
  }

  close(pipefd[1]);
  while(1)
  {
    sleep(1);
    char buffer[64]={0};
    ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);//这里要少获取一个字符,用来给下面设置的结尾
    if(s==0)//读取结e束
    {
      printf("child quit...n");
      break;
    }
    else if(s>0)//读取成功
    {
      buffer[s]=0;
      printf("child say#%sn",buffer);
    }
    else //s<0
    {
      printf("child error...n");
      break;
    }
  }
//运行结果
[wjy@VM-24-9-centos pipe]$ ./pipe_process 
pipe[0]:3
pipe[1]:4
child say#hello world!hello world!hello world!hello world!hello world!hel
child say#lo world!hello world!hello world!hello world!hello world!hello 
child say#world!hello world!hello world!hello world!hello world!hello wor
^Z

最后发现,结果打印是一行一行显示的,而不是一个字符串的显示。这是因为pipe里面write只要有缓冲区,就一直写入。read在读取的时候,只要有数据就可以一直读取。这种特性就是字节流。这是管道的第一个基本特性.其实这个就是匿名管道,但是讲到现在,并不能特别体现出它是匿名管道,在对比了命名管道,我们才能体会到匿名管道的特性。

匿名管道特性:

  • 管道是一个只能单向通信的通信信道。
  • 管道是面向字节流的!

特性2:

我们让子进程不断写入字符a,写入的时候计数。父进程不将数据读取出来。我们查看结果,发现子进程到65536就不写了,这个打出的值就是我们具体写出来多少字节。

这个65536就是64*1024,也就是64KB。写端write写满64KB的时候就不再写入了,因为管道有大小,我用的是云服务器测试,云服务器的管道大小是64KB.为什么当写端write写满64KB的时候不写了?明明服务器在写满64KB后可以将还没有读出来的数据覆盖再写入,你不读我再刷一遍。实际上现在的技术是可以做到的,但是为什么没有这么做呢?因为要让读端来读,第一如果数据还没有被读出来就覆盖没有了,那么以前读操作所做的一切就白干。第二我们实际上是跟读来协作的,当对方没有来得及读的时候,覆盖了就不能再写了。不写的本质是我要等对方来读。

if(fork()==0)//写
  {
    close(pipefd[0]);
    int count=0;
    while(1)
    {
      write(pipefd[1],"a",1);
      count++;
      printf("count:%dn",count);
    }
  exit(0);
  }

  close(pipefd[1]);
  while(1)
  {
    sleep(1);
  }


 当从管道中读取数据,先读64个,发现读64个后,子进程不会向管道写入。当我们将父进程缓冲区大小变成4*1024个大小,4KB,子进程开始写入了。

父进程读取几个/64个都不行,子进程都不能写入,父进程必须写入4KB子进程才写入。所以管道的第四个特点:当管道还有数据,就一直读,直到管道数据被读完之后才能继续向管道中写,所以管道自带同步机制。

  if(fork()==0)//写
  {
    close(pipefd[0]);
    int count=0;
    while(1)
    {
      write(pipefd[1],"a",1);
      count++;
      printf("count:%dn",count);
    }
  exit(0);
  }

  close(pipefd[1]);
  while(1)
  {
    sleep(10);
    char c[1024*4+1]={0};
    ssize_t s=read(pipefd[0],c,sizeof(c));
    c[s]=0;

    printf("father take:%cn",c[0]);
  }


在pipe中有一个pipe capacity,Linux系统下是64KB,而pipe_BUF在linux系统下是4KB。

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据大于PIPE_BUF时,linux将不再保证写入的原子性。

什么是原子性,当管道写满的时候,读出数据需要一批一批的读出,这一批的大小就是4KB.我们可以来验证一下,当父进程一次读取2KB,那么是不会写入的,当第二次再读,将4KB都读取出来,那么子进程开始写入。所以管道写入唤醒是有管道策略的。


当子进程只写入一行数据,读数据不断读,出现的结果是读出了写入的一行数据之后,就读完文件了,那么read返回0,就读不出来数据了。

当子进程写入不断写入数据,父进程读数据只读把管道数据读出之后就退出父进程,读文件关闭,那么当父进程关闭后,子进程也会随之关闭。虽然子进程一直在写入数据,但是当我们关闭读端。写端还在写入,此时站在OS的层面,这是严重不合理的,已经没有人读了,你还在写入,本质就是浪费OS的资源,OS会终止写入进程!

//不断读入
void test2(int pipefd[])
{
  if(fork()==0)//写
  {
    close(pipefd[0]);

    const char* msg ="hello world";
    while(1)
    {
      write(pipefd[1],msg,strlen(msg));
      sleep(10);
      break;
    }
    close(pipefd[1]);
    exit(0);
  }

  close(pipefd[1]);
  while(1)
  {
    char c[64]={0};
    ssize_t s=read(pipefd[0],c,sizeof(c));
    if(s>0)
    {
      c[s]=0;
      printf("father take:%sn",c);
    }
    else if(s==0)
    {
      printf("writer quit...n");
      break;
    }
    else 
      break;
  }
}

//不断写入
void test3(int pipefd[])
{
  if(fork()==0)
  {
    close(pipefd[0]);
    const char* msg="hello world";
    while(1)
    {
      write(pipefd[1],msg,strlen(msg));
    }
    close(pipefd[1]);
    exit(0);
  }

  close(pipefd[1]);
  while(1)
  {
    char c[64]={0};
    ssize_t s=read(pipefd[0],c,sizeof(c));
    if(s>0)
    {
      c[s]=0;
      printf("father take:%sn",c);
    }
    else if(s==0)
    {
      printf("write quit..n");
      break;
    }
    else 
      break;
    break;
  }
  close(pipefd[0]);
}

 那么父进程结束,子进程怎么结束的呢?OS通过给目标进程发送信号SIGPIPE!

 当父进程退出,子进程也跟着退出。子进程还在不断写入时候就被退出了,这也算异常。那么父进程可以用waitpid读取到子进程的退出状态。那么如何查看子进程如何退出的呢?

 总结:

4种情况:

  1. 读端不读或者度的慢,写端要等读端
  2. 读端关闭,写端受到SIGPIPE信号直接终止
  3. 写端不写或写的慢,读端就要等写端
  4. 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾。

匿名管道5个特点:

  1. 管道是一个只能单向通信的通信信道
  2. 管道是面向字节流的
  3. 仅限于父子通信种--具有血缘关系的进程进行进程间通信
  4. 管道自带同步机制,原子性写入
  5. 管道的生命周期是随进程的。

命名管道

为了解决解决匿名管道只能父子通信,引入了命名管道。命名管道和匿名管道非常相似,出了一点匿名管道需要在父子间进行通信,而命名管道就是为了解决这个问题的。

命名管道的创建需要用mkfifo,这就是创建命名管道的命令。它的权限是以p开头的,这种文件我们称之为命名管道。


当我们打开两个服务器,两个服务器就是两个进程,当在一个进程中写入内容,另一个内容读取信息,我们通过这个管道从一个进程输入内容,在另一个进程显示出来。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/859991.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号