UNIX/Linux进程间通信IPC系列(二)管道

管道


一般,进程之间交换信息的方法只能是经由forkexec传送打开文件,或者通过文件系统。而进程间相互通信还有其他技术——IPC(InterProcess Communication)

(因为不同的进程有不同的进程空间,我们无法自己设定一种数据结构使不同的进程都可以访问,故需要借助于操作系统,它可以给我们提供这样的机制。IPC

 

管道是UNIX系统IPC的最古老的形式,并且所有UNIX系统都提供此种通信机制。

但是其有局限性:

①它们是半双工的(即数据只能在一个方向上流动)

②它们只能在具有公共祖先的进程之间使用。(通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道)

尽管有这两种局限性,半双工管道仍是最常用的IPC形式。

 

创建管道


#include <unistd.h>

int  pipe (int fd[2]) ;                           

该函数返回两个文件描述符:fd[0]fd[1]。前者打开来读,后者打开来写。提供一个单路(单向)数据流。

单进程的管道几乎没有任何用处,通常调用pipe的进程接着调用fork,这样就创建了从父进程到子进程的IPC通道。

 

管道的典型用途


以下述方式为两个不同进程(一个是父进程,一个是子进程)提供进程间通信手段。

UNIX/Linux进程间通信IPC系列(二)管道

首先,由一个进程(将成为父进程)创建一个管道后调用fork派生一个自身的副本。

接着,父进程关闭这个通道的读出端,子进程关闭同一通道的写入端。这就在父子进程间提供了一个单向数据流。

(管道时在内核中的数据结构,故fork不会复制管道。但管道的文件描述符在进程空间中,故fork之后,子进程获得了管道的文件描述符副本)

 

【注意】

当管道的一端被关闭后

①当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处。

②当写一个读端已被关闭的管道时,则产生信号SIGPIPE,write返回-1,errno设置为EPIPE。

 

用管道实现:客户--服务器模型


UNIX/Linux进程间通信IPC系列(二)管道

管道是在内核中的,因此从客户到服务器以及从服务器到客户的所有数据都穿越了用户--内核接口两次:一次是在写入管道时,一次是在从管道读出时。

 

//客户--服务器模型:
//客户从标准输入获取请求文件的路径,用管道传给服务器
//服务器从管道获得文件路径后,读取文件内容,把文件内容写入到另一管道
//客户从管道获取文件内容后 把其显示在标准输出
//
//此启动父子进程不需要同步。
//若server先启动,它会阻塞在read上(等待client向管道写入消息)
//若client先启动,它会阻塞在fgets获取用户请求上
//故谁先启动都行。
//
//
#include <sys/types.h>
#include <unistd.h>  //包含pipe()
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <fcntl.h>

#define MAXLINE 1024

void client(int, int) ;
void server(int, int) ;

int
main(int argc, char** argv)
{
    int    pipe1[2], pipe2[2] ;
    pid_t  childpid ;

    //创建两个管道
    pipe(pipe1) ;
    pipe(pipe2) ;

    if ((childpid = fork()) == 0) //子进程
    {
        //关闭管道1的写描述符和管道2的读描述符
        close(pipe1[1]) ;
        close(pipe2[0]) ;

        //启动服务器函数
        server(pipe1[0], pipe2[1]) ;
        exit(0) ;
    }

    //父进程
    //关闭管道1的读描述符和管道2的写描述符
    close(pipe1[0]);
    close(pipe2[1]);

    //启动客户函数
    client(pipe2[0], pipe1[1]) ;

    //等待子进程结束
    waitpid(childpid, NULL, 0) ;
    exit(0) ; 
}

void
server(int readfd, int writefd)
{
    int     fd ;
    ssize_t n ;
    char    buff[MAXLINE+1] ;

    //从管道读取来自客户端的请求消息
    if ((n = read(readfd, buff, MAXLINE)) == 0)
        perror("EOF while reading pathname") ;
    buff[n] = ‘\0‘ ;

    //以只读方式打开用户请求的文件
    if ((fd = open(buff, O_RDONLY)) < 0)
    {
        //打开失败,向用户返回出错信息
        snprintf(buff + n, sizeof(buff)-n, ":can‘t open, %s\n",
                 strerror(errno)) ;
        n = strlen(buff) ;
        write(writefd, buff, n) ;
    }
    else //打开成功
    {   //读取文件内容 写入到管道中
        while ((n = read(fd, buff, MAXLINE)) > 0)
            write(writefd, buff, n) ;
        close(fd) ;
    }
}

//readfd、writefd是用于与管道对接的
//client从管道的readfd读消息,向管道的writefd写消息
void
client(int readfd, int writefd)
{
    size_t   len ;
    ssize_t  n ;
    char     buff[MAXLINE] ;

    //从标准输入获取用户的请求(文件路径)
    fgets(buff, MAXLINE, stdin) ;
    len = strlen(buff) ;
    if (buff[len-1] == ‘\n‘)
        len-- ;

    //把用户请求的文件路径写入管道
    write(writefd, buff, len) ;

    //从管道中读取服务器端的响应消息,并输出到标准输出
    while ((n = read(readfd, buff, MAXLINE)) > 0)
    {
         write(STDOUT_FILENO, buff, n) ;
    }
}

 

用管道实现:父子进程间同步

(关于进程间同步 点这里)

//管道可用来实现父子进程间的同步
//
//
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

static int pfd1[2] ; //父进程在管道1上阻塞等待 来自子进程的消息
static int pfd2[2] ; //子进程在管道2上阻塞等待 来自父进程的消息

//初始化同步机制
void
TELL_WAIT(void)
{
    if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
        perror("pipe error!\n") ;
}

//通知父进程(即向管道1写入消息,会唤醒在其上等待的父进程)
void
TELL_PARENT(pid_t pid)
{
    if (write(pfd1[1], "c", 1) != 1)
        perror("write error!\n") ;
}

//等待父进程(在管道2上读阻塞)
void
WAIT_PARENT(void)
{
    char c ;
    if (read(pfd2[0], &c, 1) != 1)
        perror("read error\n") ;

    //检查是否是来自父进程的数据(防止伪造)
    if (c != ‘p‘)
        perror("WAIT_PARENT:incorrect data\n") ;
}

//通知子进程(即向管道2写入消息,会唤醒在其上等待的子进程)
void
TELL_CHILD(pid_t pid)
{
    if (write(pfd2[1], "p", 1) != 1)
        perror("write error!\n") ;
}

//等待子进程(在管道1上读阻塞)
void
WAIT_CHILD(void)
{
    char c ;
    if (read(pfd1[0], &c, 1) != 1)
        perror("read error\n") ;

    //检查是否是来自子进程的数据(防止伪造)
    if (c != ‘c‘)
        perror("WAIT_PARENT:incorrect data\n") ;
}

//------------------测试程序--------------------
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int 
main(void)
{
    int i = 0 ;
    int cnt = 0 ;
    pid_t pid ;

    //为同步机制初始化
    TELL_WAIT() ;

    if ((pid = fork()) < 0)
        perror("fork error") ;
    else if (pid > 0) //父进程
    {
        for (i = 0; i < 3; ++i)
        {
            printf("From parent : %d\n", i) ;
        }
        TELL_CHILD(pid) ;
        WAIT_CHILD(pid) ;
        for (; i < 6; ++i)
        {
            printf("From parent : %d\n", i) ;
        }
        TELL_CHILD(pid) ;
        return 0 ;
    }
    else //子进程
    {
        WAIT_PARENT(getppid()) ;
        for (i = 0; i < 3; ++i)
        {
            printf("From child : %d\n", i) ;
        }
        TELL_PARENT(getppid()) ;
        WAIT_PARENT(getppid()) ;
        for (; i < 6; ++i)
        {
            printf("From child : %d\n", i) ;
        }
        return 0 ;
    }
}
//只要有:阻塞-等待的机制 都可以考虑实现进程间同步。(信号、管道等)


 

popen和pclose函数


这两个函数实现的操作是:创建一个管道,调用fork产生一个子进程,关闭管道的不使用端,执行一个shell以运行命令,然后等待命令终止。(把这一系列操作打包了)

 

#include <stdio.h>

FILE* popen(const char* cmdstring,  const char* type) ;

int   pclose(FILE* fp) ;

popen在调用进程和所指定的命令之间创建一个管道。若参数type是r,则返回的文件指针连接到cmdstring的标准输出。(与文件打开fopen类比)

 

 UNIX/Linux进程间通信IPC系列(二)管道

 

(这两个函数使我们可以很方便地利用shell命令来辅助我们实现程序的功能,减少了需要编写的代码量。)

(popen的作用就是重定向shell命令的输入/输出)

 


协同进程


当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。(即其标准输入/输出都重定向到了另一个进程,专门为另外一个进程服务的)

(协同进程主要强调,我们把原先用于输入/输出的程序 改造成了为我所用。它强调不修改原程序的代码,使其为我所用。而先前的客户-服务器程序两个进程都是我们撰写的 两进程中都要对约定的管道进行操作,而协同进程对我们来说是黑盒,其代码不知道有管道。)

 

popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而对于协同进程,则它有连接到另一个进程的两个单向管道——一个接到其标准输入,另一个则来自其标准输出。我们先要将数据写到其标准输入,经其处理后,再从其标准输出读取数据。

 

UNIX/Linux进程间通信IPC系列(二)管道

【注意】

协同进程对于输入/输出的处理函数。若是低级I/O函数(write、read),没问题。若是标准I/O函数,则需要做一些处理。

因为,标准输入是个管道,所以系统默认标准I/O是全缓冲的(对于终端,默认行缓冲),对于标准输出也是如此。

这样,当协同进程从其标准输入读阻塞时,主进程从管道也读阻塞。(因为协同进程标准IO函数全缓冲,未把数据写入管道) 会产生死锁。

故,要更改协同进程的缓冲类型。

setvbuf(stdin, NULL, _IOLBF, 0) ; //设置行缓冲

setvbuf(stdout, NULL, _IOLBF, 0) ;

 

若我们无法更改协同进程的源代码,则必须使被调用的协同进程认为它的标准输入和输出都被连接到一个终端。可用伪终端实现这一点。(略)

 

【示例】

//协同进程:
//从标准输入读入两个数,相加,输出到标准输出。
//
//
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h> 

#define MAXLINE 1024

int 
main(void)
{
    int n1, n2 ;
    char line[MAXLINE] ;

    setvbuf(stdin, NULL, _IOLBF, 0) ; //设置行缓冲
    setvbuf(stdout, NULL, _IOLBF, 0) ;

    //用标准IO从键盘获取用户输入
    while (fgets(line, MAXLINE, stdin) != NULL)
    {
        if (sscanf(line, "%d%d", &n1, &n2) == 2)
            if (printf("%d\n", n1+n2) == EOF)
                perror("printf error\n") ;
    }

    exit(0) ;
}

//协同进程的主控进程
//
//
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h> 

#define MAXLINE 1024

void sig_pipe_handler(int signo) ;

int 
main(void)
{
    size_t n ;
    pid_t pid ;
    int   fd1[2], fd2[2] ; //用于连接协同进程的两个管道
    char  line[MAXLINE] ;

    //涉及管道的,设置一个对SIGPIPE信号的处理程序
    if (signal(SIGPIPE, sig_pipe_handler) == SIG_ERR)
        perror("signal error\n") ;

    //创建两个管道用于和协同进程通信
    if (pipe(fd1) < 0 || pipe(fd2) < 0)
        perror("pipe error\n") ;

    if ((pid = fork()) < 0)
        perror("fork error\n") ;
    else if (pid > 0) //父进程(向管道发送消息,从管道获取协同进程对消息的处理结果)
    {
        //关闭不用的管道文件描述符
        close(fd1[0]) ;
        close(fd2[1]) ;
        
        while ((fgets(line, MAXLINE, stdin)) != NULL)
        {
            n = strlen(line) ;
            //把用户的输入 写到管道 传给协同进程
            if (write(fd1[1], line, n) != n)
                perror("write error from pipe\n") ;
            //从管道读取协同进程的反馈
            if ((n = read(fd2[0], line, MAXLINE)) < 0)
                perror("read error from pipe\n") ;
            if (n == 0)
            {
                perror("child close pipe\n") ;
                break ;
            }
            //把协同进程的处理结果 打印到屏幕
            line[n] = 0 ;
            if (fputs(line, stdout) == EOF)
                perror("fputs error\n") ;
        }
        exit(0) ;
    }
    else //子进程(把两管道分别连接到协同进程的 输入/输出端)
    {
        close(fd1[1]) ;
        close(fd2[0]) ;
        
        //把标准输入描述符 重新设置为 管道的fd1[0]
        if (fd1[0] != STDIN_FILENO)
        {   
            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) //dup2用于复制描述符
                perror("dup2 error\n") ;
            close(fd1[0]) ;
        }

        //把标准输出描述符 重新设置为 管道的fd2[1]
        if (fd2[1] != STDOUT_FILENO)
        {   
            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) //dup2用于复制描述符
                perror("dup2 error\n") ;
            close(fd2[1]) ;
        }

        //启动协同进程
        if (execl("./add", "add", (char*)0) < 0)
            perror("execl error\n") ;
    }
    exit(0) ;
}


void
sig_pipe_handler(int signo)
{
    printf("SIGPIPE caught\n");
    exit(1) ;
}


 

UNIX/Linux进程间通信IPC系列(二)管道

上一篇:特殊字符、Date、JS应用


下一篇:从12306谈起验证码的架构