システムプログラム(2009年度1学期前半)練習問題

練習問題(1)

Emacsにインデンテーションに記述されている設定を行い,TABキーを叩いた時のインデントの深さが変化することを確認しなさい. また,Emacsでもとの(インデントが浅い)スタイルで書かれたプログラムのファイルを開き,各行でTABキーを叩くことで,新しいインデンテーションスタイルが適用されることを確認しなさい.

さらに,

ことにより,プログラムが見やすくなるかどうかを確認しなさい.

練習問題(2)

Segmentation fault を起こすプログラムをかき,それをデバッガで追跡し,どこで起きたかを調べなさい. プログラムは cc -g でコンパイルすること.

練習問題(3)

Makefile はコンパイルだけに使用する必要はない. ルールにのっとって,処理をするためのコマンドを実行するだけである. そこで,Makefile を用いてよく行われるのが,コンパイルによって作られたファイルの消去である.

Makefile にルールを追加し,以下のようにオブジェクトファイルが消去されるようにしなさい.

% make clean [←]
/bin/rm -f a.out file-1.o file-2.o
% 

練習問題(4)

make は便利に使えるように,予め様々な暗黙のルールを持っており,処理の全てを事細かに Makefile に記述しなくても処理を行えるようになっている. a.out が file-1.o と file-2.o から作られる依存関係が定義されており,カレントディレクトリに file-1.c,file-2.c がある場合,make は .c というサフィックスから cc -c コマンドを用いて .o ファイルを作成するというルールを持っている. つまり,file-1.o は file-1.c に依存し,cc -c コマンドで作成するというルールを,いちいち Makefile に書く必要はなくなる. また変数を定義する機能もあり,複数のファイル名を値として持つ変数を定義することで,いちいちファイル名を列挙する必要が無くなる. 例えば,

OBJS = file-1.o file-2.o

と書くと,シェルで変数値を参照するときのように,$(OBJS) は file-1.o file-2.o を意味するようになる.

変数と暗黙のルールを用いて,練習問題(4)で作成した Makefile をできるだけ短く書き換えなさい.

参考: 暗黙のルールは make -p を使うと表示できる. 実際に処理コマンドの実行が必要でない場合が多いので,make -n -p とするとよい.

練習問題(5)

プログラミングとデバッグではバブル整列法を例にして、デバッグのために制御の流れの確認,変数の値の確認をするための出力をする文を入れている.これを,より効率の良い(即ち効率が O(n2) の単純選択法や単純挿入法以外の)整列法(ヒープソート,クィックソート,ラディックスソートなど)方法に替え,同じように制御の流れの確認,変数の値の確認のための出力文を入れることで,ソートの効率が良くなっていることがわかるようなプログラムを作りなさい.

練習問題(6)

文字列のところにあるプログラムを変更し,文字列定数の内容を変更すると,セグメンテーションフォルトが発生することを確かめよ.

また,cc のオプションに -fwritable-strings を与えると文字列定数の内容を変更が可能になることも確かめよ.

練習問題(7)

標準入出力を用いるライブラリ関数のところにある fgets,puts を用いたプログラムの LINE_LEN を 5 にして,コンパイル実行すると以下のような結果になってしまう. どうしてこのような結果になってしまうのか,その理由を調べよ.

% ./a.out [←]
1234567890[←]
1234
5678
90

abcdefg[←]
abcd
efg

[C-D]
%

練習問題(8)

文字列の中に含まれる単語の数を数える関数 wc を作成し,main 関数から wc 関数をいくつかの文字列を引数に呼び出し,wc 関数が正しく動くことを確かめなさい. 単語はスペースで区切られているものとするが,区切り文字を任意に引数として与えることができるようにしても良い.

練習問題(9)

文字,文字列の検索のところにあるプログラムは,先頭に / がある場合や末尾が / で終わる場合をうまく扱えず,実行結果は次のようになってしまう.

% ./a.out [←]
/dir1/dir2/dir3/file[←]
0:
1: dir1
2: dir2
3: dir3
4: file

dir0/dir2/dir3/[←]
0: dir0
1: dir2
2: dir3
3:

[C-D]
%

先頭の / はパスの構成要素と認識され上の最初の例では 0: のところに / が出力されるように,また末尾の / は無視され上の次の例では 3: が出力されないように,変更せよ.

練習問題(10)

strlcpy, strdup と同じ動作をする関数 my_strlcpy, my_strdup を作成せよ. main 関数から strlcpy, my_strlcpy および strdup, my_strdup のそれぞれに同じ引数をいくつかのパターンで与えるプログラムも作成し,同じように動作していることを確かめなさい. 動作確認をするプログラムでは,ヒープ領域に確保されたメモリ領域は必ず free すること.

練習問題(11)

strcmp, strcasecmp と同じ動作をする関数 my_strcmp, my_strcasecmp を作成せよ. main 関数から strcmp, my_strcmp および strcasecmp, my_strcasecmp のそれぞれに同じ引数をいくつかのパターンで与えて,同じように動作していることを確かめなさい.

練習問題(12)

以下のように,シェルのように1行入力を受け取り,コマンド名と入力のリダイレクション記号「<」があればその後のファイル名を表示し,そうでなければ入力として console と出力するプログラムを作りなさい. 入力行は,コマンド名だけ,または「コマンド名 < ファイル名」(< の前後にスペース1つ)という形式だけに対応すればよい.

% ./a.out [←]
command[←]
command name: command
input: console
command < file[←]
command name: command
input: file
%

練習問題(13)

練習問題(12)のプログラムを変更し,出力のリダイレクション記号「>」にも対応できるようにせよ. リダイレクション記号の出現順序は入力「<」の後に出力「>」が現れる場合にのみ対応すればよい.

malloc や strdup などを用いてヒープ領域に確保したメモリ領域は必ず free し,メモリリークを起こさないようにすること.

練習問題(14)

練習問題(13)のプログラムを変更し,

  1. 同じリダイレクション記号が出現順序が変わっても対応できるように
  2. 同じリダイレクション記号が複数出てきたらエラーメッセージが出力されるように
  3. スペースがコマンド,リダイレクション記号,ファイル名の周りに入っていなくても,または複数個入っていても,同じように処理できるように
しなさい.

練習問題(15)

(1) ライブラリを用いてファイルコピーを行うプログラム,(2) これを fgets, fputs を用いるように変更したもの,(3) fread, fwrite を用いるように変更したもの,(4) システムコールを用いてファイルコピーを行うプログラムのそれぞれで,コピー元のファイルをサイズが大きなものに変更し,バッファサイズをいろいろ(例えば数バイトの小さいものから64MB程度の大きなものまで)変えて実行時間がどのように変化するか実験せよ.

それぞれの結果はどのようになっただろうか? 違いについて比較し考察せよ.

プログラムの実行時間は time コマンドを用いて計測することができる. 以下の実行例では,ユーザ空間での実行に 1.05 秒,カーネルでの実行に 5.87秒,待ち時間も入れて合計で 6.96 秒かかったという結果である. 計測には,同じパラメータで何度か実行,計測し,その平均値をとるなどすると,より信頼性の高い値を得ることができる.

% time ./a.out [←]
1.050u 5.870s 0:06.96 99.4%     0+0k 0+0io 77pf+0w
%

実験後は,コピーした大きなファイルは消去すること!!

練習問題(16)

システムコールを用いてファイルをコピーするプログラムを1文字単位ではなく,BUFSIZ 単位で読み書きするように変更しなさい. このとき,実際に読み書きされた BUFSIZ 以下であった場合,BUFSIZ になるまで読み書きを繰り返すようにすること.

練習問題(17)

lseek または fseek を用いて,ファイルの末尾を引数で指定された行数だけ表示する tail コマンドに似たプログラムを作りなさい. 表示する順序は,ファイルに記述されている順序を守ること.

練習問題(18)

utmpファイルの各エントリの内容を保持するリストを作るプログラムを変更し,20秒おきに utmp ファイルを読み込み,内容に変更があった場合のみ現在時刻とともに出力するプログラムを作りなさい. なお,内容に変更があったかどうかは,ファイルの更新時間ではなく,前回読み込んだ内容と比較することによって判断するものとする. そのために,ut_name が空でないエントリについてのみ,struct utmp を含むリスト (linked list) を用いて記憶するものとする.(従って、出力もut_name が空でないエントリについてのみとなる.)

20秒間実行を停止するには SLEEP (3) を用いる. また,現在時刻としては TIME (3) により得た値を CTIME (3) により文字列に変換して表示する.

実行結果は,端末を開いたり閉じたりといった utmp ファイルの内容を変更する操作を繰り返し,正しく動作していることがわかるようにしなさい.

練習問題(19)

コマンドライン (argv) や標準入力などの処理の結果バッファオーバーフローを起こしてしまうプログラムを書き,そのプログラムを実行するプロセスに適当なデータを与えることで,シェルを起動させてみよ.

プログラムが,バッファオーバーフローを起こすためのヒントとなる情報を出力し,その情報をもとに入力を与えるようにしても良い. また,使用するマシンは orchid-calc1 〜 orchid-calc6 (Linux x86) でも構わない.

練習問題(20)

printenv コマンドは,現在のプロセスの環境変数を表示してくれる. 同様の表示をしてくれるコマンドを,Cプログラムとして記述せよ.

練習問題(21)

メモリマップを確かめるプログラムをコンパイルし,繰り返し実行してみたところ,以下のようにスタックのアドレスが毎回若干変化していることがわかった. これを確かめ,なぜスタックの場所が変わるか,その理由について考えてみなさい.

% ./a.out [←]
environ:        0xbfffe8dc
argv:           0xbfffe8d4
stack:          0xbfffe867
bss:            0x080496e8
data:           0x080495f0
% ./a.out [←]
environ:        0xbfffe6dc
argv:           0xbfffe6d4
stack:          0xbfffe667
bss:            0x080496e8
data:           0x080495f0
% ./a.out [←]
environ:        0xbfffe7dc
argv:           0xbfffe7d4
stack:          0xbfffe767
bss:            0x080496e8
data:           0x080495f0
%

練習問題(22)

以下のプログラムをコンパイル,実行すると hello が8回表示される. その理由について考えよ.

     1  #include <stdio.h>
     2
     3  main()
     4  {
     5          fork();
     6          fork();
     7          fork();
     8          printf("hello\n");
     9  }

練習問題(23)

system(3) は,シェルによるコマンドの実行を行ってくれるライブラリ関数である. 以下のように,シェル(/bin/sh)のコマンドラインを文字列で渡すと実行してくれる.

     1  #include 
     2  #include 
     3
     4  main()
     5  {
     6          system("ls *.c | wc");
     7  }

fork, execve(又は相当のライブラリ関数),wait などを使用して,system(3)相当の機能を持つ関数 mysystem を作りなさい.

マニュアルページに記述してあるシグナルについては,無視してよい.

練習問題(24)

標準入力をファイルからリダイレクションするプログラムをもとに,標準出力もファイルに対しての書き込みとなるように変更せよ. 書き込むファイルは,プログラムの第2引数としてとるものとする.

練習問題(25)

第1引数で与えられるコマンドの実行結果(exit status)に応じて,第2引数又は第3引数で与えられるコマンドを実行するプログラム if-then-else を作りなさい.例えば

% ./if-then-else "test 1 -eq 1" "echo yes" "echo no" [←]
yes
% ./if-then-else "test 1 -eq 2" "echo yes" "echo no" [←]
no
%

となるようなものをである.

コマンドの区切りは別のものでも良い. 以下は then, else が区切りになっている. execve を使用する場合は,argv の then, else の部分を NULL にすると execve にそのまま渡せるので,以下のような区切りのほうが,プログラムは作りやすいかもしれない.

% ./if-then-else test 1 -eq 1 then echo yes else echo no

練習問題(26)

最初に fork で子プロセスを作り,親プロセスと子プロセスが交互に実行を繰り返すプログラムを作りなさい. 片方のプロセスに実行が移った時に1文字出力した後に,もう一方に実行を移すようにしなさい(下の実行例を参照). また,合計何文字出力するかはプログラムへの引数で指定できるようにしなさい.
但し、親プロセス終了までに fork で作る子プロセスは1つのみとする。 (forkされた子プロセスが1文字出力後にすぐにexitするようなプログラムは正解とは認めない。)

以下は,子プロセスは数字を親プロセスはアルファベットの大文字を出力するようにしてみたプログラムでの実行例である.

% ./a.out 30 [←]
0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D
%

子プロセスが1文字(例えば0)を出力した後,実行を親プロセスに戻し,親プロセスが1文字(A)を出力の後,また実行を子プロセスに移し,子プロセスが1文字(1)を出力し…というように,親と子プロセスの実行が交互に繰り返されている.

練習問題(27)

3個のプロセスの標準入出力を2つのパイプで結びなさい.

パイプの作り方は,先にパイプを2つ作ってから2回 fork する方法と,1つパイプを作り fork し,もう1つパイプを作りまた fork する方法があるが,どちらでも良い.

練習問題(28)

popen(3), pclose(3)は,パイプにより接続された指定されたプログラムを実行するプロセスを生成し,そのプロセスに対する入力又は出力のどちらか一方を提供するライブラリ関数である.

popen(3), pclose(3)相当の機能を持つ関数 mypopen, mypclose を作りなさい. 最初は,popenで実行するプログラムからの出力を受け取る機能だけを実装するところから始めるとよい.

練習問題(29)

パイプを使用するプログラムを,以下のように,親プロセスからの出力を子プロセスの入力にするように書き換えたところ,終了しなくなってしまった. その理由を考え,プログラムの修正せよ.

     1  #include <stdio.h>
     2
     3  int pipe_fd[2];
     4
     5  void
     6  do_parent()
     7  {
     8          char    *p = "Hello, my kid.";
     9          int     status;
    10
    11          printf("this is parent.\n");
    12
    13          close(pipe_fd[0]);
    14
    15          while (*p) {
    16                  if (write(pipe_fd[1], p, 1) < 0) {
    17                          perror("write");
    18                          exit(1);
    19                  }
    20                  p++;
    21          }
    22
    23          if (wait(&status) < 0) {
    24                  perror("wait");
    25                  exit(1);
    26          }
    27  }
    28
    29  void
    30  do_child()
    31  {
    32          char    c;
    33          int     count;
    34
    35          printf("this is child.\n");
    36
    37          close(pipe_fd[1]);
    38
    39          while ((count = read(pipe_fd[0], &c, 1)) > 0) {
    40                  putchar(c);
    41          }
    42          putchar('\n');
    43
    44          if (count < 0) {
    45                  perror("read");
    46                  exit(1);
    47          }
    48  }
    49
    50  main()
    51  {
    52          int child;
    53
    54          if (pipe(pipe_fd) < 0) {
    55                  perror("pipe");
    56                  exit(1);
    57          }
    58
    59          if ((child = fork()) < 0) {
    60                  perror("fork");
    61                  exit(1);
    62          }
    63
    64          if (child)
    65                  do_parent();
    66          else
    67                  do_child();
    68  }

練習問題(30)

kill システムコールでシグナルを送るプログラムとシグナルを受け取るプログラムを使用して,プロセスからプロセスへシグナルが送られるのを確かめなさい.

ヒント:端末ウィンドウを2つ開き,1つでシグナルを受け取るプログラムを動かし,もう1つでシグナルを送るプログラムを動かす. シグナルを送る相手のプロセスのIDは ps コマンドで調べるか,プログラムを変更して自分のプロセスIDを表示させるようにする.

練習問題(31)

複数の異なるシグナルに対しシグナルハンドラを設定し,シグナルに応じたシグナルハンドラが起動されることを確かめなさい.

練習問題(32)

不正なメモリアクセスは SIGSEGV により通知される. 不正なメモリアクセスとなったアドレスを表示するプログラムを作成しなさい.

ヒント:SIGACTION(2) の siginfo_t の情報を参照.

練習問題(33)

sleep は実はライブラリ関数である. シグナルの機能を用い,SLEEP(3) 相当の機能を持つ関数 mysleep を作りなさい.

SLEEP(3) と同じ戻り値を返し,またそれを確かめる実行例を含めること. 現在時刻は TIME (3) により取得できる. 使用後,不要になったシグナルの設定はキャンセルし,もとの設定に戻すこと. NANOSLEEP(2) は使用しないこと. リエントラントである必要はない.

練習問題(34)

キーボードから1文字入力を読み込む関数 getchar にタイムアウト機能を追加した関数 mygetchar を作りなさい. タイムアウト時間は,mygetchar への引数で指定する. 戻り値は,指定された時間内にキー入力があればその値,EOFの場合は -1,指定された時間内にキー入力がなくタイムアウトが起こったならば -2,それ以外の理由は -3 とする. 実行例では,戻り値,および現在時刻を TIME (3) により得た値を CTIME (3) により文字列に変換して表示することで,正しく動作していることを示しなさい.

使用後,不要になったシグナルの設定はキャンセルし,もとの設定に戻すこと. リエントラントである必要はない.

練習問題(35)

練習問題(26)と同じように,親プロセスと子プロセスが交互に実行することにより,交互に1文字出力するプログラムを,シグナルを用いて書きなさい. fork で作る子プロセスは1つだけとする.