キャラクタ型デバイスドライバ
(悲願であった)デバイスドライバが一応の完成をみせたので、書いておく。
今まで、何度か挑んだテーマであったが、知識不足により満足のゆく結果が得られなかった。
今回、sodex さんから頂いた資料(*1)により、大分カーネルとデバイスの関係が自分の中で明瞭となった。なので、再びデバイスドライバの作成を行う。
今回のデバイスドライバは、シンプルであるが、多くのデバイスドライバモデルを無視した実装、つまり、同一バスにおける共有モジュールなどを一切考えていない実装になっている。
実装した範囲において、それぞれカーネル内部でどのような処理を行っているのかを解説する。ソースはこちら。
http://dev.ariel-networks.com/Members/ohyama/stuff/short-tar.gz/download
尚、このデバイスドライバを動作させるにあたり、以下の制限事項が存在する。
・カーネルがパラレルポートドライバをサポートしていない
・パラレルポートの IRQ は 7 番
・パラレルポートとの通信は SPP モード
まずは、I/Oアドレス領域の取得を行っている。
I/Oアドレスは、アドレスラインとデータラインを用いて、入出力を行う点で概念的には RAM へのアクセスと同じであるが、CPU やカーネルにおけるメモリ保護は存在していない(と認識している)。つまり、ここで "取得" と言っているのは、カーネルの resource 構造体における "予約" を行っているにすぎない。予約状況は、/proc/ioports から確認できる。
次に、メジャー番号の動的取得を行っている。
デバイスは、メジャー番号とマイナー番号によって識別される。静的に登録する事ももちろん可能だが、こちらが勝手に決めたメジャー番号が空いている保証は存在しないので、これを動的に取得する。mknod() によってデバイスファイルを作成した時、これらを引数に受け取り、デバイスファイルを作成する(未実装)。
そして次の処理は、キャラクタデバイスの管理構造体である cdev 構造体を初期化する為、cdev_init() を呼び出す。
内部では、cdev オブジェクトの作成、及び kobject の初期化を行っている。 そして、引数で渡された file_operations型 のポインタは、デバイスファイルを作成した場合、ファイルへの読み書きの際にフックされる。具体的には、ファイル書き込み時、vfs_write() において呼び出される。
又、cdev_add() にって対象のデバイスドライバをデバイスドライバモデルにおけるキャラクタ型デバイスに登録される。ここでは、kobj_map() によって、メジャー番号をキーとするハッシュテーブルに登録する。ここでは、メジャー番号毎に、probe 構造体を初期化し、これを kobj_map型 (probe型の配列とロックで定義された構造体) のグローバル変数 cdev_map における probe 配列の対象メジャー番号のエントリのチェーンに追加される。
管理構造体の各エントリである probe 構造体は、衝突されたノード (同じメジャー番号を持つ probe型 オブジェクト) へのポインタ、デバイス番号、範囲、そして cdev型オブジェクト を持つ。
次に、IRQ の予約と、割り込みサービスルーチンの登録を行っている。
request_irq() は、irqaction型 のオブジェクトを作成、初期化し、setup_irq() を呼び出し、指定した IRQ 番号の割り込みディスクリプタを再設定し、request_irq() で作成した irqaction型 のオブジェクトを登録する。
登録されている IRQ の状況は、/proc/interrupts から確認できる。
最後に、マスター割り込みコントローラに対して、IRQ 7 のマスク割り込み許可と、SPPモードで動作するパラレルポートのコントローラに対して割り込み許可を行う。
(*1):パソコンのレガシィI/O活用大全(CQ出版)
・おまけ
パラレルポートコントローラは、ACK 信号の立上りによって、割り込みを発生させる。しかし、実際には反転制御になっているので、~ACK ピンを接地する事で割り込みを発生させる。具体的には、パラレルポートの 10 番ピンがそれに対応しているので、伝導体をパラレルポートの後方 (下位 8 ビットは接地されている) のピンに接続することで、割り込みが発生できる。
さて。今週末は、秋葉原の千石電商で買い物でもをするかな。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30ad30e330e930af30bf578b30a430b930e930a4/tbping
ファイルシステム(その4)
前回、ext2 マウント時における、スーパーブロックオブジェクトと、各ブロックグループディスクリプタのキャッシュ処理について見てきた。
マウント処理の全体のうち、局所的な内容であったが、他の処理については、後々紹介してゆく(と思う)。
今回は、ファイルシステムによるファイル操作の実装について調べてゆく。尚、ファイルアクセスに付随する周辺の仕組みについて多く触れるが、ここでは、ファイルアクセス。特に、ブロックアドレスのアドレス変換に焦点を絞って述べてゆく。ここでは、ディスクからのデータ読み込みを例に話を進める。
ファイルシステムが提供するファイルの中枢を成すデータ構造は、inode と呼ばれ、ファイル 1つ に対して 1つ の inode が存在している。
ファイルのデータはディスク上に存在しているが、ユーザー操作によるファイルの読み書きは、ファイルデータをキャッシュしたページキャッシュに対して行われる( cf. ページキャッシュ(その2) )。
ファイルに対して読み書きを行う際、キャッシュを参照するが、キャッシュツリーに対応するページディスクリプタが存在しなかった場合、カーネルは page_cache_alloc_cold() を呼び出し、ページをキャッシュに追加、そして add_to_page_cache_lru() を呼び出し、キャッシュページを LRUリスト に登録する。
又ページが存在している場合において、ページが無効である(PageUptodate フラグが降りている)場合、有効なデータが含まれていないので、ディスクからデータを取得する。
具体的には、address_space のページ操作メソッドが登録された構造体(address_space_operations 型の a_ops メンバ) の readpage メソッドを呼び出す。
ファイルシステムに ext2 を使っている場合、ここでは、ext2_readpage() が呼ばれる。
ここでは、ページディスクリプタと、get_block のコールバック関数 ext2_get_block を引数に渡して、mpage_readpage を呼び出す。
get_block は、mpage_readpage() 関数内で呼び出され、ファイルポインタ(プロセスが開いているファイル内におけるオフセット)に対するディスクの実ブロック番号を返す、ファイルシステム固有の関数である。つまりここでは、ブロック番号のアドレス変換を低級ファイルシステムに行わせ、結果を受けて、VFS が I/O を行うようになっている。
さて、ext2 におけるディスクの使い方については、"ファイルシステム(その1)"で述べた。又、アドレス変換の仕組みについては、"ファイルシステム(その2)"で述べた。
ここでは、ext2 における get_block() 関数である ext2_get_block() から、実際にアドレス変換を行う実装をみてゆく。
まず、ext2_block_to_path() を呼び出す。
ここでは、ファイルポインタに対する ディスク inode の三段の間接参照における、それぞれの論理ブロック番号を整数型の配列(offsets[4])で表現する。つまり、直接参照の場合、2番目の要素に論理ブロック番号が格納される。二段参照の場合、2番目の要素に、13 (二段参照の際の i_block メンバのオフセット) が格納され、3番目に二段目からのオフセットが格納される。この場合、配列の先頭要素には、EXT2_IND_BLOCK マクロで表される値が格納される。三段参照の場合も同様に論理ブロック番号を求める。
ここで2つのデータ構造を紹介する。一つは、ext2_inode_info 構造体。
これは、ディスク inode に対応する構造体であり、VFS inode (以下、単に inode と呼ぶ )オブジェクトを組み込んでいる。inode からは、EXT2_I() マクロによって ext2_inode_info のアドレスを取得できる。
そしてもう一つが、Indirect 型のデータ構造である。これは、実ブロック番号と実ブロック番号を参照する変数のアドレス、そしてバッファヘッドから成るデータ構造で、それぞれ、"key", "p" そして "bh" メンバがそれに対応する。これは、後に説明する処理で使われる。
さて。ここまでの処理で、ファイルポインタから、ディスク inode のデータ構造における、データブロック位置を求めた。
次に、ext2_block_to_path() によって、ファイルポインタに対応するディスク inode の間接参照の回数を求めることが出来た。depth で返されたこの値を引数で渡し、ext2_get_branch() 関数を呼び出し、論理ブロック番号から実ブロック番号を取得する。
ここで add_chain() 関数に、ディスク inode のデータブロックの配列 i_data と ext2_block_to_path() で設定した offsets の和を引数で渡し、Indirect型 変数 chain の "p"(実ブロック番号を格納したエントリのアドレス), "key"(実ブロック番号) を設定する。初回の呼び出しでは、第二引数に NULL を渡し、chain オブジェクトのバッファヘッドを NULL に設定する。
ここまでの処理で、直接参照における実ブロック番号が取得できた( chain オブジェクトの key メンバに格納されている)。そして、sb_bread() 関数を呼び出し、VFSスーパーブロックオブジェクト(super_block型)の s_bdev メンバから参照できるブロックデバイスから、第二引数で渡される実ブロック番号のブロックバッファを参照(無ければ作成)し、バッファヘッドを返している。
ブロックバッファが作成できたので、バッファページが作成され、キャッシュにページが挿入され、VFS から読み書きを行える。
尚、作成されたキャッシュのディスクへの書き込みについては、"ページキャッシュ(その3)"で述べている。
ここまでが、ext2 ファイルシステムを巻き込んだ、ファイルからのデータ読み込みの処理の大まかな流れである。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30d530a130a430eb30b730b930c630e0-305d306e4/tbping
ファイルシステム(その5)
前回は、ファイルのデータを読み込む場合を例に、低レイヤファイルシステム(ここでは、ext2)を介して、データがどのように存在し、どのように処理され、どうやって保存されているかの実装を見てきた。
今回は、通常ファイルを例に、ファイルがどのように作成されるのか。ファイルの起源の実装を調べてみる。
ところで、プロセスがファイルを参照する場合、VFS レイヤにおいて、主に 2 つのデータ構造を参照する。一つが file_struct 構造体であり、もう一つが file 構造体。
両者の関係を図示したものが、こちら
http://opentechpress.jp/kernel/internal24/node99.shtml#SECTION03600000000000000000
(お馴染みの高橋浩和さんのサイトである。尚、二番目の図は正確ではない(はず)。カーネル 2.6 において、file_struct 構造体の fd メンバが参照するポインタ配列の各要素が file 構造体へのアドレスを持っている。そして、配列のオフセットがユーザープロセスが参照するファイルディスクリプタとなる)。
file 構造体(以下、ファイルオブジェクト) の f_mapping メンバは、ファイルのアドレス空間オブジェクトを参照している。つまり、ファイルオブジェクトが生成された時点で、inode が取得されている事を意味する。
これからの話の見通しを良くする為、open() システムコールのサービスルーチン(sys_open() 関数) 以下、VFSにおける処理ルーチンを簡単に書くと以下のようになる。尚、'>' の次に書かれた関数名は、'>'の個数が一個少ない行に対応する直上に書かれた関数から呼ばれる事を意味する(説明するまでもないか)。
sys_open()
> do_sys_open()
>> do_filp_open()
>>> open_namei()
>>>> path_lookup_create()
>>>>> __path_lookup_intent_open()
>>>> lookup_hash()
>>>> open_namei_create()
>>>>> vfs_create()
後に話をする、inode 操作関数の create メソッドに対応する低レイヤファイルシステムのルーチン以下の処理ルーチンを、以下同様に簡単に書く。
ext2_create
> ext2_new_inode
>> new_inode
>>> alloc_inode
>>>> ext2_alloc_inode
>> find_group_other
>>> ext2_group_desc
>> read_inode_bitmap
>> ext2_find_next_zero
又、以下の話の中では、opne() システムコールは、O_CREAT フラグを付けて実行されたものとする。ファイルオープン時に、O_CREAT フラグを立てて呼び出すと、ファイルが存在しなかった場合に、ファイルが作成される。
O_CREAT フラグを立てて、sys_open() 関数を呼び出した場合、open_namei() 関数において、path_lookup_create() 関数が実行される。path_lookup_create() は、LOOKUP_PARENT | LOOKUP_CREATE フラグを立てて、__path_lookup_intent_open() 関数を呼び出し、パス名検索を行う。LOOKUP_PARENT フラグは、検索対象ファイルが置かれているディレクトリ(ファイル)を検索対象とする時に使われる。LOOKUP_CREATE フラグは、何の為に存在するのかよくわからない。バイブルには、"ファイルを生成する目的で検索する(ファイルが存在しない場合)"とあるが、ソースではこのフラグ判定を用いておらず、パス名検索中に、ファイル作成を行っていない。
__path_lookup_intent_open() 関数ではまず、ファイルオブジェクトを取得する為に、get_empty_filp() 関数を呼び出す。ここでは、未使用のファイルオブジェクトの取得、初期化を行い、そのアドレスを返す。ファイルオブジェクトの数には制限があり、割当て可能なファイルオブジェクトの数(システム内で同時にアクセス可能な最大のファイル数)は、files_stat_struct 型のグローバル変数 files_stat オブジェクトの max_files メンバで設定されている値になる(/pro/sys/fs/file-max の値を操作する事で、この値を変更できるらしい。デフォルト値は、主記憶のおよそ 1/10)。現在の値は、fs/file_tables.c で宣言されている、percpu_counter型 のグローバル変数 nr_files の count メンバに保存されている。
これに成功すると、filp_cachep キャッシュからファイルオブジェクトのメモリ領域を取得し、これを 0 で初期化した後、ファイルオブジェクトの uid と gid を設定する(かつて、ファイルオープン時における、この処理を意識しなかった事で、恥をかいた)。その後、生成したファイルオブジェクトのアドレスを返している。
次に、do_path_lookup() 関数によるパス名検索処理についてだが、これまでにも何度か登場している dentry オブジェクトについて、ここでも説明(の更新)を行う。
カーネルは、プロセスが参照するパス名の要素に対して、d エントリオブジェクトを作成する。つまり、ルートファイルシステムから参照できるファイルに対してそれぞれ、d エントリオブジェクトを作成している。これは、ファイルの実態である inode と、inode が持つアドレス空間オブジェクトの存在を抽象化したデータ構造である(と解釈している)。
__path_lookup_intent_open() 関数では、linux のパス名検索 (do_path_lookup() 関数) によって、目的としているファイルの dentry を取得し、ファイルオープン時における一時データ構造の nameidata オブジェクトの dentry メンバにアドレスが格納される。
そして、do_path_lookup() 関数では、検索開始ディレクトリを指定し、link_path_walk() 関数を実行する。
検索開始ディレクトリは、指定されたパス名の先頭が'/'であるかどうかで判断する。'/' である場合には、ルートディレクトリから、そうでない場合には、カレントディレクトリの dentry のアドレスを nemeidata 型のオブジェクト nr->dentry に格納し、nr を引数に、link_path_walk() 関数を呼び出す。
ここまでで、対象ファイルが存在する dentry オブジェクトを取得できた。次に、lookup_hash() 関数を呼び出し、対象ファイルの dentry オブジェクトを取得する為、cached_lookup() を呼び出す。ここではまず、d エントリキャッシュを探すため、 __d_lookup() 関数を呼び出し、失敗した場合、d_lookup() 関数を呼び出し、ディレクトリの d エントリオブジェクトから階層構造をたどって、対象ファイルの d エントリを取得する。
これに失敗した場合(ファイルが存在しない場合)、d_alloc() 関数を呼び出し、対象ファイル用の d エントリオブジェクトを dentry_cache キャッシュから生成し、ここで初期化を行い、作成した d エントリオブジェクトを返す。尚、初期化処理において、d_inode メンバは NULL に設定される。
open_namei_create() 関数に戻り、lookup_hash() 関数によって得た d エントリオブジェクトが参照する inode が存在しない場合(対象ファイルが無い場合)、open_namei_create() 関数を呼び出す。ここでは、vfs_create() 関数を呼び出し、ディレクトリの inode オブジェクトの inode_operations型 inode 操作メソッドの create メソッドを呼び出し、ファイル用の inode を作成している。ファイルシステムが ext2 の場合、ext2_dir_inode_operations で定義されたext2_create() 関数が実行される。
ext2_new_inode() 関数ではまず、new_inode() 関数を呼び出し、inode の作成を行っている。ここでは、alloc_inode() を呼び出し、inode を作成し、inode_in_use リストに作成した inode を追加する。又、alloc_inod() では、super_block 型オブジェクトのメソッドalloc_inode() を呼び出し、低レイヤファイルシステムにおける inode を作成し、VFS inode を取得する。
ext2 の場合では、スーパーブロックオブジェクトのメソッドは、fs/ext2/super.c の ext2_sops で宣言されている。alloc_inode() で実行された alloc_inode メソッドは、ext2_alloc_inode() 関数を呼び出す。
ext2_alloc_inode() 関数は、ext2_inode_cachep キャッシュからメモリ領域を取得し、ext2_inode_info 型オブジェクトを作成し、vfs_inode メンバのアドレスを返す。ここでは、ext2_inode_info 型オブジェクト の生成だけ行い、初期化処理は、ext2_new_inode() 関数内で行う。又、VFS inode の初期化処理、及びアドレス空間オブジェクトの初期化処理は、alloc_inode() 内で行われ、ext2_new_inode() に処理が帰る。
ここまでで、inode は作成されたが、これがディスク上のどこに置かれるかは、まだ決定されていない。ここではあくまで、ディスクに置く inode をメモリ上で作成しただけである。
次に、IS_DIR() マクロで sys_open() によって渡された引数、mode をチェックし、作成されるファイルがディレクトリか、通常ファイルかをチェックする。通常ファイルの場合、find_group_other() 関数を呼び出し、ファイル用 inode を置くブロックグループ番号を選択する。原則、ディレクトリが属するブロックグループに inode を置く(ヘッドの移動時間短縮の為)。同一ブロックグループに空き inode スペースが無い場合、適当な計算((ディレクトリのブロックグループ + ディレクトリの inode 番号) % ファイルシステムに存在するブロックグループの数)によってブロックグループを検索。これによっても、inode の空きスペースを見付けられない時、線形的にブロックグループを探索する。
取得したブロックグループ番号を引数に、read_inode_bitmap() 関数を呼び出し、引数で渡したブロックグループ番号の inode ビットマップのブロックバッファヘッドを取得し、find_next_zero_bit() 関数を呼び出し、取得したブロックバッファのデータにおいて、最初に 0 が出現する位置を返す。つまり、ブロックグループ内における、最初の空き inode オブジェクトの inode 番号。ここで、取得した数値が、スーパーブロックの s_inode_per_group (ブロックグループに存在する inode 数)を越えていた場合(恐らく、SMPシステムにおける配慮)、隣のブロックグループにおいてブロックグループの数だけ同一の処理を行う。
これにより、O_CREAT フラグ付でオープンされたファイルが、作成される処理が理解できた。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30d530a130a430eb30b730b930c630e0-305d306e5/tbping