进程间通信之管道

1、介绍

管道和有名管道是最早的进程间通信机制之一,管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

2、特点

管道:本质是一个伪文件(实为内核使用环形队列机制,借助内核缓冲区(4k)实现)。

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

半双工的,数据只能向一个方向流动;

需要双方通信时,需要建立起两个管道;

由两个文件描述符引用,一个表示读端,一个表示写端;

规定数据从管道的写端流入管道,从读端流出(写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据,不支持诸如lseek()等文件定位操作);

数据一旦被读走,便不在管道中存在,不可反复读取。

3、管道的读写行为

使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。

4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

总结:

① 读管道: 1. 管道中有数据,read返回实际读到的字节数。

2. 管道中无数据:

(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)。

(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)。

② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)。

2. 管道读端没有全部关闭:

(1) 管道已满,write阻塞。

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

4、有名管道读写行为

1.读取数据:如果有进程写打开FIFO,且当前FIFO内没有数据

(1)阻塞读(fd=open(FIFO_NAME,O_RDONLY);),则将一直阻塞等待数据。

(2)非阻塞读(O_RDONLY | O_NONBLOCK),则返回-1,当前errno值为EAGAIN,提醒以后再试。

所以对于阻塞读操作而言,造成阻塞的原因有:

(1)FIFO内有数据,但有其它进程在读这些数据

(2)FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。

2.写入数据:

(1)对于设置了阻塞标志的写操作:

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。(PIPE_BUF ==>> /usr/include/linux/limits.h)

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回(多个进程都写数据交错)。

(2)对于没有设置阻塞标志的写操作:

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

5、管道创建

int pipe(int fd[2])
fd为filedescriptors的缩写,其为一个二元数组,用于存放pipe函数所创建管道的两个文件描述符,fd[0]存放管道取端的文件描述符,fd[1]用于存放管道入端的文件描述符。通常,进程pipe()创建管道后,再fork一个子进程,调用exec函数族使子进程执行所需程序,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。通信方式:父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

6、有名管道创建

 int mkfifo(const char * pathname, mode_t mode)

该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EXIST错误,所以一般典型的调用代码首先会检查是否返回该错误。 一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

7、有名管道程序

父进程传递参数和有名管道名给子进程,子进程做完操作,写管道数据,父进程阻塞直到读到数据

父进程

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <time.h> 
int main(void)
{
    char tgid[6] = "12345";
    int pid;

    int x;
    srand(time( NULL )); 
    x =rand();
    printf("x:%d
", x);

    char fifoname[10] = "xxx";
    if ((pid = fork()) < 0){
        perror ( "failed to fork " );
        return 1;
    }
    else if (pid > 0){
        //printf("parent:%d
", getpid());
        int m = mkfifo(fifoname,0664);
        if(m == -1) {
            perror("mkfifo");
            return 1;
        }
        int fd = open(fifoname, O_RDONLY);////没有O_NONBLOCK,所以read默认open会阻塞,直到子进程写打开并且写入数据。除非O_RDONLY | O_NONBLOCK才不阻塞
        if(fd == -1) {
            perror("open");
            return 1;
        }
        char buf[5] = "0";
        int r;
        while(1)//还是搞了个whild
        {
            r = read(fd, buf, sizeof(buf));
            if(strcmp(buf, "1") == 0)
            {
                break;//读到子进程python给的值后才执行下面的
            }
        }
        close(fd);
        unlink(fifoname);//删除指定参数,即管道
        //printf("read:%s
", buf);
        return 0;
    }
    else{
        //printf("child:%d
", getpid());

        char *const p[]={"python", "/home/child.py", tgid, fifoname, NULL};//指向char的常量指针数组
        if(execv("/usr/bin/python",p)<0)
        //if(execl("/usr/bin/python","python","/home/child.py",tgid,fifoname,NULL)<0)//tgid为传给子进程python的,pipename是有名管道名
        //if(execl("./test","./test",tgid,NULL)<0)//C语言
        {
            fprintf(stderr,"execl failed:%s
",strerror(errno));
            return 1;
        }
    }
    //return 0;
}

注:上述关于exec函数族 、const关键字 、指针数组

子进程:

#!/usr/bin/python
import sys
import os
def main(argv):
    print 'python pid:%s' % (os.getpid())
    print 'python tgid:%s' % (sys.argv[1])
    f = os.open(sys.argv[2], os.O_WRONLY)
    time.sleep(10)
    print 'python end sleep'
    os.write(f, "1")
    os.close(f)


if __name__ == "__main__":
    main(sys.argv[1:])

子进程-C语言

#include <stdio.h>
#include <sys/prctl.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <error.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
    int oret;
    int wret;
    prctl(PR_SET_PDEATHSIG, SIGHUP);
    printf("child.c tgid:%s
", argv[1]);
    printf("child.c fifoname:%s
", argv[2]);

    sleep(10);
    oret = open(argv[2], O_WRONLY);
    if(oret < 0)
    {
        perror("open error");
        return 1;
    }
    wret = write(oret,"1",2);
    if(wret == -1)
    {
        perror("write error");
        return 1;
    }
    close(oret);

    return 0;
}

定义参考:

https://blog.csdn.net/u011068702/article/details/54914774

https://blog.csdn.net/firefoxbug/article/details/8137762

https://www.cnblogs.com/fangshenghui/p/4039805.html

https://blog.csdn.net/oguro/article/details/53841949

原文地址:https://www.cnblogs.com/beixiaobei/p/9568418.html