課題文を読んだら何をどう実装するかイメージが湧くだろうか?
実装内容はテキストファイル(文字列が格納されたファイル) から読み込まれた文字列を1行ずつ返すというシンプルなものだ。
そもそも何故、このような実装をするのかという課題のテーマの背景はc及びC++のライブラリのgetline関数の再実装と考えると腑におちるだろう。
つまり、ファイルから1行ずつ読み取って文字列を返す(表示させる)という処理は後々多用される関数なので、このgetlineの中身はどういう仕組みになっているのかを理解する為の課題だ。
凡人: テキストファイルから文字列を1行ずつ読む関数って、どうやって作るの?
恋するコンピュータサイエンス: それは、まるで心を開いて一行ずつ読むラブレターのようなものだね。基本的には、C言語の getline
関数の再実装みたいなもの。ファイルから1行ずつ読み取って、文字列を返すんだ。
凡人: どういうステップで実装するの?
恋するコンピュータサイエンス: まず、システムコール関数 open
、read
、close
を理解することが大事。open
でファイルを開き、read
で内容を読み込み、close
でファイルを閉じるんだ。特に read
関数の理解が重要だよ。
凡人: じゃあ、具体的にはどうするの?
恋するコンピュータサイエンス: get_next_line.c
という関数を作って、ファイルから1行ずつ読み取るんだ。1行とは、改行文字までの文字列。読み取れない場合はNULLを返す。例えば、"aaa\nbbb\nccc\n" というファイルがあれば、"aaa", "bbb", "ccc" を順に出力する。
凡人: それはどうやって実現するの?
恋するコンピュータサイエンス: まずは、open
でファイルを開いて、ファイルディスクリプタ fd
に格納する。そして、get_next_line
関数に fd
を渡して読み込む。例えば read(fd, &character, 1)
で1バイトずつ読むこともできる。
凡人: read
関数でEOFに遭遇するのはどういう状況?
恋するコンピュータサイエンス: たとえば、ファイルの残りが20バイトで、50バイト読もうとする場合、最初の read
は20バイトを返し、次の read
は0を返してEOFを示すんだ。
凡人: なるほどね。じゃあ、close
は?
恋するコンピュータサイエンス: ファイルを使い終わったら close
で閉じること。これにより、カーネルがデータ構造を解放して、ファイルディスクリプタを再利用するためのプールに戻すんだ。
実装内容を再度確認しよう
get_next_line.c とは別のテキストファイルに書かれた文字列を読み取り1行ずつ出力する。
1行とは改行前までの文字列。
そうでない場合NULLを返す。
例えば下記のような文字列が、格納されているテキストファイルがあるとしよう
ーーーーー
aaa
bbb
ccc
ーーーーー
aaa, bbb, ccc を出力すれせば正しい挙動となる。
実装に移る前に、前提知識として2つ重要な要素が’ある。
1つ目はfd に関連したシステムコール関数に対する理解であり、
2つ目は文字列群実装する際に必要になってくる、ダブルポインタに対する理解だ。
1番目のシステムコール関数の
ファイルを開いてopen、ファイルの内容を読み込みread、ファイルを閉じるcloseという関数の理解をしよう。この関数はシステムコール関数と言い、シェルを通じてカーネルを操作する、特別な関数だ。
システムコールとは何か説明せよ - 恋するコンピュータサイエンス
ファイルをopen→read→close という流れの個々の関数を深掘りする必要がある。
また、開いたファイルの番号を管理するfd とは何かを理解することも重要である。
注意が必要なのはread 関数の理解だ。
gnl の実装に入る前に簡単なread 関数の具体例を作成し動かしてみると理解が深まるだろう。
ちなみにread のEOF(END OF FILE)とは、規定のファイルサイズ分のバイト数を読み込んだ場合に、EOFと呼ばれる状態となり、ファイルの末尾に明示的なEOF文字はない事に注意しよう。別の言葉でいうなら、EOFはカーネルによって検出される。実装ではread の戻り値が0になるときにEOFとなるという事を抑えておけば大丈夫だ。
読み込み中のEOFへの遭遇はどのように起こるのだろうか? 例えば、現在のファイル・ポジションからあと20バイトしかないファイルから読み込もうとしており, このファイルを50バイトの塊で読み込んでいるとしよう. 次のreadはショート・カウント20を 返し, その後のreadはショート・カウント0を返すことでEOFを知らせる. ターミナルからのテキスト行の読み込み オープンしたファイルがターミナル (キーボードやディスプレイ) に関連づけられている
ファイルのclose は少しわかりづらいかと思うが、一般的にアプリケーションが
ファイルへのアクセスを終了するときファイルのクローズを要求する事でカーネルに知らせる。カーネルはファイルがオープンされた時に作成したデータ構造を解放し利用可能なfd のプールにfd を戻す事で応じる。
ここまで理解できたら、まずはシステムコール関数を使って、メイン関数を作ってみる。(下記bonusにも対応している)
上記でやっていることはファイルをopen してfd の値を変数に格納し、
getnextline という関数にfd という変数を渡せばよいということが言える。
次にgetnextline関数を実装してみよう。
実装の方針を考えるにはいくつかの手法(テクニック)があるが、
具体例を考えて、方針を探るのがよい。
前提知識で解説した通り、色々な要素が、複合しているが、極端な場合を考えるとよい。
read(fd, &character, 1)
極端な場合とはread が一度に読み取れるBUFSIZE を1に固定するというものだ。
上記の場合、gnl の実装は簡単になる。
なぜなら、場合分け読み込んだBUFSIZEに改行が含まれるか否かの場合分けが必要なくなるからだ。
実装の方針が立たない人はまずはread のbufが1byte にした場合どのような実装になるか考えてほしい
そのうえでread が読み込んだbufsize の条件分岐を考えると複合する要素を一度に考えなくて済む
case 1) 読み込んだ文字列に改行\n が存在する場合
上記はreadで読み込んだにbufの中に改行が含まれている場合である。
読み込んだ文字列を場合分けする必要がある。
つまり を改行(¥0)を文字列の中から探索して の前の文字列、 後の文字列を分離する関数
が必要という方針を立てる。
またbuf で読み込めなかった、残りの文字を格納する変数が必要だ。
ここでread 関数を用いた、読み込み関数を考える。
具体的には読み込んだread 関数と読み込まれなった文字列を格納しておく変数だ。
case 2)読み込んだ文字列に が存在しない場合
上記はbufの中に改行が含まれていない場合である。
文字列をループで回して改行までの文字列を読み込まなければならない。
case 3)EOFになる
ただlineを返せば良い。
この時点で下記の疑問が湧く
各行の最後はNUL文字で終わっている必要がある。?
それぞれの行に読み込んだ返り値として、指定の値を返すというもの。
続く
補足
下記前提知識解説
fdとは
ファイルディスクリプタ
ファイルディスクリプタ(file descriptor)とは, プロセスがオープンされているファイルを
識別するための0以上の整数値のことです. 例えば, C言語でfopen( )標準ライブラリ関数を
利用した時, ファイルを識別するためにFILE構造体へのポインタ(ストリーム)が返されます.これをシステムコールのレベルで見ると, ファイルがオープンされたときに, 0以上の整数値
が返され, それを利用することでプロセスがファイルを識別し, アクセスが可能になります.ちなみに, ファイルディスクリプタの0, 1, 2はプロセス開始から使用されており, それらは
標準入力, 標準出力, 標準エラー出力に対応しています. また, ヘッダファイル<unistd.h>の
中で, それぞれSTDIN_FILENO, STDOUT_FILENO, STDERR_FILENOとマクロ定義されて
います. したがって, 通常新規にファイルをオープンすると3が返されることになり, 新たに
ファイルをオープンしていく度に, 4, 5, 6...と空いている非負整数値のなかで最小の値が
ファイルディスクリプタの値として返されていきます. 以下のプログラム例のExample. 2を
利用して, 実際にファイルディスクリプタの値を調べてみると少し理解が進むかもしれません.
また, ファイルディスクリプタはファイルに関連した値ではなく, プロセスに関連した値です.
そのため, 異なるプロセスが同じファイルをオープンしても同じファイルディスクリプタが返される保証はありません.
http://www.cis.shimane-u.ac.jp/~hama/se_intro_local/slide3.pptx.pdf
static とは
static は関数の前に着けた場合は外部から読み取れないようになっている。
変数の前のstatic は静的変数。