高级OS(八) - Linux内存映射
一.题目
查看学堂在线《Linux内核分析与应用》的4.5~4.7节的视频,完成其实验代码,截图,讨论,两人一组。
- 进入Linux 内核(4.19)源代码, 给出task_struct 结构,mm_struct 结构和 vm_area_struct 结构的关系图,这样的数据结构设计对你有什么启发?
- 对实验中涉及的相关内核数据结构、函数进行深入分析,并画出流程图。至少分析6个数据结构和6个函数
- 对实验结果进行分析,用调试工具对内核进行调试(截图),遇到的问题,以及心得体会
二.解答
1.进入Linux 内核(4.19)源代码, 给出task_struct 结构,mm_struct 结构和 vm_area_struct 结构的关系图,这样的数据结构设计对你有什么启发?每个数据结构至少分析2个以上字段
虚拟内存信息是通过task_struct结构中mm指针指向mm_struct结构体,其作用是描述一个进程的整个用户虚拟地址空间,包含一些虚拟地址空间的信息说明,每个进程的虚拟地址空间又被分为许多的虚拟内存地址区,如Stack区、Heap区、BSS、Data、Text区等。每个虚拟内存地址区由vm_area_struct结构体来管理。mm_struct中的mmap指针指向第一个vm_area_struct,地址区中的vm_next指向下一个vm_area_struct.
2.对实验中涉及的相关内核数据结构、函数进行深入分析,并画出流程图。至少分析6个数据结构和6个函数
驱动程序目的是将内核空间的线性地址所对应的物理地址映射到用户空间的某一个线性地址中。
流程图:
数据结构:
//file_operations结构体完成设备读取、写入、保存等等这些操作,都是由存储在file_operations结构体中的这些函数指针来处理的,这些函数指针所指向的函数都需要我们在驱动模块将其实现。
struct file_operations {
struct module *owner; //拥有该结构的模块的指针,一般为THIS_MODULES
loff_t (*llseek) (struct file *, loff_t, int); //用来修改文件当前的读写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从设备中同步读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //向设备发送数据
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); //初始化一个异步的读取操作
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); //初始化一个异步的写入操作
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *); //打开
int (*flush) (struct file *, fl_owner_t id); //flush操作在进程关闭它的设备文件描述符的拷贝时调用
int (*release) (struct inode *, struct file *); //关闭
int (*fsync) (struct file *, loff_t, loff_t, int datasync); //刷新待处理的数据
int (*fasync) (int, struct file *, int); //异步刷新待处理的数据
int (*lock) (struct file *, int, struct file_lock *); //lock方法用来实现文件加锁
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int); //这个方法允许模块检查传递给fnctl调用的标志
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);} __randomize_layout;
//vm_operations_struct结构体建立页表项。
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*split)(struct vm_area_struct * area, unsigned long addr);
int (*mremap)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf); //fault函数用来为进程中那些用来映射的线性地址建立相应的页表项。
vm_fault_t (*huge_fault)(struct vm_fault *vmf,
enum page_entry_size pe_size);
void (*map_pages)(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff);
unsigned long (*pagesize)(struct vm_area_struct * area);
//注意以前的只读页即将变为可写页,如果返回错误,将导致SIGBUS
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
// 与使用VM_PFNMAP | VM_MIXEDMAP时的page_mkwrite相同
vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);
// 当get_user_pages()失败时,由access_process_vm调用,通常供可以在内存和硬件之间切换的特殊VMA使用
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
// 由/proc/PID/maps代码调用,询问vma是否有特殊名称。返回非NULL也将导致无条件转储此vma。
const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
/*set_policy()op必须添加对任何非NULL@new mempolic的引用,才能在返回时保留策略。调用者应传递NULL@new以删除策略并返回到周围的上下文,即不要安装MPOL_默认策略,也不要安装任务或系统默认mempolicy。*/
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
unsigned long addr);#endif
/*由vm_normal_page()调用,用于特殊PTE查找@addr的页面。如果默认行为(使用pte_page()找不到正确的页面,这将非常有用。*/
struct page *(*find_special_page)(struct vm_area_struct *vma,
unsigned long addr);};
//vm_fault结构体用来存放缺页有关的参数。
struct vm_fault {
struct vm_area_struct *vma; //映射区vma指针
unsigned int flags; /* FAULT_FLAG_xxx flags */
gfp_t gfp_mask; /*用于分配的gfp掩码*/
pgoff_t pgoff; /*基于vma的逻辑页偏移 */
unsigned long address; //adress是产生缺页的线性地址
pmd_t *pmd; //pmd pud就是这个线性地址所对应的页,目录项。
pud_t *pud;
pte_t orig_pte; //pte是它所对应的页表项
struct page *cow_page; /* Page handler may use for COW fault */
struct mem_cgroup *memcg; /* Cgroup cow_page belongs to */
struct page *page; //在fault函数中为用来映射的线性地址建立页表项,首先要找到它所对应的物理地址,找到物理地址后取它的页描述符,将其填写到vm_fault结构体的page字段中,剩下建立页表项工作可以交给os自动完成了。
/* 这三个条目仅在持有ptl lock时有效 */
pte_t *pte; //指向与“地址”匹配的pte条目的指针。如果尚未分配页表,则为NULL。
spinlock_t *ptl; /* 页表锁。如果“pte”不为空,则保护pte页表,否则为pmd*/
pgtable_t prealloc_pte;
};
//mm_struct被称为内存描述符(memory descriptor),抽象并描述了Linux视角下管理进程地址空间的所有消息,其定义在include/linux/mm_types.h中
struct mm_struct {
struct {
//指向线性区对象的链表头
struct vm_area_struct *mmap; /* list of VMAs */
//指向线性区对象的红黑树
struct rb_root mm_rb;
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
//用来在进程地址空间中搜索有效的进程地址空间的函数
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
//标识第一个分配文件内映射的线性地址
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
/* Base adresses for compatible mmap() */
unsigned long mmap_compat_base;
unsigned long mmap_compat_legacy_base;
#endif
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;
//内存描述符的主使用计数器,采用引用计数的原理,当为0时代表无用户再次使用
atomic_t mm_count;
#ifdef CONFIG_MMU
atomic_long_t pgtables_bytes; /* PTE page table pages */
#endif
//线性区的个数
int map_count; /* number of VMAs */
//保护任务页表和引用计数的锁
spinlock_t page_table_lock; // Protects page tables and some * counters
struct rw_semaphore mmap_sem;
//mm_struct结构,第一个成员就是初始化的mm_struct结构
struct list_head mmlist; /* List of maybe swapped mm's. These * are globally strung together off * init_mm.mmlist, and are protected * by mmlist_lock */
//进程拥有的最大页表数目
unsigned long hiwater_rss; /* High-watermark of RSS usage */
//进程线性区的最大页表数目
unsigned long hiwater_vm; /* High-water virtual memory usage */
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; /* 可执行内存映射中的页数 */
unsigned long stack_vm; /* 用户态堆栈的页数 */
unsigned long def_flags;
spinlock_t arg_lock; /* protect the below fields */
//维护代码段和数据段
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;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
/* * Special counters, in some configurations protected by the * page_table_lock, in other configurations by being atomic. */
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
/* Architecture-specific MM context */
mm_context_t context;
//线性区的默认访问标志
unsigned long flags; /* Must use atomic bitops to access */
struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_MEMBARRIER
atomic_t membarrier_state;
#endif
#ifdef CONFIG_AIO
spinlock_t ioctx_lock;
struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG
/*“所有者”指向被视为此mm的规范用户/所有者的任务。以下所有条件必须为真才能更改:当前==mm->所有者当前->mm!=mm new_owner->mm==mm new_owner->alloc_lock被保持。*/
struct task_struct __rcu *owner;#endif
struct user_namespace *user_ns;
/* store ref to file /proc/<pid>/exe symlink points to */
struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
Struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* 受页\表\锁保护 */
#endif
#ifdef CONFIG_NUMA_BALANCING
/*numa_next_scan是下一次将pte标记为pte_numa的时间。NUMA提示错误将收集统计信息,并在必要时将页面迁移到新节点。 */
unsigned long numa_next_scan;
/* 扫描和设置pte_numa的重新启动点 */
unsigned long numa_scan_offset;
/* numa_scan_seq防止两个线程设置pte_numa*/
int numa_scan_seq;
#endif
/*正在进行分批TLB冲洗操作。移动PROT_NONE或PROT_NUMA映射页时,任何可以移动进程内存的内容都需要刷新TLB。*/
atomic_t tlb_flush_pending;#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
/* See flush_tlb_batched_pending() */
bool tlb_flush_batched;
#endif
struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
atomic_long_t hugetlb_usage;#endif
struct work_struct async_put_work;
#if IS_ENABLED(CONFIG_HMM)
/*HMM每毫米需要跟踪一些东西。 */
struct hmm *hmm;#endif
} __randomize_layout;
//mm_cpumask需要位于mm_结构的末尾,因为它是根据nr_cpu_id动态调整大小的。
unsigned long cpu_bitmap[];};
//vm_area_struct此结构定义内存VMM内存区域。每个VM区域/任务都有一个。VM区域是进程虚拟内存空间的任何部分,它对页面错误处理程序(即共享库、可执行区域等)具有特殊规则。
struct vm_area_struct {
/* 第一个缓存线包含VMA树遍历的信息。 */
unsigned long vm_start; /* 我们的起始地址在vm_mm内。 */
unsigned long vm_end; /* vm_mm中结束地址后的第一个字节。*/
/* 每个任务的VM区域的链接列表,按地址排序 */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/* 此VMA左侧的最大可用内存间隙(以字节为单位)。在该VMA和VMA->vm_prev之间,或者在VMA rbtree中我们下面的一个VMA和它的->vm_prev之间。这有助于找到大小合适的空闲区域。*/
unsigned long rb_subtree_gap;
/* 第二条缓存线从这里开始。 */
struct mm_struct *vm_mm; /* 我们所属的地址空间。 */
pgprot_t vm_page_prot; /* 此VMA的访问权限。 */
unsigned long vm_flags; /* Flags, see mm.h. */
/*对于具有地址空间和后备存储的区域,链接到address_space->i_mmap间隔树。 */
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
/* 文件的MAP_私有vma可以位于i_mmap树和anon_vma列表中,位于其中一个文件页面之后。MAP_共享vma只能位于i_mmap树中。匿名MAP_PRIVATE、stack或brk vma(带有空文件)只能位于anon_vma列表中。 */
struct list_head anon_vma_chain; /* 由mmap_sem和page_table_lock序列化 */
struct anon_vma *anon_vma; /* 由页\表\锁序列化 */
/* 处理此结构的函数指针。 */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* 以页面大小为单位的偏移量(在vm_file中)*/
struct file * vm_file; /*我们映射到的文件(可以为空)。 */
void * vm_private_data; /* was vm_pte (shared mem) */
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU映射区 */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* VMA的NUMA政策 */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;} __randomize_layout;
函数:
//模块的装载函数,所要完成的工作有两个,一是设备注册,二是在内核中为设备申请一块内存
static int __init mapdrv_init(void)
{
int result;
unsigned long virt_addr;
int i = 1;
result=register_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME,&mapdrvo_fops); //设备注册(主设备号,设备名称,它所链接的file_operations结构),返回0则成功。
if(result<0){
return result;
}
vmalloc_area=vmalloc(MAPLEN); //申请内存,vmalloc函数特点是申请的内存区域,他的内核线性地址是连续的,但是物理地址不连续。
virt_addr = (unsigned long)vmalloc_area;
for(virt_addr = (unsigned long)vmalloc_area; virt_addr < (unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE)
{
SetPageReserved(vmalloc_to_page((void *)virt_addr)); //还需要对申请到的物理页框的pagereserved标志位置位,告诉系统物理页框已被使用了。
sprintf((char *)virt_addr, "test %d",i++);
}
/* printk("vmalloc_area at 0x%lx (phys 0x%lx)\n",(unsigned long)vmalloc_area,(unsigned long)vmalloc_to_pfn((void *)vmalloc_area) << PAGE_SHIFT); */
printk("vmalloc area apply complate!");
return 0;
}
//模块的卸载函数,
static void __exit mapdrv_exit(void)
{
unsigned long virt_addr;
/* unreserve all pages */
for(virt_addr = (unsigned long)vmalloc_area; virt_addr < ( unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE)
{
ClearPageReserved(vmalloc_to_page((void *)virt_addr)); //清理pagereserved标志位
}
/* and free the two areas */
if (vmalloc_area)
vfree(vmalloc_area); //释放申请的vmalloc线性区的线性地址
unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME); //注销设备
}
//共享内存映射
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
//判断都是对异常情况的判断
//MAPLEN用来表示在内核中申请的物理页框的大小。若进程中用来映射的线性地址区大于申请的物理地址,可能访问到不该访问的地方。
if (size > MAPLEN) {
printk("size too big\n");
return -ENXIO;
}
/* only support shared mappings. */
//如果vm区标志位可写入,vm_flags是保存进程对该虚拟空间的访问权限的。vm_shared表示可共享的。
if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {
printk("writeable mappings must be shared, rejecting\n");
return -EINVAL;
}
/* do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKONFAULT; //锁住页框,不让被操作系统交换出去
if (offset == 0) {
vma->vm_ops = &map_vm_ops; //将驱动模块中定义的vm_operations_struct结构的地址赋给了当前vma的vm_ops指针
} else {
printk("offset out of range\n");
return -ENXIO;
}
return 0;
}
// 要找到物理页框,方法是vmalloc_to_pfn和vmalloc_to_page两个函数,这两个函数可以将内核vmalloc区线性地址所指向的那个物理页框找到,用vmalloc_to_page函数找页描述符,用vmalloc_to_pfn找到页帧号。
int map_fault(struct vm_fault *vmf)
{
struct page *page;
void *page_ptr;
unsigned long offset, virt_start, pfn_start;
offset = vmf->address-vmf->vma->vm_start; //产生却页的线性地址在vma映射区的偏移量,用当前的缺页地址减去ma的起始地址得到。
virt_start = (unsigned long)vmalloc_area + (unsigned long)(vmf->pgoff << PAGE_SHIFT); //内核中的线性地址,vmalloc_area是我们在模块装载函数中申请到的那个线性区的起始地址让它加上每一页的偏移得到内核每一页的线性地址。
pfn_start = (unsigned long)vmalloc_to_pfn((void *)virt_start); //内核中的线性地址所对应的那些物理页框的页帧号用vmalloc_to_pfn找到
printk("\n");
/*printk("%-25s %d\n","7)PAGE_SHIFT",PAGE_SHIFT);*/
page_ptr=NULL;
//对异常状况的判断
if((vmf->vma==NULL)||(vmalloc_area==NULL)){
printk("return VM_FAULT_SIGBUS!\n");
return VM_FAULT_SIGBUS;
}
if(offset >=MAPLEN){
printk("return VM_FAULT_SIGBUS!");
return VM_FAULT_SIGBUS;
}
page_ptr=vmalloc_area + offset; //起始地址加上offset每一页的偏移量得到。
page=vmalloc_to_page(page_ptr); //用vmalloc_to_page将内核的线性地址,找到她所对应的物理页框的页描述符
get_page(page); //找到物理页框增加它的引用次数,get_page函数实现。
vmf->page=page; //将找到的页描述符填写到vmf的page字段中,完成对产生缺页的线性地址的页表项的建立。
printk("%s: map 0x%lx (0x%016lx) to 0x%lx , size: 0x%lx, page:%ld \n", __func__, virt_start, pfn_start << PAGE_SHIFT, vmf->address,PAGE_SIZE,vmf->pgoff);
return 0;
}
remap_pfn_init(内核空间的内存映射到用户空间):用户态所要做的事情非常简单,首先open驱动设备文件,然后对该文件进行mmap操作就可以了,进行mmap操作的时候,如果第一个参数是NULL,则有libc自己分配一块虚拟内存区域(这一块虚拟内存区域将对应于上面的vma,还需特别注意三点,一是size的设置,这里的size的设置将决定上面所述vma中start和 end之间的差,所以一定要和内核中物理内存区域大小对应起来。二是PROT的设置,PROT的设置对应上面vma中的vm_page_prot,如果需要读写,则要设置为PROT_READ|PROT_WRITE。三是MAP_SHARED的设置,如果设置成MAP_SHARED,用户态的虚拟空间就确实映射到了内核态中开辟的空间的物理地址上,就是实实在在的一块内存了。当然这里也可以设置为MAP_PRIVATE,如果这么设置,那么在mmap的时候,是从内核态开辟的空间的物理地址上做一次copy,copy到用户态的虚拟地址上,所以这样事实上还是两块内存。)
static int __init remap_pfn_init(void)
{
int ret = 0;
kbuff = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!kbuff)
{
ret = -ENOMEM;
goto err;
}
ret = misc_register(&remap_pfn_misc);
if (unlikely(ret))
{
pr_err("failed to register misc device!\n");
goto err;
}
return 0;
err:
return ret;
}
mmap系统调用的执行过程
//一个用户进程,它调用mmap系统调用后,它需要依次执行以下函数。
//首先系统会为其在当前进程的虚拟地址空间中寻找一段连续的空闲地址,这是通过遍历vm_area_struct链表实现的,当找到这样一个合适的地址区间后,为其建立一个vm_area_struct结构,完成这些之后,该进程就有一个专门用于mmap映射的虚拟内存区了。
//但在进程的页表中,这个区域中的线性地址没有对应的物理页框,接着系统会调用内核空间的系统调用函数mmap,也就是我们在file_operations结构中定义的mmap,它将要完成对vm_area_struct结构中的虚拟地址,为它们建立其对应的页表项。
//建立页表项方法有两种,一种是remap_pfn_range方法,一种叫fault方法。前者在内核函数mmap也就是在file_operations中要完成的mmap函数。它在被调用时一次性的为vm_area_struct结构中的这些线性地址建立页表项,要求页表项所要映射的物理地址是连续的,fault函数是在进程访问到这个映射空间中的虚拟地址时,发现该虚拟地址的页表项为空,引起缺页时才被调用,更适合vmalloc这种不连续地址的映射。
SYSCALL_DEFINE6(mmap_pgoff,...
sys_mmap_pgoff
SYSCALL_DEFINE6(mmap_pgoff,...
vm_mmap_pgoff
do_mmap_pgoff
mmap_rgion
File->f_op->mmap()
remap_pfn_range()
fault()
- 对实验结果进行分析,用调试工具对内核进行调试(截图),遇到的问题,以及心得体会
运行成功的截图:
对代码进行make编译之后
用insmod写入内核
在用dmesg查看打印的信息
遇到的问题:make进行编译的时候报错
解法:后来仔细核对Makefile文件并查找资料发现,是因为格式的原因,必须是tab,不能使用八个空格,否则会报错。
心得体会:经过这次的实验,对内存映射的原理有了基本的了解,对linux内核有了进一步的理解,也熟悉了更对的linux指令,可能还没有更熟悉在linux下编写代码,导致会发生一些比较低级的错误,之后会更加细心,进一步深入内核。