ホーム » スタッフ » 斉藤徹 » 講義録 » 情報メディア工学 » リダイレクト・パイプ、ジョブ管理・プロセス管理

2022年6月
 1234
567891011
12131415161718
19202122232425
2627282930  

最新の投稿(電子情報)

アーカイブ

カテゴリー

リダイレクト・パイプ、ジョブ管理・プロセス管理

Linuxを使う上で、キーボードでコマンドを入力しているが、こういうコマンドの管理を行うプログラムshell と呼ぶ。shell には、色々なものがある(sh, csh, bash, zsh)が、広く使われている bash( born-again shell )について説明する。最初に、コマンドの入出力を組み合わせるために重要となるリダイレクトとパイプについて説明し、次にコマンドなどの処理単位となるジョブやプロセスの考え方について説明を行う。

標準入出力とリダイレクト

出力リダイレクト

C言語のプログラミングで、プログラムの実行結果をレポートに張り付ける時はどのように行っているだろうか?多くの人は、実行画面を PrintScreen でキャプチャした画像を張り付けているかもしれない。しかし、数十行にわたる結果であれば何度もキャプチャが必要となる。
そこで、今日の最初はリダイレクト機能について説明する。

“gcc ファイル.c” は、C言語のプログラムをコンパイルし、a.out という実行ファイルを生成する。”./a.out” にてプログラムを実行する。実行する命令に、“> ファイル名” と書くと、通常の出力画面(標準出力) をファイル名に記録してくれる。これを出力リダイレクトと呼ぶ。また、“>> ファイル名” と書くと、既存ファイルの後ろに追記してくれる。

guest00@nitfcei:~$ cat helloworld.c
#include <stdio.h>
int main() {
    printf( "Hello World\n" ) ;
    return 0 ;
}

guest00@nitfcei:~$ gcc helloworld.c
guest00@nitfcei:~$ ./a.out
Hello World

guest00@nitfcei:~$ ./a.out > helloworld.txt

guest00@nitfcei:~$ cat helloworld.txt
Hello World

guest00@nitfcei:~$ ./a.out >> helloworld.txt

guest00@nitfcei:~$ cat helloworld.txt 
Hello World
Hello World 

入力リダイレクト

次に、1行に名前と3教科の点数が書いてある複数行に渡るデータの各人の平均点を求めるプログラムを考える。

guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c .
guest00@nitfcei:~$ cat avg-each-low.c
#include <stdio.h>
// ((input))           ((output))
// saitoh  43  54 82   saitoh 59.67
// tomoko  89 100 32   tomoko 73.67
// mitsuki 79  68 93   mitsuki 80.00
int main() {
   char name[ 100 ] ;
   int point[ 3 ] ;
   while( scanf( "%s%d%d%d" ,
                 name , &point[0] , &point[1] , &point[2] ) == 4 ) {
      double sum = 0.0 ;
      for( int i = 0 ; i < 3 ; i++ )
         sum += point[i] ;
      printf( "%s %6.2f\n" , name , sum / 3.0 ) ;
   }
   return 0 ;
}

guest00@nitfcei:~$ gcc avg-each-low.c
guest00@nitfcei:~$ ./a.out
saitoh 43  54 82    入力
saitoh 59.67        出力
tomoko 89 100 32    入力
tomoko 73.67        出力
^D             ← Ctrl-D を押すとファイル入力を終了

しかし、プログラムの書き方を間違えてプログラムを修正していると、動作確認のたびに何度も同じデータを入力するかもしれないが、面倒ではないだろうか?

プログラムを実行する時に、“< ファイル名” をつけると、通常はキーボードから入力する所を、ファイルからの入力に切り替えて実行することができる。このようなscanf()を使う時のようなプログラムの入力を標準入力といい、それをファイルに切り替えることを入力リダイレクトと呼ぶ。

guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt .

guest00@nitfcei:~$ cat name-point3.txt
saitoh  43  54 82
tomoko  89 100 32
mitsuki 79  68 93 

guest00@nitfcei:~$ ./a.out < name-point3.txt
saitoh  59.67
tomoko  73.67
mitsuki 80.00

この入力リダイレクトと出力リダイレクトを合わせて使うこともできる。

guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt

guest00@nitfcei:~$ cat name-avg.txt
saitoh  59.67
tomoko  73.67
mitsuki 80.00

パイプ

先の名前と3教科のプログラムの結果から、全員の平均点をも計算したい場合、どのようなプログラムを作るだろうか?C言語だけの知識なら、各人の行のデータを計算するループの中に、全員の合計と人数を求めるプログラムを書いて、最後に平均点を出力するだろう。

一方で、複数人の名前と平均点のデータから平均点を求めるプログラムを書いて、前述のプログラムの実行結果を使う人もいるだろう。

以下の例では、“gcc -o avg-each-row avg-each-row.c” で、avg-each-row という実行ファイル、“gcc -o avg-all avg-all.c” で、avg-all という実行ファイルを生成し、avg-each-row で入力リダイレクト・出力リダイレクトを使って、name-avg.txt を作り、avg-all を入力リダイレクトで、最終結果を表示している。

guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c .
guest00@nitfcei:~$ cat avg-all.c
#include <stdio.h>
// ((input))      ((output))
// saitoh  59.67  73.11
// tomoko  73.67
// mitsuki 80.00
int main() {
   char name[ 100 ] ;
   double point ;
   double sum = 0 ;
   int count = 0 ;
   while( scanf( "%s%lf" , name , &point ) == 2 ) {
      sum += point ;
      count++ ;
   }
   printf( "%6.2f\n" , sum / (double)count ) ;
   return 0 ;
}

guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c
guest00@nitfcei:~$ gcc -o avg-all avg-all.c

guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt

guest00@nitfcei:~$ ./avg-all < name-avg.txt
71.11

しかし、いちいち入出力の結果を name-avg.txt を作るのは面倒である。であれば、以下の様なイメージで処理をすれば答えが求まる。

name-point3.txt(avg-each-row)name-avg.txt(avg-all)結果

これは、パイプ機能を使って以下の様に動かすことができる。

guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all
71.11

guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all
71.11

プログラムを実行する時に、“A | B” ように書くと、プログラムA の標準出力結果を、プログラムB の標準入力に接続させて、2つのプログラムを実行できる。このような機能を、パイプと呼ぶ。上記例の2つめ “cat… | ./avg-each-low | ./avg-all” では3つのプログラムをパイプでつないでいる。


リダイレクトのまとめ

 

入力リダイレクト(標準入力) 実行コマンド < 入力ファイル
出力リダイレクト(標準出力) 実行コマンド > 出力ファイル
 出力リダイレクト(標準出力の追記) 実行コマンド >> 出力ファイル
 標準エラー出力のリダイレクト 実行コマンド 2> 出力ファイル
パイプ
コマンドAの標準出力をコマンドBの標準入力に接続
コマンドA | コマンドB

C言語のコンパイルまとめ

 

C言語のコンパイル(実行ファイルはa.out) gcc ソースファイル
 実行ファイル名を指定してコンパイル gcc -o 実行ファイル ソースファイル

フィルタプログラム

パイプを使うと、標準入力からデータをもらい・標準出力に結果を出力するような簡単なプログラムを組み合わせて、様々な処理が簡単にできる。こういったプログラムは、フィルタと呼ぶ。

簡単な例として、入力をすべて大文字に変換するプログラム(toupper)、入力文字をすべて小文字に変換するプログラム(tolower)が、下記の例のように保存してあるので動作を確かめよ。

guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/toupper.c .
guest00@nitfcei:~$ gcc -o toupper toupper.c .
guest00@nitfcei:~$ cat toupper.c | ./toupper
#INCLUDE <STDIO.H>
#INCLUDE <CTYPE.H>
INT MAIN() {
    INT     C ;
    WHILE( (C = GETCHAR()) != EOF )
        PUTCHAR( TOUPPER( C ) ) ;
    RETURN 0 ;
}

guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/tolower.c .
guest00@nitfcei:~$ gcc -o tolower tolower.c
guest00@nitfcei:~$ cat tolower.c | ./tolower
(((何が出力されるか答えよ)))

よく使われるフィルタのまとめ

 

文字パターンを含む行だけ出力 grep 文字パターン
文字パターンを含まない行を出力
文字パターンを正規表現でマッチングし該当を出力
大文字小文字を区別しない
grep -v 文字パターン
grep -e 正規表現
grep -i 文字パターン
入力文字数・単語数・行数をカウント(word counter) wc
入力行数をカウント wc -l
データを昇順に並べる sort
データを降順に並べる
先頭を数字と見なしてソート
sort -r
sort -g
同じ行データが連続したら1つにまとめる uniq
同じ行が連続したら1つにまとめ、連続した数を出力 uniq -c
空白区切りで指定した場所(1番目)を抽出 awk ‘{print$1;}’
入力の先頭複数行を表示(10行) head
入力の末尾複数行を表示(10行) tail
指定した行数だけ、先頭/末尾を表示 head -行数
tail -行数

ジョブ管理

プログラムを実行している時、それがすごくメモリも使い計算時間もかかる処理の場合、条件を変化させながら結果が欲しい時、どのように実行すべきだろうか?1つの処理が1時間かかるとして、画面を見ながら1時間後に処理が終わったことを確認してプログラムを実行するのか?

簡単な方法としては、1つ目の処理(仮にプログラムAとする)を実行させたままで、新しくウィンドウを開いてそこで新しい条件でプログラムを並行処理すればいい(プログラムBとする)と考えるかもしれない。しかし、メモリを大量に使用する処理をいくつも並行処理させると、仮想メモリが使われるようになる。結果的にスワッピングが発生する分、プログラムAを実行させた後にプログラムBを実行するための時間以上に、時間がかかることになる。

ここで、プログラムを並行処理させるか、逐次処理させるといった、JOB(ジョブ)管理について説明を行う。
以下の説明で、複雑で時間のかかる処理を実行するとサーバの負担が高くなるので指定時間の処理待ちを行うための sleep 命令を使う。

逐次実行と並行実行

プログラムを連続して実行(処理Aの後に処理Bを実行する)場合には、セミコロン”;” で区切って A ; B のように処理を起動する。

guest00@nitfcei:~$ echo A
A
guest00@nitfcei:~$ echo A ; echo B
A
B

プログラムを並行して実行(処理Aと処理Bを並行処理)する場合には、アンド”&”で区切って A & B のように処理を起動する。

guest00@nitfcei:~$ sleep 5 &
[1] 55
guest00@nitfcei:~$ echo A
A
[1]+ 終了  sleep 5
guest00@nitfcei:~$ sleep 2 & sleep 3
[1] 56
[1]+ 終了  sleep 2
guest00@nitfcei:~$ time ( sleep 1 ; sleep 1 )   # time コマンドは、コマンドの実行時間を測ってくれる。
real    0m2.007s
user    0m0.005s
sys     0m0.002s
guest00@nitfcei:~$ time ( sleep 1 & sleep 1 )
real    0m1.002s
user    0m0.003s
sys     0m0.000s

fg, bg, jobs コマンド

プログラムを実行中に、処理(ジョブ)を一時停止したり、一時停止している処理を復帰させたりするときには、fg, bg, jobs コマンドを使う。

  • 処理をしている時に、Ctrl-C を入力すると前面処理のプログラムは強制停止となる。
  • 処理をしている時に、Ctrl-Z を入力すると前面処理のプログラムは一時停止状態になる。
  • fg (フォアグラウンド) は、指定した処理を前面処理(キー入力を受け付ける処理)に変更する。
  • bg (バックグラウンド) は、指定した処理を後面処理(キー入力が必要になったら待たされる処理)に変更する。
  • jobs (ジョブ一覧) は、実行中や一時停止している処理(ジョブ)の一覧を表示する。
guest00@nitfcei:~$ sleep 10   # 途中で Ctrl-Z を入力する
^Z
[1]+ 停止  sleep 10
guest00@nitfcei:~$ fg
sleep 10                      # 一時停止していた sleep 10 を実行再開
guest00@nitfcei:~$ sleep 3
^Z
[1]+  停止  sleep 3
guest00@nitfcei:~$ sleep 4
^Z
[2]+  停止  sleep 4
guest00@nitfcei:~$ jobs
[1]-  停止  sleep 3           # [1],[2]というのはjob番号
[2]+  停止  sleep 4
guest00@nitfcei:~$ fg %1      # ジョブ番号1 を前面処理にする
sleep 3
guest00@nitfcei:~$ fg %2      # ジョブ番号2 を前面処理にする
sleep 4

ps, kill コマンド

OS では、プログラムの処理単位は プロセス(process) と呼ぶ。OS はプロセスごとにメモリの実行範囲などの管理を行う。一連のプロセスを組み合わせて実行する単位を ジョブ(job) と呼ぶ。

複数のプロセスは間違ったメモリアクセスで他のプロセスが誤動作するようでは、安心して処理が実行できない。そこで、OS は、プロセスが他のプロセスのメモリをアクセスすると強制停止させるなどの保護をしてくれる。しかし、プロセスと他のプロセスが協調して処理を行うための情報交換のためにメモリを使うことは困難である。プロセス間で情報交換が必要であれば、パイプ機能やプロセス間共有メモリ機能を使う必要がある

最近のOSでは、共通のメモリ空間で動き 並行動作する個々の処理は スレッド(thread) と呼び、その複数のスレッドをまとめたものがプロセスとなる。OS では、プロセスごとに番号が割り振られ、その番号を プロセスID(PID) と呼ぶ。実行中のプロセスを表示するには、ps コマンドを使う。

実行中のプロセスを停止する場合には、kill コマンドを用いる。停止するプログラムは、ジョブ番号(%1など) か プロセスID を指定する。

guest00@nitfcei:~$ sleep 3
^Z
[1]+  停止  sleep 3
guest00@nitfcei:~$ sleep 4
^Z
[2]+ 停止 sleep 4
guest00@nitfcei:~$ jobs
[1]- 停止 sleep 3                 # [1],[2]というのはjob番号
[2]+ 停止 sleep 4
guest00@nitfcei:~$ ps w          # プロセスの一覧(wを付けるとコマンドの引数も確認できる)
 PID TTY   STAT TIME     CMD
  13 pts/0 Ss   00:00:00 -bash
  84 pts/0 T    00:00:00 sleep 3
  85 pts/0 T    00:00:00 sleep 4
  86 pts/0 R    00:00:00 ps w
guest00@nitfcei:~$ kill %1 
[1]- Terminated  sleep 3
guest00@nitfcei:~$ kill -KILL 85
[2]+ 強制終了     sleep 4
guest00@nitfcei:~$ ps ax          # 他人を含めた全プロセスの一覧表示 
 PID TTY STAT TIME COMMAND
   1 ?   Ss   0:52 /sbin/init
   2 ?   S    0:00 [kthreadd]
   3 ?   I<   0:00 [rcu_gp]
   :

理解度確認