欢迎大家来到IT世界,在知识的湖畔探索吧!
swapper_pg_dir 作用
内核维持着一组自己使用的页表,也即主内核页全局目录。当内核在初始化完成后,其存放在 swapper_pg_dir 中,swapper_pg_dir 其实就是一个页目录的指针,页目录指针在 x86 中是要被加载到 cr3 寄存器的,每个进程都有一个页目录指针,这个指针指示这个进程的内存映射信息,每当切换到一个进程时,该进程的页目录指针就被加载到了 cr3,然后直到切换到别的进程的时候才更改。
swapper_pg_dir 只是在内核初始化的时候被载入到 cr3 指示内存映射信息,之后在 init 进程启动后就成了 idle 内核线程的页目录指针。
/sbin/init 由一个叫做 init 的内核线程 exec 而成,而 init 内核线程是原始的内核也就是后来的 idle 线程 do_fork 而成的,而在 do_fork 中会为新生的进程重启分配一个页目录指针,由此可见 swapper_pg_dir 只是在 idle 和内核线程中被使用。
可是它的作用却不只是为 idle 进程指示内存映射信息,更多的,它作为一个内核空间的内存映射模板而存在,在 linux 中,任何进程在内核空间就不分彼此了,所有的进程都会共用一份内核空间的内存映射,因此,内核空间是所有进程共享的,每当一个新的进程建立的时候,都会将 swapper_pg_dir 的 768 项以后的信息全部复制到新进程页目录的 768 项以后,代表内核空间。另外在操作 3G+896M 以上的虚拟内存时,只会更改 swapper_pg_dir 的映射信息(因为虚拟内存中 3G~3G+896M 区域和物理内存的 0~896M 进行直接映射,已经映射好了,不需要修改),当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与 swapper_pg_dir 同步。
do_fork -> copy_mm -> mm_init -> pgd_alloc pgd_t *pgd_alloc(struct mm_struct *mm) { pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL); if (pgd) { memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t)); memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)); } return pgd; }
欢迎大家来到IT世界,在知识的湖畔探索吧!
其中 USER_PTRS_PER_PGD 值为 768,pgd_alloc 中把 swapper_pg_dir 中的 768~1023 项拷贝到进程页表中。
所有的进程共享内核空间,所以共享内核页表是很自然的事。理论上内核只有一个页表,对应的内核全局页目录 swapper_pg_dir。每个进程有自己的页目录,共 1024 项,其中的 768 项后与 swapper_pg_dir 相同,指向的是内核空间。
但是每个进程的内核地址空间的页表并不是与主内核页表完全一致的,原因就是当 vmalloc 后修改了主内核页表,但是进程的页表并没有修改。
欢迎大家来到IT世界,在知识的湖畔探索吧! asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) { ... if (address >= TASK_SIZE && !(error_code & 5)) goto vmalloc_fault; ... vmalloc_fault: { /* * Synchronize this task's top level page-table * with the 'reference' page table. * * Do _not_ use "tsk" here. We might be inside * an interrupt in the middle of a task switch.. */ int offset = __pgd_offset(address); //页目录项偏移 pgd_t *pgd, *pgd_k; pmd_t *pmd, *pmd_k; pte_t *pte_k; asm("movl %%cr3,%0":"=r" (pgd)); pgd = offset + (pgd_t *)__va(pgd); //对应在用户态页表的pgd pgd_k = init_mm.pgd + offset; //内核页表swapper_pg_dir的偏移处 if (!pgd_present(*pgd_k)) goto no_context; set_pgd(pgd, *pgd_k); //把内核页表swapper_pg_dir中目录项拷贝到用户页表中 pmd = pmd_offset(pgd, address); //页表偏移量 pmd_k = pmd_offset(pgd_k, address);//在swapper_pg_dir页表偏移量 if (!pmd_present(*pmd_k)) //判断pmd项是否存在,若否,返回失败 goto no_context; set_pmd(pmd, *pmd_k); //把内核页表swapper_pg_dir页表项拷贝到用户页表中 pte_k = pte_offset(pmd_k, address); if (!pte_present(*pte_k)) //判断pte项是否存在,若否,返回失败 goto no_context; return; } }
对于二级页表来讲,上述代码获取的 pmd 也为 pgd,同理 pmd_k 也为 pgd_k。因此对于二级页表,其只是把 swapper_pg_dir 中的页目录项复制到用户进程页表中,页表项大家共用。

欢迎大家来到IT世界,在知识的湖畔探索吧!
当进程访问到这块虚拟地址空间时,进程的页目录项是空的,此时会产生一个缺页中断,然后调用 do_page_fault,在该方法中会判断访问的虚拟地址是否大于 0xc0000000(3G),若大于 3G 说明要访问的虚拟地址在内核态,因此执行 vmalloc_fault,在 vmalloc_fault 中会把虚拟地址对应的页目录项、页表项从 swapper_pg_dir 拷贝到进程的页表中,这样进程页目录项的值与主内核页表进行同步后完全一致。结果最后只有访问到这段 vmalloc 区间的进程才会进行与主内核页表的更新,当然内核修改了主内核页表后一定要向所有 CPU 发送一个 TLB 刷新请求,因为有可能某个 CPU 上正在运行的进程对应的页表项保存在了 TLB 中。
vmalloc 介绍
vmalloc分配的虚拟地址空间则限于 VMALLOC_START 与 VMALLOC_END 之间。
每一块 vmalloc 分配的内核虚拟内存都对应一个 vm_struct 结构体。不同的内核虚拟地址被 4k 大小的空闲区间隔,以防止越界。
vmalloc 区域并不和用户空间内存映射一样,通过 page fault 来装载页面的。vmalloc 映射建立好后,逻辑地址,物理页面全部分配好,而且页表也已经更新好,只是此处为内核页表,并没有更新相关进程的页表。在 vmalloc 区发生 page fault 时,将“内核页表”同步到“进程页表”中。这部分区域对应的线性地址在内核使用 vmalloc 分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生 page fault 时,此时在 page fault 的处理流程中进行“进程页表”的更新。
static inline void * vmalloc (unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); } void * __vmalloc (unsigned long size, int gfp_mask, pgprot_t prot) { void * addr; struct vm_struct *area; //把size 参数取整为页面大小(4096)的一个倍数,也就是按页的大小进行对齐 size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > num_physpages) { BUG(); return NULL; } /* 如果有大小合适的可用内存,就调用get_vm_area()获得一个 内存区的结构。但真正的内存区还没有获得, 函数vmalloc_area_pages()真正进行非连续内存区的分配 */ area = get_vm_area(size, VM_ALLOC); // 获取一个 vm_struct 结构,代表申请的一块区域 if (!area) return NULL; addr = area->addr; //在swapper_pg_dir中建立相关页表 if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) { vfree(addr); return NULL; } return addr; } struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) { unsigned long addr; struct vm_struct p, *tmp, *area; area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); if (!area) return NULL; size += PAGE_SIZE; addr = VMALLOC_START; write_lock(&vmlist_lock); for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { if ((size + addr) < addr) goto out; if (size + addr <= unsigned long tmp->addr) break; addr = tmp->size + (unsigned long) tmp->addr; if (addr > VMALLOC_END-size) goto out; } area->flags = flags; area->addr = (void *)addr; area->size = size; area->next = *p; *p = area; write_unlock(&vmlist_lock); return area; out: write_unlock(&vmlist_lock); kfree(area); return NULL; } //该函数实际建立起了非连续内存区到物理页面的映射。 inline int vmalloc_area_pages (unsigned long address, unsigned long size, int gfp_mask, pgprot_t prot) { pgd_t * dir; unsigned long end = address + size; int ret; //导出这个内存区起始地址在页目录中的目录项 dir = pgd_offset_k(address); spin_lock(&init_mm.page_table_lock); do { pmd_t *pmd; //为新的内存区创建一个中间页目录 pmd = pmd_alloc(&init_mm, dir, address); ret = -ENOMEM; if (!pmd) break; ret = -ENOMEM; //为新的中间页目录分配所有相关的页表,并更新页的总目录 if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) break; address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; ret = 0; } while (address && (address < end)); spin_unlock(&init_mm.page_table_lock); return ret; }
释放该区域使用 vfree()方法,该方法释放区域时只是修改内核页表 swapper_pg_dir,它会把addr对应的pte页表项设置为0,也就是调用ptep_get_and_clear() 将页表项清0,设置完后调用 flush_tlb_all() 刷新下TLB 。但它不会修改所有进程的进程页表。
从 上面可知,swapper_pg_dir 和 用户进程页表共享页目录表,所以当再次访问该虚拟地址时,找不到页表项,产生 page fault。
缺页异常和上面分配的流程一样,只是最后对内核页表 pte 项做检查时候,发现内核页表关于 addr 的 pte 页表项是 0,就会报错。这样就避免了进程的非法访问。
kmalloc() 与 vmalloc() 的区别
kmalloc() 与 vmalloc() 都是在内核代码中提供给其他子系统用来分配内存的函数,但二者有何区别?
kmalloc 代表的是 kernel_malloc 的意思,它是用于内核的内存分配函数。
kmalloc 的释放对应于 kfree。
从前面的介绍已经看出,这两个函数所分配的内存都处于内核空间,即从 3GB~4GB;但位置不同,kmalloc() 分配的内存处于3GB~high_memory 之间,而 vmalloc() 分配的内存在 VMALLOC_START~4GB 之间,也就是非连续内存区。
一般情况下在驱动程序中都是调用 kmalloc() 来给数据结构分配内存,而 vmalloc() 用在为活动的交换区分配数据结构,为某些 I/O 驱动程序分配缓冲区,或为模块分配空间,例如在 include/asm-i386/module.h 中定义了如下语句:
#define module_map(x) vmalloc(x)
其含义就是把模块映射到非连续的内存区。
与 kmalloc() 和 vmalloc() 相对应,两个释放内存的函数为 kfree()和vfree()。
vmalloc() 分配的物理地址无需连续,而 kmalloc() 确保页在物理上是连续的。
函数 vmalloc()从内核的虚拟地址空间(3G以上)分配一块虚存以及相应的物理内存,类似于系统调用 brk()。
不过 brk()是由进程在用户空间启动并从用户空间中分配的,而 vmalloc()则是从系统空间,也就是从内核中启动的,从内核空间中分配的。
由 vmalloc()分配的空间不会被 kswapd换出,因为 kswapd 只扫描各个进程的用户空间,而根本就看不到通过 vmalloc()分配的页面表项。
至于通过 kmalloc()分配的数据结构,则 kswapd 只是从各个 slab 队列中寻找和收集空闲不用的 slab,并释放所占用的页面,但是不会将尚在使用 slab 所占据的页面换出。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/117333.html