ふつうのLinuxプログラミング 第2版 練習問題の解答例

Updated: 2017-09-09

第5章

問題1: 本章で作ったcatコマンドを改造して、コマンドライン引数でファイル名が渡されなかったら標準入力を読むようにしなさい。
サンプルコードのcat3.cを参照してください。
問題2: バッファ中の'\n'を数えることで、ファイルの行数を出力するコマンドを書きなさい(「wc -l」と同じ機能)。
サンプルコードのwc-l-syscall.cを参照してください。

第6章

問題1: タブ文字('\t')を「\t」という2文字、改行を「'$'+改行」の2文字として置き換えながら出力するcatコマンドを書きなさい。
サンプルコードのcat-escape.cを参照してください。
問題2: stdio APIを使ってファイルを読み込み、その行数を出力するコマンドを書きなさい(「wc -l」と同じ機能)。ファイル末尾に'\n'がない場合にも対応しましょう。
サンプルコードのwc-l-stdio.cを参照してください。
問題3: fread()とfwrite()を使ってcatコマンドを書きなさい。
サンプルコードのcat5.cを参照してください。

第7章

問題1: 第6章の練習問題で作った、'\t'や'\n'を可視化する機能を、catコマンドのオプションとして使えるようにしなさい。
サンプルコードのcat4.cを参照してください。
問題2: ファイルの最後の数行を出力するtailコマンドの実装を考えなさい。ただし、出力する行数は固定で構いません。(少し難しい)

サンプルコードのtail.cを参照してください。

このコードではリングバッファ(ring buffer)というデータ構造を使っています。 リングバッファは実体としては固定長のバッファなのですが、 末尾まで到達したら最初に戻ってデータを上書きするところが異なります。

tailコマンドの場合、ファイルを読み込みながら常にそのときの末尾10行だけがあれば十分なわけです。 言い換えると、11行目を読んだら1行目は不要になるはずです。 そこで11行目は1行目を書いたところに上書きしてしまえばいいだろうという発想が出てきます。 リングバッファはまさにそのような処理をするためのデータ構造です。

第8章

問題1: 本物のgrepコマンドにある-iオプションと-vオプションを、本章で作ったgrepコマンドに追加しなさい。
サンプルコードのgrep2.cを参照してください。
問題2: 正規表現に適合した行ではなく、適合した部分文字列を出力するコマンドsliceを書きなさい。regexec()の第3引数と第4引数を使う必要があるのでマニュアルで調べましょう。
サンプルコードのslice.cを参照してください。

第10章

問題1: コマンドライン引数で指定されたディレクトリ以下を再帰的にトラバースして、見つかったファイルのパスをすべて表示するプログラムを書きなさい。シンボリックリンクをたどってはいけません。
サンプルコードのtraverse.cを参照してください。
問題2: ファイルをopen()して、close()する前にそのファイルをrename()すると何が起きるでしょうか。unlink()はどうか、別のファイルをrename()するとどうなるか、実験して調べなさい。
open元のファイルをrename()したりunlink()したりしても、すでに接続されているストリームを使うとファイルの読み書きを継続できます。これはUNIXのファイルシステムの大きな特徴です。
問題3: コマンドライン引数で指定されたパスまでのディレクトリを再帰的に作成するコマンドを書きなさい(mkdir -pに相当)。

サンプルコードのmkpath.cを参照してください。

このmkpath.cでは、まずmkdir(2)を実行してみて、 エラーになったら親ディレクトリを作りに行くという戦術を採用しています。 例えば mkpath a/b/c を実行したときは、まず a/b/c をmkdirしてみて、成功したらそれで終わります。 mkdirがENOENTで失敗した場合は a/b が存在しないと思われるので、make_path関数を再起呼び出しして a/b を作ります。 この繰り返しでディレクトリを再帰的に作成します。

この戦術のよいところは、同じ戦術のmkpathプログラムがいくつ同時に動いていても正常に動作するところです。 例えば他の方法としてはstat(2)でディレクトリが存在するかチェックしてからmkdir(2)で作りに行く方法も考えられます。 しかし、その方法だと、stat(2)でチェックした直後に他のプロセスがディレクトリを作ってしまい、 mkdir(2)がエラーになる可能性があります。 まずmkdir(2)する戦術だと、そのような微妙なタイミングによるエラーは起きません。

第11章

問題1: 標準入力の末尾数行だけを出力するtailコマンドを書きなさい。表示する行数はコマンドラインオプションで受け取れるようにしてください。
サンプルコードのtail2.cを参照してください。 このコマンドはtail.cと同じくリングバッファを使いますが、 動的にメモリを確保する必要があるため、mallocを使うところだけが違います。

第12章

問題1: fork()したらプロセスが使うメモリは倍になるのでしょうか。調べなさい。
forkするだけではメモリ使用量はほとんど増えません。 Linuxでは、forkした直後の2つのプロセスはほとんどの論理アドレスについて元の物理メモリを共有するからです。 どちらかのプロセスでメモリへの変更を行うと、そのとき始めて新しい物理メモリが割り当てられます。 この仕組みをCopy on Writeと言います。
問題2: fork()とexec()を使ってプログラムを起動する、簡単なシェルを書きなさい。
サンプルコードのsh1.cを参照してください。
問題3: 問題2で作ったシェルにパイプとリダイレクトを実装しなさい。(難しい)
サンプルコードのsh2.cを参照してください。

第13章

問題1: SIGINTシグナルを受けたらメッセージを出力して終了するプログラムを書きなさい。シグナルを待つにはpause()というAPIが使えます。

サンプルコードのtrap.cを参照してください。

メッセージを出力してexitするだけの関数をsigaction(2)でSIGINTのハンドラーとして登録しておき、 pause(2)でシグナルをじっと待ちます。特に難しいところはないですね。

第14章

問題1: シェルのpwdコマンドを、シェルから独立したプログラムとして書くのは適切でしょうか。調べて、その理由を答えなさい。
適切です。なぜなら、プロセスのカレントディレクトリは親プロセスから引き継がれるため、 子プロセスであればシェルのカレントディレクトリを正しく取得できるからです。
問題2: シェルのcdコマンドを、シェルから独立したプログラムとして書くのは適切でしょうか。調べて、その理由を答えなさい。
こちらは不適切です。なぜなら、シェルのカレントディレクトリはシェルプロセスの属性だからです。 自分以外のプロセスの属性を変更することは、基本的にはできません。 つまり、cdコマンドがシェルから分離されていると、変更できるのはcdプロセスのカレントディレクトリだけで、 シェルプロセスのカレントディレクトリは変更できないわけです。
問題3: 第10章で作成したlsコマンドを改造して、ファイルのオーナーのユーザー名(IDではない)と最終更新時刻を表示するようにしなさい。

サンプルコードのls2.cを参照してください。

stat(2)を使ってファイルのオーナーのユーザーIDと最終更新時刻(time_t)を取得し、 ユーザーIDはgetpwuid(3)で名前に、時刻はctime(3)で文字列に変換します。 そこは難しくないと思いますが、パスのバッファを確保するのが地味に面倒ですね……。

第15章

問題1: telnetコマンドの使い方を調べて、daytimeサーバに接続してみなさい。CentOSではyumコマンドでtelnetパッケージをインストールする必要があります。

「telnet ホスト名 サービス名(またはポート番号)」で任意のサーバに接続できるので、 localhostのdaytimeサーバに接続するなら「telnet localhost daytime」で接続します。 すると次のように現在時刻が表示され、自動的に接続が切れるはずです。

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
10 SEP 2017 20:57:56 JST
Connection closed by foreign host.
問題2: echoプロトコルは、こちらからソケットに書き込んだ内容をそのまま返してくるテスト用プロトコルです。echoプロトコルのクライアントを書きなさい。echoサーバもdaytimeと同じくxinetd に組み込まれているので、同じ方法で起動できます。

サンプルコードのechoclient.cを参照してください。

内容はほとんどdaytimeクライアントと同じです。 ややハマりがちな点は、fdopenするときにモードを書き込み可能(w+)にするのを忘れる、くらいでしょう。