Memory Mapped File

内存映射文件( memory mapped file )与磁盘上的文件存在直接的对应关系。内存映射 I/O 将磁盘上的文件映射到用户进程地址空间中,这样,当我们从映射内存中获取字节时,会读取文件的相应字节。同样的,当我们将数据存储在映射内存中时,相应的字节会自动写入文件中。这样以来就可以在不使用 read()write() 系统调用的情况下执行 I/O 操作 。

mmap

mmap & munmap

mmap 系统调用将文件或设备映射到内存中,当调用成功时,它会返回映射内存的起始地址。第一个参数 addr 表示文件要映射到内存中的虚拟地址,一般都为 NULL ,表示由内核决定合适的映射地址。第二个参数 len 指定映射的大小(以字节为单位),通常情况下,内核创建的映射内存的大小是内存页面大小的整数倍。第三个参数 prot 指定访问权限,可以是 PROT_READPROT_WRITEPROT_EXEC 。第四个参数 flags 可以是 MAP_PRIVATEMAP_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 syscall

func 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 驱动。

RasberryPi

在树莓派的 /dev 目录下存在 memgpiomem 这两个文件。通过 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 {
// Open fd for rw mem access; try dev/mem first (need root)
file, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, 0)
if os.IsPermission(err) {
// try gpiomem otherwise (some extra functions like clock and pwm setting wont work)
file, err = os.OpenFile("/dev/gpiomem", os.O_RDWR|os.O_SYNC, 0)
}
// FD can be closed after memory mapping
defer file.Close()

memLock.Lock()
defer memLock.Unlock()

// Memory map GPIO registers to slice
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
}
Pieces of Valuable Programming Knowledges