swapper_pg_dir主内核页表、init和kthreadd、do_fork时新建子进程页表、vmalloc与kmalloc
都是以前看到一个点扯出的很多东西,当时做的总结,有问题欢迎讨论,现在来源难寻,侵删!
1、Init_task、idle、init和kthreadd的区别和联系
idle进程其pid=0,其前身是系统创建的第一个进程(我们称之为init_task),也是唯一一个没有通过fork或者kernel_thread产生的进程。
init_task是内核中所有进程、线程的task_struct雏形,它是在内核初始化过程中,通过静态定义构造出了一个task_struct接口,取名为init_task,然后在内核初始化的后期,在rest_init()函数中通过kernel_thread创建了两个内核线程:kernel_init线程,kthreadd内核线程, 前者后来通过演变,进入用户空间,成为所有用户进程的先祖,也就是1号进程init, 而后者则成为所有内核态其他守护线程的父线程, 也就是2号进程,负责接手内核线程的创建工作。
然后init_task通过变更调度类为sched_idle等操作演变成为idle进程, 此时系统中只有0(idle), 1(init), 2(kthreadd)3个进程, 然后执行一次进程调度, 必然切换当前进程到init。
实际上,init_task和idle描述的都是0号进程,该进程的comm字段是swapper。init_task是一个task_struct结构,该结构的comm字段是swapper,active_mm是init_mm(其中的pgd就是主内核页目录swapper_pg_dir);idle进程的名字也是swapper,通过内核模块方式可以得到0号进程名为swapper,在遍历内核维护的进程链表时,for_each_proces的起始点是:task=&init_task。
2、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的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。
3 、子进程页表中内核空间目录项的建立
创建子进程时如果没有CLONE_VM表示共享地址空间,则会初始化子进程页表:do_fork->copy_process->copy_mm-> dup_mm-> mm_init初始化子进程页表,调用mm_alloc_pgd为子进程页表分配连续的页框,调用pgd_alloc初始化新的页目录 mm指向子进程的struct mm_struct,然后调用pgd_ctor设置子进程的页目录项(主要设置从主内核页表中复制内核地址空间的页目录项)
pgd_t *pgd_alloc(struct mm_struct *mm)
{/* 初始化子进程页表mm_alloc_pgd调用,初始化新的页目录 mm指向子进程的struct mm_struct*/
pgd_t *pgd; /* 页目录指针 */
pmd_t *pmds[PREALLOCATED_PMDS];/* 预先分配的页中间目录指针数组,只有在PAE模式下才会需要预分配出一些页中间目录 */ /* 从伙伴系统中获取连续的页框作为子进程的页目录,PGALLOC_GFP标志会将新获取的内存清空 */
pgd = (pgd_t *)__get_free_page(PGALLOC_GFP); if (pgd == NULL) /* 获取内存失败处理 */
goto out; mm->pgd = pgd; /* 设置struct mm_struct中的pgd指向新的页目录,之前是指向父进程的页目录 */ if (preallocate_pmds(pmds) != )/* 预分配页中间目录,正常情况下不会预分配 */
goto out_free_pgd; if (paravirt_pgd_alloc(mm) != )
goto out_free_pmds; /*
* Make sure that pre-populating the pmds is atomic with
* respect to anything walking the pgd_list, so that they
* never see a partially populated pgd.
*/
spin_lock(&pgd_lock); pgd_ctor(mm, pgd);/* 子进程的页目录项设置,主要设置了内核地址空间的页目录项,具体见后面代码 */
pgd_prepopulate_pmd(mm, pgd, pmds);/* 子进程的页中间目录项设置,也是主要设置了内核地址空间的页中间目录项,如果没有预分配页中间目录,则直接返回 */ spin_unlock(&pgd_lock); return pgd; /* 将设置好的页目录首地址返回 */ out_free_pmds:
free_pmds(pmds);
out_free_pgd:
free_page((unsigned long)pgd);
out:
return NULL;
}
所有的进程共享内核空间,所以共享内核页表是很自然的事。理论上内核只有一个页表,对应的内核全局页目录swapper_pg_dir。每个进程有自己的页目录,共1024项,其中的768项后与swapper_pg_dir相同,指向的是内核空间。
但是每个进程的内核地址空间的页表并不是与主内核页表完全一致的,原因就是当vmalloc后修改了主内核页表,但是进程的页表并没有修改,当进程访问到这块虚拟地址空间时,进程的页目录项是空的,此时会产生一个缺页中断,在缺页中断中会先检查主内核页表中此项是否为空,如果不为空,则复制到进程的页表中。这样的最后结果就是进程页目录项的值与主内核页表完全一致,也就是指向了相同的页上级目录。结果最后只有访问到这段vmalloc区间的进程才会进行与主内核页表的更新,当然内核修改了主内核页表后一定要向所有CPU发送一个TLB刷新请求,因为有可能某个CPU上正在运行的进程对应的页表项保存在了TLB中。
而释放阶段就与进程没多大关系了,因为释放时主内核页表并不清空页上级目录、页中间目录和页表,只是单纯地清空页表项pte,而所有访问了此段vmalloc区间的进程除了页目录不同,它们的页上级目录、页中间目录、页表是同一个页框(可以简单理解为页目录中保存的目录项是一个指针,这些进程的vmalloc区间的目录项都指向了同一个页上级目录,同理页上级目录项又指向同一个页中间目录,页中间目录项又指向同一个页表),当页表项清空时实际上所有访问的进程的相应页表项都会被清空。
4、vmalloc
Vmalloc分配的虚拟地址空间则限于vmalloc_start与vmalloc_end之间。每一块vmalloc分配的内核虚拟内存都对应一个vm_struct结构体(这个结构体是通过kmalloc分配的,也就在0-896m物理内存中,可别和vm_area_struct搞混,那是进程虚拟内存区域的结构),不同的内核虚拟地址被4k大小的空闲区间隔,以防止越界。与进程虚拟地址的特性一样,这些虚拟地址与物理内存没有简单的位移关系,必须通过内核页表才可转换为物理地址或物理页。
Vmalloc区域并不和用户空间内存映射一样,通过page fault来装载页面的。vmalloc映射建立好后,逻辑地址,物理页面全部分配好,而且页表也已经更新好,只是此处为内核页表,并没有更新相关进程的页表。在vmalloc区发生page fault时,将“内核页表”同步到“进程页表”中。这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新。
在缺页异常的检查过程中,会判断这个地址所在空间,如果是在内核地址空间的VMALLOC区,那么内核会去内核页表中查看,该地址是不是在内核中有记录。如果有记录则把内核的addr对应的pmd项复制给进程的pmd项,意味着,进程和内核公用一个pte页表(感觉此处是x86_32系统下,只需要复制pmd,否则应该是还要复制pgd项和pud项,也就是说达成目的是只有页目录页框不同,页上级目录、页中间目录和页表的页框都是同一个)。
进程要vfree释放这个区域,其实修改的还是内核页表,会把addr对应的pte页表项设置为0。其它的都不做改变。那么当进程试图访问一个已经被释放区间的地址addr时候,由于它和内核对于addr的pmd项是一样的,所以,会继续去访问内核页表关于addr的pte页表,最后发现,pte页表项为0,又触发了缺页异常。
这次的缺页异常和上面分配的流程一样,只是最后对内核页表pte项做检查时候,发现内核页表关于addr的pte页表项是0,就会报错。这样就避免了进程的非法访问。Vmalloc缺页同步进程页表和vfree造成的缺页都在vmalloc_fault中:
/*
* 32-bit:
*
* Handle a fault on the vmalloc or module mapping area
由于使用vmalloc申请内存时,内核只会更新主内核页表,
所以当前使用的进程页表就有可能因为未与主内核页表同步导致这次异常的触发,
因此该函数试图将address对应的页表项与主内核页表进行同步
*/
static noinline __kprobes int vmalloc_fault(unsigned long address)
{//do_page_fault会调用
unsigned long pgd_paddr;
pmd_t *pmd_k;
pte_t *pte_k; /* Make sure we are in vmalloc area: /* 确定触发异常的地址是否处于VMALLOC区域*/
if (!(address >= VMALLOC_START && address < VMALLOC_END))
return -; WARN_ON_ONCE(in_nmi()); /*
* Synchronize this task's top level page-table
* with the 'reference' page table.
*
* Do _not_ use "current" here. We might be inside
* an interrupt in the middle of a task switch..
*/
/*获取pgd(最顶级页目录)地址,直接从CR3寄存器中读取。
*不要通过current获取,因为缺页异常可能在上下文切换的过程中发生,
*此时如果通过current获取,则可能会出问题
*/
pgd_paddr = read_cr3();//获取当前的PGD地址
pmd_k = vmalloc_sync_one(__va(pgd_paddr), address); //从主内核页表中,同步vmalloc区发生缺页异常地址对应的pmd
if (!pmd_k)
return -; /*到这里已经获取了内核页表对应于address的pmd,并且将该值设置给了当前使用页表的pmd,
最后一步就是判断pmd对应的pte项是否存在 如果同步后,相应的PTE还不存在,则说明该地址有问题引起的page_fault 此处也就是vfree,vfree把内核页表addr对应pte页表项设为0,进程试图访问addr时,
由于它和内核页表对于addr的pmd项一样,
所以,会继续访问内核页表关于addr的pte页表,最后发现pte页表项为0,缺页走到这里,避免进程非法访问
*/
pte_k = pte_offset_kernel(pmd_k, address); //获取pmd对应address的pte项
if (!pte_present(*pte_k)) //判断pte项是否存在,不存在则失败
return -; return ;
}
顺势贴一下缺页异常的处理:
/*
* This routine handles page faults. It determines the address,
* and the problem, and then passes it off to one of the appropriate
* routines.
*
缺页异常被触发原因:
1.程序设计的不当导致访问了非法的地址,不正常的,内核要采取各种可行的手段将这种异常带来的破坏减到最小
2.访问的地址是合法的,但是该地址还未分配物理页框,正常 用户空间的缺页异常可以分为两种情况--
1.触发异常的线性地址处于用户空间的vma中,但还未分配物理页,如果访问权限OK的话内核就给进程分配相应的物理页了
2.触发异常的线性地址不处于用户空间的vma中,这种情况得判断是不是因为用户进程的栈空间消耗完而触发的缺页异常,
如果是的话则在用户空间对栈区域进行扩展,并且分配相应的物理页,如果不是则作为一次非法地址访问来处理,内核将终结进程
*/
asmlinkage void __kprobes do_page_fault(struct pt_regs *regs,
unsigned long error_code,
unsigned long address)
{//
unsigned long vec;
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct * vma;
int fault;
int write = error_code & FAULT_CODE_WRITE;
unsigned int flags = (FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
(write ? FAULT_FLAG_WRITE : )); tsk = current;
mm = tsk->mm;//获取当前进程的地址空间
vec = lookup_exception_vector(); /*
* We fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.
*
* NOTE! We MUST NOT take any locks for this case. We may
* be in an interrupt or a critical region, and should
* only copy the information from the master page table,
* nothing more. 缺页地址位于内核空间。并不代表异常发生于内核空间,有可能是用户态访问了内核空间的地址 vmalloc返回值是高端内存虚拟地址,但其分配内存时,其实就已经分配了相应的物理内存,
并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,
内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,
发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新
*/
if (unlikely(fault_in_kernel_space(address))) { //判断address是否处于内核线性地址空间
if (vmalloc_fault(address) >= ) //处理vmalloc异常 将内核页表内容更新到进程页表的相应页表项
return;
if (notify_page_fault(regs, vec))
return; /*bad_area_nosemaphore表明这次异常是由于对非法的地址访问造成的
在内核中产生这样的结果的情况一般有两种:
1.内核通过用户空间传递的系统调用参数,访问了无效的地址. 内核尚且能通过异常修正机制来进行修复
2.内核的程序设计缺陷 导致OOPS错误了,内核将强制用SIGKILL结束当前进程
*/
bad_area_nosemaphore(regs, error_code, address);
return;
} if (unlikely(notify_page_fault(regs, vec)))
return; /* Only enable interrupts if they were on before the fault */
if ((regs->sr & SR_IMASK) != SR_IMASK)
local_irq_enable(); perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, , regs, address); /*
* If we're in an interrupt, have no user context or are running
* in an atomic region then we must not take the fault:
*/
if (unlikely(in_atomic() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
} retry:
down_read(&mm->mmap_sem); vma = find_vma(mm, address); //试图寻找到一个离address最近的vma,vma包含address或在address之后
/*没有找到这样的vma则说明address之后没有虚拟内存区域,因此该address肯定是无效的,
通过bad_area()路径来处理,bad_area()的主体就是__bad_area()-->bad_area_nosemaphore()*/
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
/*如果该地址包含在vma之中,则跳转到good_area处进行处理*/
if (likely(vma->vm_start <= address))
goto good_area;
/*不是前面两种情况的话,则判断是不是由于用户堆栈所占的页框已经使用完,而一个PUSH指令
引用了一个尚未和页框绑定的虚拟内存区域导致的一个异常,属于堆栈的虚拟内存区,其VM_GROWSDOWN位
被置位*/
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);//不是堆栈区域,则用bad_area()来处理
return;
}
if (unlikely(expand_stack(vma, address))) {//堆栈扩展不成功同样由bad_area()处理
bad_area(regs, error_code, address);
return;
} /*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
/*访问权限不够则通过bad_area_access_error()处理,该函数是对__bad_area()的封装,只不过
发送给用户进程的信号为SEGV_ACCERR*/
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address);
return;
} set_thread_fault_code(error_code); /*
* If for any reason at all we couldn't handle the fault,
* make sure we exit gracefully rather than endlessly redo
* the fault.
*/
fault = handle_mm_fault(mm, vma, address, flags);/*分配新的页表和页框*/ if (unlikely(fault & (VM_FAULT_RETRY | VM_FAULT_ERROR)))
if (mm_fault_error(regs, error_code, address, fault))
return; if (flags & FAULT_FLAG_ALLOW_RETRY) {
if (fault & VM_FAULT_MAJOR) {
tsk->maj_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, ,
regs, address);
} else {
tsk->min_flt++;
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, ,
regs, address);
}
if (fault & VM_FAULT_RETRY) {
flags &= ~FAULT_FLAG_ALLOW_RETRY;
flags |= FAULT_FLAG_TRIED; /*
* No need to up_read(&mm->mmap_sem) as we would
* have already released it in __lock_page_or_retry
* in mm/filemap.c.
*/
goto retry;
}
} up_read(&mm->mmap_sem);
}
5、vmalloc和kmalloc的区别
1、kmalloc对应于kfree,分配的内存处于3GB~high_memory之间,这段内核空间与物理内存的映射一一对应,可以分配连续的物理内存;vmalloc对应于vfree,分配的内存[j1] 在VMALLOC_START~4GB之间,分配连续的虚拟内存,但是物理上不一定连续。
2、vmalloc() 分配的物理地址无需连续,而kmalloc() 确保页在物理上是连续的 。
3、kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的。
4、最主要的区别是分配大小的问题,比如你需要28个字节,那一定用kmalloc,如果用vmalloc,分配不多次机器就罢工了。
尽管仅仅在某些情况下才需要物理上连续的内存块,但是,很多内核代码都调用kmalloc(),而不是用vmalloc()获得内存。这主要是出于性能的考虑。vmalloc()函数为了把物理上不连续的页面转换为虚拟地址空间上连续的页,必须专门建立页表项。还有,通过 vmalloc()获得的页必须一个一个的进行映射(因为它们物理上不是连续的),这就会导致比直接内存映射大得多的缓冲区刷新。因为这些原因,vmalloc()仅在绝对必要时才会使用,最典型的就是为了获得大块内存时,例如,当模块被动态插入到内核中时,就把模块装载到由vmalloc()分配的内存上。
源码都是Linux-3.10.1内核。书籍主要是《深入Linux内核架构》。以上来自网络部分侵删!!!
swapper_pg_dir主内核页表、init和kthreadd、do_fork时新建子进程页表、vmalloc与kmalloc的更多相关文章
- 编译linux内核前用make menuconfig设置时 Unable to find the ncurses ibraries的解决办法
今天在更新CentOS或者Ubuntu的内核时,执行make menuconfig可能看如这样的错误: *** Unable to find the ncurses libraries or the ...
- 《ucore lab5》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 加载应用程序并执行(需要编码) 题目 do_execv函数调用load_icode(位于kern/process/proc.c中) 来 ...
- 从如何使用到如何实现一个Promise
前言 这篇文章我们一起来学习如何使用Promise,以及如何实现一个自己的Promise,讲解非常清楚,全程一步一步往后实现,附带详细注释与原理讲解. 如果你觉的这篇文章有帮助到你,️关注+点赞️鼓励 ...
- python学习笔记——multiprocessing 多进程模块Process
系统自带的fork模块创建的多进程是基于Linux或Unix平台的,而window平台并不支持: python中的multiprocess为跨平台版本的多进程模块,支持子进程.通信和共享数据.执行不同 ...
- Linux-3.14.12内存管理笔记【建立内核页表(3)
前面已经分析了内核页表的准备工作以及内核低端内存页表的建立,接着回到init_mem_mapping()中,低端内存页表建立后紧随着还有一个函数early_ioremap_page_table_ran ...
- Linux-3.14.12内存管理笔记【建立内核页表(2)】-低端内存的建立
前面的前奏已经分析介绍了建立内核页表相关变量的设置准备,接下来转入正题分析内核页表的建立. 建立内核页表的关键函数init_mem_mapping(): [file:/arch/x86/mm/init ...
- 查看内核页表kernel_page_tables (aarch32)
作者 彭东林 pengdonglin137@163.com 平台 Linux-4.10.17 Qemu + vexpress-ca9 概述 通过配置内核,会在/sys/kernel/deb ...
- Linux内核分析——第三章 进程管理
第三章 进程管理 3.1 进程 1.进程就是处于执行期的程序:进程就是正在执行的程序代码的实时结果:进程是处于执行期的程序以及相关的资源的总称:进程包括代码段和其他资源. 线程:是在进程中活动的对象. ...
- linux内核数据结构学习总结
目录 . 进程相关数据结构 ) struct task_struct ) struct cred ) struct pid_link ) struct pid ) struct signal_stru ...
随机推荐
- HTTP Status 404(The requested resource is not available)的几种解决方案
1. 未部署Web应用 2.URL输入错误 排错方法:首先,查看URL的IP地址和端口号是否书写正确.其次,查看上下文路径是否正确 Project--------Properties--- ...
- monit检测语法
1.存在性检测 功能:检测文件或者服务不存在时进行相应的动作,默认是重启 语法: IF [DOES] NOT EXIST [[<X>] <Y> CYCLES] THEN ...
- MSF实现RID劫持和MSF实现PsExec执行命令
msf实现rid劫持 rid劫持原理: 每个帐户都有一个指定的RID来标识它.与域控制器不同,Windows工作站和服务器会将大部分数据存储在HKLM\SAM\SAM\Domains\Account\ ...
- golang 多个worker正常关闭的示例
代码如下,如有问题请联系 baibaibai_000@163.com package work_test import ( "math/rand" "runtime&qu ...
- hdu 1704 Rank (floyd闭包)
Rank Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submis ...
- JWT(Json Web Token—)的定义及组成
JWT定义及其组成 JWT(JSON Web Token)是一个非常轻巧的规范.这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息. 一个JWT实际上就是一个字符串,它由三部分组成,头部. ...
- C# 封装winio.dll 驱动级按键鼠标操作模拟
using System;using System.Collections.Generic;using System.Text;using System.Runtime.InteropServices ...
- 存储开头结尾使用begin tran,rollback tran作用?
BEGIN TRAN你就把它看成一个还原点,一般用在INSERT.UPDATE.DELETE等能改变数据操作前设置,如果操作后发现执行的结果和预期的不一样,就ROLLBACK,反之就COMMIT提交
- Django之get请求url的参数
当get网址是127.0.0.1:8000/mysite10这种类型的网址时 有两种方法: 1,在urls的路由的urlpatterns里面这样定义路由 re_path('^mysite(\d+)$' ...
- IDEA为了使用方便,需要改的几条配置
自动编译开关 在Eclipse中自动编译开关是开着的,如下所示那么,在IDEA中,务必要手动将其打开,非常重要! 忽略大小写开关 IDEA默认是匹配大小写,此开关如果未关.你输入字符一定要符合大小写. ...