mmapを用いたデバイスドライバ
カーネル空間のあるアドレス上のデータをユーザ空間から読みたい場合、
/dev/memデバイスから物理アドレスを指定して読む方法があります。
また、以下に説明するように新たにデバイスを作成して特定のカーネル
空間内のデータをmmapを用いて読む方法もあります。この方法の良い点は
あらかじめ分かっているメモリ領域(例えばDMAバッファのようなデータ領域や
ステータス情報を格納している領域)から単にポインタを使って、
メモリを読む方法で読み出すことが可能になることです。
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <asm/uaccess.h>
#include <linux/malloc.h>
#include <linux/vmalloc.h>
#include <asm/io.h>
#include <linux/wrapper.h>
#include <asm/page.h>
#define printf printk
static unsigned long mp;
static int map_size;
static int simple_open (struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return 0;
}
static int simple_release (struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return 0;
}
static int simple_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
struct page *map, *mapend;
printf("vm_start = %x\n", (int)vma->vm_start);
printf("vm_end = %x\n", (int)vma->vm_end);
printf("vm_pgoff = %x\n", (int)vma->vm_pgoff);
printf("vm_page_prot.pgprot = %x\n", (int)vma->vm_page_prot.pgprot);
printf("PAGE_OFFSET = %x\n", (int)PAGE_OFFSET);
printf("offset = %x\n", (int)offset);
printf("physical address of mp = %x\n", (int)__pa(mp));
// This code needs to set PG_reserved bit in the pages.
map = virt_to_page(mp);
// 1. get index to last page in mem_map array for rawbuf.
mapend = virt_to_page(mp+map_size-1);
// 2. mark each physical page in range as 'reserved'.
for (map = virt_to_page(mp); map <= mapend; map++)
mem_map_reserve(map);
printf("map = virt_to_page(mp) = %x\n", (int)map);
printf("mapend = virt_to_page(mp+map_size-1) = %x\n", (int)mapend);
if(remap_page_range(vma->vm_start, __pa(mp),
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
// vma->vm_file = file;
return 0;
}
static struct file_operations simple_fops =
{
mmap: simple_mmap,
open: simple_open,
release: simple_release,
};
int init_module (void)
{
int i;
map_size = 0x1000;
i = register_chrdev (33, "simple", & simple_fops);
if (i != 0) return - EIO;
mp = (unsigned long)kmalloc(map_size,GFP_KERNEL);
printf("mp = %x\n", (int)mp);
*(unsigned long *)mp = 0xbbbbbbbb;
return 0;
}
void cleanup_module (void)
{
kfree((const void *)mp);
unregister_chrdev (33, "simple");
}
まずはMakefileを示します。
INCDIR = -I/usr/src/linux/include
VERSIONINC = -include /usr/src/linux/include/linux/modversions.h
CFLAGS = -c -D__KERNEL__ -DMODULE -Wall $(INCDIR) $(VERSIONINC)
DRIVER = simple
TEST = test_simple
all: $(DRIVER).o $(TEST)
$(DRIVER).o: $(DRIVER).c
gcc $(CFLAGS) $(DRIVER).c
device:
mknod -m 666 /dev/$(DRIVER) c 33 0
$(TEST): $(TEST).c
gcc -o $(TEST) $(TEST).c
clean:
rm -f *.o *~ core $(TEST)
上記のように作成されたドライバをインストールする方法は下記の通りです。
% make simple
% su
Password:
# insmod simple.o
# make device
# exit
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
main() {
int fd;
int data, rdata;
char *mp;
int start, prot, flags;
size_t length;
off_t offset;
int i;
fd = open("/dev/simple", O_RDWR);
if( fd == -1) {
printf("open error...\n");
exit(0);
}
length = 0x1000;
prot = PROT_READ | PROT_WRITE;
// flags = MAP_SHARED;
flags = MAP_PRIVATE;
printf("length = %x\n", length);
printf("prot = %x\n", prot);
mp = mmap((void *)0, length, prot, flags, fd, 0);
if( mp == (char *)-1 ){
printf("mmap error...\n");
close(fd);
exit(1);
}
printf("mapped address = %x\n", mp);
data = 0xbbbbbbbb;
printf("data = %x\n", data);
printf("accessed address = %x\n", (int *)mp);
rdata = *(int *)mp;
printf("writen data = %x, read data = %x\n", data, rdata);
munmap((void *)mp, length);
close(fd);
}
上記のコードをコンパイルして実行するとつぎのようになります。
% make test_simple
% ./test_simple
length = 1000
prot = 3
mapped address = 40016000
data = bbbbbbbb
accessed address = 40016000
writen data = bbbbbbbb, read data = bbbbbbbb
カーネル空間上のメモリにマップした場合とI/Oデバイスのメモリ空間に
マップした場合ではその実装方法にあまり違いはありません。
違いは、カーネル空間上のメモリにマップした場合は下記のように
PG_reservedビットをセットする手順が必要です(すでにセットされて
いればこの手順は必要ありません)。
// This code needs to set PG_reserved bit in the pages.
map = virt_to_page(mp);
// 1. get index to last page in mem_map array for rawbuf.
mapend = virt_to_page(mp+map_size-1);
// 2. mark each physical page in range as 'reserved'.
for (map = virt_to_page(mp); map <= mapend; map++)
mem_map_reserve(map);
I/Oデバイスのメモリ空間にマップした場合はそのビットをセット
しなくてもカーネルはマップの対象とします。言い替えれば
カーネルはPG_reservedビットがセットされている通常のメモリと
I/Oデバイスのメモリ空間上のメモリをマップ可能としています。
下記の例はBit3社(現在はSBS社)のPCI-VMEアダプタ(モデル616/617)の
mmapメソッドの実装の一部です。
1つはVMEアダプタにマップすべきVME空間(アドレスと
その大きさ)を与え、そのマッピングレジスタにセットすることが求められます。
map_windows(vme_address, size, dev_prop->mmap_window_index, dev_prop->mapping_flags);
2つ目はそのVMEアドレスの対応するLinux上の物理アドレスを計算すること
です。
physical_address = (
bit3.physical_window_region_base +
dev_prop->mmap_window_index * bit3_WINDOW_SIZE +
(vme_address & bit3_PAGE_OFFSET_MASK)
);
以上の設定を行なうとあとはカーネル空間上のメモリにマップした場合
の例で示されているように、remap_page_rangeカーネル関数を
呼びます。これでおしまいです。
if (remap_page_range(vma->vm_start,
physical_address, size, vma->vm_page_prot) < 0) {
return -EAGAIN;
}
下記の例題はKEK製VME版インターラプトモジュールを使った
mmapシステムコールによるVMEアクセスを示しています。
mmapシステムコールは次のように呼ばれます。
void * mmap(void *start, size_t length, int prot , int
flags, int fd, off_t offset);
startは通常NULLにします。これは新たに仮想アドレスを得るように
するためです。length, prot, flags, fdはマニュアルを読めば
わかりますが、offsetの使い方はドライバの実装方法に依存して
いますので、ドライバの実装如何でセットの仕方が変わって
きます。1つの方法はoffsetにVMEのアドレスを入れる方法です。
これは一般的な方法で、下記の例はその実装方法を前提にした
ものです。この場合、注意すべき点はoffsetに入れる情報は
ページバウンダリーになっている必要がある点です。
x86の1ページは4KBです。ですからVMEアドレスの下位12ビットは
マスクして(ビットオフ)してoffsetに与える必要があります。
ですから、mmapシステムコールを呼び出して、仮想アドレスを
得たら、そのビットオフした分だけ仮想アドレスを増やしてやらないと
与えたVMEアドレスに対応する仮想アドレスは得られません。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include "vmedrv.h"
#define DEV_FILE "/dev/vmedrv16d16"
#define INTREG_ADDR 0x8F00
#define INTREG_SIZE 0x10
struct intreg {
unsigned short latch[2];
unsigned short flipflop;
unsigned short in;
unsigned short pulse;
unsigned short out;
unsigned short csr[2];
};
static struct intreg *mm;
int main(int argc, char **argv) {
int fd;
unsigned int vme_addr;
if ((fd = open(DEV_FILE, O_RDWR)) < 0) {
printf("open error...\n");
exit(1);
}
vme_addr = INTREG_ADDR;
printf("vme address = %x\n", vme_addr);
vme_addr = INTREG_ADDR & 0xF000;
printf("page-aligned vme address = %x\n", vme_addr);
mm = (struct intreg *)mmap(0, INTREG_SIZE,
PROT_READ|PROT_WRITE, MAP_SHARED, fd, vme_addr);
if (mm == (struct intreg *)-1 ) {
printf("mmap failed...\n");
exit(1);
}
printf("page-aligned virtual address for the vme device = %x\n", (int)mm);
mm = (struct intreg *)((char *)mm + (INTREG_ADDR & 0xFFF));
printf("virtual address for the vme device = %x\n", (int)mm);
mm->csr[0] = 0x5b;
mm->csr[1] = 0x5b;
mm->pulse = 0x0F0;
/* display contents of registers... */
printf("Latch 0 = %4x\n",mm->latch[0] & 0xFF);
printf("Latch 1 = %4X\n",mm->latch[1] & 0xFF);
printf("in = %4X\n",mm->in & 0xFF);
printf("csr 0 = %4X\n",mm->csr[0] & 0xFF);
printf("csr 1 = %4X\n",mm->csr[1] & 0xFF);
munmap(mm, INTREG_SIZE);
close(fd);
return 0;
}