Diary

Diary

日々学んだことをアウトプットする場として初めてみました

パイプの中身をシステムコールレベルで理解・実装する

パイプの中身をシステムコールレベルで理解・実装する

『ふつうのLinuxプログラミング』を読んで勉強させてもらったメモになります。
コードは載ってなかったため自分で実装してみました。

ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道

新品価格
¥2,722から
(2023/11/1 01:22時点)

[目次]

パイプ詳細説明

Linux の世界で見たパイプとはプロセスを両端に持ったストリームのことを指し、以下の特徴を持ちます。

  • 他の概念同様ファイルディスクリプタを使って表現される
  • 他のストリームと違ってパイプは一方向

以下のステップでパイプを実装できます。

  1. あるプロセスで pipe(2) を実行
    • プロセス内でストリームを繋げる
  2. fork して子プロセスを生成
    • プロセス複製時にストリームも含め複製される
  3. 必要ない fd を終端(両端で close)
    • 親プロセスの読み込みを close
    • 子プロセスの書き込みを close
  4. 残った fd をそれぞれ標準入出力と対応付
    • dup2 を使う
    • (5 で実行するコマンドが標準入出力を取るものが多いため)
  5. それぞれのプロセスでコマンド実行

パイプ実装

環境

$ uname -a
Linux ubuntu 5.4.0-1045-raspi #49-Ubuntu SMP PREEMPT Wed Sep 29 17:49:16 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux

C

とりあえず2つのコマンドを繋げるようなパイプを作ってみました。
コメント書いたため説明は割愛します。

間違ってる箇所があれば教えてください。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char **argv)
{
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <command1> <command2>\n", argv[0]);
        exit(1);
    }

    int fds[2];
    // 両端とも自プロセスに繋がったストリームを生成する。
    // fds[0] は読み込み専用 fds[1] は書き込み専用。
    pipe(fds);

    // 子プロセスと親プロセスの両方で呼び出しが戻る!!
    int pid = fork();
    if (pid < 0)
    {
        fprintf(stderr, "fork(2) failed\n");
        exit(1);
    }

    if (pid == 0)
    {
        // 子プロセス。

        // 書き込みのストリーム用の fd を閉じる。
        close(fds[1]);
        // 読み込みを標準入力のストリームに複製する。
        dup2(fds[0], STDIN_FILENO);
        close(fds[0]);

        execlp(argv[2], argv[2], NULL);

        perror("execlp child"); // execlpが失敗した場合。
        exit(1);
    }
    else
    {
        // 親プロセス。

        // 読み込みのストリーム用の fd を閉じる。
        close(fds[0]);
        // 書き込みを標準出力のストリームに複製する。
        dup2(fds[1], STDOUT_FILENO);
        close(fds[1]);

        execlp(argv[1], argv[1], NULL);

        perror("execlp parent"); // execlpが失敗した場合。
        exit(1);
    }

    wait(NULL);

    exit(0);
}
gcc -g -Wall -o pipe pipe.c

./pipe ls wc

Go

dup2 がなく、dup, dup3 しかなかったです。何の差でしょうか。 (mac には dup2 ありそう。)

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Fprintf(os.Stderr, "Usage: %s <command1> <command2>\n", os.Args[0])
        os.Exit(1)
    }

    rfd, wfd, err := os.Pipe()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Pipe creation failed: %v\n", err)
        os.Exit(1)
    }

    path1, err := exec.LookPath(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to find %s: %v\n", os.Args[1], err)
        os.Exit(1)
    }

    pid, err := syscall.ForkExec(path1, []string{os.Args[1]}, &syscall.ProcAttr{
        Dir:   ".",
        Env:   os.Environ(),
        Files: []uintptr{os.Stdin.Fd(), uintptr(wfd.Fd()), os.Stderr.Fd()},
        Sys:   nil,
    })
    if err != nil {
        fmt.Fprintf(os.Stderr, "ForkExec failed: %v\n", err)
        os.Exit(1)
    }

    wfd.Close()

    path2, err := exec.LookPath(os.Args[2])
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to find %s: %v\n", os.Args[2], err)
        os.Exit(1)
    }

    syscall.Dup3(int(rfd.Fd()), int(os.Stdin.Fd()), 0)
    rfd.Close()

    err = syscall.Exec(path2, []string{os.Args[2]}, os.Environ())
    if err != nil {
        fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
        os.Exit(1)
    }

    syscall.Wait4(pid, nil, 0, nil)
}
go run pipe.go ls wc

syscall でシステムコールを呼べた気になってしまってます。