快速上手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;

 

更完善的实现可以参考:

https://android.googlesource.com/platform/bionic/+/3884bfe9661955543ce203c60f9225bbdf33f6bb/libc/unistd/popen.c

 

应用案例

开源软件 MJPG-steamer 为例。

 

MJPG-streamer 是什么?

 

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

 

https://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。

 

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