20242828-《Linux内核原理与分析》第九周作业

20242828-《Linux内核原理与分析》第九周作业

Linux 系统中进程调度的时机

在 Linux 系统中,进程调度是由内核完成的,用于管理进程的执行顺序和资源分配。调度的时机是指内核决定从当前进程切换到另一个进程的时刻,主要包括以下几种场景:

  1. 时间片用尽
    **触发机制:**定时器中断(clock interrupt)。
    Linux 使用时间片轮转(time slicing)调度策略,每个进程被分配一定的 CPU 时间片。当时间片用尽时,内核触发调度器选择下一个合适的进程运行。
    **典型场景:**抢占式多任务系统中,确保所有进程都能公平使用 CPU。

  2. 进程主动让出 CPU
    **触发机制:**进程调用特定系统调用。
    如果一个进程因某种原因需要等待(如 I/O 操作、信号或获取锁),它会主动进入睡眠(阻塞)状态。此时,内核会触发调度器选择其他进程运行。
    常见系统调用:
    sleep():进程主动休眠。
    wait():等待子进程结束。
    阻塞式 I/O 调用:如 read()、write()。

  3. 高优先级任务唤醒
    **触发机制:**有更高优先级的进程从睡眠状态被唤醒。
    如果某个优先级较高的进程从阻塞状态被唤醒,内核可能会立即中断当前的低优先级进程,切换到高优先级进程。
    **典型场景:**实时任务、I/O 完成通知。

  4. 系统调用返回或中断处理结束
    **触发机制:**进程从内核态返回用户态时。
    每当进程完成系统调用或中断处理时,内核会检查是否需要进行进程切换。
    典型场景:
    某进程完成 I/O 操作。
    中断处理完成时触发调度器。

  5. 新进程创建或进程终止
    触发机制:
    调用 fork() 或其他进程创建函数后。
    某进程调用 exit() 终止后。
    新进程创建后,调度器会决定是否切换到新进程执行。
    如果当前运行的进程终止,调度器会选择其他进程运行。

  6. 总结
    Linux 的进程调度是多任务操作系统的核心功能,其调度时机由内核态的各种事件驱动,主要包括时间片用尽、进程状态变化以及系统资源条件变化等。Linux 通常采用 CFS(完全公平调度器) 和特定场景下的实时调度策略来实现这些调度行为。

实验:使用 gdb 跟踪分析一个 schedule()函数

1、启动MenuOS系统

cd LinuxKernel
rm -rf menu  # 无法克隆,可使用侧边栏上传代码将代码压缩包传入实验楼环境中
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

图

2、配置gdb,远程调试并设置断点;

cd ..
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
gdb  # 打开另一个终端,使用gdb调试
file linux-3.18.6/vmlinux
target remote:1234
b schedule
b context_switch
b switch_to
b pick_next_task

图

图

按c继续执行,停在schedule函数断点处,使用list查看函数的上下文代码,发现调用了schedule(),即发生了进程调度

图

按c继续执行到pick_next_task断点处

图

按c继续执行到context_switch断点处:context_switch用来实现进程的切换

图

单步调试进入switch_to内部

图

schedule() 是内核调度的核心函数,当当前任务需要让出 CPU 时被调用。pick_next_task()选择下一个要运行的任务。context_switch()完成从当前任务到新任务的上下文切换。switch_to()执行具体的寄存器切换操作。

当前任务调用 schedule() 请求调度。schedule() 调用 pick_next_task() 选择新的任务。如果任务不同,调用 context_switch() 完成上下文切换:保存当前任务状态。切换到新任务的内核栈。更新 current 指针。context_switch() 调用 switch_to(),最终切换寄存器并启动新任务。

分析 switch_to 中的汇编代码

汇编代码:

#define switch_to(prev, next, last)                    
do {                                 
  /*                              
 1. Context-switching clobbers all registers, so we clobber  
 2. them explicitly, via unused output variables.     
 3. (EAX and EBP is not listed because EBP is saved/restored  
 4. explicitly for wchan access and EAX is the return value of   
 5. __switch_to())                     
   */                                
  unsigned long ebx, ecx, edx, esi, edi;                
                                  
  asm volatile("pushfl\n\t"      /* 保存当前进程flags */   
           "pushl %%ebp\n\t"        /* 当前进程堆栈基址压栈*/ 
           "movl %%esp,%[prev_sp]\n\t"  /*保存ESP,将当前堆栈栈顶保存起来*/ 
           "movl %[next_sp],%%esp\n\t"  /*更新ESP,将下一栈顶保存到ESP中*/ 
        
            //完成内核堆栈的切换
           "movl $1f,%[prev_ip]\n\t"    /*保存当前进程EIP*/ 
           "pushl %[next_ip]\n\t"   /*将next进程起点压入堆栈,即next进程的栈顶为起点*/    
         
            //完成EIP的切换
             __switch_canary    
            //next_ip一般是$1f,对于新创建的子进程时ret_from_fork               
           "jmp __switch_to\n"  /*prev进程中,设置next进程堆栈*/ 
            //jmp不同于call是通过寄存器传递参数
           "1:\t"                //next进程开始执行
           "popl %%ebp\n\t"       
           "popfl\n"         
                                  
           /*输出变量定义*/                
           : [prev_sp] "=m" (prev->thread.sp),     //[prev_sp]定义内核堆栈栈顶
             [prev_ip] "=m" (prev->thread.ip),     //[prev_ip]当前进程EIP  
             "=a" (last),                 
                                  
             /* 要破坏的寄存器: */     
             "=b" (ebx), "=c" (ecx), "=d" (edx),      
             "=S" (esi), "=D" (edi)             
                                       
             __switch_canary_oparam                
                                  
            /* 输入变量: */                
           : [next_sp]  "m" (next->thread.sp),     //[next_sp]下一个内核堆栈栈顶    
             [next_ip]  "m" (next->thread.ip),     
           //[next_ip]下一个进程执行起点,,一般是$1f,对于新创建的子进程是ret_from_fork 
   
             /* regparm parameters for __switch_to(): */  
             [prev]     "a" (prev),              
             [next]     "d" (next)               
                                  
             __switch_canary_iparam                
                                 
           : /* 重新加载段寄存器 */           
          "memory");                  
} while (0)

1、 定义需要保存的寄存器变量unsigned long ebx, ecx, edx, esi, edi;这些变量用于存储上下文切换过程中需要保存的寄存器值,避免在切换时被破坏。

2、使用 asm volatile 指令定义一个内联汇编块,volatile 指定编译器不优化或重排这段代码。

3、pushfl:将当前进程的标志寄存器(EFLAGS)压入栈中,保存标志位。目的: 在切换回来时能恢复原任务的中断状态和标志位。

4、pushl %%ebp:将当前进程的 EBP(栈基址寄存器)压入栈中,保存堆栈基址。目的:便于恢复时正确调整栈基址。

5、movl %%esp, %[prev_sp]: 将当前任务的 ESP(栈指针)值保存到 prev->thread.sp。目的:保存当前任务的内核栈顶位置,供恢复时使用。

6、movl %[next_sp], %%esp:将 next->thread.sp(下一任务的栈指针)加载到 ESP。目的:完成内核栈的切换,切换到目标任务的内核栈。

7、movl $1f, %[prev_ip]:将当前任务的下一条指令地址(标号 1f)保存到 prev->thread.ip。目的:当恢复 prev 任务时,从保存的地址继续执行。

8、pushl %[next_ip]:将目标任务的指令地址(next->thread.ip)压入堆栈。目的:确保下一任务的执行从 next_ip 开始。

9、jmp switch_to:跳转到 switch_to 函数。目的:完成底层寄存器切换,包括通用寄存器和段寄存器。

10、popl %%ebp
popfl

​ 恢复新的 EBP 和 EFLAGS 值。目的: 确保目标任务的堆栈基址和标志寄存器被正确恢复。

switch_to 的核心是保存当前任务的状态(堆栈、寄存器、程序计数器等),切换到目标任务的上下文,并从目标任务的执行点继续运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值