Linux进程间通信——匿名管道

自古美人都是妖i 提交于 2019-12-23 10:38:46

进程如果不是独立进程,那么它就需要和别的进程进行通信。在进程协作时可以采用共享一个缓冲区的方式来实现。当然,OS的IPC提供了一种机制,以允许不必通过共享地址空间来通信和同步其动作。这就不得不提Linux的的前身Unix。因为Linux一开始就是从这儿借鉴的。加上Linux从一开始就遵守POSIX标准。

Unix最早是由AT&T的贝尔实验室开发的,值得一提的是,在Unix操作系统发展的过程中,产生了许多副产物(POSIX标准也是副产物之一),其中最著名的应当是C语言。是的,它仅仅是个副产物。那个时候Ken Thompson 与Dennis Ritchie感到用汇编语言做移植太过于头痛,他们想用高级语言来完成第三版。后来他们改造了B语言,就形成了今天大名鼎鼎的C语言。这个自发明到现在这个物联网时代仍占据编程语言榜前10的稳固位置。不得不感叹其生命力的强大以及适应性的强大。当然,Ken Thompson 与Dennis Ritchie也是图灵奖得主。

到了1980年,有两个最主要的Unix的版本线,一个是UC Berkeley的BSD UNIX,另一个是AT&T的Unix。至今为止UC Berkeley仍在维护Unix(这学校真牛逼)。

最初的Unix的IPC包括,管道,FIFO,信号。贝尔实验室对Unix早期的进程通信进行了改进,形成了system V这个操作系统的IPC。它包括:system V消息队列,system V信号灯,system V共享内存。当然POSIX IPC也有相应的一套。BSD Unix设计了socket(套接字)通信。这样将进程之间的通信不仅仅限制在单机内。Linux继承了这些。

进程间通信的目的:

  1. 数据传输:一个进程将数据发送给另一个进程
  2. 共享数据:多个进程操作共享数据(比如:售票系统),一个进程对共享数据进行了修改,另外一个进程应该立即看到,(否则票买完了,但是另一边不知道,还在卖)
  3. 通知:一个进程告诉另外一个进程发生了某些事件。
  4. 资源共享
  5. 进程控制:一个进程控制另外一个进程的执行(例如debug程序)。它希望知道另一个进程的实时状态。

Linux进程通信方式:

管道:管道(pipe)分为无名管道和有名管道。无名管道用于具有亲缘关系进程间的通信,有名管道则可以在任意的进程中间进行通信。

管道通信具有以下的特点:

  1. 管道是半双工的。(双向通信的,但是不能同时向双方传输)
  2. 只能用于父子进程或者是兄弟进程之间(就是要具有亲缘关系)
  3. 管道是一种文件(能读写),它只存在于内存之中。他是具有亲缘关系的进程共享的。
  4. 写入的内容每次都添加到管道缓冲区的末尾,并且每次都是从缓冲区的头部读取数据。

Linux建立无名管道函数是pipe函数。它需要的头文件是#include<unistd.h>.

函数原型:int pipe(int filedes[2]);

函数功能:pipe建立一个无名管道文件,若成功返回0,否则返回-1.错误原因由errno给出。管道文件的描述符由filedes数组返回。其中filedes[0]为管道的读取端,filedes[1]为写入端。

代码测试如下:

#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
#include<linux/limits.h>        //这个头文件中有PIPE_BUF


int main()
{
    int filedes[2];         //保存管道文件的文件描述符
    char str[30] = {"Hello World!"};
    char temp[30] = {0};

    if(0 != pipe(filedes))      //创建管道失败
    {
        printf("errno=%d\n",errno);
        return 0;
    }
    if(0 < fork())          //父进程
    {
        close(filedes[0]);      //为避免不必要的错误,关闭读端
        write(filedes[1],str,strlen(str));
        close(filedes[1]);
        wait(NULL);         //回收子进程
        exit(0);
    }
    else
    {
        sleep(3);      //让父进程先执行
        close(filedes[1]);      //为避免不必要的错误,关闭写端
        read(filedes[0],temp,strlen(str));
        close(filedes[0]);
        printf("%s\n",temp);
        exit(0);
    }
    
    return 0;
}

在读写管道文件的时候,最好是严格遵守文件的读写规则,在使用完毕后一定要关闭文件。为了避免不必要的一些错误,在使用管道的文件的要先创建管道文件,然后创建新进程,这样所有的进程才能共享这个管道文件。代码中为了避免向读取端写入和从写入端读取而引发的错误,在读的时候关闭写端,在写的时候关闭读端。

代码中先让父进程向管道文件中写入了字符串“Hello World!”。然后子进程读取管道文件中的字符串,并向屏幕打印。程序执行结果如下:

如果子进程读取到的管道文件为空,那么read()函数将会使得进程阻塞,这时候父进程将会执行,然后完成对管道文件的写入。之后wait()将父进程挂起,子进程完成读取。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。(典型的生产者——消费者模型)管道是存在于内存中的文件(实际上内核实现的一个队列),他是进程的资源,会随着进程的销毁而销毁。还有一点是管道中的东西在读取后就会被删除。管道文件有大小限制的,在我现在的内核版本下他是4KB。管道文件的大小由PIPE_BUF描述。它在#include<linux/limits.h>这个头文件中给出。

#define PIPE_BUF        4096	/* # bytes in atomic write to a pipe */

向管道写入数据的时候Linux不保证写入的原子性,管道缓冲区一有空闲,写进程就会去写入。所以要及时读取管道文件

同时管道还要求写端对读端的依赖性,示例代码如下:

#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
#include<linux/limits.h>        //这个头文件中有PIPE_BUF


int main()
{
    int filedes[2];         //保存管道文件的文件描述符
    char str[30] = {"Hello World!"};
    char temp[30] = {0};
    
    int num;
    if (0 < fork()) 
    {
        sleep(1);
        close(filedes[0]);      //关闭读端
       num =  write(filedes[1],str,strlen(str));
       if(-1 == num)
       {
           printf("error!\n");
       }
       else
       {
           printf("write to pipe is %d\n",num);
       }
        close(filedes[1]);
        wait(NULL);
        //exit(0);
    }
    else
    {
        //子进程不读,不写,直接将管道文件两端都关闭
        close(filedes[0]);
        close(filedes[1]);
        exit(0);
    }
    
    return 0;
}

输出结果如下:

这个时候,在父进程中将无法写入。所以管道这个描述还是很形象的,当你向一段水管里面装水的时候,需要将另一端堵上,否则装入的水全都流走了。因此在父进程写的时候,需要先关闭读;在子进程读的时候需要先关闭写。同时,不能在没有读的情况下将管子两头堵上。

当子进程结束的时候,父进程关闭读,调用write写数据,这时候父进程将会收到子进程SIGPIPE信号,当前进程将会中断,而不是阻塞。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!