ブロックとSelect
これまで ここのページに書いてある通りにドライバを作ってきていて、動いていたので安心していたのですが、wait_queue の使い方に多少の誤りがあることが分かりました。 この通りでも動作はしますが、真似なさらないことを推奨します。 いずれ、このページは書き換える予定ですが、現在時間が取れず、しばらく先になりそうです。 最近執筆したLinuxJapan誌8月号の記事(配布資料置き場にPDFあり)では、正しいと考えられる使い方で記載しましたので、お急ぎ?の方はこちらをご覧下さい。

勘違いから、誤った情報を流してしまいましたこと、ご容赦下さい。
(前から、なにか引っ掛かる気がしていたのですが、記事執筆中に不安なところを残さないようにと再チェックしていて気づきました...)

時として、ユーザのプロセスで、デバイスの準備待ちをしたい場合があります。 普段使っている用途ですと、キーボードやマウスの操作待ちであったり、通信の結果待ちであったり、様々です。 一方、制御ハードの待機というと、外部データの読み込み待ちや出力用FIFOの空き待ち、画像取り込みのタイミング待ちなどやはり様々です。

もし、待つときに、それだけを待てばいいような場合、

  • read や write で準備が出来るまで return しない設計にする
  • ioctl を使って準備待ちのリクエストをつくって、準備できるまで return しない設計にする
などの案があります。前者は「ブロック」と呼ばれています。 デバイスの状況はいつでも最新のものが読みたいけど、時々発生するイベントも待ちたい、というような場合は後者が有力でしょう(read write は即時リクエストを実行して戻るように実装)。 本当はfcntl で設定するBLOCK/NONBLOCKをつかうのがいいのかも知れませんが。
いずれにせよ、ドライバ内部で待っている場合には、以前に述べたように休眠状態などを使うことでシステムを止めないようにしなければなりません。

それに対して、

  • キー入力やネットワークなどの他のデバイスと同時に待機したい
  • 一定時間にかぎって待機したい
といった場合には select システムコールが便利です。 selectシステムコールは意味としては「準備が出来たデバイスを選ぶ」といったところでしょうか。 複数のファイル記述子(open()で得られる値)と最大待ち時間を指定して呼ぶと、どれかの「読み・書き(・特殊)」の準備が出来た段階で帰ってきます。 読み書きはそれぞれ独立に待機できますし、同時に複数の準備ができた場合には、複数まとめてかえってきます。 同様なシステムコールに poll もあります(Linux 2.2 ではpoll に指向しているようですし、SunOS5では select がpollをつかっているようです)。
蛇足ですが、ユーザインターフェイスとしてメジャーなGTKでは、これをサポートしているため、デバイスの準備を待ちつつ、GUIもこなすようなプログラムを書く場合に非常に有益です。

以上のうち、ブロックとselectについてこのページでは触れます。 どちらも

  • 準備ができるまで寝かせる
  • 準備ができたら寝てるのを起こす
という2つの機能からなります。 ブロックがreadやwriteの中で一旦停止させるのに対し、selectはOS側で他のデバイスとともにチェックして、動作する、という点のみ異なります。
今回はわりと長めの作りで、本格的になっています(とはいえ、穴はありそうですが)。特徴は
  • 内部にバッファを持ち、write によって書き込める。read によって読み出せる。
  • open-write(-write*n)-close でデータを順次書き込み、close することでデータの書込を確定する。 read に関しても close することで読み込を完了する。
  • 読み込みは1度のみ可能で、一度読んだら、だれかが書き込んでくれるまで読めない。 同様に 書き込みも1度のみ。つまり、読み書きを交互に繰り返す場合にはスムーズに行くが、どちらかが続くと待機になる。
    (最初は書込から)
  • 待機はブロック、もしくは select で可能。
  • lseek で移動できる。
  • 2つ以上の同時アクセスに対応
です。 // gcc -c blktest.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/ocrtest c 60 0; chmod a+rw /dev/ocrtest #define MODULE #define __KERNEL__ #include #include #include #include #include #if LINUX_VERSION_CODE >= 0x020100 #include #include // 注意 #else static inline unsigned long copy_to_user(void *to, const void *from, unsigned long n) { memcpy_tofs(to,from,n); return 0; } static inline unsigned long copy_from_user(void *to, const void *from, unsigned long n) { memcpy_fromfs(to,from,n); return 0; } #endif static int devmajor=60; static char *devname="blktest"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif // 読み書き領域 本当は kmalloc で取った方がたぶんいいけど、横着します。 #define MAXMLEN 8192 static char message[MAXMLEN]; // メッセージ read で読み, write で書く static int mlen=0; // 長さ。最長 MAXMLEN static unsigned long read_jiffies=1; // 読んだ時間 static unsigned long write_jiffies=0; // 書いた時間 static unsigned long read_fversion=0; // 読んだやつ static unsigned long write_fversion=0;// 書いたやつ // ファイルごとの管理領域 typedef struct { unsigned long f_version; // 識別用 f_version struct wait_queue *wait; // 休眠待機用領域 int sleep_mode; // 0: 起きてる 1: 読み込み待ち 2: 書き込み待ち } FileInfo; #define MAXFILE 3 // 同時アクセス数 FileInfo fi[MAXFILE]; // Linux 2.0/2.2 共通 static int blktest_open(struct inode * inode, struct file * file) { int i; printk("blktest_open: (%ld)\n",file->f_version); for(i=0;if_version; fi[i].sleep_mode=0; fi[i].wait=NULL; file->private_data=fi+i; // プライベートデータに構造体ポインタ設定 printk("%d\n",i); MOD_INC_USE_COUNT; file->f_pos=0; // 読み出し 書き込み 位置を 0 return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int blktest_close(struct inode * inode, struct file * file) #else static void blktest_close(struct inode * inode, struct file * file) #endif { int i; printk("blktest_close: (%ld)\n",file->f_version); MOD_DEC_USE_COUNT; if(file->f_version==read_fversion) { read_jiffies=jiffies; // 読んで閉じた時間 if(read_jiffies==write_jiffies) read_jiffies++; // はまり防止 for(i=0;if_version==write_fversion) { write_jiffies=jiffies; // 書いて閉じた時間 if(read_jiffies==write_jiffies) write_jiffies++; // はまり防止 for(i=0;iprivate_data))->f_version=0; ((FileInfo *)(file->private_data))->sleep_mode=0; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // read はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 static int blktest_read(struct file * file, char * buff, size_t count, loff_t *pos) #else static int blktest_read(struct inode * inode,struct file * file, char * buff,int count) #endif { int len; // ブロック if((file->f_pos==0)&&(read_jiffies>=write_jiffies)) // 読んだ方が最近 { ((FileInfo *)(file->private_data))->sleep_mode=1; interruptible_sleep_on(&(((FileInfo *)(file->private_data))->wait)); if(read_jiffies>write_jiffies) { printk("blktest: read: woken up ealier!(signal?)\n"); return -EINTR; } } len=mlen-file->f_pos; // のこり if(len>count) len=count; if(len<0) return -EINVAL; copy_to_user(buff,message+file->f_pos,len); printk("blktest_read: (%ld) %d bytes (at %d)\n", // 本当は loff_t だと file->f_version,len,(int)(file->f_pos)); // long long file->f_pos+=len; read_fversion=file->f_version; return len; } // write はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 static int blktest_write(struct file * file, const char * buff, size_t count, loff_t *pos) #else static int blktest_write(struct inode * inode,struct file * file, const char * buff,int count) #endif { int len; // ブロック if((file->f_pos==0)&&(write_jiffies>=read_jiffies)) // 書いた方が最近 { ((FileInfo *)(file->private_data))->sleep_mode=2; interruptible_sleep_on(&(((FileInfo *)(file->private_data))->wait)); if(write_jiffies>read_jiffies) { printk("blktest: write: woken up ealier!(signal?)\n"); return -EINTR; } } len=MAXMLEN-file->f_pos; // のこり if(len<=0) return -ENOSPC; // いっぱい if(len>count) len=count; if(len<0) return -EINVAL; copy_from_user(message+file->f_pos,buff,len); printk("blktest_write: (%ld) %d bytes (at %d)\n", file->f_version,len,(int)(file->f_pos)); file->f_pos+=len; mlen=file->f_pos; write_fversion=file->f_version; return len; } // poll / select : 2.2 では poll. 2.0 では select #if LINUX_VERSION_CODE >= 0x020100 static unsigned int blktest_poll(struct file *file, struct poll_table_struct *ptab) { unsigned long mask=0; poll_wait(file,&(((FileInfo *)(file->private_data))->wait),ptab); if(read_jiffies>write_jiffies) mask|=POLLOUT|POLLWRNORM; // 書き込みOK else ((FileInfo *)(file->private_data))->sleep_mode|=2; if(read_jiffiesprivate_data))->sleep_mode|=1; return mask; } #else // 2.0 系用 static int blktest_select(struct inode *inode, struct file *file, int sel_type, select_table *stab) { if(sel_type==SEL_OUT) { if(read_jiffies>write_jiffies) return 1; // 書き込みOK ((FileInfo *)(file->private_data))->sleep_mode|=2; select_wait(&(((FileInfo *)(file->private_data))->wait),stab); return 0; } if(sel_type==SEL_IN) { if(read_jiffiesprivate_data))->sleep_mode|=1; select_wait(&(((FileInfo *)(file->private_data))->wait),stab); return 0; } return 0; } #endif // lseek はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 static loff_t blktest_lseek(struct file *file, loff_t offset, int whence) #else static int blktest_lseek(struct inode *inode, struct file *file, off_t offset, int whence) #endif { switch(whence) { case 0: // SEEK_SET if(offset>MAXMLEN) return -EINVAL; if(offset<0) return -EINVAL; file->f_pos = offset; return file->f_pos; case 1: // SEEK_CUR if(offset+file->f_pos>MAXMLEN) return -EINVAL; // オーバー if(offset+file->f_pos<0) return -EINVAL; // 負もあり得る file->f_pos += offset; return file->f_pos; case 2: if(offset+MAXMLEN>MAXMLEN) return -EINVAL; if(offset+MAXMLEN<0) return -EINVAL; file->f_pos=offset+MAXMLEN; return file->f_pos; default: return -EINVAL; } } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations blktest_fops = { // Linux 2.2.10 より blktest_lseek,// loff_t llseek(struct file *, loff_t, int) blktest_read, // ssize_t read(struct file *, char *, size_t, loff_t *) blktest_write,// ssize_t write(struct file *, const char *, size_t, loff_t *) NULL, // int readdir(struct file *, void *, filldir_t) blktest_poll, // u. int poll(struct file *, struct poll_table_struct *) NULL, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) blktest_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) blktest_close,// int release(struct inode *, struct file *) NULL, // int fsync(struct file *, struct dentry *) NULL, // int fasync(int, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) NULL, // int lock(struct file *, int, struct file_lock *) }; #else static struct file_operations blktest_fops = { // Linux 2.0.36 より blktest_lseek,// int lseek(struct inode *, struct file *, off_t, int) blktest_read, // int read(struct inode *, struct file *, char *, int) blktest_write,// int write(struct inode *, struct file *, const char *, int) NULL, // int readdir(struct inode *, struct file *, void *, filldir_t) blktest_select,// int select(struct inode *, struct file *, int, select_table *) NULL, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) blktest_open, // int open(struct inode *, struct file *) blktest_close,// void release(struct inode *, struct file *) NULL, // int fsync(struct inode *, struct file *) NULL, // int fasync(struct inode *, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) }; #endif int init_module(void) { int i; for(i=0;i // select.c gcc -o select select.c #include #include #include #include #include #include #include int main(int argc,char **argv) { int fd,r; char c; char *message="test message\n"; int mode=0; struct timeval tv; fd_set fds; if((argc>1) && (argv[1][0]=='w')) mode=1; fd=open("/dev/blktest",O_RDWR); if(fd==0) { printf("cannnot open /dev/blktest\n"); return 1; } printf("device size: %d\n",(int)lseek(fd,0,SEEK_END)); lseek(fd,0,SEEK_SET); // lseek をつかって領域サイズ取得 while(1) { FD_ZERO(&fds); // select の準備 FD_SET(fd,&fds); tv.tv_sec=1; tv.tv_usec=0; // タイムアウト1秒 if(mode) r=select(fd+1,NULL,&fds,NULL,&tv); // 書きselect else r=select(fd+1,&fds,NULL,NULL,&tv); // 読みselect if(r<0) { printf("select returned with signal or error\n"); break; } if(r==0) { putchar('.'); fflush(stdout); continue; } if((r==1)&&(FD_ISSET(fd,&fds))) { if(mode) { printf("write operation prepaired\n"); write(fd,message,strlen(message)); // close して 初めて読んだことになるから 一旦 close close(fd); fd=open("/dev/blktest",O_RDWR); } else { printf("read operation prepaired\n"); read(fd,&c,1); close(fd); fd=open("/dev/blktest",O_RDWR); } } } close(fd); return 0; } 今回はタイミングが関係するだけに、実行結果をうまくお見せできません。 % gcc -c blktest.c -Wall -Wstrict-prototypes -O -pipe -m486 # insdev blktest /dev/blktest % cat てきとうなファイル > /dev/blktest で書き込み % cat /dev/blktest で読み込み % ./select (w) で読み(書き)待機 ..read operation prepaired < 1秒1つ . がでる。別のところで .read operation prepaired 書き込むと準備完了メッセージがでる なお、select と select w を同時に実行すると
 書き込み→読み込み→書き込み→読み込み....
と相互に読み書きを繰り返します。 今回はややこしいので 要素毎に解説します。
  • message
    書きこまれたデータを保存する場所。本当はこのくらい大きいのはkmalloc なんかを使って確保すべきところ、面倒なので static に確保。
  • read_jiffies / write_jiffies
    読んだ時間・書いた時間を管理する。これの大小関係で最後に書いたか読んだかを判断する。
  • struct FileInfo
    open されたファイル記述子(file構造体)ごとに、専用のデータを持たせるための構造体。 open 時に file->private_data で指し示すようにする。 f_version はfile構造体のf_versionで識別子として使用。未使用時に0とする。 wait は休眠に使用するための領域。sleep_mode は待機状態(読み待機・書き待機)を決定。
  • blktest_open
    未使用のFileInfoを探し、初期化し、file->privated_dataに設定する。 なければ、-EBUSY を返す。この場合 open システムコールは -1 を返し、errno にEBUSYが入る。
  • blktest_close
    read してから close, write してから close を判定し、それぞれの時刻を記録。 それと同時に FileInfo 配列を走査し、休眠し、待機している他のプロセスを wake_up_interruptible()で起こす。 その後、使用していた FileInfoを解放する。
  • blktest_read
    前回読んでから書き込まれていなければブロック動作をする。private_data をもとにフラグを立て、interruptible_sleep_on()で休眠する。 blktest_close で起こされた場合は条件式が満たされるが、^C などの signal の場合は満たされないので、その場合エラーを返す。
    読み込み動作は、大きな領域の連続読み込みに対応するために、file->f_pos を読み取り位置として使用。最後まで読んだら0を返す。
  • blktest_write
    blktest_read のまったく逆の動作を行う。 異なる点は、用意された領域の最後まで書き込んだ場合、0ではなく -ENOSPC を返す点である。 これを試しに0を返すようにして dmesg を追うと、cat などで書き込んだ場合、0を返してもひたすら書き込み続けることが確認される。 -ENOSPC にすると
    cat: write error: No space left on device
    などとエラーを出力して停止するようになる。
  • blktest_poll / blktest_select
    目的はほぼ同一であるが、Linux 2.0系と 2.2系で関数そのものの名前、動作が異なる。
    2.2系のpoll では、準備が出来ているものについて、ビットマスクで状況を報告する。 poll_wait()で「休眠の予約」をするが、もし、blktest そのものや、同時に調査された他のデバイスで準備が完了していることが確認された場合、この予約はキャンセルされる。 そうでなければ、プロセスは休眠状態にはいる。
    2.0系のselectでは、sel_type 引数によって、「読み・書き・特別」のうち、どの準備状況を応答するかが指定される。 そのため、別個に状況を判断し、準備が出来ていれば0以外を返す。こちらはselect_wait() で休眠の予約をする。
    2.2系の場合は、1回の読み出しですべてのチェックが終わる点で、パフォーマンスが向上していると考えられる。
  • blktest_lseek
    lseek(fseek)の機能を担当する。lseek のオフセットおよび原点指定をそのまま受け取り、file->f_posの位置を操作する。 領域が限られているため、範囲のチェックしている。
  • init_module / cleanup_module
    init_module では FileInfo の初期化(未使用マークつけ)を行う部分を追加。 cleanup_module ではそのままおわっていて、なにやら気持悪い気もするが、そもそも、すべて close されていなければ、これは呼ばれなく、休眠待機している分には close も呼ばれないため、これでいいはず。

  • select試験用のプログラムでは、まず、lseek の動作試験をかねて領域サイズの取得。 そのあと、プログラム引数に'w'を指定した場合は 書込の待機、それ以外では読込の待機をselectで行う。 タイムアウトを1秒に設定し、準備が出来た場合には、「書く/読む」して、一旦 close する。 へんなプログラムであるが、「close したときに読み書きを確定する」仕様のドライバなので、このようになっている。
    (たとえば、f_pos を使わないプログラム、すなわち、常に先頭から読み書きするようにすれば、read write 関数で読み書きを確定すれば良いが、read が 0 を返すまで読み込む cat などで面倒なことになる。 複数回の連続読み書きをサポートする場合は、読みかけで書いたりするなどやっかいがおきる。FIFO 型にするなども手であるが...)
ブロックを実現するには、まず、「準備ができている・できていない」をはっきりと評価する必要があります。 これはselectでも一緒です。 準備が出来ていない場合には休眠させ、待機させます。 それは struct wait_queue *q; interruptible_sleep_on(struct wait_queue ** q); を使用します。 wait_queue は休眠しているプロセスの情報を起こすときに使えるようにするための変数で、プログラムをつくる場合には struct wait_queue * で変数を宣言し、そのポインタ (&q)を interruptible_sleep_on に渡します。 interruptible がない sleep_on もありますが、違いは signal で目覚めるかどうか、です。

なんらかの要因で、この休眠を解きたい場合には wake_up_interruptible(struct wait_queue ** q); を使います。wait_queue には sleep_on したのと同じ変数(のポインタ)を与えます。

このように、なにかを待つために sleep_on して、待っているものを起こすのに wake_up をつかう、その情報は wait_queue * で渡す、というのが、待機の基本です。 select, poll の場合は実際に休眠するかどうかは、カーネルのほうでやってくれます。 そのため、 void poll_wait(struct file * file, struct wait_queue ** wait, poll_table *p); void select_wait(struct wait_queue ** wait, select_table * p); を使い、もし、休眠が必要ない場合、 wait_queue * で用意した変数の後始末をしてくれます(結局sleepしなかったときに、間違ってwake_upしても問題にならないように処理してくれるようです)。

起こす点についてはブロックと同じで wake_up_interruptible()を使います。

select などの動作を見やすく/確認しやすくしようとしたら、プログラムがやたらと大きくなってしまいましたが、要は「寝せて」「起こす」です。

はじめにも述べましたが、ブロックも select も「待機」を実現する機能です。 一般的なUNIXのデバイスとしては、なんらかのデータがくるのを待つのに使われることが多いですが、ハードウェアの制御においては、なんらかのトリガを拾うことにも使用可能です。 実際、以前開発した、Digital Parallel I/O ボードのドライバでは、割り込みをselectで受けられるようにして(割り込み関数内でwake_up する)、ユーザプロセスで割り込みを扱うことを可能にしました。 また、ポーリングでハードの状態を監視し、それに応じてwake_up をかけるなど、使い道は様々です。 signal も同じように使えそうなところですので、こちらもそのうちやってみることにしましょう。