従来のI/O操作では、ファイルへの読み書き時にディスク、カーネルバッファ、ユーザーバッファ間でのデータコピーが発生し、カーネルモードとユーザーモードの切り替えも必要でした。これは、特にファイルI/Oが頻繁な場合や大規模ファイルを扱う場合に非効率的でした。
mmapシステムコールは、ファイルのデータをプロセスの仮想アドレス空間に直接マッピングすることで、この問題を解決します。これにより、ユーザー空間から直接ファイルデータを操作でき、カーネルバッファを介したデータコピーが不要になります。
mmapとmunmapシステムコール
mmapとmunmapは、ファイルメモリマッピングを実装するためのシステムコールです。以下にそれぞれの関数シグネチャを示します。
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void* addr, int length);
mmapの引数については、以下のように扱います。
addr: 常に0とし、カーネルがマッピングする仮想アドレスを決定します。成功時にはマッピングされたアドレス、失敗時には0xffffffffffffffffを返します。length: マッピングするバイト数。prot: メモリ保護フラグ (PROT_READ,PROT_WRITE, またはその両方)。flags: マッピングの種類。MAP_SHARED(変更がファイルに書き戻される) またはMAP_PRIVATE(変更はファイルに反映されない)。fd: マッピングするファイルのファイルディスクリプタ。offset: ファイルの先頭からのオフセット。常に0とします。
munmapは、指定されたアドレス範囲のマッピングを解除します。MAP_SHAREDでマッピングされたメモリが変更されている場合、変更はファイルに書き戻されます。マッピング解除は、領域全体、先頭部分、または末尾部分のいずれかで行われると仮定します。
システムコール登録
mmapとmunmapをシステムコールとして利用可能にするために、以下の修正が必要です。
user/user.h:
//System calls
void* mmap(void*, int, int, int, int, int);
int munmap(void*, int);
user/usys.pl:
entry("mmap");
entry("munmap");
Makefile:
$U/_mmaptest\
kernel/syscall.h:
#define SYS_mmap 22
#define SYS_munmap 23
kernel/syscall.c:
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
関連データ構造
プロセスのメモリマッピング領域を追跡するために、vma (Virtual Memory Area) 構造体を定義します。
kernel/proc.h:
struct vma {
uint64 start; // 仮想メモリ領域の開始アドレス
uint64 sz; // 仮想メモリ領域のサイズ
int prot; // メモリ保護フラグ (読み取り、書き込み)
int flags; // マッピングフラグ (MAP_SHARED, MAP_PRIVATE)
struct file* file; // マッピング対象のファイル
uint64 offset; // ファイル内のオフセット
int valid; // このVMAが有効かどうかを示すフラグ
};
#define NVMA 16 // 1プロセスあたりのVMA数の最大値
struct proc {
// ... 他のメンバー ...
struct vma vmas[NVMA];
};
mmapの実装
sys_mmap関数は、引数を受け取り、プロセスの仮想アドレス空間に新しいメモリマッピング領域を作成します。
kernel/sysfile.c:
uint64
sys_mmap(void)
{
uint64 addr;
int length, prot, flags, fd, offset;
struct file* file;
// 引数の取得
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0 || argint(2, &prot) < 0 ||
argint(3, &flags) < 0 || argfd(4, &fd, &file) < 0 || argint(5, &offset) < 0)
return 0xffffffffffffffff; // エラー
// 引数の検証
if (length <= 0)
return 0xffffffffffffffff;
if ((!file->readable && (prot & PROT_READ)) ||
(!file->writable && (prot & PROT_WRITE) && (flags == MAP_SHARED)))
return 0xffffffffffffffff;
struct proc* p = myproc();
struct vma* v = 0;
// ページ境界にアラインメント
length = PGROUNDUP(length);
// プロセスのVMA配列から空きエントリを探す
for (int i = 0; i < NVMA; ++i) {
if (p->vmas[i].valid == 0) {
v = &p->vmas[i];
v->valid = 1;
// マッピング領域の設定
v->start = p->sz; // プロセスの現在のサイズを基点とする
p->sz += length;
v->sz = length;
v->prot = prot;
v->flags = flags;
v->file = file;
v->offset = offset;
// ファイルの参照カウントを増やす
filedup(file);
return v->start; // マッピングされた仮想アドレスを返す
}
}
// 空きVMAがない場合
printf("No free VMA available\n");
return 0xffffffffffffffff; // エラー
}
遅延割り当て (Lazy Allocation)
mmapは遅延割り当てを採用しています。物理メモリの割り当てと実際のデータ読み込みは、ページフォールトが発生した際に行われます。ユーザーモードのトラップハンドラ内で、ページフォールトを処理し、必要な物理ページを割り当て、ファイルからデータを読み込み、仮想アドレス空間にマッピングします。
kernel/sysfile.c:
int vamlazyalloc(uint64 va){
struct proc* p = myproc();
struct vma* v = 0;
// 仮想アドレスに対応するVMAを検索
for(int i = 0; i < NVMA; ++i){
v = &p->vmas[i];
if( v->valid == 1 && v->start <= va && va < v->start + v->sz){
break; // 対応するVMAを見つけた
}
if( i == NVMA - 1){
printf("vamlazyalloc: No VMA found for VA %p\n", (void*)va);
return -1; // VMAが見つからない
}
}
// 物理ページの割り当て
void* pa = kalloc();
if( pa == 0)
panic("vamlazyalloc: kalloc failed");
memset(pa, 0, PGSIZE);
// ファイルからデータを読み込む
begin_op();
ilock(v->file->ip);
// ページオフセットを計算して読み込み
uint64 file_offset = v->offset + (va - v->start);
readi(v->file->ip, (uint64)pa, file_offset, PGSIZE);
iunlock(v->file->ip);
end_op();
// ページを仮想アドレス空間にマッピング
if(mappages(p->pagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_U) < 0)
panic("vamlazyalloc: mappages failed");
return 0; // 成功
}
kernel/trap.c (ページフォールトハンドラ内):
// ... 他のハンドラ ...
else if (r_scause() == 13 || r_scause() == 15) { // Page fault
uint64 va = r_stval();
// プロセスのサイズ範囲内かチェック
if (va >= p->sz || va > MAXVA) {
p->killed = 1; // 無効なメモリアクセス
continue;
}
// 遅延割り当てを試みる
if (vamlazyalloc(va) == -1) {
p->killed = 1; // 割り当て失敗
}
}
// ...
また、uvmunmapやuvmcopy内のpanicの一部をコメントアウトし、エラー発生時の挙動を緩和します。
kernel/vm.c:
void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) {
// ...
for (a = va; a < va + npages * PGSIZE; a += PGSIZE) {
pte_t *pte;
if ((pte = walk(pagetable, a, 0)) == 0)
// panic("uvmunmap: walk"); // コメントアウト
continue;
if ((*pte & PTE_V) == 0)
// panic("uvmunmap: not mapped"); // コメントアウト
continue;
// ...
}
}
kernel/uvm.c:
// ... in uvmcopy ...
if ((*pte & PTE_V) == 0)
// panic("uvmcopy: page not present"); // コメントアウト
continue;
// ...
munmapの実装
sys_munmap関数は、指定された仮想アドレス範囲のマッピングを解除します。
kernel/sysfile.c:
uint64
sys_munmap(void)
{
uint64 addr;
int length;
struct proc* p = myproc();
struct vma* v = 0;
// 引数の取得
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0)
return -1; // エラー
if (length <= 0)
return -1; // 無効な長さ
// プロセスの全メモリサイズを超えないかチェック (簡易的なチェック)
if (p->sz + length > MAXVA)
return -1;
// 仮想アドレスに対応するVMAを検索
for (int i = 0; i < NVMA; ++i) {
v = &p->vmas[i];
if (v->valid == 1 && v->start <= addr && addr < v->start + v->sz) {
break; // 対応するVMAを見つけた
}
if (i == NVMA - 1) {
panic("err_munmap_va2vma: Cannot find VMA for address");
}
}
// ページ境界にアラインメント
addr = PGROUNDDOWN(addr);
length = PGROUNDUP(length);
// MAP_SHARED の場合、変更をファイルに書き戻す
if (v->flags & MAP_SHARED) {
// addrからlengthバイトをファイルに書き込む
// filewrite(v->file, addr, length); // 実際にはより正確なオフセットとサイズ指定が必要
// この実装では、単純化のためaddrとlengthを使用
// 正確には v->offset + (addr - v->start) から length バイト
begin_op();
ilock(v->file->ip);
// ここでファイルへの書き込み処理を行う (具体的には writei を呼び出す)
// 例: writei(v->file->ip, (char*)addr, v->offset + (addr - v->start), length);
iunlock(v->file->ip);
end_op();
}
// 仮想メモリのマッピングを解除
uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
// VMAの更新または削除
if (addr == v->start && length == v->sz) {
// マッピング全体を解除
fileclose(v->file); // ファイル参照カウントを減らす
v->valid = 0; // VMAを無効化
} else if (addr == v->start) {
// 先頭部分を解除
v->start += length;
v->sz -= length;
v->offset += length;
} else if (addr + length == v->start + v->sz) {
// 末尾部分を解除
v->sz -= length;
} else {
panic("err_sysmunmap: Invalid unmap region"); // 中間部分の解除はサポートしない
}
return 0; // 成功
}
forkとexit
forkシステムコールでは、子プロセスに親プロセスのvma配列をコピーし、関連するファイルディスクリプタの参照カウントを増やす必要があります。
kernel/proc.c (fork関数内):
// ... in fork() ...
// VMA配列のコピー
for(int i = 0; i < NVMA; i++) {
if(p->vmas[i].valid) {
np->vmas[i] = p->vmas[i];
if(p->vmas[i].file)
filedup(p->vmas[i].file); // ファイル参照カウントを増やす
}
}
np->state = RUNNABLE;
// ...
プロセスがexitする際には、すべてのメモリマッピングを解除し、MAP_SHAREDでマッピングされた変更をファイルに書き戻す必要があります。
kernel/proc.c (exit関数内):
// ... in exit() ...
// VMAのクリーンアップ
for(int i = 0; i < NVMA; ++i){
if( p->vmas[i].valid ){ // 有効なVMAのみ処理
// MAP_SHARED の場合、変更をファイルに書き戻す
if( p->vmas[i].flags & MAP_SHARED){
// p->vmas[i].start から p->vmas[i].sz バイトをファイルに書き込む
// filewrite(p->vmas[i].file, p->vmas[i].start, p->vmas[i].sz); // 実際には writei を使用
begin_op();
ilock(p->vmas[i].file->ip);
// writei(p->vmas[i].file->ip, (char*)p->vmas[i].start, p->vmas[i].offset, p->vmas[i].sz);
iunlock(p->vmas[i].file->ip);
end_op();
}
// マッピングを解除
uvmunmap(p->pagetable, p->vmas[i].start, p->vmas[i].sz / PGSIZE, 1);
p->vmas[i].valid = 0; // VMAを無効化
}
}
// ... 他のクリーンアップ ...
// ...
テスト結果
mmaptestおよびusertestsを実行した結果、すべてのテストに合格しました。
$ mmaptest
mmap_test starting
test mmap f: OK
test mmap private: OK
test mmap read-only: OK
test mmap read/write: OK
test mmap dirty: OK
test not-mapped unmap: OK
test mmap two files: OK
$ usertests
...
test forktest: OK
test bigdir: OK
ALL TESTS PASSED