linux进程切换(linux3.4.5,x86)
来源:互联网 发布:mac下完jdk怎么用 编辑:程序博客网 时间:2024/05/17 01:13
引言
本文描述linux x86的进程切换实现原理,叙述了寄存器、堆栈的备份与恢复操作。
Intel设计的意图是通过硬件方式切换进程,但是linux并没有使用这种方式,而是使用了软件方式,文章对这两种方式分别做了描述。
一、选择硬件切换还是软件切换?
- x86提供硬件切换方式switching task(早期内核版本采用)Intel在设计x86时,希望以硬件方式切换进程:每个进程有一个TSS(task state segment),这是一个内存中的数据结构,包含通用寄存器的值、IO权限位图信息,见图1。另外还有一个特殊的寄存器TR(Task Register),指向某个进程的TSS,见图2。更改TR的值将会触发硬件保存cpu所有寄存器的值到当前进程的TSS中,然后从新进程的TSS中读出所有寄存器值,加载到cpu对应的寄存器中。整个过程中cpu寄存器的保存、加载,无需软件参与。
图1 32-bit Task State Segment图2 Task State Segmentx86在设计上有4个特权级,称为ring0,ring1,ring2,ring3。linux中用户态对应ring3,内核态对应ring0。TSS中的Stack Seg Priv.Level0~2指向cpu处于特权级使用的栈。Stack Segment表示当前特权级的栈(特权级有0~2 三个级别)。
为什么x86有4个特权级,而TSS中记录了3个特权级的栈,并没有记录Priv.Level3的栈(也即用户态栈)呢?我的理解是系统从用户态切进入内核态,会把用户态栈指针保存在内核栈上,从内核态返回用户态时,只要从内核栈上pop出用户态的栈指针就可以了,所以不需要额外记录Priv.Level3的栈指针。进程地址空间的页目录指针在CR3寄存器中(参考图2)。从上面可以看出,TSS包含了一个进程执行所需的硬件寄存器和栈信息,所以通过TSS的切换可以实现进程切换。
硬件切换方式有以下缺点:- 每个进程都需要一个TSS,耗费内存,内核空间有限,限制了进程数量上限值。
- 每次切换,需要将old task的cpu所有寄存器值存储到这个task对应的TSS(内存中),然后从new task的TSS(内存中)取出所有寄存器值恢复到cpu寄存器。考虑到一些寄存器值并不会更改,更新全部寄存器效率低。
- 代码可移植性差。TSS是IA(intel architecture)相关的其他架构cpu不一定有。
- linux新版本内核采用软件方式切换进程
软件切换进程需要做3件事:- CR3修改进程页目录指针,也就是改变进程的地址空间的映射信息。
- cpu寄存器的保存、恢复,这些寄存器是进程执行所必需的硬件信息。
- 进程堆栈信息的更改。
<span style="font-size:14px;">void __cpuinit cpu_init(void)</span>{ int cpu = smp_processor_id(); struct task_struct *curr = current; struct tss_struct *t = &per_cpu(init_tss, cpu); struct thread_struct *thread = &curr->thread; load_idt(&idt_descr); switch_to_new_gdt(cpu); /* * Set up and load the per-CPU TSS and LDT */ atomic_inc(&init_mm.mm_count); curr->active_mm = &init_mm; BUG_ON(curr->mm); enter_lazy_tlb(&init_mm, curr); load_sp0(t, thread); set_tss_desc(cpu, t); load_TR_desc();
对于cpu而言,一旦设置Task Register指向TSS后,就不会再改变Task Register的值了,所以对于cpu来说,它认为永远是同一个进程在执行。不改变Task Register的值,就不会触发硬件自动保存、加载TSS的操作了,相当于抛弃了intel提供的硬件切换方式。保留TSS的概念,只是为了满足硬件限制而已,进程切换关心的是硬件执行环境----cpu寄存器的值,并不关心TSS。
那么问题来了,如何实现进程切换?其实问题的本质在于如何保存、恢复cpu的寄存器?intel的硬件切换方式不过就是提供了一种保存、恢复cpu寄存器的方法而已。另一种方式是通过汇编指令保存、恢复寄存器,这当然是一种可行的方法,也比较灵活,想保存哪些寄存器就保存哪些寄存器(寄存器的恢复也是一样)。对比分析可以看出,硬件切换方式必须完整地保存、恢复TSS中的寄存器,显得有些呆板。
软件切换方式保存寄存器大致分成2个部分,首先硬件单元自动保存部分寄存器至栈中,这部分寄存器称为hardware stack frame,其他的一些寄存器通过SAVE_ALL宏保存至栈中,这是一段汇编代码。恢复寄存器的操作是个逆向过程,从栈中pop出各个寄存器,加载到cpu寄存器中。本段内容在后面有详细描述。
在linux中,只用到了TSS中的esp0和iomap字段,esp0是内核态栈指针,每次切换进程时,linux会把“切换至”的进程内核栈task_struct->thread->sp0赋给tss_struct->x86_tss.sp0。当x86中断、异常时,cpu控制单元会从tss_sruct->x86_tss.sp0读取新特权级的内核栈,设置ESP寄存器,从而使ESP指向内核栈而不是指向中断前的用户栈,获取到内核栈指针后,就可以在内核栈上有选择地保存硬件寄存器信息了(对于x86而言保存的是struct pt_regs结构体中的寄存器,其中一些是硬件控制单元自动压栈,另一些是软件压栈,参考后面分析)。
优点:- 每个cpu一个TSS结构。本cpu中所有进程用的是同一个TSS。节省内存。
- 进程切换,只更改TSS中的esp0和io权限相关的寄存器,另外通过汇编指令保存部分寄存器,不用更新全部寄存器。
- 软件切换方式不再依赖于x86硬件切换机制,对所有cpu适用,可移植性高。
二、linux进程切换时栈的变化
每个task的栈分成用户栈和内核栈两部分。每个task的内核栈是8k。内核栈与current宏紧密相关,栈低地址是thread_info,栈高地址是task可以实际使用的栈空间。这样设计的目的在于屏蔽栈指针esp的低13位就可以得到thread_info,从而得到thread_info->task,也就是我们的current宏。从上面的描述可以看出,这8k栈必须在物理上连续,并且要8k地址对齐(注1)。linux内核栈与current宏的关系见图3。
图3 内核栈与current宏
pt_regs中的寄存器顺序是固定的。
下面通过图4分析一下栈是如何切换的。当cpu由ring3(用户态)变成ring0(内核态)时,用户栈切换到内核栈。过程如下:
- 在发生中断、异常时前,程序运行在用户态,ESP指向的是Interrupted Procedure's Stack,即用户栈。
- 运行下一条指令前,检测到中断(x86不会在指令执行没有指向完期间响应中断)。从TSS中取出esp0字段(esp0代表的是内核栈指针,特权级0)赋给ESP,所以此时ESP指向了Handler's Stack,即内核栈。
- cpu控制单元将用户堆栈指针(TSS中的ss,sp字段,这代表的是用户栈指针)压入栈,ESP已经指向内核栈,所以入栈指的的是入内核栈。
- cpu控制单元依次压入EFLAGS、CS、EIP、Error Code(如果有的话)。此时内核栈指针ESP位置见图4中的ESP After Transfer to Handler。
图4 Stack Usage with Privilege-Level Change
这里需要做个额外说明,我们这里的场景是从用户态进入内核态,所以图4是描绘得是有特权级变化时硬件控制单元自动压栈的一些寄存器。如果没有特权级变化,硬件控制单元自动压栈的寄存器见图5。
图5 Stack Usage with No Privilege-Level Change
图4、5区别在于如果没有发生特权级变化,硬件控制单元不会压栈SS、ESP寄存器,这2个寄存器共占用8个内存单元,如果不在内核栈高端地址处保留8个bytes,将会导致pt_regs->SS、pt_regs->ESP访问到内核栈顶端以外的地址处,也就是与内核栈高端地址相邻的另一个页中,导致缺页异常,这是一个内核bug。高端地址保留8个bytes,pt_regs->SS、pt_regs->ESP会访问到保留的8个字节单元,虽然其中的值是无效的,但是不会触发内核异常。
其他的寄存器是软件方式保存到栈上的,软件压栈的代码在linux-3.4.5/arch/x86/kernel/entry_32.S中,见SAVE_ALL宏:
.macro SAVE_ALL cld PUSH_GS pushl_cfi %fs /*CFI_REL_OFFSET fs, 0;*/ pushl_cfi %es /*CFI_REL_OFFSET es, 0;*/ pushl_cfi %ds /*CFI_REL_OFFSET ds, 0;*/ pushl_cfi %eax CFI_REL_OFFSET eax, 0 pushl_cfi %ebp CFI_REL_OFFSET ebp, 0 pushl_cfi %edi CFI_REL_OFFSET edi, 0 pushl_cfi %esi CFI_REL_OFFSET esi, 0 pushl_cfi %edx CFI_REL_OFFSET edx, 0 pushl_cfi %ecx CFI_REL_OFFSET ecx, 0 pushl_cfi %ebx CFI_REL_OFFSET ebx, 0 movl $(__USER_DS), %edx movl %edx, %ds movl %edx, %es movl $(__KERNEL_PERCPU), %edx movl %edx, %fs SET_KERNEL_GS %edx.endm
另外说明一下,SAVE_ALL宏压栈的寄存器顺序与struct pt_regs中寄存器定义的顺序是一样的(struct pt_regs中高地址部分是硬件控制单元自动压栈,与SAVE_ALL无关,参考图3),整个struct pt_regs称为hardware stack frame,定义在linux-3.4.5/arch/x86/include/asm/ptrace.h中:struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; int xds; int xes; int xfs; int xgs; long orig_eax; long eip; int xcs; long eflags; long esp; int xss;};
三、进程切换代码实现
执行context_switch汇编时,ESP已经指向内核栈(见上文)。其他通用寄存器在进入异常中断后或者进入system_call时,通过SAVE_ALL保存至内核栈。
参考linux-3.4.5/arch/x86/kernel/entry_32.S文件:
ENTRY(system_call) RING0_INT_FRAME # can't unwind into user space anyway pushl_cfi %eax # save orig_eax SAVE_ALL GET_THREAD_INFO(%ebp)
/* * Saving eflags is important. It switches not only IOPL between tasks, * it also protects other tasks from NT leaking through sysenter etc. */#define switch_to(prev, next, last) \do { \ /* \ * Context-switching clobbers all registers, so we clobber \ * them explicitly, via unused output variables. \ * (EAX and EBP is not listed because EBP is saved/restored \ * explicitly for wchan access and EAX is the return value of \ * __switch_to()) \ */ \ unsigned long ebx, ecx, edx, esi, edi; \ \ asm volatile("pushfl\n\t" /* save flags */ \ "pushl %%ebp\n\t" /* save EBP */ \ "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ "pushl %[next_ip]\n\t" /* restore EIP */ \ __switch_canary \ "jmp __switch_to\n" /* regparm call */ \ "1:\t" \ "popl %%ebp\n\t" /* restore EBP */ \ "popfl\n" /* restore flags */ \ \ /* output parameters */ \ : [prev_sp] "=m" (prev->thread.sp), \ [prev_ip] "=m" (prev->thread.ip), \ "=a" (last), \ \ /* clobbered output registers: */ \ "=b" (ebx), "=c" (ecx), "=d" (edx), \ "=S" (esi), "=D" (edi) \ \ __switch_canary_oparam \ \ /* input parameters: */ \ : [next_sp] "m" (next->thread.sp), \ [next_ip] "m" (next->thread.ip), \ \ /* regparm parameters for __switch_to(): */ \ [prev] "a" (prev), \ [next] "d" (next) \ \ __switch_canary_iparam \ \ : /* reloaded segment registers */ \ "memory"); \} while (0)
16行 push将eflags寄存器压入prev内核栈(不是用户栈,因为ESP已经指向内核栈。也不是nex内核栈,因为这时ESP指向的是prev进程内核栈)。在图4中,eflags寄存器由cpu控制单元自动压入栈中,这里为什么还要用软件再压一次呢?查了linux2.6.32版本中是没有这条指令的,不过到了linux3.4.5中增加了这条指令。猜想是因为某些cpu architecture硬件没有把eflags压栈,所以这里通过软件方式压栈?
17行 ebp压入prev内核栈。
18行 把esp值复制到prev->thread.sp。
19行 把next->thread.sp赋给esp。这个时候栈指针为next内核栈。所以current宏就已经代码next进程了(current宏是把esp低13位屏蔽得到的)。
20行 prev->thread.ip设为1标号处。这是prev恢复执行后,第一条指令的执行地址。
21行 next->thread.ip(标号1地址)压入next内核栈。
23行 jmp至__switch_to函数,这个函数设置cpu硬件寄存器。__switch_to返回时自动把next内核栈中的ip指针pop出来(21行压入的),即标号1地址。所以__switch_to返回后,代码从24行开始执行。
24~26行 恢复next进程ebp、eflags。
这个时候,可以看到prev的寄存器及用户栈等信息已经都保存在prev内核栈或prev->thread.sp中,next进程的硬件信息及栈信息已经恢复,所以此刻,已经可以安全地执行next进程了。
四、switch_to(prev, next, last)为什么存在第3个变量
首先看switch_to宏是如何调用的。
/* * context_switch - switch to the new MM and the new * thread's register state. */static inline voidcontext_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next){ .... switch_to(prev, next, prev); .... finish_task_switch(this_rq(), prev);}
所以,switch_to中的第三个参数其实就是struct task_struct *prev,注意这是个指针。另外还必须注意在switch_to后面,finish_task_switch还会用到prev。
参考switch_to宏实现,switch_to中只有31行把寄存器eax的值赋给了last(last是struct task_struct *prev,相当于改变了指针prev)。那么eax中值又是什么呢?汇编的输入部分44行把prev赋给了eax。综合起来就是:switch_to执行前,prev存在eax中,执行完后,eax赋给prev,这就是说如果在执行期间prev被改变,或者因其他因素导致prev改变,那么执行完后prev还是会恢复成执行前的值。前面说过,context_switch执行完switch_to切换到新进程中,还需要用到prev,所以必须保证prev不能变。
内核既然这样设计,说明prev可能会改变(没有last参数的话),看图6,进程A切换到进程B执行,经过N次调度后,当前运行进程为C,此时需要将C切换到A。
图6 进程切换后保留对prev的引用
switch_to(A, B, A)时,在进程A栈中prev = A, next = B。
switch_to(C, A, C)切换到A中后,根据context->switch --> finish_task_switch要求,prev必须为切换前的进程C。
假定swtich_to没有第三个参数last,那么当switch_to(C, A, C)切换至A后,A栈中的prev = A(因为已经切换到A进程,所以prev用的是A栈中的局部变量prev),并不是C,逻辑上就出问题了。
switch_to是如何解决这个问题的呢,看switch_to(C, A, C)的44行和31行,执行完后prev被改成了C。
五、struct thread_stuct中的sp与sp0
struct thread_struct {/* Cached TLS descriptors: */struct desc_structtls_array[GDT_ENTRY_TLS_ENTRIES];unsigned longsp0;unsigned longsp;
在解释这2个字段之前,先看看copy_thread函数,代码在linux-3.4.5/arch/x86/kernel/process_32.c中。
int copy_thread(unsigned long clone_flags, unsigned long sp, unsigned long unused, struct task_struct *p, struct pt_regs *regs){ struct pt_regs *childregs; struct task_struct *tsk; int err; childregs = task_pt_regs(p); *childregs = *regs; childregs->ax = 0; childregs->sp = sp; p->thread.sp = (unsigned long) childregs; p->thread.sp0 = (unsigned long) (childregs+1); p->thread.ip = (unsigned long) ret_from_fork;
先解释一下task_pt_regs,在前面的描述中,内核栈高地址部分压入了通用寄存器及用户栈指针信息,这些寄存器作为一个整体pt_regs存放在栈高地址部分(内核struct pt_regs结构)。task_pt_regs返回的就是pt_regs的起始地址。
/* * The below -8 is to reserve 8 bytes on top of the ring0 stack. * This is necessary to guarantee that the entire "struct pt_regs" * is accessible even if the CPU haven't stored the SS/ESP registers * on the stack (interrupt gate does not save these registers * when switching to the same priv ring). * Therefore beware: accessing the ss/esp fields of the * "struct pt_regs" is possible, but they may contain the * completely wrong values. */#define task_pt_regs(task) \({ \ struct pt_regs *__regs__; \ __regs__ = (struct pt_regs *)(KSTK_TOP(task_stack_page(task))-8); \ __regs__ - 1; \})KSTK_TOP(task_stack_page(task)返回内核栈高端地址处的地址值,其中-8表示从高端地址处往下偏移8个字节,参考图3。
那么什么需要保留8个字节呢?这是在2005年提交的一个patch,为了解决一个bug:
commit 5df240826c90afdc7956f55a004ea6b702df9203 [PATCH] fix crash in entry.S restore_all Fix the access-above-bottom-of-stack crash.
childregs = task_pt_regs(p);p->thread.sp = (unsigned long) childregs;p->thread.sp0 = (unsigned long) (childregs+1);可以知道sp、sp0的指向位置示意图如下:
图8 sp、sp0指向位置示意图
- linux进程切换(linux3.4.5,x86)
- 64位x86体系Linux内核进程切换
- x86体系结构下Linux-2.6.26的进程调度和切换
- x86体系结构下Linux-2.6.26的进程调度和切换
- Linux进程管理和调度-基于linux3.10
- linux进程解析--进程切换
- Linux内核的进程切换(上)
- Linux前台后台进程切换(转载)
- Linux内核进程切换
- Linux进程切换分析
- linux 进程切换
- Linux内核进程切换
- Linux进程上下文切换
- Linux arm 进程切换
- linux进程切换
- Linux内核进程切换
- Linux 进程切换
- Linux进程切换
- Add Binary 将二进制数相加
- git配置(用ssh)
- oracle数据库中重要的文件(控制文件、数据文件、重写日志文件、归档日志文件、初始化参数文件)
- Java监听和发送飞秋群消息
- Android 手机设置中的关于手机界面
- linux进程切换(linux3.4.5,x86)
- 微信6.0主界面
- 给自己三年时间,从小白混到高级程序员
- 写一个web服务器
- 字符串截取
- Spark源码学习(8)——NetWork
- [从头读历史] 第260节 左传 [BC717至BC658]
- Jboss rules规则引擎 Drools 6.4.0 Final 教程(2)
- leetcode 155 Min Stack