精华内容
下载资源
问答
  • 内核栈

    千次阅读 2017-04-13 17:37:30
    为什么需要内核栈 进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack, struct task_struct { // ... void *stack; // 指向内核栈的指针 // ... }; 内核态...

    为什么需要内核栈


    • 进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack,
    struct task_struct
    {
        // ...
        void *stack;    //  指向内核栈的指针
        // ...
    };

    内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。

    用户态进程所用的栈,是在进程线性地址空间中;

    而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。

    需要注意的是,内核态堆栈仅用于内核例程,Linux内核另外为中断提供了单独的硬中断栈软中断栈

    为什么需要thread_info


    • 内核还需要存储每个进程的PCB信息, linux内核是支持不同体系的的, 但是不同的体系结构可能进程需要存储的信息不尽相同, 这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部门进行分离

    用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

    将两种结构融合在一起


    linux将内核栈和进程控制块thread_info融合在一起, 组成一个联合体thread_union

    通常内核栈和thread_info一同保存在一个联合体中, thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针

    内核数据结构描述


    thread_union


    对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:

    • 一个是内核态的进程堆栈stack

    • 另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。

    这两个结构被紧凑的放在一个联合体中thread_union中,

    union thread_union
    {
        struct thread_info thread_info;
        unsigned long stack[THREAD_SIZE/sizeof(long)];
    };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5

    这块区域32位上通常是8K=8192(占两个页框),64位上通常是16K,其实地址必须是8192的整数倍。

    架构 THREAD_SIZE
    x86 arch/x86/include/asm/page_32_types.h, line 21
    x86_64 arch/x86/include/asm/page_64_types.h, line 11
    arm arch/arm/include/asm/thread_info.h, line 20
    arm64 arch/arm64/include/asm/thread_info.h, line 32

    出于效率考虑,内核让这8K(或者16K)空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。

    下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图:


    在这个图中,

    • esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。

    同时我们可以看到,

    • thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出

    • 系统的current指针指向了当前运行进程的thread_union(或者thread_info)的地址

    • 进程task_struct中的stack指针指向了进程的thread_union(或者thread_info)的地址, 在早期的内核中这个指针用struct thread_info *thread_info来表示, 但是新的内核中用了一个更浅显的名字void *stack, 即内核栈

    即,进程的thread_info存储在进程内核栈的最低端

    task_struct中的内核栈stack


    我们之前在描述task_struct时就提到了其stack指针指向的是内核栈的地址。

    参见 Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一)

    其被定义在include/linux/sched.h中

    http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1391

    形式如下

    struct task_struct
    {
        // ...  
        void *stack;    //  指向内核栈的指针  
        // ...
    };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在早期的linux内核中进程描述符中是不包含内核栈的, 相反包含着指向thread_info的指针

    但是在2007年的一次更新(since 2.6.22)中加入了stack内核栈指针, 替代了原来的thread_info的指针

    进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。

    stack指向了内核栈的地址(其实也就是thread_info和thread_union的地址),因为联合体中stack和thread_info都在起始地址, 因此可以很方便的转型

    相关代码在include/linux/sched.h中 
    task_thread_info用于通过task_struct来查找其thread_info的信息, 只需要一次指针类型转换即可

    #define task_thread_info(task)  ((struct thread_info *)(task)->stack)
    • 1
    • 1

    内核栈数据结构描述thread_info


    thread_info是体系结构相关的,结构的定义在thread_info.h中,保存了进程所有依赖于体系结构的信息, 同时也保存了一个指向进程描述符task_struct的指针

    架构 定义链接
    x86 linux-4.5/arch/x86/include/asm/thread_info.h, line 55
    arm linux-4.5arch/arm/include/asm/thread_info.h, line 49
    arm64 linux/4.5/arch/arm64/include/asm/thread_info.h, line 47

    函数接口


    内核栈与thread_info的通用操作


    原则上, 只要设置了预处理器常数__HAVE_THREAD_FUNCTIONS通知内核, 那么各个体系结构就可以随意在stack数组中存储数据。

    在这种情况下, 他们必须自行实现task_thread_infotask_stack_page, 这两个函数用于获取给定task_struct实例的线程信息和内核栈。

    另外, 他们必须实现dup_task_struct中调用的函数setup_thread_stack, 以便确定stack成员的具体内存布局, 当前只有ia64等少数架构不依赖于内核的默认方法

    下标给出了不同架构的task_thread_info和task_stack_page的实现

    架构 定义链接
    ia64 arch/ia64/include/asm/thread_info.h, line 53
    通用 include/linux/sched.h, line 2812
    // 未定义__HAVE_THREAD_FUNCTIONS的时候使用内核的默认操作
    #ifndef __HAVE_THREAD_FUNCTIONS  
    
    //  通过进程的task_struct来获取进程的thread_info
    #define task_thread_info(task)  ((struct thread_info *)(task)->stack)
    //  通过进程的task_struct来获取进程的内核栈
    #define task_stack_page(task)   ((task)->stack)
    
    //  初始化thread_info, 指定其存储结构的内存布局
    static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
    {
        *task_thread_info(p) = *task_thread_info(org);
        task_thread_info(p)->task = p;
    }
    
    /*
     * Return the address of the last usable long on the stack.
     *
     * When the stack grows down, this is just above the thread
     * info struct. Going any lower will corrupt the threadinfo.
     *
     * When the stack grows up, this is the highest address.
     * Beyond that position, we corrupt data on the next page.
     */
    static inline unsigned long *end_of_stack(struct task_struct *p)
    {
    #ifdef CONFIG_STACK_GROWSUP
        return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1;
    #else
        return (unsigned long *)(task_thread_info(p) + 1);
    #endif
    }
    
    #endif
    在内核的某个特定组建使用了较多的栈空间时, 内核栈会溢出到thread_info部分, 因此内核提供了kstack_end函数来判断给出的地址是否位于栈的有效部分
    #ifndef __HAVE_ARCH_KSTACK_END
    static inline int kstack_end(void *addr)
    {
        /* Reliable end of stack detection:
         * Some APM bios versions misalign the stack
         */
        return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*)));
    }
    #endif

    前面我们在讲_do_fork创建进程的时候, 提到dup_task_struct会复制父进程的task_struct和thread_info实例的内容, 但是stack则与新的thread_info实例位于同一个内存, 这意味着父子进程的task_struct此时除了栈指针之外完全相同。

    获取当前在CPU上正在运行进程的thread_info


    所有的体系结构都必须实现两个current和current_thread_info的符号定义宏或者函数,

    • current_thread_info可获得当前执行进程的thread_info实例指针, 其地址可以根据内核指针来确定, 因为thread_info总是位于起始位置,

      因为每个进程都有自己的内核栈, 因此进程到内核栈的映射是唯一的, 那么指向内核栈的指针通常保存在一个特别保留的寄存器中(多数情况下是esp)

    • current给出了当前进程进程描述符task_struct的地址,该地址往往通过current_thread_info来确定 
      current = current_thread_info()->task

    因此我们的关键就是current_thread_info的实现了,即如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。

    早期的版本中,不需要对64位处理器的支持,所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。

    我们在下面对比了,获取正在运行的进程的thread_info的实现方式

    架构 版本 定义链接 实现方式 思路解析
    x86 3.14 current_thread_info(void) return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); 屏蔽了esp的低十三位,最终得到的是thread_info的地址
    x86 3.15 current_thread_info(void) ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE);  
    x86 4.1 current_thread_info(void) (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);  

    早期版本

    当前的栈指针(current_stack_pointer == sp)就是esp,

    THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。

    ~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。

    进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在include/asm-generic/current.h中定义:

    #define get_current() (current_thread_info()->task)
    #define current get_current()
    • 1
    • 2
    • 1
    • 2

    这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current

    请参见 :http://lxr.free-electrons.com/ident?v=4.5;i=current

    分配和销毁thread_info


    进程通过alloc_thread_info_node函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。

    # if THREAD_SIZE >= PAGE_SIZE
    static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
                              int node)
    {
        struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP,
                              THREAD_SIZE_ORDER);
    
        return page ? page_address(page) : NULL;
    }
    
    static inline void free_thread_info(struct thread_info *ti)
    {
        free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER);
    }
    # else
    static struct kmem_cache *thread_info_cache;
    
    static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
                              int node)
    {
        return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node);
    }
    
    static void free_thread_info(struct thread_info *ti)
    {
        kmem_cache_free(thread_info_cache, ti);
    }

    其中,THREAD_SIZE_ORDER宏的定义请查看

    架构 版本 定义链接 实现方式 思路解析
    x86 4.5 arch/x86/include/asm/page_32_types.h, line 20 define THREAD_SIZE_ORDER 1 __get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的)
    x86_64 4.5 arch/x86/include/asm/page_64_types.h, line 10 define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)  



    展开全文
  • 内核栈和用户栈

    千次阅读 2019-03-13 15:50:14
    每个进程有两个栈:用户栈、内核栈 用户栈在用户地址空间中,内核栈在内核地址空间中。 用户栈 用户栈不难理解,用户栈是用户空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量...

    内核在创建进程的时候创建进程控制块以及进程的堆栈。每个进程有两个栈:用户栈、内核栈

    用户栈在用户地址空间中,内核栈在内核地址空间中。

    用户栈

         用户栈不难理解,用户栈是用户空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量等信息。在linux系统中,用户栈的大小一般为8M。可以通过ulimit -s来手动设置。

    进程用户栈和内核栈的切换

           当进程由于中断或系统调用从用户态转换为内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断(软中断)。进程由于中断而陷入到内核态,进程进入内核态之后,首先把用户态的堆栈地址保存在内核态堆栈中,然后设置堆栈寄存器地址为内核栈地址,这样就从用户栈转换成内核栈。

            当进程从内核态转换到用户态时,将堆栈寄存器的地址再重新设置成用户态的堆栈地址(即终端前进程在用户态执行的位置),这一过程也成为现场恢复

    内核栈

           通过上述过程不难发现一个问题:用户态陷入到内核态时,需要在内核栈保存用户栈的地址,那么在进入内核态时,去哪里找内核栈指针呢?

          首先我们需要明确一点:每当进程从用户态切换到内核态时,内核栈总是空的。当进程在用户态执行时,使用的是用户栈,切换到内核态运行时,内核栈保存的时进程在内核中运行的相关信息。重回到用户态时,内核栈中保存的信息全部恢复,因此,进程从内核态切换到用户态时,内核栈总是空的。

         弄清楚了每次进程切换到内核态时内核栈总是空的,那上述问题就很好解决了,用户态切换到内核栈时,因为内核栈是空的,只需将栈寄存器值设置成内核栈栈顶指针即可,那么下边我们通过内核栈的实现来理解如何获得内核栈的栈顶指针。

    内核栈的实现

           以linux内核为例,内核在创建进程并时,首先需要给进程分配task_struct结构体,在做这一步的时候内核实际上分配了两块连续的物理空间(一般是1个物理页),上边供堆栈使用,下边保存进程描述符task_struct。这个整体叫做进程的内核栈,因此task_struct是在进程内核栈内部的,如下图:

    当为内核栈分配地址空间的时候,分配一个页面(这里以8k为例)返回的地址是该该页面的低地址,而栈是由高地址向低地址增长的,由上图所示:栈顶指针只需将该内核栈的首地址+8k即可

    展开全文
  • 进程内核栈、用户栈

    2018-05-03 16:01:38
    1.进程的堆栈 内核在创建进程的时候,在创建task_struct的同事,会为进程创建...当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。 2.进程用户栈和内核栈的切换 当进程因为中...

    1.进程的堆栈

    内核在创建进程的时候,在创建task_struct的同事,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。

    2.进程用户栈和内核栈的切换

    当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。

    进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。

    那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?

    关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信心,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。

    3.内核栈的实现

    内核栈在kernel-2.4和kernel-2.6里面的实现方式是不一样的。
    
     在kernel-2.4内核里面,内核栈的实现是:
    
     Union task_union {
    
                       Struct task_struct task;
    
                       Unsigned long stack[INIT_STACK_SIZE/sizeof(long)];
    
     };

    其中,INIT_STACK_SIZE的大小只能是8K。

    内核为每个进程分配task_struct结构体的时候,实际上分配两个连续的物理页面,底部用作task_struct结构体,结构上面的用作堆栈。使用current()宏能够访问当前正在运行的进程描述符。

    注意:这个时候task_struct结构是在内核栈里面的,内核栈的实际能用大小大概有7K。

    内核栈在kernel-2.6里面的实现是(kernel-2.6.32):

    
     Union thread_union {
    
                       Struct thread_info thread_info;
    
                       Unsigned long stack[THREAD_SIZE/sizeof(long)];
    
     };

    其中THREAD_SIZE的大小可以是4K,也可以是8K,thread_info占52bytes。
    具体结构见图:
    这里写图片描述

    当内核栈为8K时,Thread_info在这块内存的起始地址,内核栈从堆栈末端向下增长。所以此时,kernel-2.6中的current宏是需要更改的。要通过thread_info结构体中的task_struct域来获得于thread_info相关联的task。更详细的参考相应的current宏的实现。

     struct thread_info {
    
                       struct task_struct *taskstruct exec_domain *exec_domain;
    
                       __u32 flags;
    
            __u32 status;
    
                       __u32 cpu;
    
                       …  ..
    
     };

    注意:此时的task_struct结构体已经不在内核栈空间里面了。

    一个实际的内核栈的结构将如下图所示。由于栈总是由高地址向低地址延伸的,所以栈底位于thread_union联合体的最末端,而thread_info结构则位于thread_union联合体的开始处,而且所占用的空间比较少。只要不出现内核栈特别大的极端情况,栈与thread_info可以互不干扰。该图为内核地址空间示意图。
    这里写图片描述


    用户栈

    用户栈就是应用程序直接使用的栈。如下图所示,它位于应用程序的用户进程空间的最顶端。

    当用户程序逐级调用函数时,用户栈从高地址向低地址方向扩展,每次增加一个栈帧,一个栈帧中存放的是函数的参数、返回地址和局部变量等,所以栈帧的长度是不定的。

    用户栈的栈底靠近进程空间的上边缘,但一般不会刚好对齐到边缘,出于安全考虑,会在栈底与进程上边缘之间插入一段随机大小的隔离区。这样,程序在每次运行时,栈的位置都不同,这样黑客就不大容易利用基于栈的安全漏洞来实施攻击。

    用户栈的伸缩对于应用程序来说是透明的,应用程序不需要自己去管理栈,这是操作系统提供的功能。应用程序在刚刚启动的时候(由fork()系统调用复制出新的进程),新的进程其实并不占有任何栈的空间。当应用程序中调用了函数需要压栈时,会触发一个page fault,内核在处理这个异常里会发现进程需要新的栈空间,于是建立新的VMA并映射内存给用户栈。
    这里写图片描述


    内核栈和用户栈区别:

    intel的cpu分为四个运行级别ring0~ring3

    内核创建进程,创建进程的同时创建进程控制块,创建进程自己的堆栈

    一个进程有两个堆栈,用户栈和系统栈

    用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。

    有个CPU堆栈指针寄存器,进程运行的状态有用户态和内核态,当进程运行在用户态时。CPU堆栈指针寄存器指向的是用户堆栈地址,使用的是用户堆栈;当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核堆栈地址,使用的是内核堆栈。

    1、堆栈切换

    当系统因为系统调用(软中断)或硬件中断,CPU切换到特权工作模式,进程陷入内核态,进程使用的栈也要从用户栈转向系统栈。

    从用户态到内核态要两步骤,首先是将用户堆栈地址保存到内核堆栈中,然后将CPU堆栈指针寄存器指向内核堆栈。

    当由内核态转向用户态,步骤首先是将内核堆栈中得用户堆栈地址恢复到CPU堆栈指针寄存器中。

    2、内核栈和用户栈区别

    2.1

    栈是系统运行在内核态的时候使用的栈,用户栈是系统运行在用户态时候使用的栈。

    当进程由于中断进入内核态时,系统会把一些用户态的数据信息保存到内核栈中,当返回到用户态时,取出内核栈中得信息恢复出来,返回到程序原来执行的地方。
    用户栈就是进程在用户空间时创建的栈,比如一般的函数调用,将会用到用户栈。

    2.2

    内核栈是属于操作系统空间的一块固定区域,可以用于保存中断现场、保存操作系统子程序间相互调用的参数、返回值等。

    用户栈是属于用户进程空间的一块区域,用户保存用户进程子程序间的相互调用的参数、返回值等。

    2.3.

    每个Windows 都有4g的进程空间,系统栈使用进程空间的地段部分,用户栈是高端部分如果用户要直接访问系统栈部分,需要有特殊的方式。

    为何要设置两个不同的栈?

    共享原因:

    内核的代码和数据是为所有的进程共享的,如果不为每一个进程设置对应的内核栈,那么就不能实现不同的进程执行不同的代码。

    安全原因:

    如果只有一个栈,那么用户就可以修改栈内容来突破内核安全保护。

    展开全文
  • 用户栈和内核栈

    2020-06-01 12:12:04
    操作系统中,每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。 当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈...
    1. 操作系统中,每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。

    2. 当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。(通过中断或系统调用进入内核态

    3. 内核栈是内存中属于操作系统空间的一块区域,其主要用途为:

    1)保存中断现场,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出;
    2)保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。

    1. 用户栈是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。

    PS:那么为什么不直接用一个栈,何必浪费那么多的空间呢?
    1)如果只用系统栈。系统栈一般大小有限,如果中断有16个优先级,那么系统栈一般大小为15(只需保存15个低优先级的中断,另一个高优先级中断处理程序处于运行)。
    但用户程序子程序调用次数可能很多,那样15次子程序调用以后的子程序调用的参数、返回值、返回点以及子程序(函数)的局部变量就不能被保存,用户程序也就无法正常运行了。

    2)如果只用用户栈。我们知道系统程序需要在某种保护下运行,而用户栈在用户空间(即cpu处于用户态,而cpu处于内核态时是受保护的),不能提供相应的保护措施(或相当困难)。

    5.进程用户栈和内核栈的切换

    当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。
    进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
    那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
    关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信心,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
    内核栈在kernel-2.6里面的实现是(kernel-2.6.32):

    Union thread_union {
    
                       Struct thread_info thread_info;
    
                       Unsigned long stack[THREAD_SIZE/sizeof(long)];
    
    };
    

    其中THREAD_SIZE的大小可以是4K,也可以是8K,thread_info占52bytes。

    当内核栈为8K时,Thread_info在这块内存的起始地址,内核栈从堆栈末端向下增长。

    每当进程调用一次函数,都会在用户栈中为该函数分配一个栈帧(stack frame),也称为调用栈(call stack),当该函数返回时又会释放该栈帧。释放的栈帧不会从虚拟内存中移除,它可以被之后调用的函数重新使用,所以栈空间的大小是不会减小的。
    由于函数内部调用函数时,外部函数的栈帧不会释放,只有内部函数全部退出了才会继续执行外部函数并在执行完成的时候释放外部函数的栈帧,所以,递归函数(即函数内部调用函数自身)如果递归调用的层次太多(比如无限递归),会分配大量的栈帧,并且不会释放,直到栈空间不足,无法再分配新的栈帧,这时会报栈溢出(stack overflows)错误。所以,必须要合理编写递归函数,使得递归函数能够在达到某些条件时返回,从而释放栈帧,避免无限递归。

    栈帧中保存了传递给该函数的参数、该函数中定义的局部变量、函数的返回值、调用该函数的程序计数器副本,以及一些其它重要信息。这里有必要解释下栈帧中的程序计数器副本。

    什么是程序计数器(Program Counter,PC)?这是CPU中的一个寄存器,在这个寄存器中保存了下一个要执行指令的指针。所以,CPU每执行一个指令的时候,就会设置这个寄存器使它指向下一个指令。

    展开全文
  • Linux内核栈和中断栈

    千次阅读 2019-08-09 11:47:21
    内核栈 #define MIN_THREAD_SHIFT (14 + KASAN_THREAD_SHIFT) #define THREAD_SIZE (UL(1) << THREAD_SHIFT) union thread_union { #ifndef CONFIG_THREAD_INFO_IN_TASK struct thread_info ...
  • Linux 内核:进程内核栈、用户栈 1.进程的堆栈  内核在创建进程的时候,在创建task_struct的同事,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当...
  • 4.4.1进程内核栈每个进程都有自己的内核栈。当进程从用户态进入内核态时,CPU就自动地设置该进程的内核栈,也就是说,CPU从任务状态段TSS中装入内核栈指针esp(参见下一章的进程切换一节)。X86内核栈的分布如图4.2所...
  • 进程内核栈

    2017-05-08 10:22:12
    进程内核栈 为什么有进程内核栈   进程在创建的时候也可以理解为一个程序,或者在简单的理解也可以把进程理解为一个函数,只不过这个函数很大而已,这个进程也需要有一些函数调用,也需要有一些函数去标记一些信息...
  • 用户栈和内核栈的区别
  • 进程内核栈和用户栈

    2017-06-06 10:58:51
    进程内核栈、用户栈 1.进程的堆栈  内核在创建进程的时候,在创建task_struct的同事,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户...
  • 这是毫无疑问的,但还有一点我没搞明白,内核栈是共享还是独立的? 回答:独立的。理由:要不然内核栈对应的thread_info中的tast_struct(pcb进程控制块)没有办法与每个线程对应起来,因为现在已经有多...
  • Linux内核-进程内核栈、用户栈

    千次阅读 2013-07-07 10:35:04
    1.进程的堆栈  内核在创建进程的时候,在创建task_struct的同事,会为进程创建相应的堆栈。...当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。 2.进程用户栈和内核栈的切换
  • Linux内核栈与用户栈分析

    千次阅读 2016-06-01 20:42:05
    内核栈在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,这个称作进程的“内核栈”。内核栈主要用于...
  • 内核栈大小

    千次阅读 2014-12-08 22:57:54
    最后提到的问题,说递归出现问题,就是因为内核栈大小太小,导致异常。 Linux内核栈溢出(stack overflow)问题 最近一段时间在设计和开发一个Linux内核模块,进入到最后的正确性测试与稳定性测试阶段。在这个阶段...
  • 内核栈 在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,这个称作进程的“内核栈”。 内核栈主要...
  • 进程内核栈、用户栈 进程的堆栈 内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。 当进程在用户空间运行时...
  • 进程栈和内核栈

    千次阅读 2014-01-23 17:40:34
    1.进程的堆栈  内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的...当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。 2.进程用户栈和内核栈的切换(堆栈指
  • 来源: 简书:saviochen   内核在创建进程时,会同时创建task...当进程在内核空间时,CPU堆栈寄存器的内容是内核栈地址空间,使用的是内核栈。   当进程因为中断或系统调用进入内核时,进程使用的堆栈也需要从用...
  • 内核在创建进程时,会为其创建相应的堆栈,分别是用户栈和内核栈 用户栈:存在于用户空间 内核栈:存在于内核空间 当进程在用户空间运行时,CPU堆栈指针寄存器里面的内容是用户堆栈的地址,当前为使用用户栈。 ...
  • linux内核栈和用户栈

    千次阅读 2016-05-24 22:58:52
    Linux内核栈和用户栈 一.概述 Linux进程在运行的时候有不同的状态,可以有用户态、内核态、中断异常状态,用户态由于系统调用等原因可以进入内核态,或者产生外部中断则执行中断流程。同时由于函数的调用需要进行...
  • 用户栈、内核栈

    2012-04-19 11:58:06
    基于线程的用户栈与内核栈: 内核在创建每个线程时,都会创建两个栈,一个用户栈,存在于用户空间;一个内核栈,存在于内核空间。 当线程在用户空间运行时,cpu栈指针寄存器内容保存用户栈空间地址,使用用户栈;...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 149,180
精华内容 59,672
关键字:

内核栈