分析基于内核版本2.6.12.6
在什么情况下,会触发调度?
Linux进程的调度主要分为主动调度和被动调度两大类。
◆主动调度
主动调度就是进程自己缺少相应的所申请的资源,显示调用schedule,让出处理器。
◆被动调度
在整个linux运行过程中,被动调度又可细分为两种:
●用户态抢占调度
●内核态抢占调度
下面就结合内核代码分析上述各种调度时机的详细情况。
被动调度
整个linux运行过程中,被动调度分为用户态抢占调度和内核态抢占调度。
用户态抢占调度
用户态抢占调度发生在当系统调用、中断处理、异常处理等返回用户态时,或者进程的时间片用完时。
系统调用返回
当一个进程由于系统调用进入内核态,在系统调用处理完,返回用户态时,是一个调度点。这个时候会检测有没有设置TIF_NEED_RESCHED标志。下面分析下内核中系统调用返回相关的代码片段。
ENTRY(system_call) /*系统调用入口*/ CFI_STARTPROC /*用在每个函数的开始,用于初始化一些内部数据结构*/ swapgs /*当处理器进入或者离开内核时,会使用swapg命令在gs寄存器的内核与用户值之间切换*/ movq %rsp,%gs:pda_oldrsp /*将用户态栈指针压入gs寄存器指向的x8664_pda结构体的oldrsp字段*/ movq %gs:pda_kernelstack,%rsp /*将内核栈帧赋给rsp*/ sti /*开中断*/ SAVE_ARGS 8,1 /*将一些寄存器压栈*/ movq %rax ,ORIG_RAX-ARGOFFSET(%rsp) /*120-48=72 8*9(%rsp)*/ movq %rcx ,RIP-ARGOFFSET(%rsp) /*128-48=80 8*10(%rsp)*/ GET_THREAD_INFO(%rcx) /*获取当前进程的thread_info结构的地址*/ testl $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SECCOMP),threadinfo_flags(%rcx) jnz tracesys cmpq $__NR_syscall_max ,%rax /*判断系统调用号是否大于最大值*/ ja badsys movq %r10 ,%rcx call *sys_call_table(,%rax,8) # XXX: rip relative /*rax为系统调用号*/ movq %rax ,RAX-ARGOFFSET(%rsp) |
上面代码片段是系统调用的入口。这个系统调用的入口是在syscall_init函数里调用wrmsrl(MSR_LSTAR, system_call)设置的,通过一个MSR寄存器来保存系统调用的地址,而不再像I386架构下面是通过中断。
用户进程和内核都使用gs段寄存器访问状态数据。用户进程使用这个寄存器保存每个线程的数据,内核使用这个寄存器管理每个处理器的数据。当处理器进入或者离开内核时,都会使用swapgs命令在gs寄存器的内核和用户态值之间切换。
/* Per processor datastructure. %gs points to it while the kernel runs */ struct x8664_pda { struct task_struct *pcurrent; /* Current process */ unsigned long data_offset; /* Per cpu data offset from linker address */ struct x8664_pda *me; /* Pointer to itself */ unsigned long kernelstack; /* top of kernel stack for current */ unsigned long oldrsp; /* user rsp for system call */ unsigned long irqrsp; /* Old rsp for interrupts. */ int irqcount; /* Irq nesting counter. Starts with -1 */ int cpunumber; /* Logical CPU number */ char *irqstackptr; /* top of irqstack */ unsigned int __softirq_pending; unsigned int __nmi_count; /* number of NMI on this CPUs */ struct mm_struct *active_mm; int mmu_state; unsigned apic_timer_irqs; } ____cacheline_aligned; |
x8664_pda数据结构的kernelstack字段指向当前cpu的内核栈顶,oldrsp字段存储的是由系统调用进入内核态的用户进程的栈帧rsp。在进入系统调用入口后,就会保存用户进程的栈帧,并恢复当前cpu的栈帧到rsp。
SAVE_ARGS宏主要是将一些寄存器压栈。
下面接着分析系统调用代码:
.globl ret_from_sys_call ret_from_sys_call: movl $_TIF_ALLWORK_MASK,%edi /*edi: flagmask */ sysret_check: GET_THREAD_INFO(%rcx) cli /*关中断*/ movl threadinfo_flags(%rcx),%edx andl %edi,%edx /*检测是否还有其它工作需要完成*/ jnz sysret_careful /*有工作完成则跳转到sysret_careful */ movq RIP-ARGOFFSET(%rsp),%rcx RESTORE_ARGS 0,-ARG_SKIP,1 movq %gs:pda_oldrsp,%rsp swapgs sysretq /* Handle reschedules */ /*edx: work, edi: workmask */ sysret_careful: bt $TIF_NEED_RESCHED,%edx /*检测是否有设置TIF_NEED_RESCHED 标志*/ jnc sysret_signal /*没有设置TIF_NEED_RESCHED标志,跳转到sysret_signal*/ sti pushq %rdi call schedule popq %rdi jmp sysret_check |
上面代码是系统调用后的处理部分。在处理完系统调用返回用户态前,首先检查是否还有其他工作需要完成,如果有其他工作,则跳转到sysret_careful标签处执行。如果没有其他工作,则将之前压栈的寄存器出栈,调用swapgs命令,切换gs寄存器的内核和用户态值,最后调用sysretq指令退出系统调用。
sysret_careful开始就检测是否有设置TIF_NEED_RESCHED标志,如果没有设置此标志,则跳转到sysret_signal标签处执行。如果有设置此标志,则首先开中断,然后调用schedule函数。
下面接着分析sysret_signal标签处的代码:
sysret_signal: sti /*开中断*/ testl $(_TIF_SIGPENDING|_TIF_NOTIFY_RESUME|_TIF_SINGLESTEP),%edx /*检测是否有信号需要处理*/ jz 1f /* Really a signal */ /* edx:work flags (arg3) */ leaq do_notify_resume(%rip),%rax /*将do_notify_resume函数的地址赋给rax寄存器*/ leaq -ARGOFFSET(%rsp),%rdi # &pt_regs -> arg1 /* do_notify_resume函数参数1*/ xorl %esi,%esi # oldset -> arg2 /* do_notify_resume函数参数2*/ call ptregscall_common 1:movl $_TIF_NEED_RESCHED,%edi jmp sysret_check |
上面代码片段是处理信号部分代码。首先检测是否有信号需要处理,有信号处理的情况下,将do_notify_resume函数的地址赋给rax寄存器,然后准备好do_notify_resume函数需要使用的3个参数,通过寄存器rdi,esi和edx三个寄存器传递。然后跳转到ptregscall_common函数处执行。在ptregscall_common中会调用call *%rax,进入do_notify_resume函数执行。
下面是上面分析的系统调用相关代码的流程图。
图-systemcall流程图
图-ret_from_sys_call流程图
版权声明:本文为博主原创文章,未经博主允许不得转载。