快速上手popen()

 

该函数用于运行指定命令,并且让刚启动的程序看起来像文件一样可以被读写。

 

2 个 demo

1) 从外部程序中读数据:

 


int main(int argc, char **argv)

{

    FILE *fp;

    char buf[100];

    int i = 0;



    fp = popen("ls -1X", "r");



    if (fp != NULL) {

        while(fgets(buf, 100, fp) != NULL) {

            printf("%d: %s", i++, buf);

        }

        pclose(fp);

        return 0;

    }

    return 1;

}

 

运行效果:

 


$ ./001_popen_r 

0: 001_popen_r

1: 002_popen_w

2: 001_popen_r.c

3: 002_popen_w.c

4: 004_popen_intern.c

 

2) 写数据到外部程序:

 


int main(int argc, char *argv)

{

    FILE *fp = NULL;

    char buffer[BUFSIZE];



    sprintf(buffer, "hello world\n");



    fp = popen("od -tcx1", "w");

    if (fp != NULL) {

        fwrite(buffer, sizeof(char), strlen(buffer), fp);

        pclose(fp);

        return 0;

    }

    return 1;

}

 

运行效果:

 


0000000   h   e   l   l   o       w   o   r   l   d  \n

         68  65  6c  6c  6f  20  77  6f  72  6c  64  0a

0000014

 

相关要点

函数原型


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

 

popen() 会先执行 fork,然后调用 exec 执行 command,并且返回一个标准 I/O 文件指针。

 

type = "r":

  • 文件指针连接到 command 的标准输出。

 

type = "w":

  • 文件指针连接到 command 的标准输入。

 

点击查看大图

 

优缺点

优点:

  • 由于调用了 shell,所以可以支持通配符 (例如*.c) 等各种 shell 扩展特性;减少了代码量;

 

缺点:

  • 要启动 2 个程序:shell 和 目标程序,调用成本略高,比起直接 exec 某个程序来说要慢一些;

 

内部实现

popen() 的内部实现思路如下:

 


FILE *_popen(const char *command, const char *type)

{

    pipe()

    fork();

    if (pid > 0)

        close() child's fd

        return fdopen() parent's fd

    else

        close(parent's fd)

        dup2() child's data fd to stdin or stdout

        close() child's fd

        exec("/bin/sh -c") command

}

 

  1. 创建一个管道,用于父子进程间的通讯;父进程:
    • 关闭未使用的管道端;返回父进程数据管道端的 FILE *, 它可能连接父进程的 stdin / stdout;

子进程:

  • 关闭未使用的管道端;重定位子进程的数据管道端到 stdin / stdout;执行目标命令;

 

初步的代码实现:

 


FILE *_popen(const char *command, const char *type)

{

    int pfp[2];

    int parent_end, child_end;

    int pid;



    if (*type == 'r') {

        parent_end = READ;

        child_end = WRITE;

    } else if (*type == 'w') {

        parent_end = WRITE;

        child_end = READ;

    } else {

        return NULL;

    }



    pipe(pfp);

    pid = fork();

    if (pid > 0 ) {

        close(pfp[child_end]);

        return fdopen(pfp[parent_end], type);

    } else {

        close(pfp[parent_end]);

        dup2(pfp[child_end], child_end);

        close(pfp[child_end]);

        execl("/bin/sh", "sh", "-c", command, NULL);

        exit(0);

    }

    return NULL;

}

 

这里的实现有一些不足的地方,例如:

 

为了便于阅读,省略了错误检查;

 

没有保存子进程的 pid,后续无法使用 wait() 进行收尸;

 

一个进程可能调用 popen() 多次,需要用数组 / 链表来存储所有子进程的 pid;

 

更完善的实现可以参考:

/zixunimg/eefocusimg/android.googlesource.com/platform/bionic/+/3884bfe9661955543ce203c60f9225bbdf33f6bb/libc/unistd/popen.c

 

应用案例

以开源软件 MJPG-steamer 为例。

 

MJPG-streamer 是什么?

 

简单地说,就是一个开源的流媒体服务器:

 

/zixunimg/eefocusimg/github.com/jacksonliam/mjpg-streamer

 

通过 mjpg-streamer,你可以通过 PC 浏览器访问到板子上的摄像头图像。

 

 

MJPG-streamer 就是通过 popen() 来支持 CGI 功能的:

 

CGI 是早期出现的一种简单、流行的服务端应用程序执行接口,http server 通过运行 CGI 程序来完成更复杂的处理工作,在 MJPG-streamer . 里的相关代码如下:

 


plugins/output_http/httpd.c



void execute_cgi(int id, int fd, char *parameter, char *query_string)

{

    // prepare



    // 执行浏览器指定的 CGI 程序

    f = popen(buffer, "r");



    // 获得 CGI 程序的输出

    while((i = fread(buffer, 1, sizeof(buffer), f)) > 0) {

        if (write(fd, buffer, i) < 0) {

            fclose(f);

            free(buffer);

            close(lfd);

            return;

        }

    }



}

 

这里只是简单地了解一下 MJPG-Streamer,有兴趣的小伙伴们自行阅读更多的代码吧。

 

相关参考

Unix-Linux 编程实践教程 / 11.4 popen: 让进程看似文件

 

Linux 程序设计(第 4 版) / 13.3 将输出送往 popen

 

Unix 环境高级编程第 3 版 / 15.3 函数 popen 和 pclose

 

HTTP 权威指南

 

思考技术,也思考人生

要学习技术,更要学习如何生活。

 

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。

 

对 嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,关注公众号:嵌入式 Hacker。

 

觉得文章对你有价值,还请多多 转发。