20169212《Linux内核原理与分析》 第十周作业

云课堂回顾学习

1. 进程调度的时机

  • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
  • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

总结:

  • 用户态进程只能被动调度
  • 内核线程是只有内核态没有用户态的特殊进程
  • 内核线程既可以主动调度也可以被动调度

2. 进程的切换

  • 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
  • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
  • 进程上下文包含了进程执行需要的所有信息
    用户地址空间:包括程序代码,数据,用户堆栈等
    控制信息:进程描述符,内核堆栈等
    硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
  • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
    next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
    context_switch(rq, prev, next);//进程上下文切换
    switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程

实验

cd LinuxKernel
ls
cd menu
vi test.c

这里我们使用time系统调用,在test.c中加入time函数以及main中加入相关语句,如同之前的实验。
然后make rootfs进行编译。

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage initrd rootfs.img -s -S
gdb
file linux-3.18.6/vmlinux   //加载内核
target remote:1234          //链接到menu os里
b sys_time
c

context_switch 实现进程上下文切换

这里面包装了使用某种进程调度策略,选择了下一个进程,从运行队列中挑出了一个进程作为next进行切换

查看context_switch详细内容

switch_to

指令流的切换

31#define switch_to(prev, next, last)                    
32do {                                 
33  /*                              
34   * Context-switching clobbers all registers, so we clobber  
35   * them explicitly, via unused output variables.     
36   * (EAX and EBP is not listed because EBP is saved/restored  
37   * explicitly for wchan access and EAX is the return value of   
38   * __switch_to())                     
39   */                                
40  unsigned long ebx, ecx, edx, esi, edi;                
41                                  
42  asm volatile("pushfl
	"      /* 保存当前进程flags */   
43           "pushl %%ebp
	"        /* 当前进程堆栈基址压栈*/ 

44           "movl %%esp,%[prev_sp]
	"  /*保存ESP,将当前堆栈栈顶保存起来*/ 
45           "movl %[next_sp],%%esp
	"  /*更新ESP,将下一栈顶保存到ESP中*/ 

			//完成内核堆栈的切换
46           "movl $1f,%[prev_ip]
	"    /*保存当前进程EIP*/ 
47           "pushl %[next_ip]
	"   /*将next进程起点压入堆栈,即next进程的栈顶为起点*/    
48         
			//完成EIP的切换
			 __switch_canary    
			//next_ip一般是$1f,对于新创建的子进程时ret_from_fork               
49           "jmp __switch_to
"  /*prev进程中,设置next进程堆栈*/ 
			//jmp不同于call是通过寄存器(a、d)传递参数
50           "1:	"                //next进程开始执行
51           "popl %%ebp
	"       
52           "popfl
"         
53                                  
54           /*输出变量定义*/                
55           : [prev_sp] "=m" (prev->thread.sp),     //[prev_sp]定义内核堆栈栈顶
56             [prev_ip] "=m" (prev->thread.ip),     //[prev_ip]当前进程EIP  
57             "=a" (last),                 
58                                  
59             /* 要破坏的寄存器: */     
60             "=b" (ebx), "=c" (ecx), "=d" (edx),      
61             "=S" (esi), "=D" (edi)             
62                                       
63             __switch_canary_oparam                
64                                  
65             /* 输入变量: */                
66           : [next_sp]  "m" (next->thread.sp),     //[next_sp]下一个内核堆栈栈顶    
67             [next_ip]  "m" (next->thread.ip),     
68           //[next_ip]下一个进程执行起点,一般是$1f,对于新创建的子进程是ret_from_fork 
   
69             /* regparm parameters for __switch_to(): */  
70             [prev]     "a" (prev),              
71             [next]     "d" (next)           //用a、d两个寄存器传递参数    
72                                  
73             __switch_canary_iparam                
74                                  
75           : /* 重新加载段寄存器 */           
76          "memory");                  
77} while (0)

书上内容

  1. 一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干。如果父进程希望和其子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们称这样的进程为线程。

  2. 内存区域可以包含各种内存对象,比如:可执行文件代码的内存映射,称为代码段(text section)。可执行文件的已初始化全局变量的内存映射,称为数据段(datasection)。包含未初始化全局变量,也就是bss段的零页(页面中的信息仝部为0值,所以可用于映射bss段等目的)的内存映射。用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射。每一个诸如c库或动态连接程序等共享库的代码段、数据段和bss也会被载人进程的地址空间。任何内存映射文件。任何共享内存段。任何匿名的内存映射,比如由malloc()分配的内存。

  3. 内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。

  4. VMA具体操作方法

void open (struct vm area struct *area) 当指定的内存区域被加人到一个地址空间时,该函数被调用。

void close(struct vm_area struct *area)当指定的内存区域从地址空间删除时,该函数被调用。

int fault(struct vm area sruct *area, struct 0 fault *vmf) 当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用。

int page mkwrite(struct vm_area sruct *area, struct vrn fault *vmf) 当某个页面为只读页面时,该函数被页面故障处理调用。

int access(struct vm_area struct *Vtna, unsigned long address, void *buf,int len, int write) 当get user-pages()函数调用失败时,该函数被access—process—vm()函数调用。

  1. find_vma()函数在指定的地址空间中搜索第一个vm_end大于addrd的内存区域。find_vma_prev()函数和find_vma()工作方式相同,但是它返回第一个小于adddr的VMA。find_vma_intersection()函数返回第一个和指定地址区间相交的VMA。
  2. Linux中使用三级页表完成地址转换:顶级页表是页全局目录(PGD),二级页表是中间页目录(PMI),最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。
  3. 缓存一般被实现成下面三种策略之一:第一种策略称为不缓存(nowrite),也就是说高速缓存不去缓存任何写操作。第二种策略,写操作将自动更新内存缓存,同时也更新磁盘文件。第三种策略,也是Linux所采用的,称为“回写"。
  4. 在以下三种情况发生时,脏页被写回磁盘:当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘以便释放内存,因为只有干净(不脏的)内存才可以被回收。当内存干净后,内核就可以从缓存清理数据,然后收缩缓存,最终释放出更多的内存。当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存中。当用户进程调用sync()和fsync()系统调用时,内核会按要求执行回写动作。
  5. pdflush内核线程:负责定时将脏数据刷入磁盘。pdflush线程可以有多个处理不同的设备列队。
原文地址:https://www.cnblogs.com/Jarvan210/p/6107640.html