fs\dax.c –> copy_cow_page_dax()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int copy_cow_page_dax(struct vm_fault *vmf, const struct iomap_iter *iter)
{
pgoff_t pgoff = dax_iomap_pgoff(&iter->iomap, iter->pos);/* dax_iomap_pgoff -> PHYS_PFN :获得物理页框号*/
void *vto, *kaddr;
long rc;
int id;

id = dax_read_lock();
rc = dax_direct_access(iter->iomap.dax_dev, pgoff, 1, DAX_ACCESS,
&kaddr, NULL);
if (rc < 0) {
dax_read_unlock(id);
return rc;
}
vto = kmap_atomic(vmf->cow_page);
copy_user_page(vto, kaddr, vmf->address, vmf->cow_page);
kunmap_atomic(vto);
dax_read_unlock(id);
return 0;
}

include/linux/iomap.h –> struct iomap_iter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* struct iomap_iter - Iterate through a range of a file(遍历文件的某一范围)
* @inode: Set at the start of the iteration and should not change.
* @pos: The current file position we are operating on. It is updated by
* calls to iomap_iter(). Treat as read-only in the body.
* @len: The remaining length of the file segment we're operating on.
* It is updated at the same time as @pos.
* @processed: The number of bytes processed by the body in the most recent
* iteration, or a negative errno. 0 causes the iteration to stop.
* @flags: Zero or more of the iomap_begin flags above.
* @iomap: Map describing the I/O iteration
* @srcmap: Source map for COW operations
*/
struct iomap_iter {
struct inode *inode;
loff_t pos;
u64 len;
s64 processed;
unsigned flags;
struct iomap iomap;
struct iomap srcmap;
void *private;
};

/drivers/dax/super.c –> dax_direct_access()

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
/**
* dax_direct_access() - translate a device pgoff to an absolute pfn
* @dax_dev: a dax_device instance representing the logical memory range
* @pgoff: offset in pages from the start of the device to translate
从设备开始翻译,以页为单位的偏移量
* @nr_pages: number of consecutive pages caller can handle relative to @pfn
* @mode: indicator on normal access or recovery write
* @kaddr: output parameter that returns a virtual address mapping of pfn
* @pfn: output parameter that returns an absolute pfn translation of @pgoff
*
* Return: negative errno if an error occurs, otherwise the number of
* pages accessible at the device relative @pgoff.
*/
long dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, long nr_pages,
enum dax_access_mode mode, void **kaddr, pfn_t *pfn)
{
long avail;

if (!dax_dev)
return -EOPNOTSUPP;

if (!dax_alive(dax_dev))
return -ENXIO;

if (nr_pages < 0)
return -EINVAL;

avail = dax_dev->ops->direct_access(dax_dev, pgoff, nr_pages,
mode, kaddr, pfn);
if (!avail)
return -ERANGE;
return min(avail, nr_pages);
}
EXPORT_SYMBOL_GPL(dax_direct_access);

/include/linux/dax.h –> dax_operrations->direct_access()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct dax_operations {
/*
* direct_access: translate a device-relative
* logical-page-offset into an absolute physical pfn. Return the
* number of pages available for DAX at that pfn.
*/
long (*direct_access)(struct dax_device *, pgoff_t, long,
enum dax_access_mode, void **, pfn_t *);
/*
* Validate whether this device is usable as an fsdax backing
* device.
*/
bool (*dax_supported)(struct dax_device *, struct block_device *, int,
sector_t, sector_t);
/* zero_page_range: required operation. Zero page range */
int (*zero_page_range)(struct dax_device *, pgoff_t, size_t);
/*
* recovery_write: recover a poisoned range by DAX device driver
* capable of clearing poison.
*/
size_t (*recovery_write)(struct dax_device *dax_dev, pgoff_t pgoff,
void *addr, size_t bytes, struct iov_iter *iter);
};

/drivers/nvdimm/pmem.c 中 :

  • .direct_access = pmem_dax_direct_access
1
2
3
4
5
static const struct dax_operations pmem_dax_ops = {
.direct_access = pmem_dax_direct_access,
.zero_page_range = pmem_dax_zero_page_range,
.recovery_write = pmem_recovery_write,
};

/drivers/nvdimm/pmem.c

1
2
3
4
5
6
7
8
static long pmem_dax_direct_access(struct dax_device *dax_dev,
pgoff_t pgoff, long nr_pages, enum dax_access_mode mode,
void **kaddr, pfn_t *pfn)
{
struct pmem_device *pmem = dax_get_private(dax_dev);

return __pmem_direct_access(pmem, pgoff, nr_pages, mode, kaddr, pfn);
}

/drivers/nvdimm/pmem.c

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
39
40
41
42
43
44
45
46
47
/* see "strong" declaration in tools/testing/nvdimm/pmem-dax.c */
__weak long __pmem_direct_access(struct pmem_device *pmem, pgoff_t pgoff,
long nr_pages, enum dax_access_mode mode, void **kaddr,
pfn_t *pfn)
{
/* pgoff 是在设备起始地址开始的页偏移量 */
resource_size_t offset = PFN_PHYS(pgoff) + pmem->data_offset;
sector_t sector = PFN_PHYS(pgoff) >> SECTOR_SHIFT;
unsigned int num = PFN_PHYS(nr_pages) >> SECTOR_SHIFT;
struct badblocks *bb = &pmem->bb;
sector_t first_bad;
int num_bad;

if (kaddr)
*kaddr = pmem->virt_addr + offset; //填充 pmem 对应的虚拟地址
if (pfn)
*pfn = phys_to_pfn_t(pmem->phys_addr + offset, pmem->pfn_flags);

if (bb->count &&
badblocks_check(bb, sector, num, &first_bad, &num_bad)) {
long actual_nr;

if (mode != DAX_RECOVERY_WRITE)
return -EIO;

/*
* Set the recovery stride is set to kernel page size because
* the underlying driver and firmware clear poison functions
* don't appear to handle large chunk(such as 2MiB) reliably.
*/
actual_nr = PHYS_PFN(
PAGE_ALIGN((first_bad - sector) << SECTOR_SHIFT));
dev_dbg(pmem->bb.dev, "start sector(%llu), nr_pages(%ld), first_bad(%llu), actual_nr(%ld)\n",
sector, nr_pages, first_bad, actual_nr);
if (actual_nr)
return actual_nr;
return 1;
}

/*
* If badblocks are present but not in the range, limit known good range
* to the requested range.
*/
if (bb->count)
return nr_pages;
return PHYS_PFN(pmem->size - pmem->pfn_pad - offset);
}

/include/linux/highmem.h –> kmap_atomic()

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
39
40
41
42
43
44
45
46
/**
* kmap_atomic - Atomically map a page for temporary usage - Deprecated!
* @page: Pointer to the page to be mapped
*
* Returns: The virtual address of the mapping
*
* In fact a wrapper around kmap_local_page() which also disables pagefaults
* and, depending on PREEMPT_RT configuration, also CPU migration and
* preemption. Therefore users should not count on the latter two side effects.
*
* Mappings should always be released by kunmap_atomic().
*
* Do not use in new code. Use kmap_local_page() instead.
*
* It is used in atomic context when code wants to access the contents of a
* page that might be allocated from high memory (see __GFP_HIGHMEM), for
* example a page in the pagecache. The API has two functions, and they
* can be used in a manner similar to the following::
*
* // Find the page of interest.
* struct page *page = find_get_page(mapping, offset);
*
* // Gain access to the contents of that page.
* void *vaddr = kmap_atomic(page);
*
* // Do something to the contents of that page.
* memset(vaddr, 0, PAGE_SIZE);
*
* // Unmap that page.
* kunmap_atomic(vaddr);
*
* Note that the kunmap_atomic() call takes the result of the kmap_atomic()
* call, not the argument.
*
* If you need to map two pages because you want to copy from one page to
* another you need to keep the kmap_atomic calls strictly nested, like:
*
* vaddr1 = kmap_atomic(page1);
* vaddr2 = kmap_atomic(page2);
*
* memcpy(vaddr1, vaddr2, PAGE_SIZE);
*
* kunmap_atomic(vaddr2);
* kunmap_atomic(vaddr1);
*/
static inline void *kmap_atomic(struct page *page);

CreateObj in PMDK

​ 为了完成CoW_PM项目,首先得实现一个createobj()的函数,它由fd和offset生成一个obj,并返回该obj的标识。PMDK的pmemobj_create()函数的功能类似,故学习一下它是如何实现的。

PS-ORAM

Background

Oblivious RAM

​ ORAM(Oblivious Read Access Machine)是指一种计算机,可以实现对于输入X,Y,产生的一系列指令是不可区分的。不经意随机访问机是一种重要的保护访问模式的手段,它通过混淆每一次访问过程,使其与随机访问不可区分,从而保护真实访问中的访问操作、访问位置等信息。

​ 最近这么火,是由于云计算的迅猛发展,越来越多的企业和个人把数据外包到位于公有云上,然后数据安全和隐私保护就显得越来越重要。即云环境下的数据的机密性保护成为了重要的研究课题。常见的保护方式是在数据上传到云服务器上前进行加密,但是即使数据加密,攻击者也可以从数据访问模式(Access Patterns)推测出敏感信息。这里的访问模式是指程序对存储器的一系列访问所泄露的信息,包括命令(读或写)、地址和数据。已有工作证明攻击者可以从访问模式推测出敏感信息。

​ ORAM 近来就是来解决这个问题,隐藏数据的访问模式。比如一种简单的方案,就是每次把云上存储的数据都读到本地,然后找到自己需要读取或者更新的数据,然后再全部写回到云服务器上。这样云服务器就不知道你读了或者更新了什么数据。这就是一种最简单的 ORAM 方案,只是开销太大了,ORAM 研究的一大方向就是针对计算复杂度和带宽开销进行优化。

参考链接:https://www.zhihu.com/question/45314262/answer/275838233

SubZero

论文链接:SubZero: Zero-copy IO for Persistent Main Memory File Systems

Motivation

​ POSIX标准的 read() 和 write() 长期以来一直是访问文件中数据的标准接口。但是,当文件存储在 PMEM 中时,这些方法会带来冗余的数据拷贝开销。为了避免这种数据复制,PMEM 感知型文件系统(如NOVA、PMFS等)使用 DAX 技术通过总线直接访问 PM,但这样做需要程序员管理对文件的写入原子性和并发访问。

​ 所以 SubZero 希望结合 POSIX IO 和 DAX IO 两者的优势,提供类似 DAX 的速度和一个简单的、类似 POSIX 的接口。

Design

​ SubZero IO实现了以下的系统调用:

SubZero系统调用

peek()

​ peek () 系统调用将 PM 文件映射到内存中,并返回一个指向内存区域的指针,该内存区域包含特定文件偏移处的文件内容。映射的内存区域就是执行 peek () 时文件内容的快照,该快照对于文件修改(例如 write () 或 patch () )是原子的。映射使用 O_RDONLY 标志位,使得快照是只读的、不可变的,因此尝试更改其内容会导致Segmentation Fault。

​ peek() 与 mmap() 相比,peek() 的文件偏移或返回的指针没有对齐限制,而 mmap() 需要页对齐。


​ **peek() example1:basic **

1
2
3
4
5
// peek the first 4KB of a PMEM file
int fd = open("foo", O_RDONLY); // Open the target file
char *buf = peek(fd, 0, 4096); // Peek its contents
printf(“%s\n”, buf); // Print the contents
unpeek(buf); // Unpeek the contents

peek() example2:immutability

1
2
3
4
5
6
// peek the first 4KB of a PMEM file
int fd = open("foo", O_RDONLY); // Open the target file
char *buf = peek(fd, 0, 4096); // Peek its contents
printf(“%s\n”, buf); // Print the contents
*buf = ‘a’; // Segmentation fault!
unpeek(buf); // Unpeek the contents

​ **peek() example3:isolation **

1
2
3
4
5
6
7
8
9
// Thread 1: peek the first 4KB of a PMEM file
int fd = open("foo", O_RDONLY);
char *buf = peek(fd, 0, 4096);
...
...
printf(“%s\n”, buf); // print original contents!
...
unpeek(buf);
close(fd);
1
2
3
4
5
6
7
8
9
// Thread 2: update the peek()’ed region
// of the same file
int fd = open("foo", O_WRONLY);
char *buf = malloc(4096);
memset(buf, 0xab, 4096);
write(fd, buf, 4096); // copy-on-write to
... // a new 4KB
free(buf)
close(fd);

patch()

​ patch() 系统调用通过将缓冲区的内容合并到给定偏移量的文件中来修改文件。本质上,缓冲区成为文件的一部分,而不是被复制到文件中。在 patch() 之后,缓冲区变得不可变。

patch操作

​ 这个 buffer 需要 access-aligned 。直观地说,就是缓冲区的页边界必须与文件中的页边界对齐。比如:对于在页大小为 S 的文件系统上使用缓冲区 B 和文件偏移 off 关闭的 patch() 操作,如果 B % S == off % S,则 patch() 是访问对齐的。

Conclusion

​ 为了展示它的潜力,在 PMEM 文件系统 XFS-DAX 和 NOVA 中实现了 SubZero。通过简单的基准测试表明,SubZero 可以比基于复制的 read() 和 write() 性能高出 2 倍和 2.8 倍。在应用层面,peek() 将 Apache Web Server 的 GET 性能提升了 3.6 倍,patch() 将 Kyoto Cabinet 的 SET 性能提升 1.3 倍。

mmap整理

mmap是什么

​ mmap就是memory map,是内存映射。

​ mmap 是一种内存映射文件的方法,它将一个文件或者其它对象映射到用户进程的虚拟地址空间,实现文件磁盘地址(可以不连续)和进程虚拟地址空间中一段连续虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

第三章:内存管理

内存管理框架

​ 内存管理子系统的架构如下图所示,分为用户空间、内核空间和硬件 3 个层面。

内存管理框架

用户空间

​ 应用程序使用 malloc() 申请内存,使用 free() 释放内存。

​ malloc() 和 free() 是 glibc 库的内存分配器 ptmalloc 提供的接口, ptmalloc 使用系统调用 brk 或 mmap 向内 核以页为单位申请内存 , 然后划分成小内存块分配给应用程序。

内核空间

​ (1)内核空间的基本功能

​ 虚拟内存管理负责从进程的虚拟地址空间分配虚拟页, sys_brk 用来扩大或收缩堆, sys_mmap 用来在内存映射区域分配虚拟页, sys_munmap 用来释放虚拟页。

内核使用延迟分配物理内存的策略,进程第 1 次访问虚拟页的时候,触发页错误异常,页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。

内存分配的函数调用

​ 页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。

​ 内核空间提供了把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc() 和释放内存的接口 kfree(), 支持 3 种块分配器: SLAB 分配器、 SLUB 分配器和 SLOB 分配器。

​ 在内核初始化的过程中,页分配器还没准备好,需要使用临时的引导内存分配器分配内存。

​ (2)内核空间的扩展功能

不连续页分配器提供了分配内 存的接口 vmalloc 和释放内存的接口 vfree, 在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续而物理地址不连续。

​ **连续内存分配器 (Contiguous Memory Allocator, CMA) **用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。

​ 当内存碎片化的时候,找不到连续的物理页,内存碎片整理(“memory compaction” 的意译,直译为“内存紧缩")通过迁移的方式得到连续的物理页。

​ 在内存不足的时候,页回收负责回收物理页,对于没有后备存储设备支持的匿名页, 把数据换出到交换区,然后释放物理页;对于有后备存储设备支待的文件页,把数据写回存储设备,然后释放物理页。如果页回收失败,使用最后一招:内存耗尽杀手 (OOM killer, Out-of-Memory killer), 选择进程杀掉

硬件层面

​ 处理器包含一个称为内存管理单元 (Memory Management Unit, MMU) 的部件,负责把虚拟地址转换成物理地址。

​ 内存管理单元包含一个称为**页表缓存 (Translation Lookaside Buffer, TLB) **的部件, 保存最近使用过的页表映射,避免每次把虚拟地址转换成物理地址都需要查询内存中的页表。

​ 为了解决处理器的执行速度和内存的访问速度不匹配的问题,在处理器和内存之间增加了缓存。缓存通常分为 一级缓存和二级缓存,为了支持并行地取指令和取数据,一级缓存分为数据缓存和指令缓存。

虚拟地址空间布局

虚拟地址空间划分

​ 因为目前应用程序没有那么大的内存需求,所以 ARM64 处理器不支持完全的 64 位虚 拟地址,实际支持情况如下。

​ (1)虚拟地址的最大宽度是 48 位,如图所示。高 16 位是全 1 或全 0 的地址称为规范的地址,两者之间是不规范的地址,不允许使用。

48位虚拟地址空间划分

​ (2)如果处理器实现了 ARMv8.2 标准的大虚拟地址 (Large Virtual Address, LVA) 支持,并且页长度是 64KB, 那么虚拟地址的最大宽度是 52 位。

​ 所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

用户虚拟地址空间布局

​ 用户进程虚拟地址空间包括以下区域。

  1. 代码段、 数据段和未初始化的数据段。
  2. 动态库的代码段、数据段和未初始化的数据段。
  3. 存放动态生成的数据的堆。
  4. 存放局部变量和函数调用的栈。
  5. 存放在栈底部的环境变量和参数字符串。
  6. 把文件区间映射到虚拟地址空间的内存映射区域。

​ 进程是正在执行的程序,是可执行程序的动态实例,它是一个承担分配系统资源的实体,但操作系统创建进程时,会为进程创建相应的内存空间,这个内存空间称为进程的地址空间,每一个进程的地址空间都是独立的!

虚拟内存

​ 当一个进程有了进程的地址空间,那么进程的地址空间就必须被相应的工具所管理,这个工具被称为内存描述符 mm_struct ,它被定义在include/linux/mm_types.h中,在Linux操作系统中是这样管理进程的地址空间的,如下图所示:

mm_struct

​ mm_struct 的部分定义如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct mm_struct {
struct vm_area_struct *mmap; /* 指向线性区对象的链表头 */
struct rb_root mm_rb; /* 指向线性区对象的红-黑树 */

/* 在进程地址空间中搜索有效线性地址区间的方法 */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);

unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */

/* Base adresses for compatible mmap() */
unsigned long mmap_compat_base;
unsigned long mmap_compat_legacy_base;

unsigned long task_size; /* size of task vm space 用户虚拟地址空间的长度 */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd; /* 指向进程页表起始地址 */

atomic_t mm_users; /* 共享同一个用户虚拟地址空间的进程的数量 */

/*
内存描述符的主使用计数器,每次mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内 存描述符,因为不再有用户使用它
*/
atomic_t mm_count;

atomic_long_t nr_ptes; /* 进程中用于pte的页数 */
atomic_long_t nr_pmds; /* 进程中用于pmd的页数 */

int map_count; /* number of VMAs */

spinlock_t page_table_lock; /* 线性区的自旋锁和页表的自旋锁 */

struct list_head mmlist;/* 存放链表相邻元素的地址,第一个元素是init_mm的mm_list字段 */

unsigned long hiwater_rss; /* 进程所拥有的最大页框数 */
unsigned long hiwater_vm; /* 进程线性区中的最大页数 */
unsigned long total_vm; /* 进程地址空间的大小 */
unsigned long locked_vm; /* "锁住"而不能换出的页的个数 */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* 用户态堆栈中的页数 */
unsigned long def_flags; /* 线性区默认的访问标志 */
/* 可执行代码开始地址,结束地址,已初始化数据的开始地址,结束地址 */
unsigned long start_code, end_code, start_data, end_data;
/* 堆的起始地址,堆的当前最后地址,用户态堆栈的起始地址 */
unsigned long start_brk, brk, start_stack;
/* 命令行参数的起始地址,命令行参数的最后地址,环境变量的起始地址,环境变量的最后地址 */
unsigned long arg_start, arg_end, env_start, env_end;
/* 开始执行ELF程序时会使用到saved_auxv参数 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
};

​ 进程描述符(task_struct)中和内存描述符(mm_struct)相关的成员:

1
2
3
4
struct mm_struct *mm;  /* 进程的mm指向一个内存描述符
内核线程没有用户虚拟地址空间,所以mm是空指针 */
struct mm_struct *active_mm; /* 进程的active_mm 和 mm总是指向同一个内存描述
内核线程的active_mm在没有运行时是空指针,在运行时指向从上一个进程借用的内存描述符 */

第二章:进程管理

进程

​ Linux 内核把进程称为任务 (task), 进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。

进程描述符 task_struct 被定义在sched.h头文件中:include\linux\sched.h

​ 其中重要的成员变量如下:

成员 描述
volatile long state; 进程的状态。 -1 unrunnable, 0 runnable, >0 stopped
void *stack; 指向内核栈
pid_t pid; 全局的进程号
pid_t tgid; 全局的线程组标识符
struct pid_link pids[PIDTYPE_MAX]; 进程号,进程组标识符和会话标识符
struct task_struct __ rcu *real_parent;
struct task_struct __rcu *parent;
real_parent指向真实的父进程。
parent 指向父进程:如果进程被另一个进程(通常是调试器)
使用系统调用ptrace跟踪,那么父进程是跟踪进程,否则和 real_parent相同
char comm[TASK_COMM_LEN]; 进程名称
int prio,static_prio,normal_prio;
unsigned int rt__priority;
unsigned int policy;
调度策略和优先级
cpumask_t cpus_allowed; 允许进程在哪些处理器上运行
struct mm_struct *mm, *active_mm; 指向内存描述符
进程:mm 和 active_mm指向同一个内存描述符
内核线程:mm是空指针,当内核线程运行时, active_mm指向从进程借用的内存描述符
struct fs struct *fs; 文件系统信息,主要是进程的根目录和当前工作目录
struct files_struct *files; 打开文件表
struct nsproxy *nsproxy; 命名空间

第一章:内核引导和初始化

​ 处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户空间的第一个进程。

引导程序在哪读取?

​ 处理器在上电时自动把程序计数器设置为处理器厂商设计的某个固定值,对于 ARM64 处理器,这个固定值是0。处理器的内存管理单元 (Memory Management Unit, MMU) 负责把虚拟地址转换为物理地址, ARM64 处理器刚上电的时候是没有开启内存管理单元的,物理地址和虚拟地址相同,所以 ARM64 处理器到物理地址 0 取第一条指令。嵌入式设备通常使用 NOR 闪存作为只读存储器来存放引导程序。从物理地址 0 开始的一段物理地址空间被分配给 NOR 闪存。

​ ARM64 处理器到虚拟地址 0 取指令,就是到物理地址 0 取指令,也就是到 NOR 闪存的起始位置取指令。

引导程序

​ ARM64 处理器的 U-Boot(Universal Boot Loader) 程序的执行入口是文件 “arch/arm/cp armv8/start.S” 定义的标号_start 。

​ 为U-Boot分配临时栈,并将引导程序复制到内存中。当引导程序初始化完成后,开始执行其中的命令,其中重要的操作是函数 bootm_find_os 把内核镜像从存储设备读到内存,函数 bootm_load_os 把内核加载到正确的位置,如果内核镜像是被压缩过的,需要解压缩。

内核初始化

​ 内核初始化分为汇编语言部分和 C 语言部分。

汇编部分

​ 汇编部分的主要处理如下:

arch/arm64/kernel/head.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
/*
* The following calls CPU setup code, see arch/arm64/mm/proc.S for
* details.
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set.
*/
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)

​ 第 7 行代码,调用函数 __ __create_page_tables __, 创建页表映射。

​ 第 14 行代码,调用函数 __ cpu_setup, 为开启处理器的内存管理单元做准备,初始化处理器。

​ 第 15 行代码,调用函数 __ primary_switch, 为主处理器开启内存管理单元,搭建 C 语言执行环境,进入 C 语言部分的入口函数 start_kernel 。

__create_page_tables函数

​ 函数 _create_page_tables 的主要工作如下

​ (1) 创建恒等映射(identity mapping)

​ (2) 为内核镜像创建映射。

​ 恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。函数 __enable_mnu 负责开启内存管理单元,内核把函数 enable_mmu 附近的代码放在恒等映射代码节 (.idmap.text) 里面,恒等映射代码节的起始地址存放在全局变量 _idmap_text_start 中,结束地址存放在全局变 **idmap_text_end **中。

​ 恒等映射是为恒等映射代码节创建的映射, idmap_pg_dir 是恒等映射的页全局目录(即第一级页表)的起始地址。在内核的页表中为内核镜像创建映射,内核镜像的起始地址是 _text, 结束地址是 _end, swapper_pg_dir是内核的页全局目录的起始地址。

__primary_switch函数

​ 函数**__primary_switch** 的主要执行流程如下:

  1. ​ 调用函数__enable_mmu 以开启内存管理单元
  2. ​ 调用函数**__primary_switched** 函数(不是 switch 函数)

enable_mmu 的主要执行流程如下:

  1. ​ 把转换表基准寄存器 0 (TTBR0_EL1) 设置为恒等映射的页全局目录的起始物理地址。
  2. ​ 把转换表基准寄存器 1 (TTBR1_EL1) 设置为内核的页全局目录的起始物理地址。
  3. ​ 设置系统控制寄存器 (SCTLR_EL1), 开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。

​ 函数**__primary_switched** 的执行流程如下:

​ (1) 把当前异常级别的栈指针寄存器设置为 0 号线程内核栈的顶部 (init_thread_union + THREAD_ SIZE)

​ (2) 把异常级别 0 的栈指针寄存器( SP_EL0) 设置为 0 号线程的结构体 thread_info 地址 (init_task.thread_info)

​ (3) 把向量基准地址寄存器 (VBAR_EL1) 设置为异常向量表的起始地址 (vectors)

​ (4) 计算内核镜像的起始虚拟地址 (kimage_vaddr) 和物理地址的差值,保存在全局变量 kimage_voffset 中

​ (5) 用 0 初始化内核的未初始化数据段

​ (6) 调用 C 语言函数 start_kernel

C语言部分

​ 内核初始化的 C 语言部分入口是函数 start_kernel, 函数 start_kernel 首先初始化基础设施,即初始化内核的各个子系统,然后调用函数 rest_init 函数。rest_init 的执行流程如下。

​ (1) 创建 1 号线程,即 init 线程,线程函数是 kernel_init

​ (2) 创建 2 号线程,即 kthreadd 线程,负责创建内核线程

​ (3) 0 号线程最终变成空闲线程

​ init 线程继续初始化,执行的主要操作如下:

​ (1) smp_prepare_cpus(): 在启动从处理器以前执行准备工作

​ (2) do_pre_smp_initcalls(): 执行必须在初始化 SMP 系统以前执行的早期初始化,即使用宏 early_initcall 注册的初始化函数。

​ (3) smp_init(): 初始化 SMP 系统,启动所有从处理器。

​ (4) do_initcalls(): 执行级别 0~7 的初始化。

​ (5) 打开控制台的字符设备文件 “/dev/console”, 文件描述符0、1 和 2 分别是标准输入、标准输出和标准错误,都是控制台的字符设备文件。

​ (6) prepare_namespace(): 挂载根文件系统,后面装载 init 程序时需要从存储设备上的文件系统中读文件。

​ (7) free_initmem(): 释放初始化代码和数据占用的内存。

​ (8) 装载 init 程序 (U-Boot 程序可以传递内核参数 “init= “ 以指定 init 程序),从内核线程转换成用户空间的 init 进程。

do_initcalls()函数

​ 级别 0~7 的初始化,是指使用以下宏注册的初始化函数:

​ **include/linux/init.h **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define pure_initcall(fn)		__define_initcall(fn, 0)

#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)

SMP系统

​ 对称多处理器 (Symmetric Multi-Processor, SMP) 系统包含多个处理器,并且每个处理器的地位平等。在启动过程中 ,处理器的地位不是平等的, 0 号处理器称为引导处理器, 负责执行引导程序和初始化内核;其他处理器称为从处理器,等待引导处理器完成初始化。 引导处理器初始化内核以后,启动从处理器。

ctFS

背景

PM的访问

​ PM 的出现降低了非易失性内存和DRAM的性能差距,并且带来了可字节访问的特性。当前一些针对 PM 优化的文件系统,要么使用DAX技术来绕过 Page Cache ;要么通过将不同的文件系统数据结构映射到用户空间来绕过内核,以减少由用户态切换到内核态的开销。

Ext4-DAX Index path

​ 然而上述文件系统的索引结构还是传统的 tree-based。这种树型的索引结构是在 unix 系统时期提出的,当时内存和持久存储(磁盘)的访问速度相差几个数量级,主要的开销主要是对持久外存的访问。然而,PM 的出现让这种速度差异已经显着缩小到几乎可以忽略不计的程度。并且PM的低延迟反过来又将瓶颈从 I/O 转移到了文件索引开销上。据作者的实验显示,即便是ext4-dax模式,某些情况内存索引的开销占总开销的 45%。

image-20220721212356546

方案与设计

提出ctFS文件系统使用连续文件分配来代替文件索引。

​ ctFS使用持久页表 (PPT,Persistent Page Table) 管理文件的虚拟到物理映射。 PPT 与 DRAM 中的常规易失页表具有相似的结构,不同之处在于 PPT 永久存储在 PM 上。在 ctFS 内存区域内的地址出现页面错误时,操作系统会查找 PPT 并在基于 DRAM 的页表中创建相同的映射。因此,子序列访问由 MMU 提供查询DRAM中页表的服务,从而避免了索引成本。


ctFS的架构

ctFS的架构

​ ctFS 的架构如图所示,由两个组件组成:(1) 提供文件系统抽象的用户空间文件系统库 ctU,以及 (2) 提供虚拟内存抽象的内核子系统 ctK。 ctU 实现文件系统结构并将其映射到虚拟内存空间。 ctK 使用存储在 PM 中的持久页表 (PPT) 将虚拟地址映射到 PM 的物理地址。ctU 地址范围内的虚拟地址上的任何页面错误都由 ctK 处理。如果 PPT 中不包含故障地址的映射,ctK 将分配一个 PM 页,在 PPT 中建立映射,然后将映射从 PPT 复制到内核的 DRAM 页表,从而实现虚拟到 PM 地址的转换由 MMU 执行。当 PPT 中的任何映射过时 时,ctK 将从 DRAM 页表中删除相应的映射,并删除 TLB 中的映射。

ctU

​ ctFS 的用户空间库 ctU 将文件系统的虚拟内存空间组织成分层分区,以促进连续分配。ctFS采用类似于Linux 的伙伴分配算法来管理虚拟地址空间,并将它们分层,每个level之间的大小差距是8倍,和地址的对应的关系对应如下:

size of partition

​ 虚拟内存区域被划分为两个 L9 分区。第一个 L9 分区是用于存储文件系统元数据的特殊分区:超级块、inode 的位图以及 inode 本身。每个 inode 存储文件的元数据(例如,所有者、组、保护、大小等)和一个字段,用于标识包含文件数据的分区的虚拟内存地址。 inode 位图用于跟踪是否分配了 inode。第二个 L9 分区用于数据存储。

Layout of ctFS

​ 对于空闲页的查找,ctFS设置了一个表头来加速查找,对于L4-L9,会利用第一个页去保存使用状态,对于后面的三个层级,用一个页去表述空闲资源有些浪费,转为利用bitmap去表示。

ctK:

​ 由于地址空间布局随机化,内存区域可能会在不同进程中映射到不同的起始虚拟地址,并且硬件重新配置可能会更改 PM 的起始物理地址。虽然每个进程都有自己的 DRAM 页表,但 ctK 有一个 PPT,其中包含 ctU 内存范围内所有虚拟地址的映射(即分区内的那些)。 MMU 无法访问 PPT,因此 PPT 中的映射用于按需填充 DRAM 页表中的条目,作为页面错误处理的一部分。

ctK 负责管理PPT,对于虚拟地址和物理地址,ctFS都采用的相对地址,为每个进程映射到不同的虚拟地址


Pswap系统调用:

​ 最初,一个文件被分配在一个分区中,该分区的大小刚好足以容纳该文件。当文件超出其分区时,它会被移动到虚拟内存中更大的分区,而无需复制任何物理持久内存。 ctFS 通过使用 pswap 将文件的物理页面重新映射到新分区来做到这一点,这是一种新的操作系统系统调用,可以原子地交换虚拟到物理的映射。原子交换还可以在多块写入时实现高效的崩溃一致性,而无需重复写入数据。 ctFS 中的原子写入只是将数据写入新空间,然后将其与旧数据进行 pswaps。

Pswap

​ 上图展示了一个例子,其中 pswap 需要交换两个页面序列 A 和 B, 两个序列都包含 262658 (512 × 512 + 512 + 2) 个页面。 交换两个序列的数据时,使用pswap 只需要交换 4 对页表条目或目录,因为所有 262,658 个页面都被单个 PUD 条目覆盖(覆盖 512×512 个页面),单个 PMD 条目(涵盖 512 页)和两个 PTE 条目(涵盖 2 页)。