IOCTLをつかう
ioctl は、なにやらデバイスを操作するプログラムを書いたことがあれば目にしたことがあるかも知れませんし、ifconfig のようなデバイス操作をするプログラムのエラーで見覚えがあるかも知れません。

私自身、見よう見まねでいろいろなプログラムを書いて(打ち込んで)いるときに、時々 ioctl に出会っていました。 得たいの知れない定数やら得たいの知れない構造体やらを渡すので、私にとっては不気味な存在でした。 C の関数なら、普通はマニュアルを読めば引数と機能は書いてあるのに、ioctl だけは、使い方だけで中身については一切なしなのです。

ですが、デバイスドライバをつくるようになってから、一気に謎が解けました。 ioctl は read や write とは別系統のドライバとのやり取りの口です。 read, write が基本的には連続したデータのやりとりをするように使用されることが多いのに対して、ioctl はどちらかというとデバイスの状態を制御する単発データ用といった趣きです。 ここのページでは、どちらも同じような使い方をしているので、区別はつきにくいですが。

ioctl はman ioctlしてみると、ファイル記述子の他に、request とか cmd とか書いてある整数引数(unsigned int) がひとつあり、もうひとつ何か引数ととる(取らないこともあり)ようになってます。 デバイスドライバ内部の ioctl を受け持つ関数にはこの 整数引数と何らかの引数が unsigned long の形で渡されます。 この関数の帰り値が負の数値の場合、ユーザプロセスの ioctl()には-1が返り、errno にその絶対値が設定されます。

さて、ioctl を受け持つ関数には なんの機能を実装すればいいのでしょうか? それを決めるのはドライバを設計するみなさんです。一応、例を挙げます。

  • デバイスの準備が出来ているかどうか?
    0 か -1 を返すことで、少なくとも2値的な判断結果を返すことができます。
  • 動作モードの切り替え
    複数のコマンドを定義することで、動作モードを切り替えることができます。
  • デバイスへのパラメータの受け渡し
    コマンドに続く引数で 適当な整数を渡すことができます。
  • デバイスへのパラメータ「群」の受け渡し
    コマンドに続く引数で 構造体のポインタを渡すことで、その内容を デバイス側に送ることができます。
などなど。つまり、ioctlの最後の引数の使い方次第でかわる、ということです。

使い方&デバイスによったら、コマンドがある分だけ read/write で実装するより簡単だと思います。

さすがに ioctl は cat では使えないので、専用のアクセステストプログラムもつくります。 また、両者で共有する情報はヘッダファイルにまとめました。 ファイル ocitest.h (ヘッダ) // header file of module ocitest.o #define OCI_NOP 1 // ioctl 用のコマンド数値 #define OCI_INT 2 // 一応、カッコ良く定義しておく #define OCI_STRING 3 #define OCI_STRUCT 4 struct oci_struct { char *p; int len; }; ファイル ocitest.c (モジュール) // gcc -c ocitest.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/ocitest c 60 0; chmod a+rw /dev/ocitest #define MODULE #define __KERNEL__ #include #include #include #include #include #include "ocitest.h" static int devmajor=60; static char *devname="ocitest"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif #if LINUX_VERSION_CODE >= 0x020100 #include #else // Linux 2.0.x では memcpy_tofs/_fromfs だった関数が // Linux 2.1.x 以降で copy_to/from_user に 変更。なので合わせる 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 // Linux 2.0/2.2 共通 static int ocitest_open(struct inode * inode, struct file * file) { MOD_INC_USE_COUNT; return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int ocitest_close(struct inode * inode, struct file * file) #else static void ocitest_close(struct inode * inode, struct file * file) #endif { MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // ioctl: Linux 2.0/2.2共通 static int ocitest_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { char buff[257]; struct oci_struct ocis; int i; printk("ocitest: ioctl: cmd=%04X, arg=%08lX\n",cmd,arg); switch(cmd) { case OCI_NOP: printk(" Cmd: No operation\n"); return 0; case OCI_INT: printk(" Cmd: Integer: %ld\n",arg); return 0; case OCI_STRING: for(i=0;i<256;i++) // arg をポインタと見なしてユーザデータ取得 { // get_user はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 if(get_user(buff[i],(char *)(arg+i))) return -EFAULT; #else buff[i]=get_user((char *)(arg+i)); #endif if(buff[i]=='\0') break; } buff[i]='\0'; // 安全止 printk(" Cmd: String: %s\n",buff); for(i=0;buff[i];i++) // to lower { if((buff[i]>='A')&&(buff[i]<='Z')) buff[i]+=('a'-'A'); } copy_to_user((void *)arg,buff,i); // データ返還 return 0; case OCI_STRUCT: copy_from_user(&ocis,(void *)arg,sizeof(ocis)); for(i=0;;i++) // 文字列長さカウント { #if LINUX_VERSION_CODE >= 0x020100 if(get_user(buff[0],ocis.p+i)) return -EFAULT; #else buff[0]=get_user(ocis.p+i); #endif if(buff[0]=='\0') break; } ocis.len=i; copy_to_user((void *)arg,&ocis,sizeof(ocis)); // データ返還 return 0; } return -EINVAL; } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations ocitest_fops = { // Linux 2.2.10 より NULL, // loff_t llseek(struct file *, loff_t, int) NULL, // ssize_t read(struct file *, char *, size_t, loff_t *) NULL, // ssize_t write(struct file *, const char *, size_t, loff_t *) NULL, // int readdir(struct file *, void *, filldir_t) NULL, // u. int poll(struct file *, struct poll_table_struct *) ocitest_ioctl,// int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) ocitest_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) ocitest_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 ocitest_fops = { // Linux 2.0.36 より NULL, // int lseek(struct inode *, struct file *, off_t, int) NULL, // int read(struct inode *, struct file *, char *, int) NULL, // int write(struct inode *, struct file *, const char *, int) NULL, // int readdir(struct inode *, struct file *, void *, filldir_t) NULL, // int select(struct inode *, struct file *, int, select_table *) ocitest_ioctl,// int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) ocitest_open, // int open(struct inode *, struct file *) ocitest_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) { printk("install '%s' into major %d\n",devname,devmajor); if(register_chrdev(devmajor,devname,&ocitest_fops)) // 登録 { printk("device registration error\n"); return -EBUSY; } return 0; } void cleanup_module(void) { printk("remove '%s' from major %d\n",devname,devmajor); if (unregister_chrdev(devmajor,devname)) { printk ("unregister_chrdev failed\n"); } }; ファイル ocitest_p.c (アクセス試験用プログラム) #include #include #include #include #include #include #include #include "ocitest.h" int main(void) { int fd; char buff[100]; struct oci_struct ocis; fd=open("/dev/ocitest",O_RDWR); if(fd<0) { fprintf(stderr,"cannot open device\n"); return 1; } ioctl(fd,OCI_NOP); ioctl(fd,OCI_INT,12345); strcpy(buff,"ABCDEFGhijklmnOPQRSTUvwxyz0123456789"); printf("ioctl(fd,OCI_STRING,\"%s\"); ->\n",buff); ioctl(fd,OCI_STRING,buff); printf(" -> %s\n",buff); ocis.p=buff; ioctl(fd,OCI_STRUCT,&ocis); printf("length of \"%s\"=%d\n",ocis.p,ocis.len); close(fd); return 0; } 実行方法を以下に示します。 insdev をつかってみたりしてますが、基本的にはこれまでと同じ実行方法です。 ただ、cat ではなく 自前のocitest_p を実行します。 % gcc -c ocitest.c -Wall -Wstrict-prototypes -O -pipe -m486 % gcc -o ocitest_p ocitest_p.c # insdev ocitest /dev/ocitest さっそく使ってみたりする auto-selected devcie major(c)=60 device 60:0: /dev/ocitest % ./ocitest_p ioctl(fd,OCI_STRING,"ABCDEFGhijklmnOPQRSTUvwxyz0123456789"); -> -> abcdefghijklmnopqrstuvwxyz0123456789 length of "abcdefghijklmnopqrstuvwxyz0123456789"=36 # rmdev ocitest unlink '/dev/ocitest' % dmesg | tail install 'ocitest' into major 60 ocitest: ioctl: cmd=0001, arg=4000A3C8 Cmd: No operation ocitest: ioctl: cmd=0002, arg=00003039 Cmd: Integer: 12345 ocitest: ioctl: cmd=0003, arg=BFFFF874 Cmd: String: ABCDEFGhijklmnOPQRSTUvwxyz0123456789 ocitest: ioctl: cmd=0004, arg=BFFFF86C remove 'ocitest' from major 60 このデバイスでは コマンドとして1〜4の整数を受け付けます。
  1. OCI_NOP: なにもしません
  2. OCI_INT: ioctl のコマンドのあとの引数を整数として表示します。
  3. OCI_STRING: ioctl のコマンドのあとの引数を文字列へのポインタと して認識し、画面に出力するとともに、アルファベットを小文字に変換して ioctl の呼出元に返します。
  4. OCI_STRUCT: 文字列へのポインタを含む構造体へのポインタを受取り、文字列の長さ('\0'まで)を算出し、構造体に代入して返します。

長ったらしいプログラムに見えますが、実際のところ、ioctl の動作を受け持つ関数は、コマンド数値による switch の嵐で、やってることは単純です。 ただし、readやwriteのところでもやりましたが、ユーザプロセスから渡されたポインタにアクセスするためには copy_to_user(), copy_from_user()が必須です。 特に OCI_STRUCT では構造体を得るのに1回、返すのに1回つかっています。

と、文字列を得る部分には copy_from_user()を使わず、get_user()を使っています。 copy_??_user()がある程度の長さのデータを一度にやり取りするのに対して、1,2,4バイト(char,short,long)のデータをやり取りするための「ちょっとだけ」用の関数がget_user()です。 asm/segment.h (unsigned) char get_user(const (unsigned) char *user); (unsigned) short get_user(const (unsigned) short *user); (unsigned) long get_user(const (unsigned) long *user); *user の値が帰ります asm/uaccess.h int get_user((unsigned) char val, (unsigned) char *user); int get_user((unsigned) short val, (unsigned) short *user); int get_user((unsigned) long val, (unsigned) long *user); 変数 val に *user の値が代入されます。 (valはポインタではありません。マクロで処理されます) 定義上は void ですが、実際にはこんな形で呼び出されます。 帰り値は転送完了で0、不正な転送で -14 が帰ります。 "unsigned" が () 書きなのは、これらはサイズのみをチェックするマクロであるため、unsigned はあってもなくともいいからです。
こういう根本的な関数が変ると、互換性のあるプログラムを書くのが面倒になります。 いっそのこと、get_user をさらにもう一段マクロで包むといいかもしれません。

get があるからには put もあります。 asm/segment.h void put_user(char value, char *user); asm/uaccess.h int put_user(char value, char *user); (unsigned) short, long も同形式。 不正な転送でやはり -14が帰ります。 補足
Linux 2.0 と 2.2 で変った点は、ユーザ空間の領域チェックをするようになったことです。 その点 2.0 は完全にアセンブラへマクロ展開だったのでおそらく速度はあったはずですが、2.2では安全性が上がったようです。