mmapを用いたデバイスドライバ

カーネル空間上のメモリにマップした場合

カーネル空間のあるアドレス上のデータをユーザ空間から読みたい場合、 /dev/memデバイスから物理アドレスを指定して読む方法があります。 また、以下に説明するように新たにデバイスを作成して特定のカーネル 空間内のデータをmmapを用いて読む方法もあります。この方法の良い点は あらかじめ分かっているメモリ領域(例えばDMAバッファのようなデータ領域や ステータス情報を格納している領域)から単にポインタを使って、 メモリを読む方法で読み出すことが可能になることです。

ドライバのプログラムコード(simple)

#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デバイスのメモリ空間にマップした場合

カーネル空間上のメモリにマップした場合と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デバイスのメモリ空間上のメモリをマップ可能としています。

ドライバのプログラムコード(devmem)

下記の例は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;
}