what is linux stack

WBOY
WBOYOriginal
2022-07-08 16:41:262507browse

In Linux, the stack is a data structure in the form of a series; the characteristic of this data structure is last-in-first-out, and data can only be pushed and popped at one end of the series. Linux The stacks in can be divided into process stacks, thread stacks, kernel stacks and interrupt stacks.

what is linux stack

#The operating environment of this tutorial: linux7.3 system, Dell G3 computer.

What is the Linux stack

Stack (stack) is a data structure in serial form. The characteristic of this data structure is LIFO (Last In First Out). Data can only be pushed and popped at one end of the string (called the top of the stack).

  • Process stack

  • Thread stack

  • Kernel stack

  • Interrupt Stack

what is linux stack

Most processor architectures implement a hardware stack. There is a dedicated stack pointer register and specific hardware instructions to complete the push/pop operation. For example, on the ARM architecture, the R13 (SP) pointer is the stack pointer register, PUSH is the assembly instruction for pushing on the stack, and POP is the assembly instruction for popping the stack.

[Extended reading] Introduction to ARM registers

The ARM processor has 37 registers. These registers are arranged in partially overlapping groups. Each processor mode has a different set of registers. Grouped registers provide fast context switching for handling processor exceptions and privileged operations.

The following registers are provided:

  • Thirty 32-bit general-purpose registers:

  • Fifteen general-purpose registers exist, They are r0-r12, sp, lr

  • sp (r13) is the stack pointer. C/C compilers always use sp as the stack pointer

  • lr (r14) to store the return address when calling a subroutine. If the return address is stored on the stack, lr can be used as a general purpose register

  • Program Counter (pc): Instruction Register

  • Application Status Register (APSR): Stores a copy of the Arithmetic Logic Unit (ALU) status flag

  • Current Program Status Register (CPSR): Stores the APSR flag, current processor mode, interrupt disable flag, etc.

  • Saved Program Status Register (SPSR): When an exception occurs, SPSR is used to store the CPSR

The above is the stack Principle and implementation, let’s take a look at what the stack does. The role of the stack can be reflected in two aspects: function calling and multi-task support.

1. Function call

We know that a function call has the following three basic processes:

  • Incoming call parameters

  • Space management of local variables

  • Function return

The function call must be efficient, and the data is stored in CPU general registers or RAM memory are undoubtedly the best choices. Taking passing call parameters as an example, we can choose to use CPU general registers to store parameters. However, the number of general-purpose registers is limited. When nested function calls occur, sub-functions using the original general-purpose registers again will inevitably cause conflicts. Therefore, if you want to use it to pass parameters, you must save the original register value before calling the sub-function, and then restore the original register value when the sub-function exits.

The number of calling parameters of a function is generally relatively small, so general-purpose registers can meet certain needs. However, the number and space occupied by local variables are relatively large, and it is difficult to rely on limited general-purpose registers. Therefore, we can use certain RAM memory areas to store local variables. But where is the appropriate storage location? It is necessary not only to avoid conflicts when nested functions are called, but also to focus on efficiency.

In this case, the stack undoubtedly provides a good solution. 1. For conflicts in general register parameter transfer, we can temporarily push the general register onto the stack before calling the sub-function; after the sub-function is called, pop the saved register back out. 2. To apply for space for local variables, you only need to move the top pointer of the stack downward; move the top pointer of the stack back to complete the space release of local variables; 3. For the return of the function, you only need to Before calling the sub-function, push the return address onto the stack. After the sub-function call is completed, pop the function return address to the PC pointer, which completes the return of the function call;

So the three functions called above The basic process is the process of recording a stack pointer. Every time a function is called, a stack pointer is provided. Even if functions are called in nested loops, as long as the corresponding function stack pointers are different, there will be no conflict.

what is linux stack

[Extended reading]: Function stack frame (Stack Frame)

Function calls are often nested. At the same time, there will be multiple stack frames. function information. Each unfinished function occupies an independent continuous area called a stack frame. The stack frame stores function parameters, local variables and data needed to restore the previous stack frame. The order of pushing into the stack when the function is called is:

actual parameters N~1 → return address of the main calling function → main call Function frame base pointer EBP → Called function local variable 1~N

The boundary of the stack frame is defined by the stack frame base address pointer EBP and the stack pointer ESP. EBP points to the bottom (high address) of the current stack frame. The position within the stack frame is fixed; ESP points to the top of the current stack frame (low address). When the program is executed, ESP will move as data is pushed into and out of the stack. Therefore, most of the data access in the function is based on EBP. The typical memory layout of the function call stack is shown in the figure below:

what is linux stack

2. Multi-tasking support

However, the meaning of the stack is not just about function calls. , with its existence, the multi-tasking mode of the operating system can be constructed. Let's take the main function call as an example. The main function contains an infinite loop body. In the loop body, function A is called first, and then function B is called.

func B():
  return;

func A():
  B();

func main():
  while (1)
    A();

Imagine that in a uniprocessor situation, the program will always stay in this main function. Even if there is another task in the waiting state, the program cannot jump to another task from the main function. Because if it is a function call relationship, it is essentially still a task of the main function, and it cannot be counted as multi-task switching. At this moment, the main function task itself is actually bound to its stack. No matter how nested the function calls are, the stack pointer moves within the scope of this stack.

It can be seen that a task can be characterized by the following information:

  1. main function body code

  2. main function Stack pointer

  3. Current CPU register information

If we can save the above information, we can force the CPU to handle other tasks. As long as you want to continue executing this main task in the future, you can restore the above information. With such prerequisites, multitasking has the basis for its existence, and another meaning of the existence of the stack can also be seen. In multi-tasking mode, when the scheduler deems it necessary to switch tasks, it only needs to save the task information (ie the three things mentioned above). Restore the state of another task and then jump to the last run location to resume running.

It can be seen that each task has its own stack space. With independent stack space, for code reuse, different tasks can even mix the function bodies of the tasks themselves. For example, one main function can have two Task instance. After this, the framework of the operating system has also been formed. For example, when a task calls sleep() and waits, it can actively give up the CPU to other tasks, or the time-sharing operating system task will be forced when the time slice is used up. Give up the CPU. No matter which method is used, just find a way to switch the context space of the task and switch the stack.

what is linux stack

[Extended reading]: The relationship between tasks, threads, and processes

A task is an abstract concept, which refers to a task completed by the software. Activities; threads are the actions required to complete a task; processes refer to the collective name of the resources required to complete this action; there is a vivid metaphor for the relationship between the three:

  • Task=Delivery

  • Thread=Drive delivery truck

  • System scheduling=Determine which delivery truck is suitable to drive

  • Process = Road Gas Station Delivery Truck Repair Shop

How many stacks are there in Linux? What are the memory locations of the various stacks?

After introducing the working principle and purpose of the stack, we return to the Linux kernel. The kernel divides the stack into four types:

  • Process stack

  • Thread stack

  • Kernel stack

  • Interrupt stack

1. Process stack

The process stack belongs to the user mode stack and is the same as the process virtual address space (Virtual Address Space) are closely related. So let’s first understand what virtual address space is: On a 32-bit machine, the size of the virtual address space is 4G. These virtual addresses are mapped to physical memory through page tables, which are maintained by the operating system and referenced by the processor's memory management unit (MMU) hardware. Each process has its own set of page tables, so each process appears to have exclusive access to the entire virtual address space.

Linux 内核将这 4G 字节的空间分为两部分,将最高的 1G 字节(0xC0000000-0xFFFFFFFF)供内核使用,称为 内核空间。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为 用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。

what is linux stack

Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:

  • 程序段 (Text Segment):可执行文件代码的内存映射

  • 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射

  • BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)

  • 堆区 (Heap) : 存储动态内存分配,匿名的内存映射

  • 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等

  • 映射段(Memory Mapping Segment):任何内存映射文件

what is linux stack

而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK 的值。

【扩展阅读】:如何确认进程栈的大小

我们要知道栈的大小,那必须得知道栈的起始地址和结束地址。栈起始地址 获取很简单,只需要嵌入汇编指令获取栈指针 esp 地址即可。栈结束地址 的获取有点麻烦,我们需要先利用递归函数把栈搞溢出了,然后再 GDB 中把栈溢出的时候把栈指针 esp 打印出来即可。代码如下:

/* file name: stacksize.c */

void *orig_stack_pointer;

void blow_stack() {
    blow_stack();
}

int main() {
    __asm__("movl %esp, orig_stack_pointer");

    blow_stack();
    return 0;
}
$ g++ -g stacksize.c -o ./stacksize
$ gdb ./stacksize
(gdb) r
Starting program: /home/home/misc-code/setrlimit

Program received signal SIGSEGV, Segmentation fault.
blow_stack () at setrlimit.c:4
4       blow_stack();
(gdb) print (void *)$esp
$1 = (void *) 0xffffffffff7ff000
(gdb) print (void *)orig_stack_pointer
$2 = (void *) 0xffffc800
(gdb) print 0xffffc800-0xff7ff000
$3 = 8378368    // Current Process Stack Size is 8M

上面对进程的地址空间有个比较全局的介绍,那我们看下 Linux 内核中是怎么体现上面内存布局的。内核使用内存描述符来表示进程的地址空间,该描述符表示着进程所有地址空间的信息。内存描述符由 mm_struct 结构体表示,下面给出内存描述符结构中各个域的描述,请大家结合前面的 进程内存段布局 图一起看:

struct mm_struct {
    struct vm_area_struct *mmap;           /* 内存区域链表 */
    struct rb_root mm_rb;                  /* VMA 形成的红黑树 */
    ...
    struct list_head mmlist;               /* 所有 mm_struct 形成的链表 */
    ...
    unsigned long total_vm;                /* 全部页面数目 */
    unsigned long locked_vm;               /* 上锁的页面数据 */
    unsigned long pinned_vm;               /* Refcount permanently increased */
    unsigned long shared_vm;               /* 共享页面数目 Shared pages (files) */
    unsigned long exec_vm;                 /* 可执行页面数目 VM_EXEC & ~VM_WRITE */
    unsigned long stack_vm;                /* 栈区页面数目 VM_GROWSUP/DOWN */
    unsigned long def_flags;
    unsigned long start_code, end_code, start_data, end_data;    /* 代码段、数据段 起始地址和结束地址 */
    unsigned long start_brk, brk, start_stack;                   /* 栈区 的起始地址,堆区 起始地址和结束地址 */
    unsigned long arg_start, arg_end, env_start, env_end;        /* 命令行参数 和 环境变量的 起始地址和结束地址 */
    ...
    /* Architecture-specific MM context */
    mm_context_t context;                  /* 体系结构特殊数据 */

    /* Must use atomic bitops to access the bits */
    unsigned long flags;                   /* 状态标志位 */
    ...
    /* Coredumping and NUMA and HugePage 相关结构体 */
};

what is linux stack

【扩展阅读】:进程栈的动态增长实现

进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。

如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。

动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

二、线程栈

从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。线程创建的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。

if (clone_flags & CLONE_VM) {
    /*
     * current 是父进程而 tsk 在 fork() 执行期间是共享子进程
     */
    atomic_inc(&current->mm->mm_users);
    tsk->mm = current->mm;
  }

虽然线程的地址空间和进程一样,但是对待其地址空间的 stack 还是有些区别的。对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的,使用 mmap 系统调用,它不带有 VM_STACK_FLAGS 标记。这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack() 函数中看到:

mem = mmap (NULL, size, prot, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);

由于线程的 mm->start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct 中,应该是使用 pthread_attr_t 中的 stackaddr 来初始化 task_struct->thread->sp(sp 指向 struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。由于线程栈是从进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct 的很多字段,其中包括所有的 vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

三、进程内核栈

在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候,通过 slab 分配器从 thread_info_cache 缓存池中分配出来,其大小为 THREAD_SIZE,一般来说是一个页大小 4K;

union thread_union {                                   
        struct thread_info thread_info;                
        unsigned long stack[THREAD_SIZE/sizeof(long)];
};

thread_union 进程内核栈 和 task_struct 进程描述符有着紧密的联系。由于内核经常要访问 task_struct,高效获取当前进程的描述符是一件非常重要的事情。因此内核将进程内核栈的头部一段空间,用于存放 thread_info 结构体,而此结构体中则记录了对应进程的描述符,两者关系如下图(对应内核函数为 dup_task_struct()):

what is linux stack

有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1) 后即可直接获得 thread_info 的地址。由于 thread_union 结构体是从 thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cache 在 kmem_cache_create 创建的时候,保证了地址是 THREAD_SIZE 对齐的。因此只需要对栈指针进行 THREAD_SIZE 对齐,即可获得 thread_union 的地址,也就获得了 thread_union 的地址。成功获取到 thread_info 后,直接取出它的 task 成员就成功得到了 task_struct。其实上面这段描述,也就是 current 宏的实现方法:

register unsigned long current_stack_pointer asm ("sp");

static inline struct thread_info *current_thread_info(void)  
{                                                            
        return (struct thread_info *)                        
                (current_stack_pointer & ~(THREAD_SIZE - 1));
}                                                            

#define get_current() (current_thread_info()->task)

#define current get_current()

四、中断栈

进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。

X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c 的 irq_ctx_init() 函数中 (如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用 __alloc_pages 在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq 分配一个同样大小的独立堆栈。如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。

what is linux stack

而 ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。

Recommended learning: Linux video tutorial

The above is the detailed content of what is linux stack. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn