popen関数の挙動まとめ

C言語のpopen関数の挙動にいつも混乱させられるので、自分用メモとして挙動をまとめてみる。

popen関数の挙動

popen関数はファイルを開くようなノリでプロセスをopenできる。popenの戻り値としてファイルディスクリプタが返ってくるので、それを使ってopenしたプロセスにread/writeの指示を行うことができる。

混乱の種はこのプロセスに対するread/writeである。これはpopenをコールした側の視点に立って考える必要がある。つまり、readであればopenしたプロセスの標準出力を読み取るし、writeであればopenしたプロセスに標準入力を与えることになる。考えてみれば当然なのだが、popenをコールする側・される側でin/outの関係が逆になっており、それが混乱を招く。

以下に挙動をまとめた表を示す。

popenのフラグ popenコール側の挙動 popenでopenされる側の挙動
"r" fgets関数などを使用してopenしたプロセスの標準出力をreadする。 標準出力にデータを吐き出す(かもしれない)。
"w" fputs関数などを使用してopenしたプロセスに標準入力を与える(writeする)。 標準入力からデータを受け取る(かもしれない)。

サンプルプログラム

以下のサイトを参考にサンプルプログラムを書いてみた。

popenでコマンドの出力を読み込む - C言語入門


Readモードでopenする例

#include <stdio.h>
#include <stdlib.h>
#define BUF 256

int main (int argc, char *argv[])
{
    FILE *fp;
    char *cmdline = "ls";
    if ((fp=popen(cmdline, "r")) == NULL) {
        perror ("popen failed");
        exit(EXIT_FAILURE);
    }

    char buf[BUF];
    while(fgets(buf, sizeof(buf), fp) != NULL) {
        printf("=> %s", buf);
    }

    pclose(fp);

    return 0;
}

実行結果は以下の通りである。

shinya@DESKTOP-Q1NSEEU:~/programs/popen% ./a.out
=> a.out
=> correct_read.c
=> correct_write.c
=> wrong_read.c
=> wrong_write.c

Writeモードでopenする例

#include <stdio.h>
#include <stdlib.h>
#define BUF 256

int main (int argc, char *argv[])
{
    FILE *fp;
    char *cmdline = "python3";
    if ((fp=popen(cmdline, "w")) == NULL) {
        perror ("popen failed");
        exit(EXIT_FAILURE);
    }

    char buf[BUF];
    char* data1 = "print(\"Hello\")\n";
    char* data2 = "quit()\n";
    fputs(data1, fp);
    fputs(data2, fp);

    pclose(fp);

    return 0;
}

実行結果は以下の通りである。

shinya@DESKTOP-Q1NSEEU:~/programs/popen% ./a.out
Hello

バグについて

マニュアルを見ると、popenには何やらバグがあるらしい。端的に言うと、readモードでopenしたプロセスの標準入力、及びwriteモードでopenしたプロセスの標準出力の扱いがザルということである。

Man page of POPEN

以下にこのバグを踏むプログラムの例を示す。

Readモードでopenしたプロセスと標準入力を共有してしまう例

#include <stdio.h>
#include <stdlib.h>
#define BUF 256

int main (int argc, char *argv[])
{
    FILE *fp;
    char *cmdline = "python3";
    if ((fp=popen(cmdline, "r")) == NULL) {
        perror ("popen failed");
        exit(EXIT_FAILURE);
    }

    int data0 = 0;
    scanf("%d", &data0);
    printf("input data = %d\n", data0);

    pclose(fp);

    return 0;
}

実行結果は以下の通りである。

shinya@DESKTOP-Q1NSEEU:~/programs/popen% ./a.out
Python 3.8.5 (default, Jan 27 2021, 15:41:15)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 10
input data = 10  ★標準入力がpopenした側のプロセスに吸われた

>>>

Writeモードでopenしたプロセスと標準出力を共有してしまう例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF 256

int main (int argc, char *argv[])
{
    FILE *fp;
    char *cmdline = "ls";
    printf("Hello ");

    if(argc == 2 && strncmp(argv[1], "-f", 3) == 0) {
        fflush(stdout);
    }
    if ((fp=popen(cmdline, "w")) == NULL) {
        perror ("popen failed");
        exit(EXIT_FAILURE);
    }

    char buf[BUF];

    pclose(fp);
    printf("world!\n");

    return 0;
}

実行結果は以下の通りである。

shinya@DESKTOP-Q1NSEEU:~/programs/popen% ./a.out
a.out  correct_read.c  correct_write.c  wrong_read.c  wrong_write.c
Hello world!  ★"Hello "を先に出力したはずなのにlsコマンドの出力が先に出てしまった

なお、後者についてはpopenの前にfflush関数をコールすることで回避できるようである。上で示したプログラムに"-f"オプションを付けて実行するとこの挙動になる。

shinya@DESKTOP-Q1NSEEU:~/programs/popen% ./a.out -f
Hello a.out  correct_read.c  correct_write.c    wrong_read.c  wrong_write.c
world!  ★"Hello "が先に出力された

まとめ

以上、popen関数の挙動についてまとめた。これでもう混乱することはないだろう。