[徹底解説] Linux リダイレクトのしくみ

フードの中で bash リダイレクションがどのように機能するのか疑問に思ったことはありますか?
リダイレクト自体はかなり簡単です。

bash を使うと、ファイルをプロセスの stdin にリダイレクトすることも、プロセスの stdout/stderr をファイルや他のファイル記述子にリダイレクトすることもできます( stderr を stdout にリダイレクトするなど、ファイル記述子なので)。

リダイレクトは次のようになります。

$ ls -la > output.txt

上記のコマンドは、stdout を ls コマンドから output.txt ファイルにリダイレクトします。
stderr をリダイレクトする方法は次のとおりです。

$ ls -la 2 > errors.txt

これにより、stderr から errors.txt にすべてが送信されます(許可が拒否されたメッセージなど)。
そのため、たとえば次のような場合は、

$ ls /root > /dev/null

ルート以外のユーザーは、出力を /dev/null にリダイレクトしたにもかかわらずエラーメッセージが表示されます。
通常、エラーは stderr に書き込まれます。

最後に、stdin を出力とするファイルの入力リダイレクションを行うこともできます。

$ bash < commands.txt

上記のコマンドは、commands.txt の内容を読み込み、bash インタプリタを使用してそれらを実行します。

リダイレクトの実装

とにかく、この記事のポイントは、そのような機能がどのように実装されているかです。
プロセスフォークを理解すれば、非常に簡単になることが分かります。

プロセスが別のプロセス(例えば bash を実行しているls)を実行したいとき、それは一般的に次のように動作します:

  1. メインプロセス(bash など)は、fork という glibc ラッパーを使って fork します(sidenode: fork では、実際には fork(2) システムコールではなく glibc ラッパーを呼び出しています)。glibc ラッパーは、clone(2) がより強力であるため、fork(2) のシステムコールではなく、システムコール(syscall)
  2. fork されたプロセスは、出力リダイレクトがコマンドラインで入力されたことを確認し、open(2) のシステムコールまたは同等のものを使用して指定されたファイルを開きます。
  3. fork されたプロセスは dup2 を呼び出して、新しく開かれたファイル記述子を stdin/stdout/stderr にコピーします。
  4. fork されたプロセスは元のファイルハンドラを閉じて、リソースリークを防ぎます。
  5. fork されたプロセスは execve(2) のシステムコールまたは同様のものを呼び出して、実行可能なイメージを実行するプロセスのイメージ(たとえば ls)に置き換えるか、

同じことをするコード例を次に示します。

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
char *argv[] = { "/bin/ls", "-la", 0 };
char *envp[] =
{
"HOME=/",
"PATH=/bin:/usr/bin",
"USER=brandon",
0
};
int fd = open("/home/brandon/ls.log", O_WRONLY|O_CREAT|O_TRUNC, 0666);
dup2(fd, 1); // stdout is file descriptor 1
close(fd);
execve(argv[0], &argv[0], envp);
fprintf(stderr, "Oops!\n");
return -1;
}

上記のコードは stdout を ls.log に設定し、 ls -la を実行します。
execve が失敗しない限り、fprintf 以下は実行されませんのでご注意ください。
それで、リダイレクトが bash でどのように実装されるのです!

2247  execve("./redirect", ["./redirect"], [/* 18 vars */]) = 0
2247  open("/home/vagrant/ls.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
2247  dup2(3, 1)                        = 1
2247  execve("/bin/ls", ["/bin/ls", "-la"], [/* 3 vars */]) = 0
2247  write(1, "total 64\ndrwxrwxrwx 2 root    ro"..., 620) = 620
2247  exit_group(0)                     = ?
2247 +++ exited with 0 +++

/usr/include/unistd.h

/* Standard file descriptors.  */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

# who
root pts/0 2018-10-07 20:10 (10.16.0.1)
root pts/1 2018-10-08 07:54 (10.16.0.1)
# tty
/dev/pts/0