父子进程的双向通讯简明解读(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 
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 
#include 
#include 
#include 
#include 
#include 

#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   
#include   
#include   
#include   
#include   
#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);  
    }  
}  

范例均经过测试,可直接使用。

你可能感兴趣的:(C,编程)