您的位置 首页 > 德语词汇

stack是什么意思?用法、例句?栈是什么?栈有什么作用

本篇文章给大家谈谈stack是什么意思?用法、例句,以及栈是什么?栈有什么作用对应的知识点,文章可能有点长,但是希望大家可以阅读完,增长自己的知识,最重要的是希望对各位有所帮助,可以解决了您的问题,不要忘了收藏本站喔。

首先,栈(stack)是一种串列形式的数据结构。这种数据结构的特点是后入先出(LIFO,LastInFirstOut),数据只能在串列的一端(称为:栈顶top)进行推入(push)和弹出(pop)操作。根据栈的特点,很容易地想到可以利用数组,来实现这种数据结构。但是本文要讨论的并不是软件层面的栈,而是硬件层面的栈。

stack是什么意思?用法、例句?栈是什么?栈有什么作用

大多数的处理器架构,都有实现硬件栈。有专门的栈指针寄存器,以及特定的硬件指令来完成入栈/出栈的操作。例如在ARM架构上,R13(SP)指针是堆栈指针寄存器,而PUSH是用于压栈的汇编指令,POP则是出栈的汇编指令。

嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。

需要的朋友私信【内核】即可领取。

上面是栈的原理和实现,下面我们来看看栈有什么作用。栈作用可以从两个方面体现:函数调用和多任务支持。

我们知道一个函数调用有以下三个基本过程:

函数的调用必须是高效的,而数据存放在CPU通用寄存器或者RAM内存中无疑是最好的选择。以传递调用参数为例,我们可以选择使用CPU通用寄存器来存放参数。但是通用寄存器的数目都是有限的,当出现函数嵌套调用时,子函数再次使用原有的通用寄存器必然会导致冲突。因此如果想用它来传递参数,那在调用子函数前,就必须先保存原有寄存器的值,然后当子函数退出的时候再恢复原有寄存器的值。

函数的调用参数数目一般都相对少,因此通用寄存器是可以满足一定需求的。但是局部变量的数目和占用空间都是比较大的,再依赖有限的通用寄存器未免强人所难,因此我们可以采用某些RAM内存区域来存储局部变量。但是存储在哪里合适?既不能让函数嵌套调用的时候有冲突,又要注重效率。

这种情况下,栈无疑提供很好的解决办法。一、对于通用寄存器传参的冲突,我们可以再调用子函数前,将通用寄存器临时压入栈中;在子函数调用完毕后,在将已保存的寄存器再弹出恢复回来。二、而局部变量的空间申请,也只需要向下移动下栈顶指针;将栈顶指针向回移动,即可就可完成局部变量的空间释放;三、对于函数的返回,也只需要在调用子函数前,将返回地址压入栈中,待子函数调用结束后,将函数返回地址弹出给PC指针,即完成了函数调用的返回;

于是上述函数调用的三个基本过程,就演变记录一个栈指针的过程。每次函数调用的时候,都配套一个栈指针。即使循环嵌套调用函数,只要对应函数栈指针是不同的,也不会出现冲突。

然而栈的意义还不只是函数调用,有了它的存在,才能构建出操作系统的多任务模式。我们以main函数调用为例,main函数包含一个无限循环体,循环体中先调用A函数,再调用B函数。

funcB():\nreturn;\n\nfuncA():\nB();\n\nfuncmain():\nwhile(1)\nA();

试想在单处理器情况下,程序将永远停留在此main函数中。即使有另外一个任务在等待状态,程序是没法从此main函数里面跳转到另一个任务。因为如果是函数调用关系,本质上还是属于main函数的任务中,不能算多任务切换。此刻的main函数任务本身其实和它的栈绑定在了一起,无论如何嵌套调用函数,栈指针都在本栈范围内移动。

由此可以看出一个任务可以利用以下信息来表征:

假如我们可以保存以上信息,则完全可以强制让出CPU去处理其他任务。只要将来想继续执行此main任务的时候,把上面的信息恢复回去即可。有了这样的先决条件,多任务就有了存在的基础,也可以看出栈存在的另一个意义。在多任务模式下,当调度程序认为有必要进行任务切换的话,只需保存任务的信息(即上面说的三个内容)。恢复另一个任务的状态,然后跳转到上次运行的位置,就可以恢复运行了。

可见每个任务都有自己的栈空间,正是有了独立的栈空间,为了代码重用,不同的任务甚至可以混用任务的函数体本身,例如可以一个main函数有两个任务实例。至此之后的操作系统的框架也形成了,譬如任务在调用sleep()等待的时候,可以主动让出CPU给别的任务使用,或者分时操作系统任务在时间片用完是也会被迫的让出CPU。不论是哪种方法,只要想办法切换任务的上下文空间,切换栈即可。

任务、线程、进程三者关系任务是一个抽象的概念,即指软件完成的一个活动;而线程则是完成任务所需的动作;进程则指的是完成此动作所需资源的统称;关于三者的关系,有一个形象的比喻:

介绍完栈的工作原理和用途作用后,我们回归到Linux内核上来。内核将栈分成四种:

进程栈是属于用户态栈,和进程虚拟地址空间(VirtualAddressSpace)密切相关。那我们先了解下什么是虚拟地址空间:在32位机器下,虚拟地址空间大小为4G。这些虚拟地址通过页表(PageTable)映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元(MMU)硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

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

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

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

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

/*filename:stacksize.c*/\n\nvoid*orig_stack_pointer;\n\nvoidblow_stack(){\nblow_stack();\n}\n\nintmain(){\n__asm__("movl%esp,orig_stack_pointer");\n\nblow_stack();\nreturn0;\n}

$g++-gstacksize.c-o./stacksize\n$gdb./stacksize\n(gdb)r\nStartingprogram:/home/home/misc-code/setrlimit\n\nProgramreceivedsignalSIGSEGV,Segmentationfault.\nblow_stack()atsetrlimit.c:4\n4blow_stack();\n(gdb)print(void*)$esp\n$1=(void*)0xffffffffff7ff000\n(gdb)print(void*)orig_stack_pointer\n$2=(void*)0xffffc800\n(gdb)print0xffffc800-0xff7ff000\n$3=8378368//CurrentProcessStackSizeis8M

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

structmm_struct{\nstructvm_area_struct*mmap;/*内存区域链表*/\nstructrb_rootmm_rb;/*VMA形成的红黑树*/\n...\nstructlist_headmmlist;/*所有mm_struct形成的链表*/\n...\nunsignedlongtotal_vm;/*全部页面数目*/\nunsignedlonglocked_vm;/*上锁的页面数据*/\nunsignedlongpinned_vm;/*Refcountpermanentlyincreased*/\nunsignedlongshared_vm;/*共享页面数目Sharedpages(files)*/\nunsignedlongexec_vm;/*可执行页面数目VM_EXEC&~VM_WRITE*/\nunsignedlongstack_vm;/*栈区页面数目VM_GROWSUP/DOWN*/\nunsignedlongdef_flags;\nunsignedlongstart_code,end_code,start_data,end_data;/*代码段、数据段起始地址和结束地址*/\nunsignedlongstart_brk,brk,start_stack;/*栈区的起始地址,堆区起始地址和结束地址*/\nunsignedlongarg_start,arg_end,env_start,env_end;/*命令行参数和环境变量的起始地址和结束地址*/\n...\n/*Architecture-specificMMcontext*/\nmm_context_tcontext;/*体系结构特殊数据*/\n\n/*Mustuseatomicbitopstoaccessthebits*/\nunsignedlongflags;/*状态标志位*/\n...\n/*CoredumpingandNUMAandHugePage相关结构体*/\n};线程栈

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

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

虽然线程的地址空间和进程一样,但是对待其地址空间的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指向structpt_regs对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。由于线程栈是从进程的地址空间中map出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

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

unionthread_union{\nstructthread_infothread_info;\nunsignedlongstack[THREAD_SIZE/sizeof(long)];\n};

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

有了上述关联结构后,内核可以先获取到栈顶指针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宏的实现方法:

registerunsignedlongcurrent_stack_pointerasm("sp");\n\nstaticinlinestructthread_info*current_thread_info(void)\n{\nreturn(structthread_info*)\n(current_stack_pointer&~(THREAD_SIZE-1));\n}\n\n#defineget_current()(current_thread_info()->task)\n\n#definecurrentget_current()中断栈

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

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

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

导读-最新发表-我爱内核网-构建全国最权威的内核技术交流分享论坛

一文读懂Linux内核处理器架构中的栈-资料-我爱内核网-构建全国最权威的内核技术交流分享论坛

stack是什么意思?用法、例句和栈是什么?栈有什么作用的问题分享结束啦,以上的文章解决了您的问题吗?欢迎您下次再来哦!

本站涵盖的内容、图片、视频等数据,部分未能与原作者取得联系。若涉及版权问题,请及时通知我们并提供相关证明材料,我们将及时予以删除!谢谢大家的理解与支持!

Copyright © 2023