多进程与多线程(五)

1.1.2.2          管道

管道,就是连接一个进程的输出和另一个进程的输入的单向通道。实质是一个文件。

仔细看看管道的定义,就能明白管道的用途。

首先,涉及了一个进程和另一个进程,这意味着多进程结构。

其次,管道起连接作用,连接不同进程间的输出和输入。

第三,管道是单向的,方向是输出到输入。

多进程结构其实无所不在的,管道的使用也非常普遍。

以我们熟悉的popen(const char * command, const char * type)函数为例。我们知道,在Linux等Xnix系统中,popen()会调用fork()产生子进程,然后从子进程中调用“/bin/sh –c”来执行参数command的指令。那么,Windows系统下,popen函数又是怎么实现的呢?

其实,不管是在Windows还是Linux下,其实现机制是一样的道理。都是创建出一个进程来负责执行“command”命令。我们以Windows的代码为例,看看popen具体做了些什么,在这个例子中,我们将看到多进程和管道的协调通讯。

第一,popen函数通过_pipe函数创建了一个管道,这个管道象我们现实生活中的水管一样,有进水的一头和出水的一头,进水的一头我们称之为“写端”,“写”水到可单向流动水的出水的方向,出水的一头我们称之为“读端”,即把水管中流出的水接收。

第二,由于父子进程之间能共享句柄等父进程中的资源,但是作为物理性通道的管道,却不能被父子进程共享[1],所以,需要复制管道的一端给父进程,而另外一端给子进程(或者说:需要复制管道的一端给子进程,而另外一端给父进程)。所以,DuplicateHandle函数被调用来完成管道的复制工作,以便得到新的句柄被父或子继承单独享用。没有被复制的管道的另外一端,就被作为popen函数的返回值在函数结束后返回。

第三,为创建子进程所做的准备。具体表现就是要为子进程的标准输入、标准输出、标准错误输出赋值。这时,需要我们把握popen是如何为上述三者赋值的。如果是写(“w”)模式,创建管道时的读端就被直接赋给子进程的标准输入;否则,如果是读(“r”)模式,则被复制出的写端被赋值给子进程的标准输出(读模式这种管道的使用,可参见PG代码exec.c的P413页pipe_read_line函数的实现)。

第四,万事俱备。这一步就是调用CreateProcess函数创建子进程。

第五,关闭被复制而得的newhnd,父进程中一定要关闭被赋给子进程的管道那端,否则,如果管道的缓冲区满了之后,而父进程或子进程退出,则会造成子进程或父进程被阻塞。[2]

第六,其他一些错误处理的相关代码,就不再说明。

 

FILE *_popen (

        const _TSCHAR *cmdstring,

        const _TSCHAR *type

        )

{

......

// int phdls[2],创建管道,将来给子进程使用,注意读端和写端的使用

    if ( _pipe( phdls, PSIZE, tm ) == -1 )          goto error1;

             

    //根据参数中的读写模式.,决定子进程如何和管道的读端和写端挂钩

    if ( *type == _T('w') ) {

            stdhdl = STDIN;

            i1 = 0;

            i2 = 1;

    }

    else {

            stdhdl = STDOUT;

            i1 = 1;

            i2 = 0;

    }

……

    //获取当前进程的一个伪句柄

prochnd = GetCurrentProcess();

 

       //如果是”w”模式,i1的值是0,则复制phdls[0],即复制管道的读端,将来作为子进程的输入

//如果是”r”模式,i1的值是1,则复制phdls[1],即复制管道的写端,将来作为子进程的输出

//注释1: 注意与”注释2”处的代码结合,体会管道的单向流通性

    if ( !DuplicateHandle( prochnd,

                           (HANDLE)_osfhnd( phdls[i1] ),

                           prochnd,

                           &newhnd, //复制出一个新句柄给子进程使用

                           0L,

                           TRUE,                    /* inheritable */

                           DUPLICATE_SAME_ACCESS )

    )

    {

              goto error2;

    }

      

       //一定要记着在父进程中关闭管道无用的那一端

       //即:

       //如是“w”模式,关闭读端(读端被复制给子进程使用,从而可以从父进程写数据给子进程)

       //如是“r”模式,关闭写端(写端被复制给子进程使用,从而可以从子进程写数据给父进程)

    (void)_close( phdls[i1] );

 

//把句柄准备作为返回值传出,即传出pstream

//如果是”w”模式,i2的值是1,即对应管道的写端,将来作父子进程的输出

//如果是”r”模式,i2的值是0,即对应管道的读端,将来作父子进程的输入

//注释2: 注意与”注释1”处的代码结合,体会管道的单向流通性

    if ( (pstream = _tfdopen( phdls[i2], type )) == NULL )

                     goto error2;

......

    //构造cmd.exe或command.com命令,将来作为CreateProcess的第一个参数

    if ( ((cmdexe = _tgetenv(_T("COMSPEC"))) == NULL &&

          ((errno == ENOENT) || (errno == EACCES))) )

        cmdexe = ( _osver & 0x8000 ) ? _T("command.com") : _T("cmd.exe");

 

    memset(&StartupInfo, 0, sizeof(StartupInfo));

    StartupInfo.cb = sizeof(StartupInfo);

 

//很重要,根据读写模式,决定着子进程的输入和输出的句柄

//另外,可以根据实际需求为StartupInfo的其他属性赋值,以决定子进程的一些属性

//_osfhnd是系统定义的宏,用于获取句柄,参数0/1/2分别表示stdin/stdout/stderr

    StartupInfo.hStdInput = stdhdl == STDIN ? (HANDLE) newhnd

                                            : (HANDLE) _osfhnd(0);

 

    StartupInfo.hStdOutput = stdhdl == STDOUT ? (HANDLE) newhnd

                                              : (HANDLE) _osfhnd(1);

    StartupInfo.hStdError = (HANDLE) _osfhnd(2);

    StartupInfo.dwFlags = (STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES);

 

       //根据传入的第一个参数构造使用CreateProcess创建的子进程的第二个参数

    if ((CommandLine = _malloc_crt( (_tcslen(cmdexe) + _tcslen(_T(" /c ")) + (_tcslen(cmdstring)) +1) * sizeof(_TSCHAR))) == NULL)

        goto error3;

    _tcscpy(CommandLine, cmdexe);

    _tcscat(CommandLine, _T(" /c "));

    _tcscat(CommandLine, cmdstring);

……

       //创建子进程,在Windows的控制台下执行用户指定的命令

    if (_taccess(cmdexe, 0) != -1) {      

        childstatus = CreateProcess( (LPTSTR) cmdexe,

                                     (LPTSTR) CommandLine,

                                     NULL,

                                     NULL,

                                     TRUE,

                                     0,

                                     NULL,

                                     NULL,

                                     &StartupInfo,

                                     &ProcessInfo

                                     );

    }  

    else

    {

//一些错误处理代码,省略……   

}

……

//在父进程中关闭曾赋值给子进程标准输入或标准输出的句柄

//此处的句柄关闭很必要,比如,在“w”模式下,如果此处不关闭这个句柄,

//则当子进程异常退出时,父进程有可能因写满管道缓存而被阻塞

CloseHandle((HANDLE)newhnd);

……

       //一些错误处理代码,省略……

    return pstream;

}

 

       上面的例子是Windows的代码,用户可以自己写代码使用popen函数,然后把断点打在popen函数处,使用单步调试,进入到popen函数中仔细研读一下popen的实现代码。

       另外一个小窍门:微软的SDK包中包括有开放的popen函数实现,在popen.c文件的P104行处,可以查阅popen和pclose的实现代码。

       管道是有生命期的,当存取它的进程结束时,管道也自动消亡。另外有种管道叫做“有名管道”,这种管道因为有名有姓,所以在操作系统下已经标名挂号,所以它有文件属主、大小、访问权限等属性,这个需要我们注意。

在PG的代码中,对于管道的使用,还可以参见syslogger.c文件中SysLogger_Start函数。



[1] 为什么管道不能被父子进程共享而必须是单向的?

  管道的实现依赖于操作系统,而操作系统没有为管道提供锁定的保护机制,这决定了管道必须是单向的。假设管道可以被父子进程共享,则一个写端即可以被父进程写入数据也可以被子进程写入数据,这时,依据管道现有的实现机制,区分数据该被父进程读取还是被子进程读取就很困难。

[2] MSDN [keyword: _pipe]: In the Windows operating system, a pipe is destroyed when all its descriptors have been closed. (If all read descriptors on the pipe have been closed, writing to the pipe causes an error.) All read and write operations on the pipe wait until there is enough data or enough buffer space to complete the I/O request.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值