Memory Mapped File
Posted on
May 31, 2018
内存映射文件( memory mapped file
)与磁盘上的文件存在直接的对应关系。内存映射 I/O
将磁盘上的文件映射到用户进程地址空间中,这样,当我们从映射内存中获取字节时,会读取文件的相应字节。同样的,当我们将数据存储在映射内存中时,相应的字节会自动写入文件中。这样以来就可以在不使用 read()
或 write()
系统调用的情况下执行 I/O
操作 。
mmap & munmap mmap
系统调用将文件或设备映射到内存中,当调用成功时,它会返回映射内存的起始地址。第一个参数 addr
表示文件要映射到内存中的虚拟地址,一般都为 NULL
,表示由内核决定合适的映射地址。第二个参数 len
指定映射的大小(以字节为单位),通常情况下,内核创建的映射内存的大小是内存页面大小的整数倍。第三个参数 prot
指定访问权限,可以是 PROT_READ
,PROT_WRITE
,PROT_EXEC
。第四个参数 flags
可以是 MAP_PRIVATE
,MAP_SHARED
。第五个参数 fd
标识映射文件的文件描述符。第六个参数 offset
指定了文件映射的起点。为了映射整个文件,我们将 offset
指定为0,将 len
指定为整个文件的大小。
1 2 3 4 5 6 7 #include <sys/mman.h> void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t len);
Golang中系统调用参数有些许不同,但本质是一样的。
1 2 3 4 5 package syscallfunc Mmap (fd int , offset int64 , length int , prot int , flags int ) (data []byte , err error) func Munmap (b []byte ) (err error)
GPIO 当然了,如果光介绍 mmap
这个系统调用就太无聊了,我们下面来看一下 mmap
能帮助我们干什么事情,如果你玩树莓派的话,我们可以用 mmap
来写一个 GPIO
驱动。
在树莓派的 /dev
目录下存在 mem
和 gpiomem
这两个文件。通过 mmap
/dev/gpiomem
文件我们可以在没有 root
权限的情况下访问 GPIO
寄存器。打开 /dev/gpiomem
设备文件并使用 mmap()
函数可将 GPIO
寄存器映射到进程内存空间中去。/dev/mem
代表整个系统的内存空间。/dev/gpiomem
仅允许访问 GPIO
外设寄存器, /dev/mem
允许访问所有外设寄存器以及所有内存,相对来说更加危险。为了保护内存空间,最好使用 /dev/gpiomem
而非 /dev/mem
来控制 GPIO
寄存器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Rev 2 and 3 Raspberry Pi +-----+---------+----------+---------+-----+ | BCM | Name | Physical | Name | BCM | +-----+---------+----++----+---------+-----+ | | 3.3v | 1 || 2 | 5v | | | 2 | SDA 1 | 3 || 4 | 5v | | | 3 | SCL 1 | 5 || 6 | 0v | | | 4 | GPIO 7 | 7 || 8 | TxD | 14 | | | 0v | 9 || 10 | RxD | 15 | | 17 | GPIO 0 | 11 || 12 | GPIO 1 | 18 | | 27 | GPIO 2 | 13 || 14 | 0v | | | 22 | GPIO 3 | 15 || 16 | GPIO 4 | 23 | | | 3.3v | 17 || 18 | GPIO 5 | 24 | | 10 | MOSI | 19 || 20 | 0v | | | 9 | MISO | 21 || 22 | GPIO 6 | 25 | | 11 | SCLK | 23 || 24 | CE0 | 8 | | | 0v | 25 || 26 | CE1 | 7 | | 0 | SDA 0 | 27 || 28 | SCL 0 | 1 | | 5 | GPIO 21 | 29 || 30 | 0v | | | 6 | GPIO 22 | 31 || 32 | GPIO 26 | 12 | | 13 | GPIO 23 | 33 || 34 | 0v | | | 19 | GPIO 24 | 35 || 36 | GPIO 27 | 16 | | 26 | GPIO 25 | 37 || 38 | GPIO 28 | 20 | | | 0v | 39 || 40 | GPIO 29 | 21 | +-----+---------+----++----+---------+-----+
如果通过 /dev/mem
访问 GPIO
外设寄存器,那么我们需要确定其内存中的基地址,我们通过读取 /proc/device-tree/soc/ranges
来确定基地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const ( bcm2835Base = 0x20000000 gpioOffset = 0x200000 memLength = 4096 ) var gpioBase int64 var ( memLock sync.Mutex gpioMem []byte ) func init () { base := getBase() gpioBase = base + gpioOffset } func getBase () (base int64 ) { base = bcm2835Base ranges, err := os.Open("/proc/device-tree/soc/ranges" ) defer ranges.Close() if err != nil { return } b := make ([]byte , 4 ) n, err := ranges.ReadAt(b, 4 ) if n != 4 || err != nil { return } buf := bytes.NewReader(b) var out uint32 err = binary.Read(buf, binary.BigEndian, &out) if err != nil { return } return int64 (out) }
Open
函数通过映射 /dev/mem
文件来将 GPIO
寄存器映射到内存中,这样以来我们就可以通过直接改变 gpioMem
的值来操控 GPIO
寄存器了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func Open () error { file, err := os.OpenFile("/dev/mem" , os.O_RDWR|os.O_SYNC, 0 ) if os.IsPermission(err) { file, err = os.OpenFile("/dev/gpiomem" , os.O_RDWR|os.O_SYNC, 0 ) } defer file.Close() memLock.Lock() defer memLock.Unlock() gpioMem, err = syscall.Mmap( int (file.Fd()), gpioBase, memLength, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, ) if err != nil { return } }
Close
函数使用 syscall.Munmap
系统调用来解除内存映射。
1 2 3 4 5 6 7 8 func Close () error { memLock.Lock() defer memLock.Unlock() if err := syscall.Munmap(mem); err != nil { return err } return nil }