首页 > 其他分享 >内核栈、用户态栈

内核栈、用户态栈

时间:2022-11-16 20:47:34浏览次数:47  
标签:用户 态栈 保存 cpu 内核 寄存器 tss

1、背景

当线程从用户态进入内核态的时候,内核会将栈从用户态栈切换成内核栈,每个线程都有自己的内核栈;在x86架构下,内核栈保存在tss里;tss里包括sp0、sp1、sp2三个指针;内核当前是将线程的内核栈保存到sp1的;sp0、sp2是作为用户态栈与内核栈切换时的一个临时栈使用;每个cpu都有一个tss结构体,cpu在线程调度switch_to流程里,会将下一个运行的task的内核栈保存到当前cpu的sp1、sp0里;

2、内核栈

为每个线程分配内核栈的原因:
1)、我们首先必然要区分用户栈和内核栈以达到起码的保护目的[7],在进入内核态前,如果用户态将栈指针放在一个恶意区域(比如内核地址空间等等),那么内核就会轻易地覆写不该写入的区域;
2)、再之,对于linux来说,给每个进程分配一个内核栈也是非常必要的,一方面进程进行系统调用时可能会阻塞在内核态(比如等待用户输入等等),此时进程在内核态的状态需要保留在内核态上,切换到其他 进程(内核可抢占),假如其他进程没有自己的内核栈,则栈上又会压入其他进程的使用数据,这样一来原进程状态就无法恢复[8];另一方面,为了提高进程响应速度,linux内核是可抢占的,也就是说,进程即便不是在执行系统调用而是在处理异常/中断,只要不是在time-critical region,进程也可能会被切换,也就是其他进程执行时发生中断/异常也可能触发切换到某个进程,为了确保进程状态即便是在内核态也能够恢复,内核栈内容仍然需要被保存[9];除此之外,给每个进程分配一个内核栈使得进程从一个cpu迁移到另一个cpu变得十分便利[9],因为进程内核栈上保存了context info,只需要保存栈指针等等的信息到进程的descriptor,其他cpu就可以轻易地恢复进程,而无需拷贝栈内容;
3)、最后,退出内核态时,当前进程的内核栈应当是全部清空的;

内核栈的分配:

do_fork
    _do_fork
        copy_process
            dup_task_struct
                alloc_task_struct_node(分配tsk)
                alloc_thread_stack_node(分配内核栈)
                    __vmalloc_node_range(通过vmalloc分配内核栈,保证虚机地址连续,分配大小为THREAD_SIZE = 4096 << 2 = 16k)
                    memcg_charge_kernel_stack(将内核栈的内存统计到cgroup的kmem里)
            tsk->stack = stack;   (将内核栈保存到tsk->stack里)

3、tss

tss(struct tss_struct)用于保存不同特权级别下所使用的寄存器,尤其是esp寄存器。 内核为每个线程创建了一个内核栈,在线程由用户态进入内核态的时候,需要将栈从用户栈(glibc分配)切换成内核栈;
但是linux只使用sp0,linux的做法:

1)、linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段到新进程的内核栈。
2)、linux的tss段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,tss中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上而不保存在tss中。
3)、结果,linux中每一个cpu只有一个tss段,tr寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的后果是开销更小了,因为不必切换tr寄存器了。

3.1、初始化

1)、linux为每个cpu保存一份tss;

 

 

 2)、

setup_arch
    smp_init_cpus
        smp_cpu_setup
            cpu_init
            t = &per_cpu(cpu_tss_rw, cpu); (获取当前cpu的tss)
            t->x86_tss.io_bitmap_base = IO_BITMAP_OFFSET; (初始化iobitmap)
       load_sp0((unsigned long)(cpu_entry_stack(cpu) + 1)); (初始化tss的sp0)

4、内核栈保存与切换

4.1、线程切换保存内核栈信息到

4、内核栈保存与切换
4.1、线程切换保存内核栈信息到tss
__schedule
    next = pick_next_task(rq, prev, &rf);         (选择下一个要执行的task)
    context_switch(上下文切换)
        switch_to
            __switch_to
                this_cpu_write(current_task, next_p);     (设置current_task为next_p,x86_64的current宏就是从current_task里获取当前的running task)
                this_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p));   (将next_task的内核栈顶保存到tss的sp1处)
                update_sp0(next_p);(如果是xen虚拟机,则进一步更新tss的esp0为新task的内核栈,因此正常的x86_64机器,并不会执行load_sp0,sp0的值为一开始初始化的值,用于内核栈切用户态栈时的一个临时存放区域)

4.2、系统调用时用户态栈与内核栈切换

4.2.1、系统调用入口初始化

在syscall_init里,将entry_SYSCALL_64的地址写到MSR_LSTAR寄存器里;当用户态执行syscall指令时,cpu会只做如下动作:

1)、将syscall的下一条指令地址存放到R13;
2)、将当前的RFLAGS寄存器保存到R11寄存器上;
3)、将MSR_LSTAR寄存器的值(系统调用入口,即entry_SYSCALL_64的地址)保存到RIP寄存器;
4)、cpu从ring0调转到ring3;

 

 

 当cpu执行sysret执行返回用户态时,cpu会自动做如下动作:

1)、将栈里保存的的RIP、CS、EFLAGS值弹出到RIP、CS、EFLAGS寄存器上;
2)、如果是内核态回到用户态,则进一步将栈里保存的rsp、ss弹出到RSP、SS寄存器上;
3)、执行完iret后,就会到了用户态,此时RSP的值为用户态栈;

 

 

 

系统调用过程:
1)、进入entry_SYSCALL_64;
2)、保存用户态栈到sp2;
3)、从cpu_current_top_of_stack里获取内核栈,保存到rsp,切换成内核栈;
4)、通过pushq将用户态的ss、sp、cs、ip、orig_ax保存到内核栈里;
5)、通过PUSH_AND_CLEAR_REGS保存剩余的通用寄存器;保存完后将寄存器清零;
6)、将系统调用号赋给rax,当前已经保存好用户态信息的内核栈赋给rsi;
7)、进入c语言do_syscall_64,执行对应的系统调用;
8)、进入swapgs_restore_regs_and_return_to_usermode,开始准备返回用户态;
9)、通过POP_REGS,将当前保存在内核栈的用户态寄存器pop出来;
10)、rsp指向sp0的临时栈上;
11)、将用户态的现场信息保存到sp0的栈上;
12)、执行iret指令回到用户态,该指令会将R13寄存器取出来赋值给RIP,同时将栈里的用户态栈rsp值弹出赋值到RSP寄存器,完成内核栈到用户态栈的切换;

 

用户态进入内核态:

 

 

 

内核态回到用户态:

 

 

 

标签:用户,态栈,保存,cpu,内核,寄存器,tss
From: https://www.cnblogs.com/hhdd666/p/16897427.html

相关文章