Linuxパイプのヒントとコツ

パイプ-それは何ですか?



パイプ(パイプライン)は、プロセス間通信の単方向チャネルです。 この用語は、 Douglas McIlroyによってUnixシェル用に作成され、パイプラインにちなんで命名されました。 パイプラインはシェルスクリプトで最もよく使用され、コンベアシンボル「|」を使用して、1つのコマンドの出力(stdout)を後続のコマンドの入力(stdin)にリダイレクトすることにより、複数のコマンドをリンクします:

cmd1 | cmd2 | .... | cmdN
      
      





例:

 $ grep -i “error” ./log | wc -l 43
      
      





grepは、ログファイル内の文字列「error」に対して大文字と小文字を区別しない検索を実行しますが、検索結果は表示されませんが、wcコマンドの入力(stdin)にリダイレクトされ、行数の計算が実行されます。



ロジック



パイプラインは、I / Oバッファリングを使用したコマンドの非同期実行を提供します。 したがって、パイプライン内のすべてのコマンドは、それぞれ独自のプロセスで並行して動作します。



カーネルバージョン2.6.11以降のバッファーのサイズは65536バイト(64Kb)で、古いカーネルのメモリページと同じです。 空のバッファから読み取ろうとすると、データが表示されるまで読み取りプロセスがブロックされます。 同様に、いっぱいになったバッファに書き込もうとすると、必要なスペースが解放されるまで記録プロセスがブロックされます。

パイプラインが入力/出力ストリームのファイル記述子で動作するという事実にもかかわらず、すべての操作はディスクをロードせずにメモリ内で実行されることが重要です。

以下の情報はすべて、bash-4.2シェルおよび3.10.10カーネルに関するものです。



簡単なデバッグ



straceユーティリティを使用すると、プログラムの実行中にシステムコールを追跡できます。

 $ strace -f bash -c '/bin/echo foo | grep bar' .... getpid() = 13726 <– PID   ... pipe([3, 4]) <–      .... clone(....) = 13727 <–      (echo) ... [pid 13727] execve("/bin/echo", ["/bin/echo", "foo"], [/* 61 vars */] ..... [pid 13726] clone(....) = 13728 <–     (grep)      ... [pid 13728] stat("/home/aikikode/bin/grep", ...
      
      



pipe()システムコールを使用してコンベアを作成していること、および両方のプロセスが異なるスレッドで並行して実行されていることがわかります。



多くのbashソースコードとカーネル

ソースコード、レベル1、シェル



最良のドキュメントはソースコードであるため、ここで説明します。 BashはYaccを使用して入力コマンドを解析し、文字「|」を検出すると「command_connect()」を返します。

parse.y

 1242 pipeline: pipeline '|' newline_list pipeline 1243 { $$ = command_connect ($1, $4, '|'); } 1244 | pipeline BAR_AND newline_list pipeline 1245 { 1246 /* Make cmd1 |& cmd2 equivalent to cmd1 2>&1 | cmd2 */ 1247 COMMAND *tc; 1248 REDIRECTEE rd, sd; 1249 REDIRECT *r; 1250 1251 tc = $1->type == cm_simple ? (COMMAND *)$1->value.Simple : $1; 1252 sd.dest = 2; 1253 rd.dest = 1; 1254 r = make_redirection (sd, r_duplicating_output, rd, 0); 1255 if (tc->redirects) 1256 { 1257 register REDIRECT *t; 1258 for (t = tc->redirects; t->next; t = t->next) 1259 ; 1260 t->next = r; 1261 } 1262 else 1263 tc->redirects = r; 1264 1265 $$ = command_connect ($1, $4, '|'); 1266 } 1267 | command 1268 { $$ = $1; } 1269 ;
      
      



また、ここでは文字ペア「|&」の処理を確認します。これは、stdoutとstderrの両方をパイプラインにリダイレクトすることと同等です。 次に、command_connect()を参照してください: make_cmd.c

 194 COMMAND * 195 command_connect (com1, com2, connector) 196 COMMAND *com1, *com2; 197 int connector; 198 { 199 CONNECTION *temp; 200 201 temp = (CONNECTION *)xmalloc (sizeof (CONNECTION)); 202 temp->connector = connector; 203 temp->first = com1; 204 temp->second = com2; 205 return (make_command (cm_connection, (SIMPLE_COM *)temp)); 206 }
      
      



ここで、コネクタは文字「|」です intのような。 コマンドシーケンス(「&」、「|」、「;」などを介してリンクされている)を実行すると、execute_connection()が呼び出されます: execute_cmd.c

 2325 case '|': ... 2331 exec_result = execute_pipeline (command, asynchronous, pipe_in, pipe_out, fds_to_close);
      
      





PIPE_INおよびPIPE_OUTは、入力および出力ストリームに関する情報を含むファイル記述子です。 NO_PIPEの値を取ることができます。つまり、I / Oはstdin / stdoutです。

execute_pipeline()はかなり膨大な関数であり、その実装はexecute_cmd.cに含まれています。 私たちにとって最も興味深い部分を検討します。

execute_cmd.c

 2112 prev = pipe_in; 2113 cmd = command; 2114 2115 while (cmd && cmd->type == cm_connection && 2116 cmd->value.Connection && cmd->value.Connection->connector == '|') 2117 { 2118 /*      */ 2119 if (pipe (fildes) < 0) 2120 { /*   */ } ....... /*     ,      prev —   ,     fildes[1] —   ,     pipe() */ 2178 execute_command_internal (cmd->value.Connection->first, asynchronous, 2179 prev, fildes[1], fd_bitmap); 2180 2181 if (prev >= 0) 2182 close (prev); 2183 2184 prev = fildes[0]; /*        */ 2185 close (fildes[1]); ....... 2190 cmd = cmd->value.Connection->second; /* “”      */ 2191 }
      
      



したがって、bashは、検出した各「|」文字に対してpipe()システムコールを呼び出すことにより、パイプラインシンボルを処理します。 適切なファイル記述子を入力および出力ストリームとして使用して、各コマンドを個別のプロセスで実行します。



ソースコード、レベル2、カーネル



カーネルコードに戻り、pipe()関数の実装を確認します。 この記事では、カーネルバージョン3.10.10安定版について説明します。

fs / pipe.c (この記事のコードスニペットがありません):

 /*       .       /proc/sys/fs/pipe-max-size */ 35 unsigned int pipe_max_size = 1048576; /*    ,   POSIX     , .. 4 */ 40 unsigned int pipe_min_size = PAGE_SIZE; 869 int create_pipe_files(struct file **res, int flags) 870 { 871 int err; 872 struct inode *inode = get_pipe_inode(); 873 struct file *f; 874 struct path path; 875 static struct qstr name = {. name = “” }; /*  dentry  dcache */ 881 path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb, &name); /*     file.    FMODE_WRITE,     O_WRONLY, ..             .   O_NONBLOCK   . */ 889 f = alloc_file(&path, FMODE_WRITE, &pipefifo_fops); 893 f->f_flags = O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)); /*      file   (. FMODE_READ   O_RDONLY) */ 896 res[0] = alloc_file(&path, FMODE_READ, &pipefifo_fops); 902 res[0]->f_flags = O_RDONLY | (flags & O_NONBLOCK); 903 res[1] = f; 904 return 0; 917 } 918 919 static int __do_pipe_flags(int *fd, struct file **files, int flags) 920 { 921 int error; 922 int fdw, fdr; /*   file     (.  ) */ 927 error = create_pipe_files(files, flags); /*     */ 931 fdr = get_unused_fd_flags(flags); 936 fdw = get_unused_fd_flags(flags); 941 audit_fd_pair(fdr, fdw); 942 fd[0] = fdr; 943 fd[1] = fdw; 944 return 0; 952 } /*    int pipe2(int pipefd[2], int flags)... */ 969 SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags) 970 { 971 struct file *files[2]; 972 int fd[2]; /*    /     */ 975 __do_pipe_flags(fd, files, flags); /*     kernel space  user space */ 977 copy_to_user(fildes, fd, sizeof(fd)); /*       */ 984 fd_install(fd[0], files[0]); 985 fd_install(fd[1], files[1]); 989 } /* ... int pipe(int pipefd[2]),        pipe2   ; */ 991 SYSCALL_DEFINE1(pipe, int __user *, fildes) 992 { 993 return sys_pipe2(fildes, 0); 994 }
      
      



気付いた場合、コードはO_NONBLOCKフラグをチェックしています。 fcntlのF_SETFL操作を使用して設定できます。 彼は、パイプラインのI / Oフローをブロックせずにモードに切り替える責任があります。 このモードでは、ロックする代わりに、ストリームの読み取り/書き込みプロセスはerrnoコードEAGAINで終了します。



パイプラインに書き込まれるデータブロックの最大サイズは、armのアーキテクチャのメモリの1ページ(4K)です。

arch / arm / include / asm / limits.h

  8 #define PIPE_BUF PAGE_SIZE
      
      



kernels> = 2.6.35の場合、パイプラインバッファーのサイズを変更できます。

 fcntl(fd, F_SETPIPE_SZ, <size>)
      
      



上で見たように、最大​​許容バッファサイズは、ファイル/ proc / sys / fs / pipe-max-sizeで指定されます。



ヒントとトリック



以下の例では、既存のDocumentsディレクトリと2つの存在しないファイル(./non-existent_file and)でlsを実行します。 / other_non-existent_file。



  1. stdoutとstderrの両方をパイプにリダイレクトする


     ls -d ./Documents ./non-existent_file ./other_non-existent_file 2>&1 | egrep “Doc|other” ls: cannot access ./other_non-existent_file: No such file or directory ./Documents
          
          



    または、文字 '|&'の組み合わせを使用することもできます(シェルのドキュメント(man bash)と、Yaccパーサーbashを調べた上記のソースの両方から学ぶことができます)。

     ls -d ./Documents ./non-existent_file ./other_non-existent_file |& egrep “Doc|other” ls: cannot access ./other_non-existent_file: No such file or directory ./Documents
          
          





  2. _only_ stderrをパイプにリダイレクトする


     $ ls -d ./Documents ./non-existent_file ./other_non-existent_file 2>&1 >/dev/null | egrep “Doc|other” ls: cannot access ./other_non-existent_file: No such file or directory
          
          



    足で自分を撃つ

    stdoutおよびstderrのリダイレクト順に従うことが重要です。 たとえば、「> / dev / null 2>&1」の組み合わせは、stdoutとstderrの両方を/ dev / nullにリダイレクトします。



  3. 正しいパイプライン終了コードを取得する


    デフォルトでは、パイプラインの終了コードは、パイプラインの最後のコマンドの終了コードです。 たとえば、ゼロ以外のコードで終わる元のコマンドを使用します。

     $ ls -d ./non-existent_file 2>/dev/null; echo $? 2
          
          



    そして、それをパイプに入れます:

     $ ls -d ./non-existent_file 2>/dev/null | wc; echo $? 0 0 0 0
          
          



    これで、パイプラインの終了コードはwcコマンドの終了コードになります。 0。



    通常、パイプラインの実行中にエラーが発生したかどうかを知る必要があります。 これを行うには、pipefailオプションを設定します。これは、パイプライン終了コードがパイプラインコマンドのいずれかの最初のゼロ以外の終了コードと一致すること、またはすべてのコマンドが正常に完了した場合はゼロであることをシェルに通知します:

     $ set -o pipefail $ ls -d ./non-existent_file 2>/dev/null | wc; echo $? 0 0 0 2
          
          



    足で自分を撃つ

    ゼロ以外を返す可能性のある「無害な」コマンドに注意してください。 これは、コンベアでの作業だけではありません。 たとえば、grepの例を考えてみましょう。

     $ egrep “^foo=[0-9]+” ./config | awk '{print “new_”$0;}'
          
          



    ここで、見つかったすべての行を印刷し、各行の先頭に「new_」を割り当てます。または、目的の形式の行が見つからなかった場合は何も印刷しません。 問題は、一致が見つからなかった場合にgrepがコード1で終了するため、スクリプトでpipefailオプションが設定されている場合、この例はコード1で終了することです。

     $ set -o pipefail $ egrep “^foo=[0-9]+” ./config | awk '{print “new_”$0;}' >/dev/null; echo $? 1
          
          



    複雑な設計と長いコンベヤーを備えた大規模なスクリプトでは、この点を見落とす可能性があり、誤った結果につながる可能性があります。



  4. パイプラインの変数に値を割り当てる


    まず、パイプラインのすべてのコマンドがclone()を呼び出して受信した個別のプロセスで実行されることを思い出してください。 原則として、変数の値を変更する場合を除き、これは問題を引き起こしません。

    次の例を考えてみましょう。

     $ a=aaa $ b=bbb $ echo “one two” | read ab
          
          



    これで、変数aとbの値がそれぞれ「1」と「2」になると予想されます。 実際、それらは「aaa」と「bbb」のままです。 一般に、外部のパイプラインの変数の値を変更しても、変数は変更されません。

     $ filefound=0 $ find . -type f -size +100k | while true do read f echo$f is over 100KB” filefound=1 break #      done $ echo $filefound;
          
          



    findが100Kbを超えるファイルを検出した場合でも、filefoundフラグの値は0のままです。

    この問題に対するいくつかの解決策が考えられます。

    • 使用する
       set -- $var
            
            





      この構成体は、var変数の内容に従って位置変数を設定します。 たとえば、上記の最初の例のように:

       $ var=”one two” $ set -- $var $ a=$1 # “one” $ b=$2 # “two”
            
            



      スクリプトでは、呼び出された元の位置パラメータが失われることに留意する必要があります。
    • 変数値を処理するすべてのロジックをパイプラインの同じサブプロセスに転送します。

       $ echo “one” | (read a; echo $a;) one
            
            



    • ロジックを変更して、パイプライン内で変数を割り当てないようにします。

      たとえば、findを使用して例を変更します。

       $ filefound=0 $ for f in $(find . -type f -size +100k) #   ,     do read f echo “$f is over 100KB” filefound=1 break done $ echo $filefound;
            
            



    • (bash-4.2以降のみ)lastpipeオプションを使用します

      lastpipeオプションは、メインプロセスで最後のパイプラインコマンドを実行するようシェルに指示します。

       $ (shopt -s lastpipe; a=”aaa”; echo “one” | read a; echo $a) one
            
            



      lastpipeオプションは、対応するパイプラインが呼び出されるプロセスと同じプロセスのコマンドラインで設定する必要があるため、上記の例の括弧が必要です。 スクリプトでは、括弧はオプションです。


追加情報






All Articles