とりあえず遊んでみる
Linuxのcatとecho
Linuxの基本コマンドcatとechoはご存知ですね.なんていうと「バカにするな〜」と返されそうです.catはファイルの内容を表示して,echoはディスプレイ(標準出力)にメッセージを出力するコマンドです.
echoコマンドは'>'という文字で出力先をディスプレイからファイルに変更できます.これをリダイレクトといいましたね.
$echo 1 > text.txt
とすると,text.txtというファイルに数字の1という文字が書き込まれます.ここで1と>の間にはスペースを入れておいて下さいね.1>のように続けて書くと別の意味になってしまいますので,ご注意を.このファイルを読み出すと,
$cat text.txt
1
まあ,あたりまえといえばその通りです.
変なファイルに1を書いてみる
それでは先ほどと同じ1という文字をこのプロジェクトで作る変なファイル/dev/henに書き込んでみます.実際に自分のマシンでやってみたいなら,先に次節のビルドおよびインストールを先にやっておいてください(WindowsのサブシステムのWSLではビルドもインストールもできません.VirtualBoxのVMをお勧めします).
$echo 1 > /dev/hen
$cat /dev/hen
1
まあ当たり前の結果になりました.
もう一度同じコマンドを実行してみましょう.
$echo 1 >/dev/hen
$cat /dev/hen
2
あれれ!?1が1で上書きされるのだから1が表示されると思ったのですが,変ですね.
さらにもう一度,今度は5を書き込んでみます.
$echo 5 >/dev/hen
$cat /dev/hen
7
なんということでしょう〜.どうやらこのファイルは,これまで書き込んだ数値を足し合わせた結果を格納しているようですね.
では数値以外の文字を書き込むとどうなるでしょうか.
$echo X >/dev/hen
$cat /dev/hen
おばかさんですね〜
うゎ!しかられちゃいました.とりあえず挨拶で返しておきます.
$echo Hello >/dev/hen
$cat /dev/hen
Good day!
挨拶してくれました.
念のためにもう一度catすると
$cat /dev/hen
7
先ほど怒られたけど,もう怒りはおさまった模様で,計算結果はちゃんと覚えてくれているようです.
このファイル/dev/henは入力によって反応が変化するので,ファイルというよりむしろプログラムと呼ぶべきではないか,と感じていただければ,ここまでの芝居がかった説明は一応成功です.
計算結果はClearで0にリセットできます.
$echo Clear >/dev/hen
$cat /dev/hen
0
ビルドおよびインストール
##ビルド
動作はUbuntu18(5.0.0-31-generic)とCentos8(4.18.0-80.7.1.el8_0.x86_64)で確認済みです.まず開発環境をインストールします.
Ubuntu18
apt install build-essential
Centos8
yum groupinstall "Development Tools"
yum install elfutils-libelf-devel
Githubからソースコードを取得します
$ git clone https://github.com/h-hata/cdev.git
cdevが取得できたら,そこにチェンジディレクトリしてmakeしてください.
$ make
make -C /lib/modules/`uname -r`/build M=/home/hata/src/cdev modules
make[1]: ディレクトリ '/usr/src/kernels/4.18.0-80.7.1.el8_0.x86_64' に入ります
CC [M] /home/hata/src/cdev/add.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/hata/src/cdev/add.mod.o
LD [M] /home/hata/src/cdev/add.ko
make[1]: ディレクトリ '/usr/src/kernels/4.18.0-80.7.1.el8_0.x86_64' から出ます
add.koというファイルができたら成功です.
##インストール
ビルドできたadd.koはLinuxカーネルの一部として動作するカーネルモジュールと呼ばれる実行可能ファイルです.このadd.koをカーネルに組み込むのにinsmodコマンドを使います.
#insmod add.ko
何もコマンド応答が返らなければ成功したものと思われます.組み込まれたカーネルモジュールの一覧はlsmodコマンドで取得できます.
$ lsmod |grep add
add 16384 0
addモジュールが組み込まれています.アプリケーションがこのaddモジュールを使うための玄関のような役目をするデバイスファイルを作成します.まず,addモジュールのカーネルの内部における背番号のような意味をもつMajor番号を取得します.
#grep add /proc/devices
ここで表示された番号を覚えておきましょう.この番号をXXXとします.そしてmknodコマンドでデバイスファイルを生成して,そのデバイスファイルとaddモジュールをMajor番号で結びつけます.
#mknod /dev/hen c XXX 0
ここでのhenは,henでなくても構いません.好きな名前を付けてください.
一般ユーザにも利用可能なように,アクセス権を付与しておきます.
#chown a+wr /dev/hen
これで完了です.
##アンインストール
カーネルモジュールをカーネルから取り外すにはrmmodコマンドを使います.パラメータはinsmodとは異なり,ファイル名ではなくモジュール名です.
#rmmod add
rmコマンドでデバイスファイルも削除できます.
#rm /dev/hen
insmodコマンドで組み込んだモジュールは,システムがリブートしたら再度自動的に組み込まれることはありません.
#解説
概要
/dev/henはアプリケーションからはファイルのように操作できますが,実体はファイルではなくプログラムになっていました.このファイルのふりをして動作するプログラムはカーネルモジュールと呼ばれます.
このカーネルモジュールを使うのに必要なアプリケーションは,catコマンドとechoコマンドの2つでした.catプロセスはは指定されたファイルをopenしてファイルの終わりまでreadしてからcloseします.リダイレクト付きのechoプロセスは指定されたファイルをopenして,文字列をwriteしてからcloseします.これらのシステムコールはいったんカーネルで受け取ります.カーネルはこれらのシステムコールで発生したリクエストを自分で処理するのではなく,対応するカーネルモジュールを呼び出して処理させます.このような働きをするカーネルモジュールをキャラクタデバイス型カーネルモジュールといいます.対してブロック型カーネルモジュールというものもありますが,これはmountしてストレージのように動作します.ここではキャラクタデバイス型の説明だけに絞ります.
カーネルモジュールがハードウェアを操作する場合にはデバイスドライバと呼ばれますが,add.koのようにハードウェアを扱わないカーネルモジュールも多くあります.
キャラクタデバイス型カーネルモジュールは,アプリケーションがopen,read,write,closeシステムコールを発行するたびに,カーネルから呼び出されるopen,read,write,close関数を実装しているライブラリ関数のようなものです.
これに加えて,モジュールをカーネルに組み込んだり(insmod)取り外す時(rmmod)に呼び出される初期化関数と終了関数も実装しておく必要があります.これ以外にもアプリケーションはopenできたファイルディスクリプタに対して,ioctlやmmap,flushなど色々な種類のシステムコールを発行します.カーネルモジュールでは,これらのシステムコールに対応する関数を実装することができますが,必ずしも全てのシステムコールに対応しなければならないわけではありません.add.koではopen,read,write,closeの4つしか実装していません.また読み取り専用モジュールではwriteを実装しないこともあり得ます.
まとめるとキャラクタデバイス型カーネルモジュールは以下の機能を実装していることになります.
- insmod,rmmod時に呼び出される関数
- システムコール時に呼び出される関数
- open
- read
- write
- close
- そのカーネルモジュールに特有な機能
それでは実際のソースコードで,各機能がどのように記述されているのかをみてゆきます.
詳細
まずadd.cの全体を示します.C言語で150行程度です.
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPLv2");
#define MODULE_NAME "add"
#define DEV_COUNT 1
#define ja_silly "おばかさんですね〜"
#define en_silly "You are so silly"
static dev_t dev;
static struct cdev cdev;
static int total=0;
static int greeting=0;
static int error=0;
static int open_add(struct inode *inode,struct file *file);
static int release_add(struct inode *inode,struct file *file);
static ssize_t read_add(
struct file *file,char __user *ubuf,size_t count,loff_t *offt);
static ssize_t write_add(
struct file *file,const char __user *ubuff,size_t count,loff_t *offt);
static struct file_operations fo={
.open=open_add,
.release=release_add,
.read=read_add,
.write=write_add,
.owner=THIS_MODULE,
};
static int open_add(struct inode *inode,struct file *file)
{
printk(MODULE_NAME":opened minor=%d\n",iminor(inode));
return 0;
}
static int release_add(struct inode *inode,struct file *file)
{
printk(MODULE_NAME":closed minor=%d\n",iminor(inode));
return 0;
}
static int __init init_add(void)
{
int err;
int firstminor=0;
total=0;
err=alloc_chrdev_region(&dev,firstminor,DEV_COUNT,MODULE_NAME);
if(err<0){
printk(MODULE_NAME":Failed reg\n");
goto err1;
}
cdev_init(&cdev,&fo);
cdev.owner=THIS_MODULE;
err=cdev_add(&cdev,dev,DEV_COUNT);
if(err<0){
goto err2;
}
printk(MODULE_NAME":reg OK maj=%d\n",MAJOR(dev));
return 0;
err2:
cdev_del(&cdev);
unregister_chrdev_region(dev,DEV_COUNT);
err1:
return err;
}
static void __exit exit_add(void)
{
int mj=MAJOR(dev);
cdev_del(&cdev);
unregister_chrdev_region(dev,DEV_COUNT);
printk(MODULE_NAME":exit MAJ=%d\n",mj);
}
static ssize_t read_add(
struct file *file,char __user *ubuf,size_t count,loff_t *offt)
{
size_t len=0;
char buff[256];
const char *ptr="Good day!";
const char *ptr2=en_silly;
printk(MODULE_NAME":read count=%ld off=%lld\n",count,*offt);
if(*offt!=0){
return 0;
}
if(error){
sprintf(buff,"%s\n",ptr2);
}else if(greeting){
sprintf(buff,"%s\n",ptr);
}else{
sprintf(buff,"%d\n",total);
}
error=greeting=0;
len=strlen(buff);
if(count<len){
len=count;
}
if(raw_copy_to_user(ubuf,buff,len)){
return -EFAULT;
}
*offt+=len;
return (ssize_t)len;
}
static int checkStr(char *ptr)
{
if(*ptr!='-' && *ptr!='+' && !(*ptr>='0' && *ptr<='9') ){
printk(MODULE_NAME":NG1 %c\n",*ptr);
return -1;
}
for(ptr++;*ptr;ptr++){
if( !(*ptr>='0' && *ptr<='9') && *ptr!='\n'){
printk(MODULE_NAME":NG2 %c\n",*ptr);
return -1;
}
}
return 0;
}
static ssize_t write_add(
struct file *file,const char __user *ubuff,size_t count,loff_t *offt)
{
char buff[256];
int d;
printk(MODULE_NAME":write count=%ld off=%lld\n",count,*offt);
if(count >250){
return -EFAULT;
}
if(raw_copy_from_user(buff,ubuff,count)){
return -EFAULT;
}
buff[count]='\0';
printk(MODULE_NAME":input|%s|\n",buff);
*offt+=count;
if(strncmp(buff,"Hello",5)==0){
greeting=1;
printk(MODULE_NAME":said Hello\n");
return count;
}
if(strncmp(buff,"Clear",5)==0){
total=0;
printk(MODULE_NAME":said Clear\n");
return count;
}else if(checkStr(buff)!=0){
error=1;
return count;
}
sscanf(buff,"%d",&d);
printk(MODULE_NAME":input %d\n",d);
if(d!=0){
total+=d;
}else{
error=1;
}
return count;
}
module_init(init_add);
module_exit(exit_add);
insmod,rmmod時に呼び出される関数
init_addとexit_addの2つの関数をinsmod,rmmodコマンド時に呼び出すように,最後の2行module_init(init_add)とmodule_exit(exit_add)でカーネルに通知しています.これらの関数の大きな仕事は,キャラクタデバイスとして振る舞えるようにデバイス構造体を初期化してカーネルに登録することです.この構造体には,file_operations構造体が関係付けられています.このfile_operations構造体には,open,read,write,closeが発行された時に呼び出してほしい関数が指定されています.
システムコール時に呼び出される関数
open/close-> init_add/release_add関数
アプリケーションがopen,closeシステムコールを発行するとそれぞれinit_addかrelease_add関数が呼び出されます.add.cではオープンクローズ時には何もすることはありません.printk関数でsyslogにメッセージを出力しています.Centosでは/var/log/messages,Ubuntuでは/var/log/syslogをtailしてみてください.addモジュールからのメッセージが記録されているはずです.
write->write_add関数
アプリケーションがwriteシステムコールを発行するとwrite_add関数が呼び出されます.パラメータのubuffとcountが書き込まれたデータとバイト数です.ubuffはユーザ空間の領域なので,カーネルモジュールからアクセスするにはカーネル領域にコピーします.それをraw_copy_from_userで実行します.Helloだったら,次回readの時に挨拶を返すgreetingフラグをセットします.Clearだったら内部データのtotalを0にします.数字が書き込まれたのなら数値に変換してtotalに加算します.数値以外が書き込まれたらerrorフラグをセットします.add.cでは書き込まれてくるデータは文字列であるものとしています.しかし一般的にwriteシステムコールに渡されるデータは文字列とは限らずNULL終端されていませんので注意が必要です.
read->read_add関数
アプリケーションがreadシステムコールを発行するとread_add関数が呼び出されます.readの場合重要なことは,catなどの一般的なアプリケーションはファイルの最後まで読み取れるまで,永遠にreadシステムコールを発行し続けることです.カーネルモジュールが適当なところでファイルの最後であることを示してやることことで,アプリケーションはreadループをbreakできます.read_add関数の最後のパラメータであるloff_t型データでアプリケーションがすでに読み込み済みのバイト数を示しています.これが0であれば,初回の読み込みであり,0でなければ2回目以降の読み込みであることがわかります.add.cでは2回目以降のreadシステムコールでは読み出しバイト数を0とすることでファイルの終わりまで到達したことを通知しています.
初回のreadでは,errorフラグがセットされていればお叱りのお言葉を,greetingフラグがセットされていれば挨拶を返します.そしてフラグをクリアしています.フラグがクリア時にはtotalの値を文字列に変換して返します.
writeとは逆に返すデータをカーネル領域からユーザ領域にコピーする必要があり,それをraw_copy_to_userで行います.最後にoffsetに読み込みデータ量を加算しておくことで,次回のread発行時には,アプリケーションはこの続きから読み込もうとするのでファイルの終わりを通知することができます.
##注意
ソースコードを改造して自分のオリジナルな変なファイルを作って遊んでみてください.しかしカーネルモジュールはカーネル空間で実行されるためにNULL番地アクセスなどのエラーを含むバグを発生させると簡単にカーネルパニックを起こします.そのプロセスだけがなくなってcoredumpファイルが残るような安全機能が働かないのです.リモートの実マシンで遊ぶと,最悪の場合には現地まで行って電源を入れ直さなければなりません.カーネルと遊ぶ時にはKVMやVirtualBox上のVMを使いましょう.