1. 程式人生 > >父子程序的雙向通訊簡明解讀(c程式)。

父子程序的雙向通訊簡明解讀(c程式)。

兩個程序之間通訊的方式

你可以用system, popen,pipe 實現兩個程序的通訊!

1.如何利用一個現有的程式? 系統呼叫system()是個不錯的選擇,

1.system 呼叫的實現方式

//下面是uClibc-0.9.33 的實現,為方便閱讀,程式碼有刪減。
//由以下程式碼可知, system 就是呼叫 fork() 函式,
//子程序呼叫execl 執行command命令;
//主程序等待子程序完成。

int __libc_system(const char *command)
{
    //此簡化程式碼忽略了對訊號的處理程式碼,那部分功能主要實現主程序忽略SIGQUIT, SIGINT ...
int wait_val, pid; if (command == 0) return 1; if ((pid = vfork()) < 0) return -1; if (pid == 0) { // 子程序執行程式 execl, execl("/bin/sh", "sh", "-c", command, (char *) 0); _exit(127); } //主程序等待子程序結束 if (wait4(pid, &wait_val, 0, 0) == -1) return -1 return
wait_val; }

優點: system() 呼叫使用簡單,
缺點:當我們需要與程式互動時,system 就不能勝任了。
system() 函式呼叫相當於批命令呼叫或者執行程式命令

2. 用pipe實現父子程序的通訊popen()

popen 函式可以按讀的方式或者寫的方式開啟管道,通過管道與command 程式進行通訊。

2.1 popen() 函式的實現方式

程式碼有刪減, 英文是原註釋/**/,含中文行為本人註釋//。
由以下程式碼可知,popen 就是建立一個pipe, 父程序開啟pipe的一端,子程序開啟另一端。
子程序把pipe 端與stdin 或 stdout 關聯,執行 execl(command) 命令, 這樣通過pipe完成
了父子程序的通訊。
它就等價於linux-shell 中的重定向> 或者<

FILE *popen(const char *command, const char *modes)
{
    FILE *fp;
    int pipe_fd[2];            //pipe_fd[0]為讀端,pipe_fd[1]為寫端
    int parent_fd;
    int child_fd;
    int zeroOrOne;            
    pid_t pid;

    zeroOrOne = 0;            // Assume write mode. 
    if (modes[0] != 'w') {        // if not write mode, it must be read mode
        ++zeroOrOne;        
        if (modes[0] != 'r') {    /* Oops!  Parent not reading either! */
            __set_errno(EINVAL); // 開啟模式只能是讀或者寫, 否則出錯。
            goto RET_NULL;        // 寫模式,zeroOrOne = 0; 讀模式 zeroOrOne = 1;
        }
    }

    if (pipe(pipe_fd)) {// 建立pipe,得到兩個描述符 總是fd[0]為輸入端, fd[1]為輸出端
        goto FREE_PI;
    }

    child_fd = pipe_fd[zeroOrOne];         //使得child_fd, parent_fd 為管道的兩端
    parent_fd = pipe_fd[1-zeroOrOne];    //寫模式(父寫子讀),pipe_fd[0]->child_fd, pipe_fd[1]->parent_fd
                                            //讀模式(父讀子寫),pipe_fd[0]->parent_fd,pipe_fd[1]->child_fd

    if (!(fp = fdopen(parent_fd, modes))) {  // 父程序讀或寫管道。返回檔案流指標。
        close(parent_fd);
        close(child_fd);
        goto FREE_PI;
    }

    if ((pid = vfork()) == 0) {    /* Child of vfork... */
        close(parent_fd);
        if (child_fd != zeroOrOne) {
            dup2(child_fd, zeroOrOne);// 子程序會複製描述符到zeroOrOne, 
            close(child_fd);// 實際上是將管道端關聯重定向到stdin(主程序寫模式) 或關聯重定向到stdout(主程序讀模式)
        }

        execl("/bin/sh", "sh", "-c", command, (char *)0);
        _exit(127);
    }

    if (pid > 0) {                /* Parent of vfork... */
        return fp;
    }

    /* If we get here, vfork failed. */
    fclose(fp);                    /* Will close parent_fd. */
 FREE_PI:
    free(pi);
 RET_NULL:
    return NULL;
}

補充: dup2(child_fd, 0); 就是把child_fd 複製到stdin上,這樣子程序從stdin讀取,實際上是讀取的child_fd, 就是所謂的輸入重定向.
同理:dup2(child_fd, 1); 就是把child_fd 複製到stdout上,這樣子程序向stdout輸出資訊,實際上是向child_fd輸出資訊.
哈哈哈!!! 使用了偷樑換柱之法. 由此騙過了子程序. 子程序以為向stdout列印,實際上打到了管道的一端, 另一端連線的是父程序的讀端,被父程序讀走了. 同理,父程序打管道的一端搭在了子程序的stdin端, 父程序向管道寫東西,子程序以為是從stdin讀進來的呢! 很有趣! 是嗎? 這是欺騙的手段,或者溝通的橋樑!

2.2 popen應用1: 將ls命令的輸出逐行讀出到記憶體,再顯示到螢幕上,

//本例演示了popen,pclose的使用

#include <stdio.h>
int main()
{
    FILE * fp;
    char buf[20] = {0};
    fp = popen("ls","r");
    if(NULL == fp)
    {
        perror("popen error!\n");
        return -1;
    }
    while(fgets(buf, 20, fp) != NULL)
    {
        printf("%s", buf);
    }
    pclose(fp);
    return 0;
}

popen按讀方式開啟”ls”程式,已經將ls 輸出重定向到管道輸入端,我們的fp 是管道輸出端, 所以程式執行達到了目的.

2.3 popen應用2: 將應用程式的輸出儲存到一個變數中。

//執行一個shell命令,輸出結果逐行儲存在vecStr中,並返回行數

int readPipe(const char *cmd, vector<string> &vecStr) {
    vecStr.clear();
    FILE *pp = popen(cmd, "r"); //建立管道
    if (!pp) {
        return -1;
    }
    char buf[1024]; //設定一個合適的長度,以儲存每一行輸出
    while (fgets(buf, sizeof(buf), pp) != NULL) {
        if (buf[strlen(buf) - 1] == '\n') {
            buf[strlen(buf) - 1] = '\0'; //去除換行符
        }
        vecStr.push_back(buf);
    }
    pclose(pp); //關閉管道
    return vecStr.size();
}

//但是,當我們即想向pipe 寫, 又想從pipe 讀, 現成的popen 就不能勝任了,
//popen 只能建立一條管道,或者是讀管道,或者是寫管道。
//要想同時與程式實現讀,寫操作(雙向互動), 需要我們自己書寫程序程式碼.
//是的,通過兩個管道,一個讀管道,一個寫管道。下面給一個範例.

3.靈活使用pipe()函式

3.1 範例1,重定向子程序的stdin,stdout

這個範例演示了我們的父程序與子程序通訊, 並列印了與子程序的通訊內容.
子程序並不知道與它通訊的到底是人通過鍵盤跟它下命令,還是程式在跟它下命令,它的描述符已經被接到管道上了.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define READ    0  
#define WRITE   1  

// 注,回車有非常重要的作用,否則對端會阻塞在讀上而不能繼續
// printf 沒有"\n"也不會顯示列印,除非用fflush或程式退出。不信可以試試。
// 這些都是緩衝若得禍!
int doubleInteract(const char* cmdstring)
{
    const char *sendStr = "hello,child\n";
    int   pipeR[2];      // 父讀子寫
    int   pipeW[2];   // 父寫子讀
    pid_t pid;
    char buf[1024];
    int len=sizeof(buf);
    memset(buf, 0, len);

    /*初始化管道*/  
    pipe(pipeR);  
    pipe(pipeW);  
    if ((pid = fork()) < 0) return -1;
    if (pid > 0)     /* parent process */
    {
        close(pipeW[READ]);
        close(pipeR[WRITE]);
        read(pipeR[READ], buf, len);  //由於這個len足夠大,讀不到回車又不夠len長度不返回。 
        // 讀到了東西,需要分析內容,做出正確迴應... , 更好的做法是啟動一個執行緒,專門接受管道輸入
        // 這裡只簡單迴應"hello..."
        printf("child:%s", buf);  // 這裡會寫到螢幕上
        write(pipeW[WRITE], sendStr, strlen(sendStr)+1); // 這裡把"..." 發到管道上
        printf("parent:%s", sendStr);
#if 1
        memset(buf,0,len);
        read(pipeR[READ], buf, len);  //再讀child 響應
        printf("child:%s", buf);  // 這裡會寫到螢幕上
#endif
        close(pipeW[WRITE]);
        close(pipeR[READ]);
        waitpid(pid, NULL, 0);
    }
    else /* child process, 關閉寫管道的寫端,讀管道的讀端,與父程序正好相對 */
    {
        close(pipeW[WRITE]);  
        close(pipeR[READ]);     
        //重定向子程序的標準輸入,標準輸出到管道端
        if (pipeW[READ] != STDOUT_FILENO)
        {
            if (dup2(pipeW[READ], STDIN_FILENO) != STDIN_FILENO)
            {
                return -1;
            }
            close(pipeW[READ]);
        }

        if (pipeR[WRITE] != STDOUT_FILENO)
        {
            if (dup2(pipeR[WRITE], STDOUT_FILENO) != STDOUT_FILENO)
            {
                return -1;
            }
            close(pipeR[WRITE]);
        }
        execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
        exit(127);
    }
    return 0;
}

int main()
{
    doubleInteract("./myShell");
    return 0;
}
//這裡給一個myShell指令碼範例,從鍵盤輸入,向螢幕輸出.如下:
#!/bin/bash
echo "hello! say Something"
read
echo "you say:$REPLY,bye bye!"

3.2另一個簡單的父子程序雙向互動的例子,

該例沒有采用把管道端向輸入或輸出重定向的技術,
而是直接用pipe通訊, 父子程序協作,將數值從0加到10;
規則是從管道中拿到數,加1,再把數推出去。

$ cat main.cpp
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/wait.h>  
#include <sys/types.h>  
#define READ    0  
#define WRITE   1  
int main(void)  
{  
    int x;  
    pid_t pid;  
    int pipe1[2],pipe2[2];  
    /*初始化管道*/  
    pipe(pipe1);  
    pipe(pipe2);  
    pid = fork();  
    if(pid < 0)  
    {  
        printf("create process error!/n");  
        exit(1);  
    }  
    if(pid == 0)        //子程序  
    {  
        close(pipe1[WRITE]);  
        close(pipe2[READ]);  
        do  
        {  
            read(pipe1[READ],&x,sizeof(int));  
            printf("child %d read: %d\n",getpid(),x++);  
            write(pipe2[WRITE],&x,sizeof(int));  
        }while(x<=9);  
        //讀寫完成後,關閉管道  
        close(pipe1[READ]);  
        close(pipe2[WRITE]);  
        exit(0);  
    }  
    else if(pid > 0) //父程序  
    {  
        close(pipe2[WRITE]);  
        close(pipe1[READ]);  
        x = 1;  
        //每次迴圈向管道11 端寫入變數X 的值,並從  
        //管道20 端讀一整數寫入X 再對X 加1,直到X 大於10  
        do{  
            write(pipe1[WRITE],&x,sizeof(int));  
            read(pipe2[READ],&x,sizeof(int));  
            printf("parent %d read: %d\n",getpid(),x++);  
        }while(x<=9);  
        //讀寫完成後,關閉管道  
        close(pipe1[WRITE]);  
        close(pipe2[READ]);  
        waitpid(pid,NULL,0);  
        exit(0);  
    }  
}  

範例均經過測試,可直接使用。