pipe

Table of Contents

色々なパイプ

さる高名な解剖学者さん曰く、人間(生物)は、管である、と。口が入口で肛門 が出口。この管に、手や足、更には頭等の装飾品がくっついている。数学的には、 トポロジィーの分野。取っ手の付いたマグカップや、ミスドで売ってるベーシッ クなドーナツとかも、変形次第では、パイプになるな。ならば、世の中にはパ イプが溢れているはず。

from man

どこにも出てる、標準的な利用例

#include <stdlib.h>
#include <unistd.h>
...

int fildes[2];
const int BSIZE = 100;
char buf[BSIZE];
ssize_t nbytes;
int status;

status = pipe(fildes);
switch (fork()) {
case 0:  /* Child - reads from pipe */
    close(fildes[1]);                       /* Write end is unused */
    nbytes = read(fildes[0], buf, BSIZE);   /* Get data from pipe */
    /* At this point, a further read would see end-of-file ... */
    close(fildes[0]);                       /* Finished with pipe */
    exit(EXIT_SUCCESS);
default:  /* Parent - writes to pipe */
    close(fildes[0]);                       /* Read end is unused */
    write(fildes[1], "Hello world\n", 12);  /* Write data on pipe */
    close(fildes[1]);                       /* Child will see EOF */
    exit(EXIT_SUCCESS);
}

forkする前に、pipeを作成しておくのが味噌。親のパイプが子に継承される。 unixの大発明だと思うぞ。

from gauche

前回lsofで調べた時、pipeは内部的に使用してなかったけど、ユーザーには公 開されてる。

(receive (in out) (sys-pipe)
  (display "abc\n" out)
  (flush out)
  (read-line in)) ⇒ "abc"

(sys-pipe)
#<iport (pipe) 0x7f43dbe54cc0>
#<oport (pipe) 0x7f43dbe54bb0>

わざわざポートをクローズしなくても、トイレの水を流す様に、フィラッシュ すればいいのか。この方法は、標準的な技だな。pipeでも通用する。そりゃ、 ファイルの一形態ですからね。

guileだと flush の代わりに force-output を使う。schemeって方言有杉。

scheme@(guile-user)> (define my (pipe))
scheme@(guile-user)> (display "hello" (cdr my))
scheme@(guile-user)> (force-output (cdr my))
scheme@(guile-user)> (read (car my))
$6 = hello

from ruby

rubyにも有るはず。

ob$ ri pipe
  :
  rd, wr = IO.pipe

  if fork
    wr.close
    puts "Parent got: <#{rd.read}>"
    rd.close
    Process.wait
  else
    rd.close
    puts 'Sending message to parent'
    wr.write "Hi Dad"
    wr.close
  end

manの説明と同様なのが出てきた。カジュアルに使うには大袈裟すぎる。もっ と軽く使いたい。そんな要求には、

ob$ ri popen
 :
 IO.popen('ls -l') do |pipe|
    pipe.readlines
  end

こういうのが良い。昔、散々お世話になったな。懐かしい!!

pipe and pipe2

これからpipeの実装を探っていくけど、その起点として仕様書を確認。

NAME
     pipe, pipe2 - create descriptor pair for interprocess communication

SYNOPSIS
     int pipe(int fildes[2]);

     int pipe2(int fildes[2], int flags);

ERRORS
     pipe() and pipe2() will succeed unless:

     [EMFILE]           Too many descriptors are active.
     [ENFILE]           The system file table is full.
     [EFAULT]           The fildes buffer is in an invalid area of the
                        process's address space.

     In addition, pipe2() may return the following error:
     [EINVAL]           flags is invalid.

HISTORY
     A pipe() function call appeared in Version 3 AT&T UNIX.  Since Version 4
     AT&T UNIX, it allocates two distinct file descriptors.  The pipe2()
     function appeared in OpenBSD 5.7.

FreeBSDだと、pipe2は、10.0の時代に追加されてる。

普段は余り注目しないエラーについて目を光らせてみたい。なぜかと言うと、 pipeをカスタマイズしよと思って、sysctlを調べたんだけど、それらしいのが 無かった。で、リミット・エラーが有るんじゃないかと、思った次第。

それから、pipe2って割りと新らしいね。リナの連中が便利さを求めてシステ ムコールを追加。それを利用したguileみたいな物が出てきたものだから、仕 方がなくBSD軍も追従って事なんだろう。

Debianのpipe(2)には、こんな事が記述されてた。

STANDARDS
       pipe(): POSIX.1-2001, POSIX.1-2008.

       pipe2() is Linux-specific.

これって、世界情勢と一緒だな。リナは自由奔放な米国。BSDは伝統を重視す るEU圏。良くも悪くも米を中心に回っている。ね、ピッタリと符号するでしょ。

sysctlうんぬんて嘆いたけど、ArchLinuxには有ったぞ。

[sakae@arch ~]$ sudo sysctl -a | grep pipe
fs.pipe-max-size = 1048576
fs.pipe-user-pages-hard = 0
fs.pipe-user-pages-soft = 16384
kernel.core_pipe_limit = 16

そして、もっとお手軽な奴もね。

[sakae@arch ~]$ ulimit -a
pipe size                (512 bytes, -p) 8

ring buffer

pipeって、read/writeの2つのディスクリプターを返すシシテムコールだった。 これって、

fdr = open("hoge.txt", "r")
fdw = open('hoge.txt", "w")

を、一度にやってしまう機能とも言える。但し、hoge.txtに相当するファイル は、カーネル内に隠されていて、直接にはアクセスできないけど。pipeのごと く振る舞う名前付きのやつも有るぞ。mkfifo(3)

書き込んだ順番に、読み出される。勿論、データが無ければ読み出しはできな い。普通のファイルの様に無限(DISK容量の制限はあるけど)に、書き込む事は できない仕様になってる。内部のファイルが一杯になった場合、それ以上は読 み出しして、空をつくるまで、書き込めない。

こういう機能って、キューだな。お手軽に作成するには、配列を用意して、そ れがリング状につながった物として扱かうのが常套手段だ。

リングバッファよるキューの実装

リングバッファサンプルプログラム in C

徐々に高度になるリングバッファの話

何でも有るgaucheでは、ライブラリーとして提供されてる。凄い事ですよ。 12.22 data.ring-buffer - リングバッファ

popen in stdio

ついでに、pipeを便利に利用する奴を見て億。

sample

#include <stdio.h>
#define MAXLINE 256

int main() {
  char buf[MAXLINE];
  FILE *fdr, *fdw;

  fdr = popen("ls", "r");
  fdw = popen("sort -r", "w");

  while(fgets(buf, MAXLINE, fdr) != NULL){
    fputs(buf, fdw);
  }

  pclose(fdr);
  pclose(fdw);
  return 0;
}

ls | sort -r 相当を実現してみた。もっと実用的なコードとしては、オイラー の大好きな奴との組み合わせ。 C言語からgnuplotを操作する なんてのがネットにあがっていました。

libc/popen

/usr/src/lib/libc/gen/popen.c にpopenとpcloseが定義されてた。下記は、 popenの骨格。

/*      $OpenBSD: popen.c,v 1.22 2019/06/28 13:32:41 deraadt Exp $ */

FILE *
popen(const char *program, const char *type)

        if (pipe2(pdes, O_CLOEXEC) == -1) {

        switch (pid = vfork()) {
        case 0:                         /* Child. */
                execl(_PATH_BSHELL, "sh", "-c", program, (char *)NULL);

       /* Parent; assume fdopen can't fail. */
        target = *type == 'w';
        iop = fdopen(pdes[target], type);
        fwide(iop, -1);

	return (iop);

ちゃっかり、pipe2が利用されてた。このソースの作成日は、丁度pipe2が出た あたりと思う。便利かどうか、試してみましたって感じだな。

昔、解析された方がおられた。大変なコードだったのね。 C言語系/「デーモン君のソース探検」読書メモ/A10, popen(3)

ちょいと気になるのは、popen.cのソースの在処。libc/gen/ がオイラー的に は謎だ。単なるラッパーだとは思うけど。サンプルをgdbしてみる。

(gdb) b pipe2
Function "pipe2" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (pipe2) pending.
(gdb) r
Starting program: /tmp/a.out

Breakpoint 1, pipe2 () at /tmp/-:2
2       /tmp/-: No such file or directory.
(gdb) bt
#0  pipe2 () at /tmp/-:2
#1  0x0b6e607c in _libc_popen (program=0x140ef3d4 "ls", type=0x140ef3d7 "r")
    at /usr/src/lib/libc/gen/popen.c:73
#2  0x140f07cb in main () at popen.c:8

うん、確かに、接頭語の _libc_ が付いて、ラッパーにはなってるけど、ど うして、こんな面倒な事をやってるの?

ob$ find /usr/src/lib -type d -name gen
/usr/src/lib/libc/arch/aarch64/gen
/usr/src/lib/libc/arch/alpha/gen
/usr/src/lib/libc/arch/amd64/gen
/usr/src/lib/libc/arch/arm/gen
/usr/src/lib/libc/arch/hppa/gen
/usr/src/lib/libc/arch/i386/gen
/usr/src/lib/libc/arch/m88k/gen
/usr/src/lib/libc/arch/mips64/gen
/usr/src/lib/libc/arch/powerpc/gen
/usr/src/lib/libc/arch/powerpc64/gen
/usr/src/lib/libc/arch/riscv64/gen
/usr/src/lib/libc/arch/sh/gen
/usr/src/lib/libc/arch/sparc64/gen
/usr/src/lib/libc/gen

libと言えどもマシンに依存する部分が有るんだな。それを考慮して自動変換 とかをやってるっぽいな。

pipeの予習

そしていよいよ、パイプに潜ってみたいんだけど、 いきなり本番はキツいと思うので、学習用OSで予習をしておく。

xv6-riscv

MITに入学します。前の教材はxv6と言う悪貨でしたけど、それを若者に押しつ けるのは、忍びないって事で、希望に溢れたRISCVな石です。

ユーザーランドでは、shでpipeが利用されてました。カーネル側では、 sysfile.c に sys_pipe が実装されてました。

if(pipealloc(&rf, &wf) < 0)
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
   copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){

概略はこんな感じ。pipe.c内で定義されてる、pipeallocで4Kのエリアを持つ パイプ構造体をカー ネル内に確保。それをパイプ用のファイルと見做す様に設定。

次にfdallocで、現在のプロセスに結びつける。後は、その結果ををcopyoutを 使って、ユーザーランドに転送。fdarrayってのは、ユーザー側の受領先。何 で、2回に分けて転送してるのだろう?

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

これを見る限り、1回の転送でも(この場合は)大丈夫と思うんだけど。 凡クラー ズには、想像も出来ないような、深淵な理由があるのだろうか? じゃ、ちょっ と河岸を変えてみるか。

minix1

今更感は有りますが、ソースがDISKの肥やしにしとくのでは、勿体ないので、 脳の肥やしとして、活用します。ついでに、古い本、"MINIXオペレーティグシ ステム"なんてのも活用します。

fs/pipe.c

* The entry points into this file are
*   do_pipe:     perform the PIPE system call
*   pipe_check:  check to see that a read or write on a pipe is feasible now
*   suspend:     suspend a process that cannot do a requested read or write
*   release:     check to see if a suspended process can be released and do
*   revive:      mark a suspended process as able to run again
*   do_unpause:  a signal has been sent to a process; see if it suspended  

do_pipe から追跡を始めたんだけど、こりゃ本を読むのが先だな。密接に ファイルシステムと結合してるわい。 それにminixと言う特殊性も有るみたいだし。。。

次回に続く(かも知れない)。

_lib_xxx

Q: OpenBSD の libcについて質問します。下記の様に popen には、接頭語として _libc_ がついています。何故ですか? popen.c は、/usr/src/lib/libc/gen/popen.cと いう場所にあり、gen が示すように動的にバイナリーを作成してると想像されますが。

ob$ nm libc.a  | grep popen
popen.o:
00000000 T _libc_popen
00000000 W popen
00000000 F popen.c

A: OpenBSD の libc において、`popen` 関数に ` _libc_ ` という接頭語が付いている理由について説明します。

### 理由 `libc_` という接頭語が付いているのは、内部実装と外部公開シンボルを区別するためです。具体的には、以下のような理由が考えられます:

  1. 名前衝突の回避: 標準ライブラリや他のライブラリと同じ名前の関数がある場合、名前の衝突を避けるために ` _libc_ ` を付けることがあります。これにより、同じ名前の関数が複数存在する場合でも、内部実装が他のシンボルと干渉することを防げます。
  2. 内部実装の隠蔽: 内部で使用される実装を外部から見えなくするために ` _libc_ ` を付けることがあります。これにより、外部からは標準的なインターフェース (`popen`) のみが見え、内部実装の詳細は隠蔽されます。
  3. 互換性の維持: 互換性を維持するために、内部実装が変更されても、外部インターフェースに影響を与えないようにすることができます。これにより、プログラムが特定の内部実装に依存しないようにすることができます。

### 実装の流れ `popen.c` が `gen` ディレクトリにある理由は、`popen` 関数が汎用的な(ジェネリックな)関数であり、さまざまなプログラムで使用される可能性があるからです。`gen` ディレクトリは、一般的な汎用関数の実装が含まれている場所です。

以下は `popen` 関数の実装とエクスポートの流れの概略です:

  1. 関数の実装: `popen` 関数は `popen.c` ファイル内で ` _libc_ popen` として実装されます。
  2. エクスポートシンボルの指定: 実装された ` _libc_ popen` 関数は、公開シンボル `popen` としてエクスポートされます。これは、通常、ヘッダーファイルやリンカスクリプトで指定されます。
  3. 名前のエイリアス: リンカによって、公開シンボル `popen` が内部シンボル ` _libc_ popen` にリンクされます。これにより、外部から `popen` 関数が呼び出された場合に、内部実装 ` _libc_ popen` が実行されます。

このようにして、内部実装の詳細を隠蔽しつつ、標準的なインターフェースを提供することができます。

Q: popen.cの中には、 _libc_ popen の実装は有りません。ただ、関数の定 義が終了した所に、 DEF_WEAK (popen); が記述されていました。これは何でしょうか?

A: ` DEF_WEAK ` マクロは、OpenBSDのソースコードで使用されるマクロで、弱いシンボル(weak symbol)を定義するために使われます。弱いシンボルとは、同じ名前のシンボルが複数定義された場合に、リンカがどのシンボルを使用するかを制御するためのものです。

### ` DEF_WEAK ` マクロの役割

`DEFWEAK(popen);` のような記述は、次のような意味を持ちます:

  1. 弱いシンボルの定義: `popen` 関数のシンボルを弱いシンボルとして定義します。弱いシンボルは、強いシンボル(通常のシンボル)が存在する場合にそれを優先します。
  2. 他の実装の優先: `popen` 関数に対して他の強い実装が存在する場合、その強い実装が使用されます。例えば、ユーザーが独自の `popen` 関数を定義した場合、そのユーザー定義の `popen` 関数が優先されます。
  3. 互換性の維持: 弱いシンボルを使用することで、ライブラリの互換性を維持しやすくなります。これは、特定の環境やアプリケーションで異なる実装が必要な場合に役立ちます。

### ` DEF_WEAK ` マクロの実装例

OpenBSDのソースコードにおける `DEFWEAK` マクロの具体的な実装は次のようになっている可能性があります(実際の実装はソースコードを確認する必要がありますが、一般的な例として):

```c #define DEFWEAK(name) \ _attribute_((weak, alias (#name))) ```

このように定義されている場合、`DEFWEAK(popen);` は次のように展開されます:

```c _attribute_((weak, alias ("popen"))) ```

これにより、`popen` 関数が弱いシンボルとして定義され、他の強いシンボルが存在する場合にはそちらが優先されるようになります。

### まとめ

`DEFWEAK(popen);` は、`popen` 関数を弱いシンボルとして定義するためのマクロです。これにより、特定の条件下で他の強い実装が優先されるようになり、ライブラリの互換性や拡張性を高めることができます。

実際のコードを、探してみる。 libc/include/namespace.h

#define HIDDEN(x)               _libc_##x
#define DEF_WEAK(x)             __weak_alias(x, HIDDEN(x))

確かに、接頭語を付けて、見えない様にしているな。こういう努力は、libc の中だけで、467個の関数に及んでいたよ。これだけ、ユーザーに自由度を与 えているんだな。凄いぞ、OpenBSD!!

それから、genはジェネレートじゃなくて、ジェネラルなんか。まぎらわしい な。genと言ったら、普通はどちらを想像するんかな。

そう言えば、NHKあたりも、 カスハラ なんて言う省略語を平気で使うようになったな。カスタマー ハラス メントなる英語を取り出してきて、それを省略して造語にしてる。英語の乱用 と勝手な造語。日本語を乱す不埒な事を平気でやってて委員会? 国語審議会 に訴えてやるぞ。


This year's Index

Home