# Linux mmap 原理 一
本文将详细解释 mmap 系统调用的原理。
# 描述
我们先来看如下函数原型。
#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);
2
3
4
5
6
1、mmap() 函数表示在调用进程的虚拟地址空间中创建一个新的映射,新映射的起始地址由参数 addr 指定,length 参数指定映射的长度。
2、如果 addr 为 NULL,则内核可以自由的选择创建映射的地址,这是创建新映射的最具备可移植性的方法(不同内核的实现对于地址选择可能存在某些约束)。如果 addr 不为NULL,那么内核将它作为一个关于映射位置的提示,在 Linux内核上,映射将在 addr 地址附近的页面边界上创建映射,新映射的地址作为调用的结果返回。
3、当我们需要映射文件的内容时(与匿名映射相反,参见下面的MAP_ANONYMOUS),可以传入文件描述符 fd ,并且指定参数 offset 表示从 fd 表示的文件内容偏移量处开始映射,其中 offset 必须是 sysconf(_SC_PAGE_SIZE) 返回的页面大小的倍数,也即需要与页面对齐。
4、prot 参数描述映射所需的内存保护属性(并且不能与文件的打开模式冲突)。它可以由以下属性描述:
PROT_EXEC : 映射的页可以被执行
PROT_READ : 映射的页只读
PROT_WRITE : 映射的页只写
PROT_NONE : 映射的页不能被访问
2
3
4
5
6
7
5、flags 参数用于表示映射的页面更新对映射同一区域的其他进程是否可见,以及是否将更新传递到底层文件。它可以由以下属性描述:
MAP_SHARED : 在进程间共享此映射。映射的更新对于映射此文件的其他进程是可见的,并被传递到底层文件。在调用msync()或 munmap()之前,磁盘文件内容实际上可能不会被更新(因为内容还缓存在映射页面中)。
MAP_PRIVATE :创建一个进程私有的写时复制(COW)映射。对映射的更新对于映射同一文件的其他进程是不可见的,并且不会将修改传递到底层文件。
2
3
此外,以下值中的零个或多个可以用在 flag 参数中,用于指定一些特殊的标识:
MAP_ANONYMOUS:创建一个匿名映射吗,该映射与底层文件无关,仅仅创建页面,页面中的内容被初始化为零。fd 和 offset参数被忽略,但是,如果指定了 MAP_ANONYMOUS 标志位,一些内核实现要求fd为-1,创建可移植应用程序应该确保这一点。从 Linux 内核 2.4 开始,Linux上才支持 MAP_ANONYMOUS 与 MAP_SHARED 结合使用,此时可以实现内存共享机制。
MAP_FIXED:通知内核不要将 addr 参数解释为一个提示,此时必须将映射确切地放在 addr 参数指定的地址。addr 必须是页面大小的倍数。如果由 addr 和 len 指定的内存区域与任何现有映射的页面重叠,则将丢弃现有映射的重叠部分。如果指定的地址不能使用,mmap()将失败。由于映射固定地址的可移植性较差,因此不建议使用此选项。
MAP_HUGETLB(从 Linux 2.6.32 开始支持):从 HUGETLBFS 透明大页内存文件系统中分配内存
MAP_LOCKED(从 Linux 2.5.37 开始支持):用 mlock(2) 函数 将映射区域的页面锁定到内存中。在较老的内核中,这个标志被忽略。
MAP_NONBLOCK(从 Linux 2.5.46 开始支持):只有 与 MAP_POPULATE 标志位一起使用才有意义。不执行任何预读:仅为虚拟中已经存在的页创建页表项。从 Linux 2.6.23 开始,这个标志导致 MAP_POPULATE 不做任何事情。但相信总有一天 MAP_POPULATE 和 MAP_NONBLOCK 的组合可能会被重新实现。
MAP_POPULATE(从 Linux 2.5.46 开始支持):为映射填充页表,对于文件映射,这将导致文件的预读。以后对映射的访问将不会被 page fault 处理的阻塞,仅从Linux 2.6.23开始支持私有映射与该参数联合使用。
MAP_STACK(从 Linux 2.6.27 开始支持):表示 进程 或 线程堆栈的地址分配映射。这个标志目前是无操作的,但是在 glibc 函数库中的pthread 线程库实现中使用,因此如果某些架构需要对堆栈分配进行特殊处理,以后可以透明地实现对glibc的支持。
2
3
4
5
6
7
8
9
10
11
12
13
6、munmap() 系统调用 删除指定地址范围的映射。当函数调用结束时,该映射区域也会自动解除映射,同时应该注意:关闭文件描述符 fd 不会解除该区域的映射。
# sys_mmap2 函数
为保持简单,我们这里使用 Linux 2.6.0 的内核来分析 mmap 原理,有些参数如果只有超过该版本才支持,那么读者可以自行下载对应内核代码完成阅读。
sys_mmap2 系统调用用于完成 mmap 的操作。流程较为简单:检测标志位与fd、然后执行映射。
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
return do_mmap2(addr, len, prot, flags, fd, pgoff);
}
static inline long do_mmap2(
unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
int error = -EBADF;
struct file * file = NULL;
flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); // 当前内核不支持这两个标志位,所以将其清零
if (!(flags & MAP_ANONYMOUS)) { // 若没有指定匿名映射,那么检测 fd 指定的 file 是否存在,如果不存在直接推出
file = fget(fd);
if (!file)
goto out;
}
down_write(¤t->mm->mmap_sem);
error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff); // 完成实际映射操作
up_write(¤t->mm->mmap_sem);
if (file) // 释放当前对 file 结构的引用
fput(file);
out:
return error;
}
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
# do_mmap_pgoff 函数
该函数用于实现完整的映射。流程如下:
1、检测 文件映射 fd 的 file结构 是否支持 mmap 函数
2、检测 映射长度、偏移量、映射次数 是否超出限制
3、根据传入参数 获取一个 起始映射地址
4、将传入的 prot 和 flags 标志位转为 vm_flags 标志位
5、若指定 VM_LOCKED ,那么检测锁定页的限制
6、检测文件映射属性并设置相关 vm_flags 标志位
7、找到一个可以进行映射的 vma 和 它的 红黑树父节点
8、检测映射空间总大小限制
9、尝试进行 vma 的地址空间合并减少空间碎片
10、若合并失败,那么分配一个新的 vma 结构 然后完成映射
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff)
{
struct mm_struct * mm = current->mm;
struct vm_area_struct * vma, * prev;
struct inode *inode;
unsigned int vm_flags;
int correct_wcount = 0;
int error;
struct rb_node ** rb_link, * rb_parent; // vma 映射结构体的红黑树节点
unsigned long charged = 0;
if (file) { // 文件映射,那么需要使用 文件的 mmap 操作来完成,若文件不支持 映射,那么直接返回
if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
if ((prot & PROT_EXEC) && (file->f_vfsmnt->mnt_flags & MNT_NOEXEC))
return -EPERM;
}
if (!len) // 映射长度为0,直接返回
return addr;
len = PAGE_ALIGN(len);
if (!len || len > TASK_SIZE) // 映射长度超出用户态的内存范围(映射长度不能超过用户态的内存大小,否则将会把内核态的信息进行映射)
return -EINVAL;
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) // 映射的文件偏移量溢出
return -EINVAL;
if (mm->map_count > MAX_MAP_COUNT) // 当前进程的映射超出了最大映射个数:#define MAX_MAP_COUNT (65536)
return -ENOMEM;
addr = get_unmapped_area(file, addr, len, pgoff, flags); // 获取能够映射的 addr 地址
if (addr & ~PAGE_MASK) // addr 没有对齐到 页大小的边界处直接返回
return addr;
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; // 将用户传递的 prot 和 flags 参数转为 vma 的标志位
if (flags & MAP_LOCKED) { // 若指定页面锁定操作,检测是否支持,若支持那么合并上 VM_LOCKED 标志位
if (!capable(CAP_IPC_LOCK))
return -EPERM;
vm_flags |= VM_LOCKED;
}
if (vm_flags & VM_LOCKED) { // 若指定了 VM_LOCKED 标志为,那么检测 锁定的映射页大小 是否超出限制
unsigned long locked = mm->locked_vm << PAGE_SHIFT;
locked += len;
if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
return -EAGAIN;
}
inode = file ? file->f_dentry->d_inode : NULL; // 由于 VFS 中,文件的实际操作需要由 inode来完成,所以这里取 文件的 inode 结构
if (file) { // 完成文件映射检查
switch (flags & MAP_TYPE) { // 取映射类型进行判断
case MAP_SHARED: // 执行进程间 fd 映射的共享
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE)) // 指定了映射页可写,但文件当前不可写
return -EACCES;
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE)) // 确保当前文件不允许 append 追加内容,因为这会改变文件内容得大小
return -EACCES;
if (locks_verify_locked(inode)) // 当前 inode 没有存在任何锁
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE; // 合并共享标志位
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
case MAP_PRIVATE: // 私有映射
if (!(file->f_mode & FMODE_READ)) // 文件不可读直接退出
return -EACCES;
break;
default:
return -EINVAL;
}
} else { // 完成非文件的映射,此时用于创建内存页映射到当前进程的虚拟地址中
vm_flags |= VM_SHARED | VM_MAYSHARE; // 先合并上共享标志位(这里笔者觉得没有必要先合并,因为实际上私有映射用得场景很多)
switch (flags & MAP_TYPE) {
default:
return -EINVAL;
case MAP_PRIVATE: // 若用户指定私有映射,那么去掉标志位
vm_flags &= ~(VM_SHARED | VM_MAYSHARE);
case MAP_SHARED:
break;
}
}
error = security_file_mmap(file, prot, flags); // 检测权限的安全file映射,这里忽略
if (error)
return error;
error = -ENOMEM;
munmap_back: // 开始执行映射
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); // 首先找到一个可以用于保存映射信息的 vma 结构
if (vma && vma->vm_start < addr + len) { // 若 vma 已经存在,同时 映射长度大于 vma 管理的映射页,那么先将原有映射解除,然后再次尝试查找合适的 vma(造成这一现象很简单:映射的内存外碎片)
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
// 检查映射的总地址空间限制
if ((mm->total_vm << PAGE_SHIFT) + len
> current->rlim[RLIMIT_AS].rlim_cur)
return -ENOMEM;
// 若标志位没有设置 不保留SWAP分区的页(MAP_NORESERVE) 或者 overcommit_memory 参数为 2 时(注意:overcommit_memory 可以取值 0 1 2,当 overcommit_memory = 0(默认值), 表示内核将检查是否有足够的可用内存供应用进程使用,如果有足够的可用内存,内存申请允许,否则,内存申请失败,并把错误返回给应用进程。当 overcommit_memory = 1, 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。当 overcommit_memory = 2, 表示内核允许分配超过所有物理内存和交换空间总和的虚拟内存)
if (!(flags & MAP_NORESERVE) || sysctl_overcommit_memory > 1) {
if (vm_flags & VM_SHARED) {
// 进程共享页将 在 shmem_file_setup 中检查内存可用性,了解即可
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
// 私有可写映射,检查内存可用性
charged = len >> PAGE_SHIFT; // 映射长度对齐到4kb,看看所需页面数
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
// 若当前不是文件映射 且 不是进程间共享页映射 且 当前找到合适分配的 vma 的父节点存在,那么尝试进行合并兄弟 vma 相连的虚拟地址
if (!file && !(vm_flags & VM_SHARED) && rb_parent)
if (vma_merge(mm, prev, rb_parent, addr, addr + len,
vm_flags, NULL, 0))
goto out;
// 若混合失败,那么需要新创建一个新的 vma 结构来表示这段新映射的 虚拟地址空间
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
error = -ENOMEM;
if (!vma)
goto unacct_error;
// 设置 vma 属性
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
vma->vm_ops = NULL;
vma->vm_pgoff = pgoff;
vma->vm_file = NULL;
vma->vm_private_data = NULL;
vma->vm_next = NULL;
INIT_LIST_HEAD(&vma->shared);
if (file) { // 调用 file 文件的 mmap 回调函数处理 文件映射
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP)) // 文件映射不支持 这两个参数
goto free_vma;
if (vm_flags & VM_DENYWRITE) { // VM_DENYWRITE 标志位 表示尝试对文件进行写访问,若失败,则退出
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file); // 增加当前 file 结构的引用计数
error = file->f_op->mmap(file, vma); // 完成映射
if (error)
goto unmap_and_free_vma;
} else if (vm_flags & VM_SHARED) { // 共享页映射,那么调用 shmem_file_setup 函数完成映射,这里了解即可
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
// 在共享映射的 vm_flags 中设置VM_ACCOUNT,以通知 shmem_zero_setup (可能通过 /dev/zero->mmap 调用)必须检查内存预留空间,但是该预留属于共享内存对象,而不是vma,所以现在将其清除
if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
vma->vm_flags &= ~VM_ACCOUNT;
// 现在获取当前 vma 映射的起始地址
addr = vma->vm_start;
if (!file || !rb_parent || !vma_merge(mm, prev, rb_parent, addr,
addr + len, vma->vm_flags, file, pgoff)) { // 非文件映射 或者 父节点不存在 或者 尝试对兄弟节点进行混合,减少地址空间碎片失败,那么将其链入 vma 链表 同时插入 红黑树
vma_link(mm, vma, prev, rb_link, rb_parent);
if (correct_wcount)
atomic_inc(&inode->i_writecount);
} else {
if (file) {
if (correct_wcount)
atomic_inc(&inode->i_writecount);
fput(file);
}
kmem_cache_free(vm_area_cachep, vma);
}
out: // 分配成功退出
mm->total_vm += len >> PAGE_SHIFT;
if (vm_flags & VM_LOCKED) { // 指定锁定页面,那么 调用 make_pages_present 函数 将物理页映射到虚拟地址空间中的页表中
mm->locked_vm += len >> PAGE_SHIFT;
make_pages_present(addr, addr + len);
}
if (flags & MAP_POPULATE) { // 对文件映射页进行预读,之后再次访问时,将不会导致 page fault
up_write(&mm->mmap_sem);
sys_remap_file_pages(addr, len, prot,
pgoff, flags & MAP_NONBLOCK);
down_write(&mm->mmap_sem);
}
return addr;
unmap_and_free_vma: // 发生错误解除映射并且释放 vma 结构
...
return error;
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# get_unmapped_area 函数
该函数用于根据传入参数找到一个可以映射的 addr 地址。可以看到如果我们指定了 addr 同时指定 MAP_FIXED,那么检查没问题后直接返回,否则我们将尝试从所有 vma 结构中找到一片 空闲的 没有 vma 映射的虚拟内存域进行映射。
unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags)
{
if (flags & MAP_FIXED) { // 指定固定映射(也即必须从 addr 参数处映射)
unsigned long ret;
if (addr > TASK_SIZE - len)
return -ENOMEM;
if (addr & ~PAGE_MASK)
return -EINVAL;
if (file && is_file_hugepages(file)) { // 文件映射或者大页文件映射,确保边界对齐
ret = is_aligned_hugepage_range(addr, len);
} else { // 确保正常的请求没有落在保留的大页范围内。对于 IA-64 平台来说,有一个单独保留的大页面区
ret = is_hugepage_only_range(addr, len);
}
// 若检测没什么问题,那么将用户指定的 addr 地址返回即可
if (ret)
return -EINVAL;
return addr;
}
// 文件对象本身设置了自己的 get_unmapped_area 函数,那么直接调用
if (file && file->f_op && file->f_op->get_unmapped_area)
return file->f_op->get_unmapped_area(file, addr, len,
pgoff, flags);
// 否则执行公用分配 addr 函数
return arch_get_unmapped_area(file, addr, len, pgoff, flags);
}
static inline unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
unsigned long len, unsigned long pgoff, unsigned long flags)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long start_addr;
if (len > TASK_SIZE) // 映射长度只能在 用户态 虚拟地址中
return -ENOMEM;
if (addr) { // 若已经指定了 addr ,那么将其对齐 到 页面边界处,然后 调用 find_vma 查找红黑树,找到该地址落到的 vma 结构
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr);
if (TASK_SIZE - len >= addr && // 没有超过用户态虚拟地址空间
(!vma || addr + len <= vma->vm_start)) // vma 不存在 或者 该地址并没有vma 占用(addr + len <= vma->vm_start vm_start 表示 该 vma 的起始地址,说明需要的地址映射在 该 vma 下面还没有 vma 持有)那么返回该地址
return addr;
}
start_addr = addr = mm->free_area_cache;
full_search: // 否则从 当前 addr 地址 附近的 vma 结构 开始 遍历 vma 链表(通过 vma->vm_next 遍历)找到 一个 vma 不存在 或者 该地址并没有 vma 占用的地址返回
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
if (TASK_SIZE - len < addr) { // 超出用户态虚拟地址
if (start_addr != TASK_UNMAPPED_BASE) { // 起始地址不为 未映射的基地址,那么从 TASK_UNMAPPED_BASE 地址处重新查找( 在 i386 中 #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3)) 这决定了内核在mmap的过程中在哪里搜索空闲的vm空间块,其中 #define TASK_SIZE (PAGE_OFFSET) 3GB)
start_addr = addr = TASK_UNMAPPED_BASE;
goto full_search;
} // 从未映射区中还是没有找到,那么返回缺少虚拟内存
return -ENOMEM;
}
if (!vma || addr + len <= vma->vm_start) { // vma 不存在 或者 该地址并没有 vma 占用的地址返回
mm->free_area_cache = addr + len;
return addr;
}
addr = vma->vm_end; // 每次循环没有找到时,将地址更新为 当前 vma 映射的末尾虚拟地址,然后重新查找
}
}
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
55
56
57
58
59
60
61
62
63
# find_vma_prepare 函数
该函数用从指定 addr 处 找到一个 vma 或者 红黑树的 父节点(在 Linux 内核中,vma 用于管理虚拟内存空间段,为了方便遍历 将其按照地址高低形成一个链表,为了方便查找将其也组织成了一棵红黑树)。
static struct vm_area_struct * find_vma_prepare(struct mm_struct *mm, unsigned long addr,
struct vm_area_struct **pprev, struct rb_node ***rb_link,
struct rb_node ** rb_parent)
{
struct vm_area_struct * vma;
struct rb_node ** __rb_link, * __rb_parent, * rb_prev;
__rb_link = &mm->mm_rb.rb_node; // 获取当前进程的根节点
rb_prev = __rb_parent = NULL; // 初始化链表与红黑树父节点
vma = NULL;
while (*__rb_link) { // 遍历直到找到一个合适的节点:*__rb_link 为 0,也即红黑树叶子节点 (标准的红黑树查找流程)
struct vm_area_struct *vma_tmp;
__rb_parent = *__rb_link; // 当前查找节点为 父节点
vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb); // 获取其中的 vm_area_struct 结构(该结构保存了完整的虚拟内存域信息:起始地址,结束地址等等)
if (vma_tmp->vm_end > addr) { // 当前 vma 的虚拟地址的 结束地址 大于 addr
vma = vma_tmp;
if (vma_tmp->vm_start <= addr) // 若需要的虚拟地址 包含在 当前 vma 管理的地址中,那么返回该 vma
return vma;
__rb_link = &__rb_parent->rb_left; // 否则查找左子树
} else { // 否则查找右子树
rb_prev = __rb_parent;
__rb_link = &__rb_parent->rb_right;
}
}
*pprev = NULL;
// 最终通过右子树还是没有找到包含需要地址的 vma,那么设置该 vma 为 父节点然后返回 NULL
if (rb_prev)
*pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
*rb_link = __rb_link;
*rb_parent = __rb_parent;
return vma;
}
// 标准红黑树定义,将内嵌于 vm_area_struct 结构中
struct rb_node
{
struct rb_node *rb_parent;
int rb_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node *rb_right;
struct rb_node *rb_left;
};
struct vm_area_struct {
struct mm_struct * vm_mm; // 所属进程的内存结构
unsigned long vm_start; // 起始虚拟地址
unsigned long vm_end; // 结束虚拟地址
struct vm_area_struct *vm_next; // vma 链表
struct rb_node vm_rb; // 内嵌红黑树属性
... // 省略其他属性
}
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