进程的用户栈和内核栈

进程是程序的一次执行过程。用剧本和演出来类比,程序相当于剧本,而进程则相当于剧本的一次演出,舞台、灯光则相当于进程的运行环境。
进程的堆栈
每个进程都有自己的堆栈,内核在创建一个新的进程时,在创建进程控制块task_struct的同时,也为进程创建自己堆栈。一个进程 有2个堆栈,用户堆栈和系统堆栈;用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器指向的 用户堆栈地址,使用用户堆栈,当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核栈空间地址,使用的是内核栈;
进程用户栈和内核栈之间的切换
当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断,称为软中断。进程因为中断(软中断或硬件产生中断),使得CPU切换到特权工作模式,此时进程陷入内核态,进程进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆栈指针寄存器的地址为内核栈地址,这样就完成了用户栈向内核栈的切换。
当进程从内核态切换到用户态时,最后把保存在内核栈中的用户栈地址恢复到CPU栈指针寄存器即可,这样就完成了内核栈向用户栈的切换。
这里要理解一下内核堆栈。前面我们讲到,进程从用户态进入内核态时,需要在内核栈中保存用户栈的地址。那么进入内核态时,从哪里获得内核栈的栈指针呢?
要解决这个问题,先要理解从用户态刚切换到内核态以后,进程的内核栈总是空的。这点很好理解,当进程在用户空间运行时,使用的是用户 栈;当进程在内核态运行时,内核栈中保存进程在内核态运行的相关信息,但是当进程完成了内核态的运行,重新回到用户态时,此时内核栈中保存的信息全部恢 复,也就是说,进程在内核态中的代码执行完成回到用户态时,内核栈是空的。
理解了从用户态刚切换到内核态以后,进程的内核栈总是空的,那刚才这个问题就很好理解了,因为内核栈是空的,那当进程从用户态切换到内核态后,把内核栈的栈顶地址设置给CPU的栈指针寄存器就可以了。
X86 Linux内核栈定义如下(可能现在的版本有所改变,但不妨碍我们对内核栈的理解):
在/include/linux/sched.h中定义了如下一个联合结构:
union task_union {
struct task_struct task;
unsigned long stack[2408];
};
从这个结构可以看出,内核栈占8kb的内存区。实际上,进程的task_struct结构所占的内存是由内核动态分配的,更确切地说,内核根本不给task_struct分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分给task_struct使用。
这样内核栈的起始地址就是union task_union变量的地址+8K 字节的长度。例如:我们动态分配一个union task_union类型的变量如下:
unsigned char *gtaskkernelstack
gtaskkernelstack = kmalloc(sizeof(union task_union));

那么该进程每次进入内核态时,内核栈的起始地址均为:(unsigned char *)gtaskkernelstack + 8096

进程上下文
进程切换现场称为进程上下文(context),包含了一个进程所具有的全部信息,一般包括:进程控制块(Process Control Block,PCB)、有关程序段和相应的数据集。

进程控制块PCB(任务控制块)
进程控制块是进程在内存中的静态存在方式,Linux内核中用task_struct表示一个进程(相当于进程的人事档案)。进程的静 态描述必须保证一个进程在获得CPU并重新进入运行态时,能够精确的接着上次运行的位置继续进行,相关的程序段,数据以及CPU现场信息必须保存。处理机 现场信息主要包括处理机内部寄存器和堆栈等基本数据。
进程控制块一般可以分为进程描述信息、进程控制信息,进程相关的资源信息和CPU现场保护机构。

进程的切换
当一个进程的时间片到时,进程需要让出CPU给其他进程运行,内核需要进行进程切换。
Linux 的进程切换是通过调用函数进程切换函数schedule来实现的。进程切换主要分为2个步骤:
1. 调用switch_mm()函数进行进程页表的切换;
2. 调用 switch_to() 函数进行 CPU寄存器切换;

__switch_to定义在/arch/arm/kernel目录下的entry-armv.S 文件中,源码如下:


-----------------------------------------------------------------------------
ENTRY(__switch_to)
 UNWIND(.fnstart    )
 UNWIND(.cantunwind )
    add ip, r1, #TI_CPU_SAVE
    ldr r3, [r2, #TI_TP_VALUE]
    stmia   ip!, {r4 - sl, fp, sp, lr} @ Store most regs on stack
#ifdef CONFIG_MMU
    ldr r6, [r2, #TI_CPU_DOMAIN]
#endif
#if __LINUX_ARM_ARCH__ >= 6
#ifdef CONFIG_CPU_32v6K
    clrex
#else
    strex   r5, r4, [ip]            @ Clear exclusive monitor
#endif
#endif
#if defined(CONFIG_HAS_TLS_REG)
    mcr p15, 0, r3, c13, c0, 3      @ set TLS register
#elif !defined(CONFIG_TLS_REG_EMUL)
    mov r4, #0xffff0fff
    str r3, [r4, #-15]          @ TLS val at 0xffff0ff0
#endif
#ifdef CONFIG_MMU
    mcr p15, 0, r6, c3, c0, 0       @ Set domain register
#endif
    mov r5, r0
    add r4, r2, #TI_CPU_SAVE
    ldr r0, =thread_notify_head
    mov r1, #THREAD_NOTIFY_SWITCH
    bl atomic_notifier_call_chain
    mov r0, r5
    ldmia   r4, {r4 - sl, fp, sp, pc}   @ Load all regs saved previously
 UNWIND(.fnend      )
ENDPROC(__switch_to)
----------------------------------------------------------

Switch_to的处理流程如下:

  1. 保存本进程的CPU寄存器(PC、R0 ~ R13)到本进程的栈中;
  2. 保存SP(本进程的栈基地址)到task->thread.save 中;
  3. 从新进程的task->thread.save恢复SP为新进程的栈基地址;
  4. 从新进程的栈中恢复新进程的CPU相关寄存器值,
  5. 新进程开始运行,完成任务切换。

这里读者可能会问,在进行任务切换的时候,到底是在运行进程1还是运行进程2呢?进程切换的时候,已经进行页表切换,那页表切换之后,切换进程使用的是进程1还是进程2的页表呢?

要回答这个问题,首先我们要明白由谁来完成进程切换?

通过对操作系统的理解,毫无疑问,进程切换是由内核来完成的,也就是说,在进行进程切换时,CPU运行在内核模式,使用的是内核空间的内核代码,它既不属于进程1,也不属于进程2,当进程的时间片到时,内核提供服务来完成进程的切换。既不使用进程1的页表,也不使用进程2的页表,使用的内核映射页表。这样我们就很好理解上面的问题了。

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值