簡単なキャラクタデバイスをつくる
ここでは 実際に簡単なキャラクタデバイスをつくってみます。さしあたって、
  • open
  • close
  • read
  • write
に対応します。最初は無難に open・close から。徐々に増やしてきます。

ここでキャラクタ(型)デバイスとはなんぞや、ということです。 Linux ではデバイスはキャラクタ型とブロック型があります。 キャラクタ型が1バイト単位の細かい読み書きが可能なのに対して、ブロック型はブロックというデータの塊を単位に読み書きします。 ブロック型のデバイスは mountすることでファイルシステムに組み込むことが可能で、またディスクキャッシュも働きます。 ただ、ちょっと難しいのでここではおいておきます。 実際にハードウェアを操作するときに、ブロック型の必要性があることはほとんどないと思います。

まずはサンプルのソースと実行例から。
(ソースは例によって手抜きなので "//コメント" です。) // gcc -c octest.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/octest c 60 0; chmod a+rw /dev/octest #define MODULE #define __KERNEL__ #include #include #include #include #include static int devmajor=60; static char *devname="octest"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif // Linux 2.0/2.2 共通 static int octest_open(struct inode * inode, struct file * file) { printk("oc_open:"); printk(" file->f_version : %lu \n",file->f_version); printk(" MINOR(inode->i_rdev): %d \n",MINOR(inode->i_rdev)); MOD_INC_USE_COUNT; return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int octest_close(struct inode * inode, struct file * file) #else static void octest_close(struct inode * inode, struct file * file) #endif { printk("oc_close:"); printk(" file->f_version : %lu \n",file->f_version); MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations octest_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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) octest_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) octest_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 // LINUX_VERSION_CODE < 0x20100: Linux 2.0.x static struct file_operations octest_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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) octest_open, // int open(struct inode *, struct file *) octest_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,&octest_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"); } }; % gcc -c octest.c -Wall -Wstrict-prototypes -O -pipe -m486 # mknod /dev/octest c 60 0 アクセス用の # chmod a+rw /dev/octest 特殊ファイルをつくる # insmod octest % cat /dev/octest cat: /dev/octest: Invalid argument read できなかったので文句をいう # rmmod octest # dmesg | tail install 'octest' into major 60 oc_open: file->f_version : 25546 MINOR(inode->i_rdev): 0 oc_close: file->f_version : 25546 remove 'octest' from major 60 # 急にソースが長くなりましたが、よくみると file_operationsなる構造体の定義が大きいだけで、中身は難しいことありません。 しかも、Linux 2.0/2.2 どちらの系列でも対応できるようにしているせいで、長さが倍近くなっています。 どちらかにのみ対応すればいいのであれば、#ifdef な部分を削ってしまえば短くなります。

実行例のところで % で始まる行は一般ユーザでも実行可能、# で始まる行は root でなければ実行できないところです。

なお、とりあえず、メジャー番号(後述)を60にしていますが、もしかすると、ほかのデバイスがこの番号を使っているかも知れません。 念のため、/proc/devices を見てみて、60 という数字があるようなら、ソースのdevmajormknodするときの 60 を別の数字(61など)に変えてください。 まず、ドライバをつくったら、その登録をしなければ、Linuxに認知してもらえません。 また、登録したまま rmmodしてカーネルからモジュールを取り払うと、すでに関数などがなくなったところにカーネルがアクセスしてしまって、大変なことになります。 そのため、init_module(), cleanup_moduleは、ドライバとしての機能を登録・登録解除を行う必要があります。 それらを行うのが include/linux/fs.h int register_chrdev(unsigned int, const char *, struct file_operations *); int unregister_chrdev(unsigned int major, const char * name); です。両関数の最初の2つの引数は同じ値で、前者がメジャー番号、後者がデバイスを示す文字列(適当)です。

デバイスの識別にはメジャー番号・マイナー番号がつかわれます。 メジャー番号は登録されたドライバを区別する番号で、マイナー番号はそれぞれのドライバが使える番号です。 たとえば、IDEのデバイスは /dev/hda(ドライブ全部) /dev/hda1(第一パーティション)... と幾つか種類がありますが、すべてメジャー番号3のデバイスでマイナー番号を0,1,2.. と区別することで、ドライバ側でどこが要求されたかを知ることができます。 マイナー番号を使う例はあとで述べることにします。

register_chrdev の3つめの引数は、デバイスに対する操作をどの関数が受け持つかを定める一覧表です。 多くはシステムコールと対応していて open, close(release), read, write などがあります。 上の例でもその一覧を定義しています。項目は多いのですが、サポートしないものは NULL にするため、非常に無駄に見えます。 が、NULL は NULL で大切で、Linux内部で、NULL なら自動的にエラー処理やデフォルト処理をするようになっています。 上の例では open と close のみ定義しています。これらの関数はとりあえず、printk するだけです。

octest_open, octest_closeには、 MOD_INC_USE_COUNT; MOD_DEC_USE_COUNT; なる記述がみられます。 これらは、「モジュールの利用度数」を増やしたり、減らしたりするものです(初期値0)。 だれかが使用中(open してcloseしてない(含 プロセスの終了))にドライバが無くなってしまってはこまるので、このような仕組みがあります。 この利用度数が0でなければ除去できない仕組みのため、開くときに増やして、閉じるときに減らす、などの組合わせで使用中は0より大きくするとよいでしょう。 ただし、誤ってINC したのに DEC しそこねたりすると、除去できないモジュールになってしまうので要注意です。

次に実行例の説明ですが、モジュールのところでの例に加え、 # mknod /dev/octest c 60 0 が増えています。 これは一度つくってしまえば、2度目以降はいらないのですが(やるとエラーがでます)、1回は必要です。 UNIXの類い(MS-DOSも一部まねてますが)はデバイスもファイルシステムの一部にするという特徴があります。 そのためのアクセスする口をつくるのが mknod です。 この例では「/dev/octest がアクセスされたら メジャー60 マイナー0のキャラクタ型デバイスをアクセスする」という定義を行っています。 逆に、(60,0) の部分がそのままなら、どこでつくっても、いくつつくってもかまいません。 mknod ~/auau 60 0 とすれば、ホームディレクトリのauau をアクセスしたときにデバイスが参照されることでしょう。 ただ、伝統的に /dev/ に集めてあります(もっとも、今回のようなテスト用途だと /dev/ につくる理由は全くありません)。

実際の動作についてまとめておきます。

  • insmod:
    init_moduleがメジャー番号60のキャラクタ型デバイスを登録。 ドライバの機能は open と close のみ。
  • cat の open:
    /dev/octestを open しようとすると、Linuxカーネルが メジャー60、マイナー0のデバイスだと認識して、登録されているデバイスを検索する。 見つけたら、その機能一覧をチェックしてopen があれば、登録されている open の関数を呼び出す。
    ここで MOD_INC_USE_COUNT するので、モジュールははずせなくなる。
    (もちろん、ほかにもいろいろやってます)
  • cat の read:
    すでに open して得たファイル記述子をつかって アクセスするとLinuxカーネルが read の機能があるかをチェック。 ないのでエラー(EINVAL)を返す。
  • cat の close:
    同じく、close(release)の機能を探して実行。MOD_DEC_USE_COUNT してはずせるようになる。
こまかいことをいうと、実際には read でエラーを起こした段階でrelease されてしまうようです。 また、いくらプロセス側からcloseが呼ばれても、release は一度しかよばれないように、設計されてます(そうでないと困ります:-))。
先ほどの例に read が加わります。 // gcc -c ocrtest.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/ocrtest c 60 0; chmod a+rw /dev/ocrtest // mknod /dev/ocrtest10 c 60 10; chmod a+rw /dev/ocrtest #define MODULE #define __KERNEL__ #include #include #include #include #include #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 static int devmajor=60; static char *devname="ocrtest"; static char *message="linux drivers!\n"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); MODULE_PARM(message, "s"); #endif static int linecount; // Linux 2.0/2.2 共通 static int ocrtest_open(struct inode * inode, struct file * file) { printk("ocr_open:"); printk(" file->f_version : %lu \n",file->f_version); printk(" MINOR(inode->i_rdev): %d \n",MINOR(inode->i_rdev)); MOD_INC_USE_COUNT; linecount=5+MINOR(inode->i_rdev); return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int ocrtest_close(struct inode * inode, struct file * file) #else static void ocrtest_close(struct inode * inode, struct file * file) #endif { printk("ocr_close:"); printk(" file->f_version : %lu \n",file->f_version); MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // read はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 static int ocrtest_read(struct file * file, char * buff, size_t count, loff_t *pos) #else static int ocrtest_read(struct inode * inode,struct file * file, char * buff,int count) #endif { int len; if(linecount==0) return 0; // ネタギレ :参照 open() linecount--; len=strlen(message); if(len>count) len=count; copy_to_user(buff,message,len); printk("ocr_read:"); printk(" file->f_version : %lu \n",file->f_version); printk(" requested: %d bytes returned: %d bytes\n",count,len); return len; } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations ocrtest_fops = { // Linux 2.2.10 より NULL, // loff_t llseek(struct file *, loff_t, int) ocrtest_read, // 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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) ocrtest_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) ocrtest_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 ocrtest_fops = { // Linux 2.0.36 より NULL, // int lseek(struct inode *, struct file *, off_t, int) ocrtest_read, // 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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) ocrtest_open, // int open(struct inode *, struct file *) ocrtest_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,&ocrtest_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"); } }; % gcc -c ocrtest.c -Wall -Wstrict-prototypes -O -pipe -m486 # mknod /dev/ocrtest c 60 0; chmod a+rw /dev/ocrtest # mknod /dev/ocrtest10 c 60 10; chmod a+rw /dev/ocrtest10 # insmod ocrtest % cat /dev/octest linux drivers! linux drivers! linux drivers! linux drivers! linux drivers! # cat /dev/ocrtest10 linux drivers! : 全部で15個 linux drivers! # rmmod ocrtest # insmod ocrtest message='another message ' % cat /dev/octest another message another message another message another message another message # rmmod ocrtest 増えたのは
  • read の機能
  • read でつかうデータ転送関数のバージョン依存部
  • open でMINOR をつかってみる
のみです。 基本的な動作は先ほどの例と変わりません。 ただ、cat がread システムコールをつかったときに、ちゃんと何かかえってくる、というあたりが変わってます。

Linux のバージョンによって微妙に動作が異なりますが、read のすべき仕事は 「プロセスが指定した領域に要求された大きさを上限として、データをコピーする」
「コピーした量を return する」
ことです。ここで大事な問題があります。 プロセスから指定される領域は、プロセスの動作しているメモリ空間でのアドレスであり、カーネルの動作しているメモリ空間とは異なる、という点です。 このあたりはやっかいな話なのですが、プロセスでつかっているポインタの示すアドレスが、そもそも物理的なメモリのアドレスではないことによります。
そのようなわけで、データのコピーには特別な手順が必要です。それには以下の関数を使用します。 include/asm/segment.h memcpy_tofs (void *to, const void *from, unsigned long n); memcpy_fromfs (void *to, const void *from, unsigned long n); include/asm/uaccess.h copy_to_user (void *to, const void *from, unsigned long n); copy_from_user(void *to, const void *from, unsigned long n); 形はmemcpyとおなじで、名前だけ違います。 Linux のバージョン間でも名前だけが違います。 "to"の関数はユーザ空間(プロセスの動作しているメモリ)へ、"from"の関数はユーザ空間からデータの転送をします。 read はユーザ空間へ送るのが目的なので、"to" の関数をつかいます。 ここではソースを共通にするために、Linux 2.0 の場合にはcopy_to_user, copy_from_user を定義して、copy_to_userを使うようにしています。

なお、return する値が 0 の場合、ふつうはデータの終点と見なします。 負の値を返した場合、その絶対値がC言語では errno に設定されます。 読み取りエラーなどの状況を EBUSY, EINVAL などで返すなどの応用が可能です。 このデバイスは open してから close するまで、read が来る度に同じメッセージをわたします。回数は5回限定です。 おまけとして、マイナー番号が0で無い場合に、「5+マイナー番号」回出力するようにしてあります(変数 linecount 参照)。 問題点として、回数を共通の変数にしているので、同時に複数の open や read があった場合、回数の処理が変になることが容易に予想がつきます。 この対策として

  • 同時に1回しか open できなくする
    (ocrtest_open でフラグを立てて、ocrtest_close でフラグを寝せる)
  • 回数カウントを別々に行う
があります。後者のためには、いくつかの手段があります。
  • loff_t file->f_posをつかう
  • void* file->private_dataをつかう
  • 自前で適当な領域をつくって管理する
file->f_posはもともと、ファイルの読み書き位置、すなわちlseek, fseekで指定される位置を保持するために用意された 変数なのですが、この例のように、読む度同じデータを返す、様な場合には本来の目的に使わないので、流用できます。
file->private_dataは自前の作業領域用構造体などを確保した場合に、それを保持しておくためなどに使うことの出来る汎用ポインタです。 これは3つめの対策案に使用できます。 ただ、ポインタとはいえ、所詮は数値なので:-) キャストして流用することは不可能じゃありません。
3つめの方法が正当な方法で、多くのドライバでみられる方法です。 大抵、file->private_dataで情報(へのポインタ)を保持しますが、適当に配列をつくる、という方法もあります。 そのときの目印として、file->f_versionを使用できます。この数字は open するたび異なる数値が設定されるので、識別子としてつかえます。

あとは前の例とかわりません。つぎは write を組み込みます。

こんどは書込みを実装します。上の例に付け加えてもいいのですが、 分かりにくくなるので、また別のサンプルをつくります。 // gcc -c ocwtest.c -Wall -Wstrict-prototypes -O -pipe -m486 // mknod /dev/ocwtest c 60 0; chmod a+rw /dev/ocwtest #define MODULE #define __KERNEL__ #include #include #include #include #include #if LINUX_VERSION_CODE >= 0x020100 #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="ocwtest"; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif // Linux 2.0/2.2 共通 static int ocwtest_open(struct inode * inode, struct file * file) { printk("ocw_open:\n"); MOD_INC_USE_COUNT; return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int ocwtest_close(struct inode * inode, struct file * file) #else static void ocwtest_close(struct inode * inode, struct file * file) #endif { printk("ocw_close:\n"); MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // write はバージョン依存 #if LINUX_VERSION_CODE >= 0x020100 static int ocwtest_write(struct file * file, const char * buff, size_t count, loff_t *pos) #else static int ocwtest_write(struct inode * inode,struct file * file, const char * buff,int count) #endif { int len; char k_buff[50]; len=49; if(len>count) len=count; copy_from_user(k_buff,buff,len); k_buff[len]='\0'; printk("ocw_write:\n"); printk(" offered: %d bytes obtained: %d bytes\n",count,len); printk(" message: '%s'\n",k_buff); return len; } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations ocwtest_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 *) ocwtest_write,// 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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) ocwtest_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) ocwtest_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 ocwtest_fops = { // Linux 2.0.36 より NULL, // int lseek(struct inode *, struct file *, off_t, int) NULL, // int read(struct inode *, struct file *, char *, int) ocwtest_write,// 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 *) NULL, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) ocwtest_open, // int open(struct inode *, struct file *) ocwtest_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,&ocwtest_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"); } }; # mknod /dev/ocwtest c 60 0 # insmod ocwtest # echo -n "abcdefghijklmnopqrstuvwxyz" > /dev/ocwtest # rmmod ocwtest # dmesg | tail install 'ocwtest' into major 60 ocw_open: ocw_write: offered: 26 bytes obtained: 26 bytes message: 'abcdefghijklmnopqrstuvwxyz' ocw_close: remove 'ocwtest' from major 60 # insmod ocwtest # echo -n "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" > /dev/ocwtest # rmmod ocwtest # dmesg | tail install 'ocwtest' into major 60 ocw_open: ocw_write: offered: 52 bytes obtained: 49 bytes message: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW' ocw_write: offered: 3 bytes obtained: 3 bytes message: 'XYZ' ocw_close: remove 'ocwtest' from major 60 # 前の例から read がなくなって、write ができ、実行例も echo からリダイレクトで投げ込むようになりました。 read までやってしまうと、writeも簡単です。 ひとつ恐いといえば、ユーザ空間にコピーを失敗してもなんとかなりそうですが、write の場合は copy_from_userでカーネル空間にコピーしてきますので、まちがったら、大変なことになりかねない、ということでしょうか。

とりたてて、特別な点はありません。write の場合も受け取ったバイト数を返します。

ここでは 簡単なキャラクタデバイスをつくるべく、open, close, read, writeにしぼって、実際例を題材に解説しました。 さて、これらでなにができるでしょう?
じつはこれだけあれば、簡単なデバイスドライバはつくることができます。 デバイスドライバの仕事は、ユーザプロセスからの要求をうけとって、ユーザが普段さわれないハードウェアにアクセスし、読み書きを行うことです。 read, write はべつにファイルのように連続したデータを扱うことを強制するわけではなく、毎回、特定の構造体を単位に読み書きしても何ら問題ないわけです。 そのときは、プロセス側で read(),write() するときに構造体のサイズを渡せばいいだけです。

今回解説した open, close, read, write はいわば、カーネル内部のデバイスドライバと それを利用するユーザ空間のプロセスとの通信手段、といえるでしょう。