Linux カーネル空間からのファイルIOの話 + 他
本当は libapr のネットワークプログラミングについての訳を載せようとしたのだが。その前に、ソケットとは何ぞやという話をしたかった。しかし、書き残せるほど知識が溜っていないので、これについても次回以降に話をする。
書き残したい話は、どれもこれも中途半端な知識しかないので、今回は GNU/Linux の小ネタの紹介を行う。
○ カーネル空間からのファイルの(読み)書きを行う話
ユーザ空間からファイルの読み書きを行う時は、open システムコールを呼び出し、read/write システムコールを呼び出せばいい。
ファイルオープン処理では、ファイルオープン時にプロセスが持つファイルテーブルに、生成したファイルオブジェクトを登録して、その登録したファイルオブジェクトのファイルテーブル内のオフセットをファイルディスクリプタとしてユーザに返す。以降、ユーザプログラムはこのファイルテーブルのオフセットを指定して、ファイルに対する操作を行う。
しかし、カーネルはプロセスではない為 task_struct のように自身を管理するようなオブジェクトは持っていない。よってファイルディスクリプタ云々という操作は使えない。
オープンしたファイルを管理するコアとなるデータ構造は include/linxu/fs.h で定義されているファイルオブジェクト (struct file) になる。ファイル操作を行う場合には、ファイルオブジェクトの f_op メンバが持つ file_operations オブジェクトで定義されるメソッドを呼び出す。このオブジェクトは inode の i_fop メンバを基に生成され、inode はディレクトリの inode のそれを基に生成される。つまり、 file_operations オブジェクトはファイルシステム固有のファイル操作メソッド郡を定義したデータ構造を表している。
ext2 における write メソッドの実態は mm/filemap.c で定義されている generic_file_write() 関数になる。
これより、カーネルモードでファイルに対する書き込みを行う為にはファイルオブジェクトを生成し、ファイルオブジェクトの file_operations メソッドを呼び出す事で実現できる。ファイルオブジェクトの取得には、filp_open() 関数を利用する。
ここでいくつか注意が必要な事がある。ひとつは、プロセスの thread_info オブジェクトが持つ address_limit メンバの値である。書き出す文字列が存在するメモリ領域がカーネル空間に存在する場合、ユーザプロセスが参照できる領域はユーザ空間に限定されている為、処理が制限される。
シングルプロセッサのシステムであるならば、ファイル操作処理を行う直前に address_limit メンバの値を退避させ、ここに KERNEL_DS や 0xffffffff といった値を設定し、処理が終了したら元の値に戻せばそれほど恐ろしい事は起こらないはず。
2 つ目の注意点として、filp_open() を呼び出してファイルオブジェクトを取得する時に指定するフラグ情報がある。filp_open() 関数では、第二引数にフラグ情報を渡す (この内容は open システムコールの第二引数と同じであるので、詳しくはオンラインマニュアルを参照されたし) 。ここに O_DIRECT フラグを渡すと、カーネルは遅延書き込みを行わずに直接 buffer_head オブジェクトを生成してブロックデバイスドライバにデータを渡す。このとき、バッファのアドレス及びカウンタの値はセクタサイズにアライメントされていなければ、エラーとなる。
更に、ダイレクト I/O を発行する時には、指定さてたバッファをカーネル側で新たなページを確保して、それを基にバッファヘッドを作成する事はせずに、指定されたアドレスのページを参照する為に、カレントプロセスのメモリディスクリプタを参照する。write メソッドの第二引数で指定したバッファ領域がカーネル空間のアドレスである場合には、当然カレントプロセスのメモリリージョンには入っておらずエラーを返す。
これを回避する為には、ユーザプロセス側でカーネルが参照する用にわざわざメモリ領域を確保し、そのメモリ領域をシステムコール等を用いてカーネル側に知らせる。そしてカーネル側では copy_to_user() 関数等を駆使して、書き込みたい文字列を書き込んだ領域をユーザ領域に転送し、write メソッドを呼び出すといった事をしなければならない。
実際にこのようにする事で、いちおうはファイルへのデータ書き込みを行う事ができる。しかし、この方法は Unix プログラミング的には、ありえない手法である (カーネルプログラマ曰く)。この方法で攻めると、遅延は存在せずに直線的な処理が行われるが、任意のタイミングでのログを吐くのが格段に難しくなる。これを行うためには、カーネルスタックにあるカレントプロセスの thread_info オブジェクトを、処理を行う度に特定のプロセスのそれと置き換えなければならない。又、内部ではプロセスのメモリディスクリプタも参照しているので、thread_info の task メンバも挿げ替える必要がある。バグの温床であるこのようなやり方で長時間動かした時に、まともな結果が得られるとは思えない。
結局どのようにすればいいかというと、1 つめの事に注意して遅延書き込みを行えばよい。generic_file_buffered_write() 関数では、新たにページキャッシュを作成して、そこに指定されたバッファの中身を書き出す処理を行うので、バッファのアドレスのアライメント等は気にしなくてよい。
以下がプログラムソースになる。ここでは上述した内容の処理に加えて、ファイルが存在するかどうかを調べる為にパス名検索処理を実施し、ファイルが存在しない時には O_CREAT フラグ付きで filp_open() を呼び出している。
○ おわりに
結局 30 分程度で終わる作業に半日要した上に、つまらない所ではまってしまって恥をかいた。
だがとても勉強になった。
○ IPA 未踏の本体に採択をしていただいた
未踏IT人材発掘・育成事業に「Onix OS」が採択された。
http://www.ipa.go.jp/jinzai/mitou/2008/2008_2/hontai/k_koubokekka.html
採択されて何が嬉しいかと言ったら、上記の本文で書いたような、今まで趣味としてやってきた活動をやり続けて、自分を含めて誰からも文句を言われない事である。
採択を受ける前に、自分の立案のプレゼンをさせてもらったわけだが。そこでは、的確に自分の考えの甘さや、システムの不完全性についての指摘、反論に苦しむようなポイントを見事に突かれた。
正直、採択をもらえるとは思ってもいなかったので、採択の連絡を受けた時には非常に驚いた。
そして、自分の活動を評価してくれた事に対して心底嬉しく思う。
又、この結果は自分一人の力によって得られたものでは決して無い。自分に関ってくれた人達の知識やアドバイスそしてコミュニケーションが無ければ、自分は今ここに立っている事ありえない。特にアリエルネットワークという会社組織に属する人達がこれに該当する。
別段、会社が自分に対して高度な教育を行ってくれているというわけでは無い。一般的にそう言えるかどうかは不明だが、自分にとっては会社に属している事がとても重要である。自分よりも遥か上で活躍する超人たちがごろごろ居る環境に属している事で、自身が啓発されるからだと思う。
自身の活動を評価してくださった人達や応援してくれている人達に対して、頂いた期待に反する事の無いよう奮励努力する次第である。
最後に、ブートローダ製作のきっかけをくれたアリエルネットワークの matsuyama さん、同じく n.fujita さん。又、普段忙しい中 onix の話につきあってくださる同社の開発者で sodex の製作者の sodeyama さん、そして、カーネルの師である同社 CTO の inoue さんに。改めて深く感謝。
書き残したい話は、どれもこれも中途半端な知識しかないので、今回は GNU/Linux の小ネタの紹介を行う。
○ カーネル空間からのファイルの(読み)書きを行う話
ユーザ空間からファイルの読み書きを行う時は、open システムコールを呼び出し、read/write システムコールを呼び出せばいい。
ファイルオープン処理では、ファイルオープン時にプロセスが持つファイルテーブルに、生成したファイルオブジェクトを登録して、その登録したファイルオブジェクトのファイルテーブル内のオフセットをファイルディスクリプタとしてユーザに返す。以降、ユーザプログラムはこのファイルテーブルのオフセットを指定して、ファイルに対する操作を行う。
しかし、カーネルはプロセスではない為 task_struct のように自身を管理するようなオブジェクトは持っていない。よってファイルディスクリプタ云々という操作は使えない。
オープンしたファイルを管理するコアとなるデータ構造は include/linxu/fs.h で定義されているファイルオブジェクト (struct file) になる。ファイル操作を行う場合には、ファイルオブジェクトの f_op メンバが持つ file_operations オブジェクトで定義されるメソッドを呼び出す。このオブジェクトは inode の i_fop メンバを基に生成され、inode はディレクトリの inode のそれを基に生成される。つまり、 file_operations オブジェクトはファイルシステム固有のファイル操作メソッド郡を定義したデータ構造を表している。
ext2 における write メソッドの実態は mm/filemap.c で定義されている generic_file_write() 関数になる。
これより、カーネルモードでファイルに対する書き込みを行う為にはファイルオブジェクトを生成し、ファイルオブジェクトの file_operations メソッドを呼び出す事で実現できる。ファイルオブジェクトの取得には、filp_open() 関数を利用する。
ここでいくつか注意が必要な事がある。ひとつは、プロセスの thread_info オブジェクトが持つ address_limit メンバの値である。書き出す文字列が存在するメモリ領域がカーネル空間に存在する場合、ユーザプロセスが参照できる領域はユーザ空間に限定されている為、処理が制限される。
シングルプロセッサのシステムであるならば、ファイル操作処理を行う直前に address_limit メンバの値を退避させ、ここに KERNEL_DS や 0xffffffff といった値を設定し、処理が終了したら元の値に戻せばそれほど恐ろしい事は起こらないはず。
2 つ目の注意点として、filp_open() を呼び出してファイルオブジェクトを取得する時に指定するフラグ情報がある。filp_open() 関数では、第二引数にフラグ情報を渡す (この内容は open システムコールの第二引数と同じであるので、詳しくはオンラインマニュアルを参照されたし) 。ここに O_DIRECT フラグを渡すと、カーネルは遅延書き込みを行わずに直接 buffer_head オブジェクトを生成してブロックデバイスドライバにデータを渡す。このとき、バッファのアドレス及びカウンタの値はセクタサイズにアライメントされていなければ、エラーとなる。
更に、ダイレクト I/O を発行する時には、指定さてたバッファをカーネル側で新たなページを確保して、それを基にバッファヘッドを作成する事はせずに、指定されたアドレスのページを参照する為に、カレントプロセスのメモリディスクリプタを参照する。write メソッドの第二引数で指定したバッファ領域がカーネル空間のアドレスである場合には、当然カレントプロセスのメモリリージョンには入っておらずエラーを返す。
これを回避する為には、ユーザプロセス側でカーネルが参照する用にわざわざメモリ領域を確保し、そのメモリ領域をシステムコール等を用いてカーネル側に知らせる。そしてカーネル側では copy_to_user() 関数等を駆使して、書き込みたい文字列を書き込んだ領域をユーザ領域に転送し、write メソッドを呼び出すといった事をしなければならない。
実際にこのようにする事で、いちおうはファイルへのデータ書き込みを行う事ができる。しかし、この方法は Unix プログラミング的には、ありえない手法である (カーネルプログラマ曰く)。この方法で攻めると、遅延は存在せずに直線的な処理が行われるが、任意のタイミングでのログを吐くのが格段に難しくなる。これを行うためには、カーネルスタックにあるカレントプロセスの thread_info オブジェクトを、処理を行う度に特定のプロセスのそれと置き換えなければならない。又、内部ではプロセスのメモリディスクリプタも参照しているので、thread_info の task メンバも挿げ替える必要がある。バグの温床であるこのようなやり方で長時間動かした時に、まともな結果が得られるとは思えない。
結局どのようにすればいいかというと、1 つめの事に注意して遅延書き込みを行えばよい。generic_file_buffered_write() 関数では、新たにページキャッシュを作成して、そこに指定されたバッファの中身を書き出す処理を行うので、バッファのアドレスのアライメント等は気にしなくてよい。
以下がプログラムソースになる。ここでは上述した内容の処理に加えて、ファイルが存在するかどうかを調べる為にパス名検索処理を実施し、ファイルが存在しない時には O_CREAT フラグ付きで filp_open() を呼び出している。
void sys_write_log()
{
struct file *file;
struct nameidata nd;
char *buf;
char *filename = "/var/bar";
int count = 5;
int ret = 0;
int error;
mm_segment_t segment_upper;
buf= kmalloc(ONIX_REQUEST_LENGTH, GFP_KERNEL);
if(buf){
/* reserve current segment upper value*/
segment_upper = current_thread_info()->addr_limit;
current_thread_info()->addr_limit = KERNEL_DS;
error = path_lookup(filename, O_WRONLY, &nd);
if(error){
file = filp_open(filename, O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);
}else{
file = filp_open(filename, O_WRONLY, 0);
}
memset(buf, 0, ONIX_STRING_SEGMENT);
strncpy(buf, "abcd\n", count);
if (unlikely(*(&(file->f_pos)) < 0)){
printk(KERN_WARNING "[DEBUG] sys_write_log > file->f_pos is negative\n");
return;
}
if (file->f_op->write){
ret = file->f_op->write(file, buf, count, &(file->f_pos));
}else{
ret = do_sync_write(file, buf, count, &(file->f_pos));
}
/*retrive current data segment upper value*/
current_thread_info()->addr_limit = segment_upper;
kfree(buf);
}else{
printk(KERN_WARNING "[DEBUG] sys_write_log > fail to get string buffer.\n");
}
}
○ おわりに
結局 30 分程度で終わる作業に半日要した上に、つまらない所ではまってしまって恥をかいた。
だがとても勉強になった。
○ IPA 未踏の本体に採択をしていただいた
未踏IT人材発掘・育成事業に「Onix OS」が採択された。
http://www.ipa.go.jp/jinzai/mitou/2008/2008_2/hontai/k_koubokekka.html
採択されて何が嬉しいかと言ったら、上記の本文で書いたような、今まで趣味としてやってきた活動をやり続けて、自分を含めて誰からも文句を言われない事である。
採択を受ける前に、自分の立案のプレゼンをさせてもらったわけだが。そこでは、的確に自分の考えの甘さや、システムの不完全性についての指摘、反論に苦しむようなポイントを見事に突かれた。
正直、採択をもらえるとは思ってもいなかったので、採択の連絡を受けた時には非常に驚いた。
そして、自分の活動を評価してくれた事に対して心底嬉しく思う。
又、この結果は自分一人の力によって得られたものでは決して無い。自分に関ってくれた人達の知識やアドバイスそしてコミュニケーションが無ければ、自分は今ここに立っている事ありえない。特にアリエルネットワークという会社組織に属する人達がこれに該当する。
別段、会社が自分に対して高度な教育を行ってくれているというわけでは無い。一般的にそう言えるかどうかは不明だが、自分にとっては会社に属している事がとても重要である。自分よりも遥か上で活躍する超人たちがごろごろ居る環境に属している事で、自身が啓発されるからだと思う。
自身の活動を評価してくださった人達や応援してくれている人達に対して、頂いた期待に反する事の無いよう奮励努力する次第である。
最後に、ブートローダ製作のきっかけをくれたアリエルネットワークの matsuyama さん、同じく n.fujita さん。又、普段忙しい中 onix の話につきあってくださる同社の開発者で sodex の製作者の sodeyama さん、そして、カーネルの師である同社 CTO の inoue さんに。改めて深く感謝。
2009-02-03 01:44
nice!(0)
コメント(0)
トラックバック(0)
コメント 0