zoukankan      html  css  js  c++  java
  • Linux进程间通信——匿名管道

    进程如果不是独立进程,那么它就需要和别的进程进行通信。在进程协作时可以采用共享一个缓冲区的方式来实现。当然,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
    ",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
    ",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!
    ");
           }
           else
           {
               printf("write to pipe is %d
    ",num);
           }
            close(filedes[1]);
            wait(NULL);
            //exit(0);
        }
        else
        {
            //子进程不读,不写,直接将管道文件两端都关闭
            close(filedes[0]);
            close(filedes[1]);
            exit(0);
        }
        
        return 0;
    }

    输出结果如下:

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

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

  • 相关阅读:
    使用集合组织相关数据
    深入类的方法
    深入C#数据类型
    上机练习1 更新会员积分
    魔兽争霸登录
    jQuery
    打卡系统
    [工具]kalilinux2016.2 更新后
    [技术分享]借用UAC完成的提权思路分享
    [技术分享]利用MSBuild制作msf免杀的后门
  • 原文地址:https://www.cnblogs.com/zy666/p/10504274.html
Copyright © 2011-2022 走看看