カーネル空間
読み:カーネルくうかん
外語:kernel space
オペレーティングシステム(OS)のカーネルが稼働しているアドレス空間のこと。
概要
オペレーティングシステムにも様々なものがあるが、一般的なものは二つの「層」に分けることができる。
そのうち下位の層にあり、よりハードウェアに近い処理、機能を担うのがカーネルで、このカーネルが動作する層をカーネル空間と呼ぶ。対するはユーザー空間である。
特徴
空間の分離
アドレス空間をどのように分けて扱うかは、OSごとに様々である。パーソナルコンピューター用OSと、組み込み機器用OSとでは、CPUの違いなどもあり、かなり構造が異なる。
組み込み用の例
組み込み用の例として、SuperH用のNetBSDの実装を説明する。
SuperHでは、アドレス空間4Giバイトを二分割し、上位2Giバイトをユーザー空間、下位2Giバイトをカーネル空間とする。
- 0x00000000〜0x7fffffff ‐ ユーザー空間(P0/U0領域)
- 0x80000000〜0xffffffff ‐ カーネル空間
SuperHの場合、0x00000000〜0x1fffffffの512Miバイト程度をよく用い、この中に様々なものが割り付けられる。この空間は、MMUの有無、キャッシュの有無等の各条件で、0x80000000〜0xdfffffffの範囲にもマッピングされている。
- 0x80000000〜0x9fffffff ‐ P1領域 ‐ MMU無し、キャッシュあり
- 0xa0000000〜0xbfffffff ‐ P2領域 ‐ MMU無し、キャッシュ無し
- 0xc0000000〜0xdfffffff ‐ P3領域 ‐ MMUあり、キャッシュあり
NetBSDの実装はこの特徴を用いており、下位の領域のうちストアキュー領域以外は特権モード以外では利用できないように設定し、ここをカーネル空間としている。
P3領域であれば、通常のP0/U0領域からのアクセスと何ら変わらない環境となり、またフラッシュメモリーの書き換え処理のようにキャッシュがあっては邪魔な場合はP2領域にアクセスすれば良いことになる。
なお、物理的なメモリーはユーザー領域、カーネル領域とも同じものであるので、この管理もCPUとOSがすることになる。
i386の例
パーソナルコンピューター用のLinux/i386やFreeBSD/i386の場合、仮想アドレス空間4Giバイトを二分割し、次のようにしている。
- 0x00000000〜0xbfffffff ‐ ユーザー空間 (3Giバイト)
- 0xc0000000〜0xffffffff ‐ カーネル空間 (1Giバイト)
実際の物理メモリーはこれよりも少ないことから、OSが、仮想アドレスを適当な物理アドレスへとマッピングすることになる。従って、仮想空間上は有効なページであっても、そのページに物理メモリーが割り当てられているとは限らない。
ディスクドライブのファイルブロックやSWAPを指していたり、あるいは実際にアクセスされたときに0クリアされたメモリーを割り当てる指定になっていることもある。
プログラム
カーネル空間のプログラミングは、ユーザー空間とは異なっている。標準関数の一切は、基本的に利用できず、別途カーネル用の関数が用意される。
例えば、printfが使えないカーネルは多い。mallocも、一応使えるが引数などが全く違っている。
カーネル空間用のプログラムを書くときには、その作法を知らねばならない。
相互のやりとり
通知
ユーザー空間→カーネル空間
ユーザー空間からカーネル空間に通知をする場合、次のような方法がよく使われる。
- /dev以下にデバイスを作り、ioctlしてもらう (古典的な方法)
- sysfsでデバイスを作り、read/writeしてもらう (近代的なLinuxで一般的な方法)
カーネル空間→ユーザー空間
シグナル
カーネル空間からユーザー空間への通知は、OSごとに異なり、またOSのカーネルバージョンごとに異なる可能性もあるが、何らかのシステムコールは存在する。
Linuxの場合、カーネルからユーザー空間のプロセスに対して任意のシグナルを送ることができる。プロセスを識別するためにユーザー空間でのPID(プロセスID)を用いるため、送信に際して予め宛先のPIDを知る必要があり、何らかの方法で情報を伝える手段を用意する必要がある。
一般的には/dev以下、あるいはsysfsやdebugfsなど手頃な場所にデバイスを用意し、ユーザー空間からPIDをioctlやwriteしてカーネルに伝える手法がよく取られている。
カーネル側実装例
以下は実際の送信処理のみを記載するが、このほかに、デバイスを作り、PIDをユーザーランドから得る処理が必要である。
#include <asm/siginfo.h> // siginfo
#include <linux/rcupdate.h> // rcu_read_lock
#include <linux/sched.h> // find_task_by_pid_type
#define SIG_NO (SIGRTMIN+10) // 他と衝突しないものを使う(SIGRTMAXを超えないこと)
static pid_t userland_pid;
static int sendsig_sample(void)
{
struct siginfo si;
struct task_struct *t;
memset(&si, 0, sizeof(struct siginfo));
si.si_signo = SIG_NO;
si.si_code = SI_QUEUE;
si.si_int = 0; /* 任意の32ビット値を一つだけ通知可能 */
rcu_read_lock();
t = find_task_by_vpid(userland_pid); /* 送信先のPIDが格納されているものとする */
rcu_read_unlock();
if (t == NULL)
return -ENODEV;
return send_sig_info(SIG_NO, &si, t);
}
より新しいLinuxカーネルでは、find_task_by_vpid ではなく find_task_by_pid_type など違う関数になっている可能性がある。
ユーザーランド側実装例
ユーザーランドでは、シグナルハンドラーを定義するだけである。シグナル番号はカーネルと一致させる。
#define SIG_NO (SIGRTMIN+10) // カーネルと同じものを使う
struct sigaction sa;
sa.sa_sigaction = signal_handler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIG_NO, &sa, NULL);
シグナルハンドラーは以下の形式である。
void signal_handler(int n, siginfo_t *si, void *dummy)
{
/* ここに処理を追加 */
}
このほかに、デバイスに対してPIDをioctlまたはwriteするための処理が別途必要になる。
メモリーアクセス
手法
ユーザー空間から、カーネル空間に直接アクセスする術は無いのが一般的である。カーネル空間にアクセスすれば、通常はアドレスエラーが発生する。
そこで、カーネル空間にデバイスドライバー等を用意し、このデバイスを経由してカーネル空間にアクセスすることになる。
カーネル空間から、ユーザー空間に直接アクセスできるかどうかは実装による。カーネルそのものであれば特権があるためアクセスは自在であろうが、カーネル空間で動くデバイスドライバーとなると、若干権限が落とされており、出来ないことが多いようである。
そこで、ユーザー空間の領域にアクセスするための関数が用意される。この仕様はOSごとに様々であり、互換性はない。
NetBSD
NetBSDでは、copy関数群、fetch関数群、store関数群という関数が用意されている。これらは、FreeBSDでも同様と思われる。
fetch/storeは、1バイト〜1ワードまでの単位でのアクセス手段を提供し、copy関数群はデータ列、文字列単位でのアクセス手段を提供する。
copy関数群
ユーザー空間とカーネル空間をまたぐ、memcpyやstrncpy相当の関数群である。
#include <sys/systm.h>
int copyin(const void *uaddr, void *kaddr, size_t len);
int copyout(const void *kaddr, void *uaddr, size_t len);
int copyinstr(const void *uaddr, void *kaddr, size_t len, size_t *done);
int copystr(const void *kfaddr, void *kdaddr, size_t len, size_t *done);
具体的な機能は次の通り。
- copyin
- ユーザー空間→カーネル空間。アドレスuaddrからアドレスkaddrに、lenバイトのデータを複写する
- copyout
- カーネル空間→ユーザー空間。アドレスkaddrからアドレスuaddrに、lenバイトのデータを複写する
- copyinstr
- ユーザー空間→カーネル空間。アドレスuaddrからアドレスkaddrに、NUL文字で終端された最大len文字の文字列を複写する。実際に複写された文字数は、*doneに返される。
- copystr
- カーネル空間→ユーザー空間。アドレスkaddrからアドレスuaddrに、NUL文字で終端された最大len文字の文字列を複写する。実際に複写された文字数は、*doneに返される。
fetch関数群
ユーザー空間からカーネル空間にデータを取り込む関数群である。
#include <sys/types.h>
#include <sys/time.h>
#include <sys/systm.h>
#include <sys/resourcevar.h>
int fubyte(const void *base);
int fusword(void *base);
int fuswintr(void *base);
int fuword(const void *base);
具体的な機能は次の通り。
- fubyte
- ユーザー空間のアドレスbaseから、1バイトのデータを取り込む。
- fusword
- ユーザー空間のアドレスbaseから、1ショートワードのデータを取り込む。
- fuswintr
- ユーザー空間のアドレスbaseから、1ショートワードのデータを取り込む。割り込みコンテクスト中でも利用可能。
- fuword
- ユーザー空間のアドレスbaseから、1ワードのデータを取り込む。
store関数群
カーネル空間からユーザー空間にデータを格納する関数群である。
#include <sys/time.h>
#include <sys/systm.h>
#include <sys/resourcevar.h>
int subyte(void *base, int byte);
int susword(void *base, int word);
int suswintr(void *base, int word);
int suword(void *base, int word);
具体的な機能は次の通り。
- subyte
- ユーザー空間のアドレスbaseに、1バイトのデータを格納する。
- susword
- ユーザー空間のアドレスbaseに、1ショートワードのデータを格納する。
- suswintr
- ユーザー空間のアドレスbaseに、1ショートワードのデータを格納する。割り込みコンテクスト中でも利用可能。
- fuword
- ユーザー空間のアドレスbaseに、1ワードのデータを格納する。
Linux
Linuxでは、単純なアクセス関数、ブロック関数、文字列関数などが用意されている。
Linux 2.1.x以降で概ね共通。Linux 2.0.xでは仕様が異なる。現在ではLinux 2.6、ときにLinux 2.4程度まで古いものに対応すれば充分なので、古いものは無視しても問題にならないだろう。
整数値アクセス
ユーザー空間とカーネル空間の間で整数値(1、2、4バイト)をfetch/storeする機能である。
#include <asm/uaccess.h>
int get_user(var, ptr);
int put_user(var, ptr);
おそらくマクロとして定義されており、sizeof(*(ptr))の大きさ(1、2、4)に合わせて動作することになる。get時は結果はvarに得られる。put時はvarが書き込まれる。
ブロックアクセス
ユーザー空間とカーネル空間をまたぐ、memcpy相当の関数群である。
#include <asm/uaccess.h>
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
long strncpy_from_user(char *dst, const char __user *src, long count);
具体的な機能は次の通り。
- copy_from_user
- ユーザー空間→カーネル空間。アドレスfromからアドレスtoに、nバイトのデータを複写する
- copy_to_user
- カーネル空間→ユーザー空間。アドレスfromからアドレスtoに、nバイトのデータを複写する
- strncpy_from_user
- ユーザー空間→カーネル空間。アドレスsrcからアドレスdstに、NUL文字で終端された最大n文字の文字列を複写する。成功時はコピー先のポインター、失敗時は-EFAULTが返る(EFAULTが14なら、-14が返ることになる)。
再検索