Linux-API:mmap&flock

Linux API: introduction to mmap(2) and flock(2)

mmap

mmap(2) 比较折磨人,它的接口如下:

1
2
3
4
5
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);

mmap 有两种 mapping:

  1. file mapping
  2. anonymous mapping

对于 1,fdmmap 后可以 close:

1
2
After the mmap() call has returned, the file descriptor, fd, can
be closed immediately without invalidating the mapping.

mmapprot 有下列的语义:

  1. PROT_EXEC: Pages may be executed.
  2. PROT_READ: Pages may be read.
  3. PROT_WRITE: Pages may be written.
  4. PROT_NONE:Pages may not be accessed.

mmap 的逻辑和 os 的 page cache 强相关,上述语义可能作用在 page 上,必要的时候给你个 SIGSEGV

rwx 三个选项都理解,PROT_NONE 可能用于做 Guard Page: https://stackoverflow.com/questions/12916603/what-s-the-purpose-of-mmap-memory-protection-prot-none

同时,flags 也有许多选项,除了 MAP_NONBLOCK MAP_LOCKED 这些,需要留意 MAP_SHAREDMAP_PRIVATE , 使用可以见下表。

0E0F6E12-FAB6-434A-8177-68F7CFE39FB2

上述也会影响 fork 的时候父子进程的 mmap 行为(可以回到一下之前对 fork 的吐槽)

文件映射

mmap 的时候,文件会被按照 offsetlength 映射到内存中,如图:

0FCE129310878CAB48CB0E6D47DC8169

这种 mapping 可能是 on-demand 的,即 mmap 之后不把 page 加载,需要的时候再访问。

对于 MAP_PRIVATE, 它可以:

  • 被用在 PROT_READ | PROT_EXEC , 运行程序,这样不会修改程序的数据
  • 共享一个只读的 .text

而共享的文件则如下图所示:

6CFC2E40-CF95-4C33-B6F9-D6836D857F80

这种 memory mapping IO 会有特点:文件和 kernel 的 buffer 是由 os 自动 manage 的,同时,内核和用户进程不再和 write 一样,先写用户 buffer, 再写 kernel buffer.

当需要强制 flush 的时候,可以走 msync(2) 来强制刷盘

mmap 的映射可能比文件本身还大,未来的内存可能需要 ftruncate 处理:

D3EA25A8-9FBE-4215-8CCF-408BAF69183C

匿名映射

数据会被初始化为 0

1
2
3
4
5
6
7
8
MAP_ANONYMOUS
The mapping is not backed by any file; its contents are
initialized to zero. The fd argument is ignored; however,
some implementations require fd to be -1 if MAP_ANONYMOUS
(or MAP_ANON) is specified, and portable applications
should ensure this. The offset argument should be zero.
The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is
supported on Linux only since kernel 2.4.

杂项

mprotect(2) 切换进程页的访问权限

mlock 会讲内存 Page 驻留在物理内存中,不会swap 出去

madvise(2)

用户改内核的代码和调度是不现实的,但是很多时候用户比内核自己清楚需要调度成啥样。

madvise(2) 相当于提供上述相关的信息,“建议”内核做相关的操作。详见:https://www.man7.org/linux/man-pages/man2/madvise.2.html

Usage

BoltDB 的读取使用了 mmap. 内存空间是小于磁盘的,而 bolt 没有自己写 BufferPool, 而是用了 mmap, 可以看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// mmap memory maps a DB's data file.
func mmap(db *DB, sz int) error {
// Map the data file to memory.
b, err := syscall.Mmap(int(db.file.Fd()), 0, sz, syscall.PROT_READ, syscall.MAP_SHARED|db.MmapFlags)
if err != nil {
return err
}

// Advise the kernel that the mmap is accessed randomly.
if err := madvise(b, syscall.MADV_RANDOM); err != nil {
return fmt.Errorf("madvise: %s", err)
}

// Save the original byte slice and convert to a byte array pointer.
db.dataref = b
db.data = (*[maxMapSize]byte)(unsafe.Pointer(&b[0]))
db.datasz = sz
return nil
}

DB::mmap 具体走到 bolt_unix.go 下的 mmap, 这里用 madvise + MADV_RANDOM 来表达访问的模式,同时把这次数据存储到 db

dataref 防止 syscall.Mmap 的结果被回收,具体访问的数据被丢到 data 上。

munmap 是一个反向操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// munmap unmaps a DB's data file from memory.
func munmap(db *DB) error {
// Ignore the unmap if we have no mapped data.
if db.dataref == nil {
return nil
}

// Unmap using the original byte slice.
err := syscall.Munmap(db.dataref)
db.dataref = nil
db.data = nil
db.datasz = 0
return err
}

flock

https://man7.org/linux/man-pages//man2/flock.2.html

flock(2) 源自 BSD,flock 可以指定 LOCK_SHLOCK_EX, 表示共享/互斥,也可以 LOCK_UN 解锁

本身调用 flock 会阻塞进程,不希望阻塞可以带上 LOCK_UB flag

flock 本身和 fd 是绑定的,也就是说,dup 产生的 fd 和 fork 子进程 产生的 fd 是同一把锁,这意味着,对它们的 LOCK_UN 处理需要额外注意。

46EDACEC-B756-477A-877C-302378852132

可以参考一下代码的 flockfunlock, 内容在 bolt_unix.go:

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
func flock(db *DB, mode os.FileMode, exclusive bool, timeout time.Duration) error {
var t time.Time
for {
// If we're beyond our timeout then return an error.
// This can only occur after we've attempted a flock once.
if t.IsZero() {
t = time.Now()
} else if timeout > 0 && time.Since(t) > timeout {
return ErrTimeout
}
flag := syscall.LOCK_SH
if exclusive {
flag = syscall.LOCK_EX
}

// Otherwise attempt to obtain an exclusive lock.
err := syscall.Flock(int(db.file.Fd()), flag|syscall.LOCK_NB)
if err == nil {
return nil
} else if err != syscall.EWOULDBLOCK {
return err
}

// Wait for a bit and try again.
time.Sleep(50 * time.Millisecond)
}
}

// funlock releases an advisory lock on a file descriptor.
func funlock(db *DB) error {
return syscall.Flock(int(db.file.Fd()), syscall.LOCK_UN)
}

这里的 funlock 比较朴素,flock 采取了 LOCK_NB 和一个 retry 尝试结合,超过 timeout 后,程序会自动退出。

flock 只能以文件为粒度上锁,需要更细的控制需要使用 fcntl

References