缺页异常处理源码分析并实践

缺页异常处理源码分析并实践:

本文章是在阅读了相关博客、书本的前提下撰写的,是站在前人的肩膀上对所学内容的汇总,包含了部分个人理解。本文章会放出参考博客的链接。

一、缺页异常知识储备:

CPU访问到的是虚拟地址,通过MMU将虚拟地址转换成物理地址,而MMU上的这种虚拟地址和物理地址的转换关系是需要创建的==(怎么创建?通过触发缺页中断)==,当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,但那个物理页不可写的时候,MMU将会通知CPU产生了一个缺页异常。

在这里插入图片描述

而何时会触发缺页中断呢?在这里可以总结为以下四种情况:

在这里插入图片描述

  • MMU中无虚拟地址——>物理地址的映射关系;
  • linux并未真正给新创建的vma映射物理页,当使用malloc(库函数)/mmap(系统调用),访问物理空间
  • 使用fork()创建子进程后,父子进程任何一方要写相应物理页时,导致缺页异常的写时复制,才分配物理页面。

不到万不得以,Linux是不会轻易交出物理地址的;

本章节参考如下文章:https://zhuanlan.zhihu.com/p/583396235?

二、源码分析:

再有了一定的知识储备后,开始着手对内核中缺页异常源码的阅读和分析,主要是Linux0.11及Linux5.15中源码的分析;

2.1、linux0.11源码分析:

本部分参考了csdn中如下博主的链接:https://blog.csdn.net/THEANARKH/article/details/100549972?

2.1.1、源码展示:

以下为0.11相关源码的注释几自己理解:

void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;
	//取得线性地址对应页的页首地址,与0xfffff000即减去页偏移;
	address &= 0xfffff000;//线性地址对应页的页首地址
	//算出离代码段首地址的偏移;
	tmp = address - current->start_code;

	// tmp大于等于end_data说明是访问堆或者栈的空间时发生的缺页,直接申请一页
//如果缺页的是堆、栈的空间,则直接分配一页新的物理地址。
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);//直接申请一页,此时current->end_data > tmp+4kb;
		return;
	}

	// 是否有进程已经使用了
/*否则先判断是否有另一个进程和当前进程使用了同一个执行文件。
是的话,则判断是否可以共享*/
	if (share_page(tmp))
		return;

/*都不满足,则到硬盘中把一页内容加载到内存中,并且修改页表项内容*/
	// 获取一页,4kb
	/*get_free_page()从操作系统的内存池中分配一个空闲的物理页或虚拟页
	oom 通常是 "Out of Memory" 的缩写,表示内存耗尽的情况。
	在这里,它表示系统遇到了内存分配失败,无法继续正常运行。*/
	if (!(page = get_free_page()))
		oom();

/* remember that 1 block is used for header 一块用于开头*/
/*
	 算出要读的硬盘块号,但是最多读四块。
	 tmp/BLOCK_SIZE算出线性地址对应页的
	 页首地址离代码块距离了多少块,然后读取页首
	 地址对应的块号,所以需要加一。比如距离2块的距离,则
	 需要读取的块是第三块
	*/
	block = 1 + tmp/BLOCK_SIZE;//即当前线性地址对应的逻辑硬盘块号;
	for (i=0 ; i<4 ; block++,i++)// 查找文件前4块对应的物理硬盘号
		nr[i] = bmap(current->executable,block);// bmap算出逻辑块号对应的物理块号
	
	/*bread_page用于从磁盘上读取一个页面(page)的数据*/
	bread_page(page,current->executable->i_dev,nr);
	/*
	 tmp是小于end_data的,因为从tmp开始加载了4kb的数据,
     所以tmp+4kb(4096)后大于end_data,所以大于的部分需要清0,
	 i即超出的字节数
	*/
	i = tmp + 4096 - current->end_data;//i即超出的字节数
	tmp = page + 4096;
	// page是物理页首地址,加上4kb,从后往前清0
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;//将 tmp 指针所指的内存位置的值设置为 0
		/*首先,tmp 被强制类型转换为 char 指针,
		这是因为我们想要将这个位置视为一个字符(1 字节),然后将其值设置为 0。
		这通常用于清零内存区域,也可以用于将字符数组的内容清零。*/
	}

	// 建立线性地址和物理地址的映射
	if (put_page(page,address))//当页面不再被需要或引用时,相关的代码会调用put_page()函数来减少页面的引用计数。
		return;
	// 失败则释放刚才申请的物理页
	free_page(page);
	oom();
}
2.1.2、0.11源码分析:
  • 源码调用关系与分析:
    在这里插入图片描述

do_no_page是linux0.11源码中用于缺页中断具体处理函数;代码逻辑可以简化为三个if语句:

  • ①如果缺页的是堆、栈的空间,则直接分配一页新的物理地址。
  • ②否则先判断是否有另一个进程和当前进程使用了同一个执行文件。是的话,则判断是否可以共享。
  • ③都不满足,则到硬盘中把一页内容加载到内存中,并且修改页表项内容。

①中主要是使用get_empty_page()函数来直接获得一个新的物理地址,并把页对应的物理地址存储在页面项中。其中通过put_page()函数来建立物理地址和虚拟地址之间的关联;其中get_empty()函数以及put_page()函数的源码分析如下:

  • get_empty_page():
/*给address分配一个新的页,并且把页对应的物理地址存储在页面项中*/
void get_empty_page(unsigned long address)
{
	unsigned long tmp;

	if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
		free_page(tmp);		/* 0 is ok - ignored */
		oom();
	}
}
  • put_page():
/*
 * This function puts a page in memory at the wanted address.
 * It returns the physical address of the page gotten, 0 if
 * out of memory (either when trying to access page-table or
 * page.)
 */
 /*page是物理地址,address是线性地址。
 建立物理地址和线性地址的关联,即给页表和页目录项赋值*/
unsigned long put_page(unsigned long page,unsigned long address)
{
	unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

	/*用于检查一个页面(通常是虚拟内存页)的地址是否在合法的内存范围内,
	以确保不会出现非法内存访问。*/
	if (page < LOW_MEM || page >= HIGH_MEMORY)
		printk("Trying to put page %p at %p\n",page,address);

	
	/*mem_map 是一个指向页表的指针数组,
	用于跟踪系统中每个页面的状态和分配情况。
	当内核需要分配一个物理页面用于存储数据时,
	它可以使用 mem_map 来查找空闲页面并标记它们为已分配状态。
	当页面不再需要时,内核可以将其标记为空闲,以供将来的分配。*/
	/*mem_map 还可以用于建立页帧号(通常是页面在物理内存中的索引)
	与实际物理地址之间的映射关系。这对于内核来说是很重要的,
	因为它需要知道页面在物理内存中的位置*/
	//page对应的物理页面没有被分配则说明有问题;
	if (mem_map[(page-LOW_MEM)>>12] != 1)
		printk("mem_map disagrees with %p at %p\n",page,address);
	/*计算页目录项的偏移地址,页目录首地址在物理地址0处。
	这里算出偏移地址后,就是绝对地址,与0xffc即四字节对齐*/
	page_table = (unsigned long *) ((address>>20) & 0xffc);
	
	if ((*page_table)&1)// 页目录项已经指向了一个有效的页表;
		//算出页表首地址,*page_table的高20位是有效地址
		page_table = (unsigned long *) (0xfffff000 & *page_table);
	else {
		//页目录项还没有指向有效的页表,分配一个新的物理页
		if (!(tmp=get_free_page()))
			return 0;
		//把页表地址写到页目录项,tmp为页表的物理地址,或7代表页面是用户级、可读、写、执行、有效
		*page_table = tmp|7;
		// 页目录项指向页表的物理地址
		page_table = (unsigned long *) tmp;
	}
	/* 
		address是32位,右移12为变成20位,再与3ff就是取得低10位,
		即address在页表中的索引,或7代表该页面是用户级、可读、写、执行、有效
	*/
	page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate */
	return page;
}

②中主要是通过share_page()函数来判断当前位置的可执行文件是否与其他进程共享;其代码逻辑是依次判断:

  • 当前进程是否有可执行文件;
  • 当前进程的可执行文件是否已共享;
  • 当前进程的可执行文件已共享,则找到不是当前进程,但执行了该可执行文件的进程,并通过try_to_share()去共享该可执行文件的地址;

以下是两个代码的注释:

  • share_page():
/*
 * share_page() tries to find a process that could share a page with
 * the current one. Address is the address of the wanted page relative
 * to the current data space.
 *
 * We first check if it is at all feasible by checking executable->i_count.
 * It should be >1 if there are other tasks sharing this inode.
 */
// 判断有没有多个进程执行了同一个可执行文件 
static int share_page(unsigned long address)
{
	struct task_struct ** p;

	if (!current->executable)//如通过当前进程没有可执行文件,则退出
		return 0;
	/*i_count 通常是一个表示文件引用计数的字段。
	这个字段用于跟踪有多少个进程正在共享同一个文件*/
	if (current->executable->i_count < 2)//未共享文件;
		return 0;
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)
			continue;
		if (current == *p)
			continue;
		if ((*p)->executable != current->executable)
			continue;
		/*找到一个不是当前进程,但都执行了同一个可执行文件的进程*/
		//不同进程,但执行了同一个可执行文件;
		if (try_to_share(address,*p))//判断是否可以共享;
			return 1;
	}
	return 0;
}
  • try_to_share():
/*
 * try_to_share() checks the page at address "address" in the task "p",
 * to see if it exists, and if it is clean. If so, share it with the current
 * task.
 *
 * NOTE! This assumes we have checked that p != current, and that they
 * share the same executable.
 */
// 使得另一个进程的页目录和页表项指向另一个进程的正在使用的物理地址
static int try_to_share(unsigned long address, struct task_struct * p)
{
	unsigned long from;
	unsigned long to;
	unsigned long from_page;
	unsigned long to_page;
	unsigned long phys_addr;
	/*
		address是距离start_code的偏移。这里算出这个距离跨了多少个页目录项,
		然后加上start_code的页目录偏移就得到address在页目录里的绝对偏移
	*/
	from_page = to_page = ((address>>20) & 0xffc);
	// p进程的代码开始地址(线性地址),取得p进程的页目录项地址,再加上address算出的偏移
	from_page += ((p->start_code>>20) & 0xffc);
	// 取得当前进程的页目录项地址,页目录物理地址是0,所以这里就是该地址对应的页目录项的物理地址
	to_page += ((current->start_code>>20) & 0xffc);
/* is there a page-directory at from? */
	// from是页表的物理地址和标记位
	from = *(unsigned long *) from_page;
	// 没有指向有效的页表则返回
	if (!(from & 1))
		return 0;
	// 取出页表地址
	from &= 0xfffff000;
	// 算出address对应的页表项地址,((address>>10) & 0xffc)算出页表项偏移,0xffc说明是4字节对齐
	from_page = from + ((address>>10) & 0xffc);
	// 页表项的内容,包括物理地址和标记位信息
	phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
	// 是否有效和是否是脏的,如果不是有效并且干净的则返回
	if ((phys_addr & 0x41) != 0x01)
		return 0;
	// 取出物理地址的页首地址
	phys_addr &= 0xfffff000;
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;
	// 目的页目录项内容
	to = *(unsigned long *) to_page;
	// 目的页目录项是否指向有效的页表
	if (!(to & 1))
		// 没有则新分配一页,并初始化标记位
		if (to = get_free_page())
			*(unsigned long *) to_page = to | 7;
		else
			oom();
	// 取得页表地址
	to &= 0xfffff000;
	// 取得address对应的页表项地址
	to_page = to + ((address>>10) & 0xffc);
	// 是否指向了有效的物理页,是的话说明不需要再建立线性地址到物理地址的映射了
	if (1 & *(unsigned long *) to_page)
		panic("try_to_share: to_page already exists");
/* share them: write-protect */
	// 标记位不可写
	*(unsigned long *) from_page &= ~2;
	// 把address对应的源页表项内容复制到目的页表项中
	*(unsigned long *) to_page = *(unsigned long *) from_page;
	// 使tlb失效
	invalidate();
	// 算出页数,物理页引用数加一
	phys_addr -= LOW_MEM;
	phys_addr >>= 12;
	mem_map[phys_addr]++;
	return 1;
}

③都不满足,则到硬盘中把一页内容加载到内存中,并且修改页表项内容并通过put_page()函数建立物理地址和虚拟地址之间的关联。主要涉及到get_free_page()、bmap()、bread_page()函数:

  • get_free_page()从操作系统的内存池中分配一个空闲的物理页或虚拟页;
  • bmap算出逻辑块号对应的物理块号;
  • bread_page()用于从磁盘上读取一个页面(page)的数据;
2.1.3、阅读0.11中缺页中断处理源码小结:

在自己虚拟机中结合CSDN相关博客阅读了linux-0.11源码中的相关文件之后,真切的感受到了最原始的缺页中断处理思想,其实就是三个if判断加上一些处理函数。在宏观上对缺页中断处理有了一定的理解,但是对于各个函数细节仍然存在不懂、不理解的地方,对于更细节的代码逻辑仍有疑惑,需要反复看反复学。

2.2、linux5.15源码分析:

本部分参考了知乎中如下文章:https://zhuanlan.zhihu.com/p/583396235;

在这里插入图片描述

2.2.1、源码展示:
(1)、do_page_fault:

该代码被记录在arch/arm/mm/fault.c文件中;

static int __kprobes//静态函数,用于处理页面错误
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
	/*参数:
	1、unsigned long addr:页面错误发生的地址;
	2、unsigned int fsr:页面错误的状态寄存器;
	3、struct pt_regs *regs:包含进程寄存器状态的结构
	*/
	struct task_struct *tsk;
	struct mm_struct *mm;
	int sig, code;
	vm_fault_t fault;
	unsigned int flags = FAULT_FLAG_DEFAULT;

	/*调用kprobe_page_fault,检查是否有与 Kprobes(内核探针)相关的页面错误*/
	if (kprobe_page_fault(regs, fsr))
		return 0;

	tsk = current;//tsk指向当前进程task_struct结构;
	mm  = tsk->mm;//mm指向当前进程的内存地址管理结构体;

	/* Enable interrupts if they were enabled in the parent context. */
	/*检查在父上下文中是否启用了中断*/
	if (interrupts_enabled(regs))//why??????????????????
		local_irq_enable();//调用local_irq_enable() 来启用中断;

	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	 /*如果处于中断上下文或者没有用户上下文,
	 那么不应该处理页面错误,会跳转到 no_context 标签。*/
	if (faulthandler_disabled() || !mm)//判断当前状态;
		goto no_context;

	/*检查当前进程是否在用户模式下运行,
	如果是,将 FAULT_FLAG_USER 标志设置在 flags 变量中。
	这表示页面错误是在用户模式下发生的。*/
	if (user_mode(regs))
		flags |= FAULT_FLAG_USER;//falgs置位

	/*检查页面错误是否是写错误(写操作引起的错误)而不是上下文切换错误。
	如果是,将 FAULT_FLAG_WRITE 标志设置在 flags 变量中。*/
	if ((fsr & FSR_WRITE) && !(fsr & FSR_CM))//fsr是传入的参数,表示页面错误的状态寄存器
		flags |= FAULT_FLAG_WRITE;//falgs置位

	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);//记录性能事件,用于跟踪页面错误的发生

	/*
	 * As per x86, we may deadlock here.  However, since the kernel only
	 * validly references user space from well defined areas of the code,
	 * we can bug out early if this is from code which shouldn't.
	 */
	 /*处理页面错误引发的情况,包括加锁*/
	if (!mmap_read_trylock(mm)) {
		/*如果无法获取内存映射区的读锁,
		它会尝试再次获取锁或者根据特定条件跳转到 no_context 标签
		这些锁和条件检查用于确保页面错误的处理不会导致死锁或不一致的情况*/
		if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))
			goto no_context;

	/*调用 might_sleep() 来标记可能引发睡眠的情况。
	此行为通常用于调试和验证。*/	
retry:
		mmap_read_lock(mm);//尝试获取内存映射区的读锁,确保在处理页面错误时对内存映射区的访问是同步的
	} else {
		/*
		 * The above down_read_trylock() might have succeeded in
		 * which case, we'll have missed the might_sleep() from
		 * down_read()
		 */
		 /*might_sleep()这个函数用于标记可能引发睡眠的情况
		 它用于调试和验证,以确保在合适的情况下发出警告*/
		might_sleep();
#ifdef CONFIG_DEBUG_VM
		/*在调试 VM 并且不在用户模式下,且未找到异常表项时,
		跳转到 no_context 标签。这意味着如果在内核模式下,
		或者找到了异常处理表项,将不跳转,继续执行后续代码。*/
		if (!user_mode(regs) &&
		    !search_exception_tables(regs->ARM_pc))
			goto no_context;
#endif
	}
/*!!!!!调用 __do_page_fault 函数来实际处理页面错误,并将结果存储在 fault 变量中。!!!!!!!!!!*/
	fault = __do_page_fault(mm, addr, fsr, flags, tsk, regs);

	/* If we need to retry but a fatal signal is pending, handle the
	 * signal first. We do not need to release the mmap_lock because
	 * it would already be released in __lock_page_or_retry in
	 * mm/filemap.c. */
	 /*处理在页面错误处理期间可能发生的信号处理:*/
	 /*用于检查在页面错误处理期间是否出现了挂起的致命信号。
	 如果是这样,将首先处理信号,然后跳到 no_context 标签。*/
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))//若在内核态,同样跳到 no_context 标签。
			goto no_context;
		return 0;
	}

	/*接下来的条件检查处理页面错误的重试。如果页面错误没有错误标志(VM_FAULT_ERROR),
	 *并且 flags 变量中包含 FAULT_FLAG_ALLOW_RETRY 标志,那么进入下面的条件块:*/
	if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
		if (fault & VM_FAULT_RETRY) {
			flags |= FAULT_FLAG_TRIED;//将 FAULT_FLAG_TRIED 标志设置在 flags 变量中,表示已经尝试过一次。
			goto retry;
		}
	}
	/*无论是成功处理页面错误还是进行了重试,
	 *都需要释放之前获取的内存映射区的读锁,以解锁对内存映射区的访问。*/
	mmap_read_unlock(mm);

	/*
	 * Handle the "normal" case first - VM_FAULT_MAJOR
	 *首先,代码检查了页面错误的一些常见情况,即 "VM_FAULT_MAJOR"。
	 *通过检查 fault 变量中的位掩码,如果页面错误没有错误标志(VM_FAULT_ERROR),
	 *不在坏映射(VM_FAULT_BADMAP)和不在坏访问(VM_FAULT_BADACCESS)情况下,
	 *表示这是一种"正常"情况,直接返回0,不需要进一步处理。
	 */
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
		return 0;

	/*
	 * If we are in kernel mode at this point, we
	 * have no context to handle this fault with.
	 *如果不在用户模式(!user_mode(regs))下,
	 *说明内核没有足够的上下文来处理页面错误,
	 *因此跳转到 no_context 标签。
	 *这是因为内核模式下的错误处理会有不同的要求,
	 *可能需要特殊处理。
	 */
	if (!user_mode(regs))
		goto no_context;

	if (fault & VM_FAULT_OOM) {
		/*
		 * We ran out of memory, call the OOM killer, and return to
		 * userspace (which will retry the fault, or kill us if we
		 * got oom-killed)
		 *如果页面错误包含 VM_FAULT_OOM 标志,表示系统内存耗尽,
		 *这时会调用 "OOM killer"(内存耗尽杀手)并返回用户空间。
		 *用户空间将在这里重试页面错误,或者如果进程已被杀死,则终止当前进程。
		 */
		pagefault_out_of_memory();
		return 0;
	}

	if (fault & VM_FAULT_SIGBUS) {
		/*
		 * We had some memory, but were unable to
		 * successfully fix up this page fault.
		 *如果页面错误包含 VM_FAULT_SIGBUS 标志,
		 *表示虽然有一些内存,但无法成功修复此页面错误。
		 *在这种情况下,会设置 sig 为 SIGBUS,并设置 code 为 BUS_ADRERR。
		 */
		sig = SIGBUS;
		code = BUS_ADRERR;
	} else {
		/*
		 * Something tried to access memory that
		 * isn't in our memory map..
		 *否则,如果页面错误不包含 VM_FAULT_SIGBUS 标志,
		 *这意味着尝试访问不在内存映射中的内存,
		 *这是典型的"段错误"(SIGSEGV)情况。在这种情况下,
		 *会将 sig 设置为 SIGSEGV,并根据 fault 的不同值设置 code。
		 *如果 fault 是 VM_FAULT_BADACCESS,则 code 设置为 SEGV_ACCERR,
		 *否则设置为 SEGV_MAPERR。
		 */
		sig = SIGSEGV;
		code = fault == VM_FAULT_BADACCESS ?
			SEGV_ACCERR : SEGV_MAPERR;
	}
	/*
	 *根据前面的设置,会调用__do_user_fault或__do_kernel_fault处理页面错误。
	 *如果在用户模式下,会调用 __do_user_fault 来触发用户空间的信号处理程序。
	 *如果没有足够的上下文来处理错误,会跳转到 no_context 标签,
	 *然后调用 __do_kernel_fault 来处理错误。
	 */
	__do_user_fault(addr, fsr, sig, code, regs);
	return 0;

no_context:
	__do_kernel_fault(mm, addr, fsr, regs);
	return 0;
}static int __kprobes//静态函数,用于处理页面错误

/*重点函数*/
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
	/*参数:
	1、unsigned long addr:页面错误发生的地址;
	2、unsigned int fsr:页面错误的状态寄存器;
	3、struct pt_regs *regs:包含进程寄存器状态的结构
	*/
	struct task_struct *tsk;
	struct mm_struct *mm;
	int sig, code;
	vm_fault_t fault;
	unsigned int flags = FAULT_FLAG_DEFAULT;

	/*调用kprobe_page_fault,检查是否有与 Kprobes(内核探针)相关的页面错误*/
	if (kprobe_page_fault(regs, fsr))
		return 0;

	tsk = current;//tsk指向当前进程task_struct结构;
	mm  = tsk->mm;//mm指向当前进程的内存地址管理结构体;

	/* Enable interrupts if they were enabled in the parent context. */
	/*检查在父上下文中是否启用了中断*/
	if (interrupts_enabled(regs))//why??????????????????
		local_irq_enable();//调用local_irq_enable() 来启用中断;

	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	 /*如果处于中断上下文或者没有用户上下文,
	 那么不应该处理页面错误,会跳转到 no_context 标签。*/
	if (faulthandler_disabled() || !mm)//判断当前状态;
		goto no_context;

	/*检查当前进程是否在用户模式下运行,
	如果是,将 FAULT_FLAG_USER 标志设置在 flags 变量中。
	这表示页面错误是在用户模式下发生的。*/
	if (user_mode(regs))
		flags |= FAULT_FLAG_USER;//falgs置位

	/*检查页面错误是否是写错误(写操作引起的错误)而不是上下文切换错误。
	如果是,将 FAULT_FLAG_WRITE 标志设置在 flags 变量中。*/
	if ((fsr & FSR_WRITE) && !(fsr & FSR_CM))//fsr是传入的参数,表示页面错误的状态寄存器
		flags |= FAULT_FLAG_WRITE;//falgs置位

	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);//记录性能事件,用于跟踪页面错误的发生

	/*
	 * As per x86, we may deadlock here.  However, since the kernel only
	 * validly references user space from well defined areas of the code,
	 * we can bug out early if this is from code which shouldn't.
	 */
	 /*处理页面错误引发的情况,包括加锁*/
	if (!mmap_read_trylock(mm)) {
		/*如果无法获取内存映射区的读锁,
		它会尝试再次获取锁或者根据特定条件跳转到 no_context 标签
		这些锁和条件检查用于确保页面错误的处理不会导致死锁或不一致的情况*/
		if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))
			goto no_context;

	/*调用 might_sleep() 来标记可能引发睡眠的情况。
	此行为通常用于调试和验证。*/	
retry:
		mmap_read_lock(mm);//尝试获取内存映射区的读锁,确保在处理页面错误时对内存映射区的访问是同步的
	} else {
		/*
		 * The above down_read_trylock() might have succeeded in
		 * which case, we'll have missed the might_sleep() from
		 * down_read()
		 */
		 /*might_sleep()这个函数用于标记可能引发睡眠的情况
		 它用于调试和验证,以确保在合适的情况下发出警告*/
		might_sleep();
#ifdef CONFIG_DEBUG_VM
		/*在调试 VM 并且不在用户模式下,且未找到异常表项时,
		跳转到 no_context 标签。这意味着如果在内核模式下,
		或者找到了异常处理表项,将不跳转,继续执行后续代码。*/
		if (!user_mode(regs) &&
		    !search_exception_tables(regs->ARM_pc))
			goto no_context;
#endif
	}
/*!!!!!调用 __do_page_fault 函数来实际处理页面错误,并将结果存储在 fault 变量中。!!!!!!!!!!*/
	fault = __do_page_fault(mm, addr, fsr, flags, tsk, regs);

	/* If we need to retry but a fatal signal is pending, handle the
	 * signal first. We do not need to release the mmap_lock because
	 * it would already be released in __lock_page_or_retry in
	 * mm/filemap.c. */
	 /*处理在页面错误处理期间可能发生的信号处理:*/
	 /*用于检查在页面错误处理期间是否出现了挂起的致命信号。
	 如果是这样,将首先处理信号,然后跳到 no_context 标签。*/
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))//若在内核态,同样跳到 no_context 标签。
			goto no_context;
		return 0;
	}

	/*接下来的条件检查处理页面错误的重试。如果页面错误没有错误标志(VM_FAULT_ERROR),
	 *并且 flags 变量中包含 FAULT_FLAG_ALLOW_RETRY 标志,那么进入下面的条件块:*/
	if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
		if (fault & VM_FAULT_RETRY) {
			flags |= FAULT_FLAG_TRIED;//将 FAULT_FLAG_TRIED 标志设置在 flags 变量中,表示已经尝试过一次。
			goto retry;
		}
	}
	/*无论是成功处理页面错误还是进行了重试,
	 *都需要释放之前获取的内存映射区的读锁,以解锁对内存映射区的访问。*/
	mmap_read_unlock(mm);

	/*
	 * Handle the "normal" case first - VM_FAULT_MAJOR
	 *首先,代码检查了页面错误的一些常见情况,即 "VM_FAULT_MAJOR"。
	 *通过检查 fault 变量中的位掩码,如果页面错误没有错误标志(VM_FAULT_ERROR),
	 *不在坏映射(VM_FAULT_BADMAP)和不在坏访问(VM_FAULT_BADACCESS)情况下,
	 *表示这是一种"正常"情况,直接返回0,不需要进一步处理。
	 */
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
		return 0;

	/*
	 * If we are in kernel mode at this point, we
	 * have no context to handle this fault with.
	 *如果不在用户模式(!user_mode(regs))下,
	 *说明内核没有足够的上下文来处理页面错误,
	 *因此跳转到 no_context 标签。
	 *这是因为内核模式下的错误处理会有不同的要求,
	 *可能需要特殊处理。
	 */
	if (!user_mode(regs))
		goto no_context;

	if (fault & VM_FAULT_OOM) {
		/*
		 * We ran out of memory, call the OOM killer, and return to
		 * userspace (which will retry the fault, or kill us if we
		 * got oom-killed)
		 *如果页面错误包含 VM_FAULT_OOM 标志,表示系统内存耗尽,
		 *这时会调用 "OOM killer"(内存耗尽杀手)并返回用户空间。
		 *用户空间将在这里重试页面错误,或者如果进程已被杀死,则终止当前进程。
		 */
		pagefault_out_of_memory();
		return 0;
	}

	if (fault & VM_FAULT_SIGBUS) {
		/*
		 * We had some memory, but were unable to
		 * successfully fix up this page fault.
		 *如果页面错误包含 VM_FAULT_SIGBUS 标志,
		 *表示虽然有一些内存,但无法成功修复此页面错误。
		 *在这种情况下,会设置 sig 为 SIGBUS,并设置 code 为 BUS_ADRERR。
		 */
		sig = SIGBUS;
		code = BUS_ADRERR;
	} else {
		/*
		 * Something tried to access memory that
		 * isn't in our memory map..
		 *否则,如果页面错误不包含 VM_FAULT_SIGBUS 标志,
		 *这意味着尝试访问不在内存映射中的内存,
		 *这是典型的"段错误"(SIGSEGV)情况。在这种情况下,
		 *会将 sig 设置为 SIGSEGV,并根据 fault 的不同值设置 code。
		 *如果 fault 是 VM_FAULT_BADACCESS,则 code 设置为 SEGV_ACCERR,
		 *否则设置为 SEGV_MAPERR。
		 */
		sig = SIGSEGV;
		code = fault == VM_FAULT_BADACCESS ?
			SEGV_ACCERR : SEGV_MAPERR;
	}
	/*
	 *根据前面的设置,会调用__do_user_fault或__do_kernel_fault处理页面错误。
	 *如果在用户模式下,会调用 __do_user_fault 来触发用户空间的信号处理程序。
	 *如果没有足够的上下文来处理错误,会跳转到 no_context 标签,
	 *然后调用 __do_kernel_fault 来处理错误。
	 */
	__do_user_fault(addr, fsr, sig, code, regs);
	return 0;

no_context:
	__do_kernel_fault(mm, addr, fsr, regs);
	return 0;
}

在使用了chatgpt代码解读相关功能后,对于do_page_fault()函数有了初步的理解;将其总结为如下几点:

  • 不同的条件逻辑判断;
  • 如果是内核态出了异常,跳到no_context,进入内核态异常处理,由_do_kernel_fault 来处理内核错误;
  • 如果不是内核的缺页异常而是用户进程的缺页异常,调用__do_page_fault()函数来实际处理页面错误,并将结果存储在 fault 变量中;
  • 不同的条件逻辑判断;
  • __do_user_fault()函数触发用户空间的信号处理程序;
(2)、__do_page_fault():

在do_page_fault()函数中,而是用户进程的缺页异常,那么调用__do_page_fault()内部函数来进行实际处理页面错误,该代码被记录在arch/arm/mm/fault.c文件中;为了方便理解,这里先放上用户态缺页异常的几种情况:

在这里插入图片描述

static vm_fault_t __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
		unsigned int flags, struct task_struct *tsk,
		struct pt_regs *regs)
	/*内存管理结构 mm,访问的地址 addr,错误码 fsr,
	标志 flags,进程的任务结构 tsk,处理页面错误时使用的寄存器 regs。*/
{
	
	struct vm_area_struct *vma;//
	vm_fault_t fault;
	
	/*搜索出现异常的地址前向最近的的vma
	 函数开始通过调用 find_vma 函数查找
	 与给定地址 addr 相关的虚拟内存区域
	 结构 vma;
	 *如果没有找到匹配的 vma,
	 则将 fault 设置为 VM_FAULT_BADMAP,
	 表示遇到了坏的内存映射,然后跳转到 out 标签。
	 */
	vma = find_vma(mm, addr);
	fault = VM_FAULT_BADMAP;
/*情况1:如果vma为NULL,说明addr之后没有vma,所以这个addr是个错误地址*/
	if (unlikely(!vma))
		goto out;

	
/*情况2:如果addr后面有vma,但不包含addr,不能断定addr是错误地址,还需检查*/
	/*如果找到了匹配的 vma,则检查该 vma 的
	 起始地址(vma->vm_start)是否大于访问的
	 地址 addr。
	 *如果是,说明地址addr不在这个 vma 的范围内,
	 因此跳转到 check_stack 标签,进一步检
	 查是否需要扩展用户栈。
	 */
	if (unlikely(vma->vm_start > addr))
		goto check_stack;

	/*
	 * Ok, we have a good vm_area for this
	 * memory access, so we can handle it.
	 *如果地址在合法的 vma 范围内,进入 good_area 
	 *标签。这表示已经找到了适当的虚拟内存区域,
	 *可以继续处理页面错误。
	 */
good_area:
	/*
	 *权限错误也要返回,比如缺页报错(由参数fsr标识)报的是不可写/不可执行的错误,
	 但addr所属vma线性区本身就不可写/不可执行,那么就直接返回,因为问题根本不是缺页,
	 而是vma就已经有问题。
	 *调用 access_error 函数,以错误码 fsr 和 vma 为参数,来检查是否存在访问错误。
	 如果存在访问错误,将 fault 设置为 VM_FAULT_BADACCESS,表示发生了坏的访问错误,
	 然后跳转到 out 标签。
	 */
	if (access_error(fsr, vma)) {
		fault = VM_FAULT_BADACCESS;
		goto out;
	}
	/*
	 *为引发缺页的进程分配一个物理页框,
	 *它先确定与引发缺页的线性地址对应的各级页目录项是否存在,
	 *如不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault完成的
	 */
	return handle_mm_fault(vma, addr & PAGE_MASK, flags, regs);

check_stack:
	/* Don't allow expansion below FIRST_USER_ADDRESS */
	/*在 check_stack 标签下,代码检查了 vma 的flags标志,如果 
	vma 允许用户栈向下扩展(VM_GROWSDOWN),并且访问的地址
	在用户空间的地址范围 FIRST_USER_ADDRESS 之上,且通过 
	expand_stack 函数成功扩展了用户栈,那么就跳转到 
	good_area 标签,继续处理页面错误。*/
	if (vma->vm_flags & VM_GROWSDOWN &&
	    addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
		goto good_area;
out:
	return fault;
}
  • 首先,查看缺页异常的这个虚拟地址addr,找它后面最近的vma,如果真的没有找到,那么说明访问的地址是真的错误了,因为它根本不在所分配的任何一个vma线性区;这是一种严重错误,将返回错误码(fault) VM_FAULT_BADMAP,内核会杀掉这个进程;
  • 如果addr后面有vma,但addr并未落在这个vma的区间内,这存在一种可能,要知道栈的增长方向和堆是相反的即栈是向下增长,所以也许addr实际上是栈的一个地址,它后面的vma实际上是栈的vma,栈已无法扩展,即访问addr时,这个addr并没有落在vma中,所以更无二级页表映射,导致缺页异常。所以进入check_stack标签,查看addr后面的vma是否是向下增长并且栈是否无法扩展,以此界定addr是不是栈地址,如果是则进入缺页异常处理流程(goto good_area;),否则同样返回错误码(fault)VM_FAULT_BADMAP,内核会杀掉这个进程;
  • 权限错误也要返回,比如缺页报错(fsr)报的是不可写,但vma本身就不可写,那么就直接返回,因为问题根本不是缺页,而是vma就已经有问题;返回错误码(fault) VM_FAULT_BADACCESS,这也是一种严重错误,内核会杀掉这个进程;
  • ==最后是对确实缺页异常的情况进行处理,调用函数handle_mm_fault,==正常情况下将返回VM_FAULT_MAJOR或VM_FAULT_MINOR,返回错误码fault并加一task的maj_flt或min_flt成员 ;

在这里插入图片描述

(3)、handle_mm_fault():

handle_mm_fault,就是为引发缺页的进程分配一个物理页框。

  • 如果是大页面,调用 hugetlb_fault函数处理页面错误
  • 如果不是大页面,调用 __handle_mm_fault 函数来处理。
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
			   unsigned int flags, struct pt_regs *regs)
{
	vm_fault_t ret;
	//调用 __set_current_state将当前进程状态设置为 TASK_RUNNING
	__set_current_state(TASK_RUNNING);
	/*调用count_vm_event和count_memcg_event_mm
	 增加相应的事件计数器,用于跟踪页面错误的发生*/
	count_vm_event(PGFAULT);
	count_memcg_event_mm(vma->vm_mm, PGFAULT);

	/* do counter updates before entering really critical section.
	   调用 check_sync_rss_stat(current) 更新计数器;
	 */
	check_sync_rss_stat(current);

	/*通过 arch_vma_access_permitted 函数检查对虚拟内存区域的访问权限。
	如果权限不被允许,函数返回 VM_FAULT_SIGSEGV,表示发生了段错误(Segmentation Fault)。*/
	if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
					    flags & FAULT_FLAG_INSTRUCTION,
					    flags & FAULT_FLAG_REMOTE))
		return VM_FAULT_SIGSEGV;

	/*
	 * Enable the memcg OOM handling for faults triggered in user
	 * space.  Kernel faults are handled more gracefully.
	 */
	/*如果页面错误发生在用户空间(flags & FAULT_FLAG_USER 为真),
	 *函数调用 mem_cgroup_enter_user_fault() 启用内存控制组(memory cgroup)的OOM(Out-of-Memory)处理机制。
	 *OOM处理机制用于处理内存不足的情况,确保系统可以更加优雅地处理内存分配失败的情况。
	 */
	if (flags & FAULT_FLAG_USER)
		mem_cgroup_enter_user_fault();//处理内存不足的情况

	if (unlikely(is_vm_hugetlb_page(vma)))//函数检查虚拟内存区域是否是大页面(Huge Page)
		ret = hugetlb_fault(vma->vm_mm, vma, address, flags);//如果是大页面,调用 hugetlb_fault 函数处理页面错误
	else
		ret = __handle_mm_fault(vma, address, flags);//否则调用 __handle_mm_fault 函数来处理。

	/*页面错误发生在用户空间,
	函数调用 mem_cgroup_exit_user_fault() 退出内存控制组的OOM处理。*/
	if (flags & FAULT_FLAG_USER) {
		mem_cgroup_exit_user_fault();
		/*
		 * The task may have entered a memcg OOM situation but
		 * if the allocation error was handled gracefully (no
		 * VM_FAULT_OOM), there is no need to kill anything.
		 * Just clean up the OOM state peacefully.
		 *检查当前任务是否处于内存控制组的OOM状态,
		 *并且之前的分配错误被优雅地处理(!(ret & VM_FAULT_OOM))
		 *如果是这样,函数调用 mem_cgroup_oom_synchronize(false)
		 *来清除OOM状态,确保系统可以平稳地恢复。
		 */
		if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
			mem_cgroup_oom_synchronize(false);//清除OOM状态,确保系统可以平稳地恢复。
	}
	
	/*用 mm_account_fault 更新页面错误的计数器,并返回错误码 ret,表示处理页面错误的结果。*/
	mm_account_fault(regs, address, flags, ret);

	return ret;
}
(4)、__handle_mm_fault():
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
		unsigned long address, unsigned int flags)
{
	struct vm_fault vmf = {
		.vma = vma,
		.address = address & PAGE_MASK,
		.flags = flags,
		.pgoff = linear_page_index(vma, address),
		.gfp_mask = __get_fault_gfp_mask(vma),
	};
	unsigned int dirty = flags & FAULT_FLAG_WRITE;//从 flags 中提取出错误标志 FAULT_FLAG_WRITE,用于检查页面错误是否是写错误。
	struct mm_struct *mm = vma->vm_mm;

	/*指向页表项的指针,用于管理虚拟内存的页表*/
	pgd_t *pgd;
	p4d_t *p4d;
	vm_fault_t ret;

	/*获取用于映射虚拟地址到物理内存地址的页表项*/
	pgd = pgd_offset(mm, address);//根据虚拟内存地址 (address) 获取一级页表项 (pgd)
	p4d = p4d_alloc(mm, pgd, address);//根据一级页表项获取四级页表项 (p4d)
	if (!p4d)
		return VM_FAULT_OOM;

	vmf.pud = pud_alloc(mm, p4d, address);//分配一个指向二级页表项 (pud) 的指针
	if (!vmf.pud)
		return VM_FAULT_OOM;

retry_pud:
	/*
	 *如果 pud_none(*vmf.pud) 且启用了透明大页面 (THP) (__transparent_hugepage_enabled(vma)),
	 *则尝试创建巨大页 (create_huge_pud(&vmf)),否则继续处理。如果 create_huge_pud 成功,
	 *返回 VM_FAULT_FALLBACK,否则继续处理。
	 */
	if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
		ret = create_huge_pud(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} else {
		pud_t orig_pud = *vmf.pud;

		barrier();
		/*如果 pud_trans_huge(orig_pud) 或 pud_devmap(orig_pud),
		表示已经存在一个巨大页或者是设备映射,进行相应处理,否则继续。*/
		if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {

			/* NUMA case for anonymous PUDs would go here */
			/*如果页面标记为脏 (dirty) 且 pud_write(orig_pud) 失败,
			则尝试处理写保护巨大页 (wp_huge_pud(&vmf, orig_pud))。
			如果成功,返回 VM_FAULT_FALLBACK,否则继续。*/
			if (dirty && !pud_write(orig_pud)) {
				ret = wp_huge_pud(&vmf, orig_pud);
				if (!(ret & VM_FAULT_FALLBACK))
					return ret;
			} else {
				huge_pud_set_accessed(&vmf, orig_pud);//否则,标记巨大页为已访问 (huge_pud_set_accessed),返回 0。
				return 0;
			}
		}
	}

	vmf.pmd = pmd_alloc(mm, vmf.pud, address);//分配一个指向三级页表项 (pmd) 的指针。
	if (!vmf.pmd)
		return VM_FAULT_OOM;

	/* Huge pud page fault raced with pmd_alloc? */
	/*表示二级页表项处于不稳定状态,返回到 retry_pud 标签重新尝试处理。*/
	if (pud_trans_unstable(vmf.pud))
		goto retry_pud;
	/*如果 pmd_none(*vmf.pmd) 且启用了透明巨大页面 (THP),
	则尝试创建巨大三级页 (create_huge_pmd(&vmf)),否则继续处理*/
	if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
		ret = create_huge_pmd(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} else {
		vmf.orig_pmd = *vmf.pmd;

		/*如果三级页表项标记为交换 (is_swap_pmd(vmf.orig_pmd)),
		则处理与交换页面相关的情况,包括等待页面迁移 (pmd_migration_entry_wait)。*/
		barrier();
		if (unlikely(is_swap_pmd(vmf.orig_pmd))) {
			VM_BUG_ON(thp_migration_supported() &&
					  !is_pmd_migration_entry(vmf.orig_pmd));
			if (is_pmd_migration_entry(vmf.orig_pmd))
				pmd_migration_entry_wait(mm, vmf.pmd);
			return 0;
		}
		/*检查页面中间目录项(pmd)是否对应于透明大页面或设备映射页面。
		透明大页面是可以提高系统性能的大内存页面,而设备映射页面是内存映射到设备,
		允许直接访问设备内存。*/
		if (pmd_trans_huge(vmf.orig_pmd) || pmd_devmap(vmf.orig_pmd)) {
			/*在第一个if块中,这一行检查pmd条目是否标记为“protnone”,
			表示允许访问的保护设置。此外,它检查虚拟内存区域(vma)是否可访问。
			如果两个条件都满足,它调用名为do_huge_pmd_numa_page(&vmf)的函数。
			这个函数可能处理当发生透明大页面故障并且访问被允许时的情况。*/
			if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
				return do_huge_pmd_numa_page(&vmf);
			/*如果pmd条目不可写并且页面是脏的(已修改),则执行此代码块。
			它检查页面是否可写(!pmd_write(vmf.orig_pmd))并且页面是否脏。
			如果两个条件都为真,它调用wp_huge_pmd(&vmf),这可能处理当遇到脏大页面时的情况。
			如果ret(wp_huge_pmd(&vmf)的返回值)不包含VM_FAULT_FALLBACK标志,它返回ret。*/
			if (dirty && !pmd_write(vmf.orig_pmd)) {
				ret = wp_huge_pmd(&vmf);
				if (!(ret & VM_FAULT_FALLBACK))
					return ret;
			} else {
				/*如果pmd条目可写或页面不是脏的,则执行此代码块。
				它调用huge_pmd_set_accessed(&vmf),这可能更新大页面的访问位,
				表示已访问该页面。然后,它返回0,表示已成功处理页面故障,而无需从磁盘加载数据。*/
				huge_pmd_set_accessed(&vmf);
				return 0;
			}
		}
	}
	/*如果之前的条件都不满足,代码会回退到处理页面表项(PTE)级别的页面故障。
	它调用handle_pte_fault(&vmf)函数,这可能处理常规(非大)页面故障。*/
	return handle_pte_fault(&vmf);
}

其中struct vm_fault结构体被定义在/include/linux/mm.h文件中,所包含成员如下:

struct vm_fault {
	const struct {
		struct vm_area_struct *vma;	/* Target VMA */
		gfp_t gfp_mask;			/* gfp mask to be used for allocations */
		pgoff_t pgoff;			/* Logical page offset based on vma */
		unsigned long address;		/* Faulting virtual address */
	};
	enum fault_flag flags;		/* FAULT_FLAG_xxx flags
					 * XXX: should really be 'const' */
	pmd_t *pmd;			/* Pointer to pmd entry matching
					 * the 'address' */
	pud_t *pud;			/* Pointer to pud entry matching
					 * the 'address'
					 */
	union {
		pte_t orig_pte;		/* Value of PTE at the time of fault */
		pmd_t orig_pmd;		/* Value of PMD at the time of fault,
					 * used by PMD fault only.
					 */
	};

	struct page *cow_page;		/* Page handler may use for COW fault */
	struct page *page;		/* ->fault handlers should return a
					 * page here, unless VM_FAULT_NOPAGE
					 * is set (which is also implied by
					 * VM_FAULT_ERROR).
					 */
	/* These three entries are valid only while holding ptl lock */
	pte_t *pte;			/* Pointer to pte entry matching
					 * the 'address'. NULL if the page
					 * table hasn't been allocated.
					 */
	spinlock_t *ptl;		/* Page table lock.
					 * Protects pte page table if 'pte'
					 * is not NULL, otherwise pmd.
					 */
	pgtable_t prealloc_pte;		/* Pre-allocated pte page table.
					 * vm_ops->map_pages() sets up a page
					 * table from atomic context.
					 * do_fault_around() pre-allocates
					 * page table to avoid allocation from
					 * atomic context.
					 */
};

未完,待续………

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
深入分析Linux内核源码 前言 第一章 走进linux 1.1 GNU与Linux的成长 1.2 Linux的开发模式和运作机制 1.3走进Linux内核 1.3.1 Linux内核的特征 1.3.2 Linux内核版本的变化 1.4 分析Linux内核的意义 1.4.1 开发适合自己的操作系统 1.4.2 开发高水平软件 1.4.3 有助于计算机科学的教学和科研 1.5 Linux内核结构 1.5.1 Linux内核在整个操系统中的位置 1.5.2 Linux内核的作用 1.5.3 Linux内核的抽象结构 1.6 Linux内核源代码 1.6.1 多版本的内核源代码 1.6.2 Linux内核源代码的结构 1.6.3 从何处开始阅读源代码 1.7 Linux内核源代码分析工具 1.7.1 Linux超文本交叉代码检索工具 1.7.2 Windows平台下的源代码阅读工具Source Insight 第二章 Linux运行的硬件基础 2.1 i386的寄存器 2.1.1通用寄存器 2.1.2段寄存器 2.1.3状态和控制寄存器 2.1.4 系统地址寄存器 2.1.5 调试寄存器和测试寄存器 2.2 内存地址 2.3 段机制和描述符 2.3.1 段机制 2.3.2 描述符的概念 2.3.3系统段描述符 2.3.4 描述符表 2.3.5 选择符与描述符表寄存器 2.3.6 描述符投影寄存器 2.3.7 Linux中的段 2.4 分页机制 2.4.1 分页机构 2.4.2页面高速缓存 2.5 Linux中的分页机制 2.5.1 与页相关的数据结构及宏的定义 2.5.2 对页目录及页表的处理 2.6 Linux中的汇编语言 2.6.1 AT&T与Intel汇编语言的比较 2.6.2 AT&T汇编语言的相关知识 2.6.3 Gcc嵌入式汇编 2.6.4 Intel386汇编指令摘要 第三章中断机制 3.1 中断基本知识 3.1.1 中断向量 3.1.2 外设可屏蔽中断 3.1.3异常及非屏蔽中断 3.1.4中断描述符表 3.1.5 相关汇编指令 3.2中断描述符表的初始化 3.2. 1 外部中断向量的设置 3.2.2中断描述符表IDT的预初始化 3.2.3 中断向量表的最终初始化 3.3异常处理 3.3.1 在内核栈中保存寄存器的值 3.3.2 中断请求队列的初始化 3.3.3中断请求队列的数据结构 3.4 中断处理 3.4.1中断和异常处理的硬件处理 3.4.2 Linux异常和中断的处理 3.4.3 与堆栈有关的常量、数据结构及宏 3.4.4 中断处理程序的执行 3.4.5 从中断返回 3.5中断的后半部分处理机制 3.5.1 为什么把中断分为两部分来处理 3.5.2 实现机制 3.5.3数据结构的定义 3.5.4 软中断、bh及tasklet的初始化 3.5.5后半部分的执行 3.5.6 把bh移植到tasklet 第四章 进程描述 4.1 进程和程序(Process and Program) 4.2 Linux中的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存中的存放 4.4.1 进程内核栈 4.4.2 当前进程(current宏) 4.5 进程组织的方式 4.5.1哈希表 4.5.2双向循环链表 4.5.3 运行队列 4.5.4 等待队列 4.6 内核线程 4.7 进程的权能 4.8 内核同步 4.8.1信号量 4.8.2原子操作 4.8.3 自旋锁、读写自旋锁和大读者自旋锁 4.9 本章小节 第五章进程调度 5.1 Linux时间系统 5.1.1 时钟硬件 5.1.2 时钟运作机制 5.1.3 Linux时间基准 5.1.4 Linux的时间系统 5.2 时钟中断 5.2.1 时钟中断的产生 5.2.2.Linux实现时钟中断的全过程 5.3 Linux的调度程序-Schedule( ) 5.3.1 基本原理 5.3.2 Linux进程调度时机 5.3.3 进程调度的依据 5.3.4 进程可运行程度的衡量 5.3.5 进程调度的实现 5.4 进程切换 5.4.1 硬件支持 5.4.2 进程切换 第六章 Linux内存管理 6.1 Linux的内存管理概述 6.1.1 Linux虚拟内存的实现结构 6.1.2 内核空间和用户空间 6.1.3 虚拟内存实现机制间的关系 6.2 Linux内存管理的初始化 6.2.1 启用分页机制 6.2.2 物理内存的探测 6.2.3 物理内存的描述 6.2.4 页面管理机制的初步建立 6.2.5页表的建立 6.2.6内存管理区 6.3 内存的分配和回收 6.3.1 伙伴算法 6.3.2 物理页面的分配和释放 6.3.3 Slab分配机制 6.4 地址映射机制 6.4.1 描述虚拟空间的数据结构 6.4.2 进程的虚拟空间 6.4.3 内存映射 6.5 请页机制 6.5.1 页故障的产生 6.5.2 页错误的定位 6.5.3 进程地址空间中的缺页异常处理 6.5.4 请求调页 6.5.5 写时复制 6.6 交换机制 6.6.1 交换的基本原理 6.6.2 页面交换守护进程kswapd 6.6.3 交换空间的数据结构 6.6.4 交换空间的应用 6.7 缓存和刷新机制 6.7.1 Linux使用的缓存 6.7.2 缓冲区高速缓存 6.7.3 翻译后援存储器(TLB) 6.7.4 刷新机制 6.8 进程的创建和执行 6.8.1 进程的创建 6.8.2 程序执行 6.8.3 执行函数 第七章 进程间通信 7.1 管道 7.1.1 Linux管道的实现机制 7.1.2 管道的应用 7.1.3 命名管道(FIFO) 7.2 信号(signal) 7.2.1 信号种类 7.2.2 信号掩码 7.2.3 系统调用 7.2.4 典型系统调用的实现 7.2.5 进程与信号的关系 7.2.6 信号举例 7.3 System V 的IPC机制 7.3.1 信号量 7.3.2 消息队列 7.3.3 共享内存 第八章 虚拟文件系统 8.1 概述 8.2 VFS中的数据结构 8.2.1 超级块 8.2.2 VFS的索引节点 8.2.3 目录项对象 8.2.4 与进程相关的文件结构 8.2.5 主要数据结构间的关系 8.2.6 有关操作的数据结构 8.3 高速缓存 8.3.1 块高速缓存 8.3.2 索引节点高速缓存 8.3.3 目录高速缓存 8.4 文件系统的注册、安装与拆卸 8.4.1 文件系统的注册 8.4.2 文件系统的安装 8.4.3 文件系统的卸载 8.5 限额机制 8.6 具体文件系统举例 8.6.1 管道文件系统pipefs 8.6.2 磁盘文件系统BFS 8.7 文件系统的系统调用 8.7.1 open 系统调用 8.7.2 read 系统调用 8.7.3 fcntl 系统调用 8 .8 Linux2.4文件系统的移植问题 第九章 Ext2文件系统 9.1 基本概念 9.2 Ext2的磁盘布局和数据结构 9.2.1 Ext2的磁盘布局 9.2.2 Ext2的超级块 9.2.3 Ext2的索引节点 9.2.4 组描述符 9.2.5 位图 9.2.6 索引节点表及实例分析 9.2.7 Ext2的目录项及文件的定位 9.3 文件的访问权限和安全 9.4 链接文件 9.5 分配策略 9.5.1 数据块寻址 9.5.2 文件的洞 9.5.3 分配一个数据块 第十章 模块机制 10.1 概述 10.1.1 什么是模块 10.1.2 为什么要使用模块? 10.2 实现机制 10.2.1 数据结构 10.2.2 实现机制的分析 10.3 模块的装入和卸载 10.3.1 实现机制 10.3.2 如何插入和卸载模块 10.4 内核版本 10.4.1 内核版本与模块版本的兼容性 10.4.2 从版本2.0到2.2内核API的变化 10.4.3 把内核2.2移植到内核2.4 10.5 编写内核模块 10.5.1 简单内核模块的编写 10.5.2 内核模块的Makefiles文件 10.5.3 内核模块的多个文件 第十一章 设备驱动程序 11.1 概述 11.1.1 I/O软件 11.1.2 设备驱动程序 11.2 设备驱动基础 11.2.1 I/O端口 11.2.2 I/O接口及设备控制器 11.2.3 设备文件 11.2.4 VFS对设备文件的处理 11.2.5 中断处理 11.2.6 驱动DMA工作 11.2.7 I/O 空间的映射 11.2.8 设备驱动程序框架 11.3 块设备驱动程序 11.3.1 块设备驱动程序的注册 11.3.2 块设备基于缓冲区的数据交换 11.3.3 块设备驱动程序的几个函数 11.3.4 RAM 盘驱动程序的实现 11.3.5 硬盘驱动程序的实现 11.4 字符设备驱动程序 11.4.1 简单字符设备驱动程序 11.4.2 字符设备驱动程序的注册 11.4.3 一个字符设备驱动程序的实例 11.4.4 驱动程序的编译与装载 第十二章 网络 12.1 概述 12.2 网络协议 12.2.1 网络参考模型 12.2.2 TCP/IP 协议工作原理及数据流 12.2.3 Internet 协议 12.2.4 TCP协议 12.3 套接字(socket) 12.3.1 套接字在网络中的地位和作用 12.3.2 套接字接口的种类 12.3.3 套接字的工作原理 12.3.4 socket 的通信过程 12.3.5 socket为用户提供的系统调用 12.4 套接字缓冲区(sk_buff) 12.4.1 套接字缓冲区的特点 12.4.2 套接字缓冲区操作基本原理 12.4.3 sk_buff数据结构的核心内容 12.4.4 套接字缓冲区提供的函数 12.4.5 套接字缓冲区的上层支持例程 12.5 网络设备接口 12.5.1 基本结构 12.5.2 命名规则 12.5.3 设备注册 12.5.4 网络设备数据结构 12.5.5 支持函数 第十三章 启动系统 13.1 初始化流程 13.1.1 系统加电或复位 13.1.2 BIOS启动 13.1.3 Boot Loader 13.1.4 操作系统的初始化 13.2 初始化的任务 13.2.1 处理器对初始化的影响 13.2.2 其他硬件设备对处理器的影响 13.3 Linux 的Boot Loarder 13.3.1 软盘的结构 13.3.2 硬盘的结构 13.3.3 Boot Loader 13.3.4 LILO 13.3.5 LILO的运行分析 13.4 进入操作系统 13.4.1 Setup.S 13.4.2 Head.S 13.5 main.c中的初始化 13.6 建立init进程 13.6.1 init进程的建立 13.6.2 启动所需的Shell脚本文件 附录: 1 Linux 2.4内核API 2.1 驱动程序的基本函数 2.2 双向循环链表的操作 2.3 基本C库函数 2.4 Linux内存管理中Slab缓冲区 2.5 Linux中的VFS 2.6 Linux的连网 2.7 网络设备支持 2.8 模块支持 2.9 硬件接口 2.10 块设备 2.11 USB 设备 2 参考文献

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值