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

    2018-10-10 18:45:11
    在Java虚拟机进程中,每个线程都会拥有一个方法调用栈(线程栈),用来跟踪线程运行中 的一系列方法调用过程,栈中的每一个元素被称为栈帧,每当线程调用一个方法的时候会 向方法栈中压入一个新帧。这里的帧用来存储...

    在Java虚拟机进程中,每个线程都会拥有一个方法调用栈(线程栈),用来跟踪线程运行中

    的一系列方法调用过程,栈中的每一个元素被称为栈帧,每当线程调用一个方法的时候会

    向方法栈中压入一个新帧。这里的帧用来存储方法的参数、局部变量、方法的返回地址、

    和运算过程中的临时数据。

     

    线程栈模型是理解线程调度原理以及线程执行过程的基础。线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶,线程栈的内容是随着线程的运行状态变化而变化的,研究线程栈必须选择一个运行的时刻(指代码运行到什么地方)

    上图中的栈A是主线程main的运行栈信息,当执行new JavaThreadDemo().threadMethod();方法时,threadMethod方法位于主线程栈中的栈顶,在threadMethod方法中运行的start()方法新建立了一个线程,此时,新建立的线程也将拥有自己的线程栈B,可以看到在不同运行时刻栈的信息在发生变化,栈A的变化可以从上图中看出来。此时栈A和栈B并行运行,main线程和新建立的线程并行运行。

    由此可以看出方法调用和线程启动的区别。方法调用只是在原来的线程栈中调用方法的代码,而线程启动会新建立一个独属于自己的线程栈来运行自己的这个线程。

    展开全文
  • Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    万次阅读 多人点赞 2016-09-01 21:52:02
    栈有什么作用?首先, (stack) 是一种串列形式的 数据结构。这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作。

    转载请注明出处: http://kyang.cc/

    栈是什么?栈有什么作用?

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

    栈结构

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

    【扩展阅读】:ARM 寄存器简介

    ARM 处理器拥有 37 个寄存器。 这些寄存器按部分重叠组方式加以排列。 每个处理器模式都有一个不同的寄存器组。 编组的寄存器为处理处理器异常和特权操作提供了快速的上下文切换。

    提供了下列寄存器:
    - 三十个 32 位通用寄存器:
    - 存在十五个通用寄存器,它们分别是 r0-r12、sp、lr
    - sp (r13) 是堆栈指针。C/C++ 编译器始终将 sp 用作堆栈指针
    - lr (r14) 用于存储调用子例程时的返回地址。如果返回地址存储在堆栈上,则可将 lr 用作通用寄存器
    - 程序计数器 (pc):指令寄存器
    - 应用程序状态寄存器 (APSR):存放算术逻辑单元 (ALU) 状态标记的副本
    - 当前程序状态寄存器 (CPSR):存放 APSR 标记,当前处理器模式,中断禁用标记等
    - 保存的程序状态寄存器 (SPSR):当发生异常时,使用 SPSR 来存储 CPSR

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

    一、函数调用

    我们知道一个函数调用有以下三个基本过程:
    - 调用参数的传入
    - 局部变量的空间管理
    - 函数返回

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

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

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

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

    函数栈结构

    【扩展阅读】:函数栈帧 (Stack Frame)

    函数调用经常是嵌套的,在同一时刻,栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等,函数调用时入栈的顺序为:

    实参N~1 → 主调函数返回地址 → 主调函数帧基指针EBP → 被调函数局部变量1~N

    栈帧的边界由 栈帧基地址指针 EBP栈指针 ESP 界定,EBP 指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。函数调用栈的典型内存布局如下图所示:

    函数调用栈的典型内存布局


    二、多任务支持

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

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

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

    由此可以看出一个任务可以利用以下信息来表征:
    1. main 函数体代码
    2. main 函数栈指针
    3. 当前 CPU 寄存器信息

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

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

    多任务模型

    【扩展阅读】:任务、线程、进程 三者关系

    任务是一个抽象的概念,即指软件完成的一个活动;而线程则是完成任务所需的动作;进程则指的是完成此动作所需资源的统称;关于三者的关系,有一个形象的比喻:
    - 任务 = 送货
    - 线程 = 开送货车
    - 系统调度 = 决定合适开哪部送货车
    - 进程 = 道路 + 加油站 + 送货车 + 修车厂



    Linux 中有几种栈?各种栈的内存位置?

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

    • 进程栈
    • 线程栈
    • 内核栈
    • 中断栈

    一、进程栈

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

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

    Linux虚拟地址空间

    Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:
    - 程序段 (Text Segment):可执行文件代码的内存映射
    - 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
    - BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
    - 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
    - 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
    - 映射段(Memory Mapping Segment):任何内存映射文件

    Linux标准进程内存段布局

    而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,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 相关结构体 */
    };

    mm_struct 内存段

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

    进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (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()):

    进程内核栈与进程描述符

    有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1) 后即可直接获得 thread_info 的地址。由于 thread_union 结构体是从 thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cachekmem_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.cirq_ctx_init() 函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用 __alloc_pages 在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq 分配一个同样大小的独立堆栈。如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。

    中断栈

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



    Linux 为什么需要区分这些栈?

    为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:

    1. 为什么需要单独的进程内核栈?

      • 所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;
    2. 为什么需要单独的线程栈?

      • Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1:
        • 此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
        • 如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
    3. 进程和线程是否共享一个内核栈?

      • No,线程和进程创建的时候都调用 dup_task_struct 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。
    4. 为什么需要单独中断栈?

      • 这个问题其实不对,ARM 架构就没有独立的中断栈。

    大家还有什么观点,可以在留言下来 :-D



    文章写到这也就结束了,您要是还能看到这,我一定要表示忠心的感谢,文章是在太长了,我都写快一个星期了。好了,终于可以 Released 了,hexo deploy,再会!

    • K.Yang



    参考文章

    栈的作用

    C语言函数调用栈(一)

    函数调用栈 剖析+图解

    进程地址空间分布

    Linux 进程栈和线程栈的区别

    Linux基础:进程管理

    stackoverflow - How Process Size is determined?

    stackoverflow - RLIMIT_STACK seems to change according to setrlimit/getrlimit, but stack size doesn’t seem to actually change

    stackoverflow - Relation between stack limit and threads

    对 Linux 的进程内核栈的认识

    Linux进程内核栈

    Linux中的栈:用户态栈/内核栈/中断栈

    What Every Programmer Should Know About Memory? - Ulrich Drepper (Red Hat, Inc.)

    展开全文
  • 线程栈和堆

    2014-12-15 11:24:14
    我们先来说说线程栈线程栈是每个线程独有的,他的生命周期和线程相当,默认情况下他的大小是1MB,系统会在线程开始时调拨两个页面!...线程栈一个固定的最大大小的(不能像堆那样动态增长),如果线程

    我们先来说说线程栈!线程栈是每个线程独有的,他的生命周期和线程相当,默认情况下他的大小是1MB,系统会在线程开始时调拨两个页面!线程栈给我们程序员发挥的空间似乎不大,我们只需要知道他的一些工作原理就行了!

    首先,线程栈的地址空间必定是所属进程地址空间的自己!作用嘛,应该是保存一些局部变量和函数调用的现场保护以及返回时的现场恢复!

    线程栈是有一个固定的最大大小的(不能像堆那样动态增长),如果线程栈溢出了会抛出一个EXCEPTION_STACK_OVERFLOW异常。操作系统为了防止栈溢出破坏其他的进程数据,使用了一个防护页面的概念!

    首先,我们假设有一个栈的地址是0x08100000,Windows中栈是向低地址增长的,那么我们可以假设栈的最后一页是0x08000000。栈在使用过程中系统会不断的为其调拨内存,栈会因此慢慢长大!栈顶的一页与地址比他高的页不同,他的页面保护属性被设成PAGE_GUARD,这一页我们称他为保护页。一旦程序在这一页写入,系统会得到一个通知:这个线程的栈快不够用了,我必须在给他调拨一些内存!这时线程会再给栈调拨一页(属性被设为PAGE_GUARD,新的保护页),然后把刚刚保护页设为PAGE_READWRITE!

    PS:栈虽然是向低地址增长的,但是栈中的数组,地址确是向高处增长的,比如DATA[0]的地址要比DATA[3]低!

    栈溢出的情况:

    我们假设栈的最后三页依次为0x08002000,0x08001000,0x08000000,假设0x08002000是当前的保护页!现在我要访问保护页的内容,系统会去除保护页的page_guard属性,并给0x08001000调拨内存,这时他不会再把0x08001000设为保护了,系统会发出EXCEPTION_STACK_OVERFLOW异常!也就是说系统永远不会给栈的最后一页调拨内存。


    堆与线程栈不同,不同的线程在同一个进程中共享堆!堆用一个句柄来表示,他的分配和释放速度比较慢,适合存放大量小型数据!(感觉堆就像虚拟内存的封装,存储器为堆调拨的存储空间来自页交换文件,我们没有办法直接控制内部的操作)。由于堆是多个线程共享的,因此堆的操作需要进行线程同步,就是说HeapAlloc和HeapFree应该要存在原子性!进程运行的时候,会创建一个默认堆!这个堆不能由我们销毁,并且所有WindowsAPI都使用这个堆!

    当然我们可以创建直及的堆,Heap *****系列函数使用起来都很简单,参数也不多。这里不赘述!

    《Windows核心编程》中提出了一个很好的想法,让一个类专用一个私有的堆,通过重载new和delete来实现内存的管理!

    展开全文
  • 线程栈的内容是随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什么地方)。   下面通过一个示例性的代码说明线程(调用)栈的变化过程。     这幅图描述

    Java线程:线程栈模型与线程的变量

    要理解线程调度的原理,以及线程执行过程,必须理解线程栈模型。

    线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶。线程栈的内容是随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什么地方)

     

    下面通过一个示例性的代码说明线程(调用)栈的变化过程。

     

     

    这幅图描述在代码执行到两个不同时刻12时候,虚拟机线程调用栈示意图。

     

    当程序执行到t.start();时候,程序多出一个分支(增加了一个调用栈B),这样,栈A、栈B并行执行。

     

    从这里就可以看出方法调用和线程启动的区别了。

    Java线程:线程状态的转换

    一、线程状态

     

    线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是生、死、可运行、运行、等待/阻塞。用一个图来描述如下:

     

    1、新状态:线程对象已经创建,还没有在其上调用start()方法。

     

    2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。

     

    3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

     

    4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。

     

    5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

     

    有关详细状态转换图可以参看本人的Java多线程编程总结中的图

     

    二、阻止线程执行

    对于线程的阻止,考虑一下三个方面,不考虑IO阻塞的情况:

    睡眠;

    等待;

    因为需要一个对象的锁定而被阻塞。

     

    1、睡眠

    Thread.sleep(long millis)Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以减慢线程。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。

     

    线程睡眠的原因:线程执行太快,或者需要强制进入下一轮,因为Java规范不保证合理的轮换。

     

    睡眠的实现:调用静态方法。

            try {
                Thread.sleep(123);
            } catch (InterruptedException e) {
                e.printStackTrace();  
            }

     

    睡眠的位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。

     

    例如,在前面的例子中,将一个耗时的操作改为睡眠,以减慢线程的执行。可以这么写:

     

        public void run() {
            for(int i = 0;i<5;i++){

    // 很耗时的操作,用来减慢线程的执行
    //            for(long k= 0; k <100000000;k++);
                try {
                    Thread.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();  .
                }

                System.out.println(this.getName()+" :"+i);
            }
        }

     

    运行结果:

    阿三 :0
    李四 :0 
    阿三 :1 
    阿三 :2 
    阿三 :3 
    李四 :1 
    李四 :2 
    阿三 :4 
    李四 :3 
    李四 :4 

    Process finished with exit code 0

     

    这样,线程在每次执行过程中,总会睡眠3毫秒,睡眠了,其他的线程就有机会执行了。

     

    注意:

    1、线程睡眠是帮助所有线程获得运行机会的最好方法。

    2、线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。

    3sleep()是静态方法,只能控制当前正在运行的线程。

     

    下面给个例子:

    /** 
    一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

    * @author leizhimin 2008-9-14 9:53:49 
    */
     
    publicclass MyThreadextends Thread {

        
    publicvoid run() {
            
    for (int i = 0; i < 100; i++) {
                
    if ((i) % 10 == 0) {
                    System.out.println(
    "-------" + i);
                } 
                System.out.print(i); 
                
    try {
                    Thread.sleep(1); 
                    System.out.print(
    "    线程睡眠1毫秒!\n");
                } 
    catch (InterruptedException e) {
                    e.printStackTrace(); 
                } 
            } 
        } 

        
    publicstaticvoid main(String[] args) {
            
    new MyThread().start();
        } 
    }

     

    -------0 
    0    
    线程睡眠1毫秒!
    1    
    线程睡眠1毫秒!
    2    
    线程睡眠1毫秒!
    3    
    线程睡眠1毫秒!
    4    
    线程睡眠1毫秒!
    5    
    线程睡眠1毫秒!
    6    
    线程睡眠1毫秒!
    7    
    线程睡眠1毫秒!
    8    
    线程睡眠1毫秒!
    9    
    线程睡眠1毫秒!
    -------10 
    10    
    线程睡眠1毫秒!
    11    
    线程睡眠1毫秒!
    12    
    线程睡眠1毫秒!
    13    
    线程睡眠1毫秒!
    14    
    线程睡眠1毫秒!
    15    
    线程睡眠1毫秒!
    16    
    线程睡眠1毫秒!
    17    
    线程睡眠1毫秒!
    18    
    线程睡眠1毫秒!
    19    
    线程睡眠1毫秒!
    -------20 
    20    
    线程睡眠1毫秒!
    21    
    线程睡眠1毫秒!
    22    
    线程睡眠1毫秒!
    23    
    线程睡眠1毫秒!
    24    
    线程睡眠1毫秒!
    25    
    线程睡眠1毫秒!
    26    
    线程睡眠1毫秒!
    27    
    线程睡眠1毫秒!
    28    
    线程睡眠1毫秒!
    29    
    线程睡眠1毫秒!
    -------30 
    30    
    线程睡眠1毫秒!
    31    
    线程睡眠1毫秒!
    32    
    线程睡眠1毫秒!
    33    
    线程睡眠1毫秒!
    34    
    线程睡眠1毫秒!
    35    
    线程睡眠1毫秒!
    36    
    线程睡眠1毫秒!
    37    
    线程睡眠1毫秒!
    38    
    线程睡眠1毫秒!
    39    
    线程睡眠1毫秒!
    -------40 
    40    
    线程睡眠1毫秒!
    41    
    线程睡眠1毫秒!
    42    
    线程睡眠1毫秒!
    43    
    线程睡眠1毫秒!
    44    
    线程睡眠1毫秒!
    45    
    线程睡眠1毫秒!
    46    
    线程睡眠1毫秒!
    47    
    线程睡眠1毫秒!
    48    
    线程睡眠1毫秒!
    49    
    线程睡眠1毫秒!
    -------50 
    50    
    线程睡眠1毫秒!
    51    
    线程睡眠1毫秒!
    52    
    线程睡眠1毫秒!
    53    
    线程睡眠1毫秒!
    54    
    线程睡眠1毫秒!
    55    
    线程睡眠1毫秒!
    56    
    线程睡眠1毫秒!
    57    
    线程睡眠1毫秒!
    58    
    线程睡眠1毫秒!
    59    
    线程睡眠1毫秒!
    -------60 
    60    
    线程睡眠1毫秒!
    61    
    线程睡眠1毫秒!
    62    
    线程睡眠1毫秒!
    63    
    线程睡眠1毫秒!
    64    
    线程睡眠1毫秒!
    65    
    线程睡眠1毫秒!
    66    
    线程睡眠1毫秒!
    67    
    线程睡眠1毫秒!
    68    
    线程睡眠1毫秒!
    69    
    线程睡眠1毫秒!
    -------70 
    70    
    线程睡眠1毫秒!
    71    
    线程睡眠1毫秒!
    72    
    线程睡眠1毫秒!
    73    
    线程睡眠1毫秒!
    74    
    线程睡眠1毫秒!
    75    
    线程睡眠1毫秒!
    76    
    线程睡眠1毫秒!
    77    
    线程睡眠1毫秒!
    78    
    线程睡眠1毫秒!
    79    
    线程睡眠1毫秒!
    -------80 
    80    
    线程睡眠1毫秒!
    81    
    线程睡眠1毫秒!
    82    
    线程睡眠1毫秒!
    83    
    线程睡眠1毫秒!
    84    
    线程睡眠1毫秒!
    85    
    线程睡眠1毫秒!
    86    
    线程睡眠1毫秒!
    87    
    线程睡眠1毫秒!
    88    
    线程睡眠1毫秒!
    89    
    线程睡眠1毫秒!
    -------90 
    90    
    线程睡眠1毫秒!
    91    
    线程睡眠1毫秒!
    92    
    线程睡眠1毫秒!
    93    
    线程睡眠1毫秒!
    94    
    线程睡眠1毫秒!
    95    
    线程睡眠1毫秒!
    96    
    线程睡眠1毫秒!
    97    
    线程睡眠1毫秒!
    98    
    线程睡眠1毫秒!
    99    
    线程睡眠1毫秒!

    Process finished with exit code 0


    2
    、线程的优先级和线程让步yield()

    线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。

     

    要理解yield(),必须了解线程的优先级的概念。线程总是存在优先级,优先级范围在1~10之间。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下,当前运行的线程优先级将大于或等于线程池中任何线程的优先级。但这仅仅是大多数情况。

     

    注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。

     

    当线程池中线程都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能:一是选择一个线程运行,直到它阻塞或者运行完成为止。二是时间分片,为池内的每个线程提供均等的运行机会。

     

    设置线程的优先级:线程默认的优先级是创建它的执行线程的优先级。可以通过setPriority(int newPriority)更改线程的优先级。例如:

            Thread t = new MyThread();
            t.setPriority(8);
            t.start();

    线程优先级为1~10之间的正整数,JVM从不会改变一个线程的优先级。然而,1~10之间的值是没有保证的。一些JVM可能不能识别10个不同的值,而将这些优先级进行每两个或多个合并,变成少于10个的优先级,则两个或多个优先级的线程可能被映射为一个优先级。

     

    线程默认优先级是5Thread类中有三个常量,定义线程优先级范围:

    static int MAX_PRIORITY 
              
    线程可以具有的最高优先级。
    static int MIN_PRIORITY 
              
    线程可以具有的最低优先级。
    static int NORM_PRIORITY 
              
    分配给线程的默认优先级。

     

    3Thread.yield()方法

     

    Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

    yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

    结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

     

    4join()方法

     

    Thread的非静态方法join()让一个线程B“加入到另外一个线程A的尾部。在A执行完毕之前,B不能工作。例如:

            Thread t = new MyThread();
            t.start();
            t.join();

    另外,join()方法还有带超时限制的重载版本。例如t.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。

     

    线程的加入join()对线程栈导致的结果是线程栈发生了变化,当然这些变化都是瞬时的。下面给示意图:

     

     

     

    小结

    到目前位置,介绍了线程离开运行状态的3种方法:

    1、调用Thread.sleep():使当前线程睡眠至少多少毫秒(尽管它可能在指定的时间之前被中断)。

    2、调用Thread.yield():不能保障太多事情,尽管通常它会让当前运行线程回到可运行性状态,使得有相同优先级的线程有机会执行。

    3、调用join()方法:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。

     

    除了以上三种方式外,还有下面几种特殊情况可能使线程离开运行状态:

    1、线程的run()方法完成。

    2、在对象上调用wait()方法(不是在线程上调用)。

    3、线程不能在对象上获得锁定,它正试图运行该对象的方法代码。

    4、线程调度程序可以决定将当前运行状态移动到可运行状态,以便让另一个线程获得运行机会,而不需要任何理由。

    展开全文
  • 关于Linux线程的线程栈以及TLS

    万次阅读 2014-03-23 22:52:22
    本文描述Linux NPTL的线程栈简要实现以及线程本地存储的原理,实验环境中Linux内核版本为2.6.32,glibc版本是2.12.1,Linux发行版为ubuntu,硬件平台为x86的32位系统。 b.对于Linux NPTL线程,很多话题。本文挑选...
  • Linux 进程栈和线程栈的区别

    万次阅读 2012-11-05 11:31:14
    这里如下几个问题,线程栈的空间是开辟在那里的? 线程栈之间可以互访吗?为什么在使用pthread_attr_setstack函数时,需要设置栈的大小,而进程task_struct的 mm_struct *mm 成员中却并没有却并没有stack_size
  • Java线程:线程栈模型与线程的变量 要理解线程调度的原理,以及线程执行过程,必须理解线程...线程栈的内容是随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什么地方)。
  • java多线程学习(三)——线程栈

    千次阅读 2014-09-19 21:19:31
    线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶,线程栈的内容是随着线程的运行状态变化而变化的,研究线程栈必须选择一个运行的时刻(指代码运行到什么地方) 上图中的栈A是主线程main...
  • 线程栈和进程栈 区别

    万次阅读 2013-07-09 10:07:55
    要搞清线程栈和进程栈的区别,首先要弄清线程和进程之间的关系。 线程和进程很多类似的地方,人们习惯上把线程称为轻量级进程,这个所谓的轻量级是指线程并不拥有自己的系统资源,线程依附于创建自己的进程。 ...
  • 目录(?)[-] 一Linux虚拟地址空间布局1 内核空间2 ...8 保留区二Linux 中的各种栈进程栈 线程栈 内核栈 中断栈 Linux 中几种栈各种栈的内存位置 一进程栈二线程栈三进程内核栈四中断栈 Linux 为什么需要区分这些栈
  • jstack命令的主要作用是打印指定Java进程中每一个线程的工作状态,以及每个线程栈当前的方法执行顺序等详细情况。为什么jstack命令不和jmap、jinfo、jstat等命令一同讲解,而要单独成文呢?因为通过jstack命令给出的...
  • 线程栈的说明、使用及解析

    千次阅读 2014-07-22 00:12:27
    线程栈:  线程包含了表示进程内执行环境必需的信息,其中包括进程中标示线程的线程ID,一组寄存器值,栈,调度优先级和策略, 信号屏蔽字,errno变量以及线程私有数据。进程的所有信息对该进程的所有线程都是...
  • 看来其他人的几篇文章,可能理解也不是很到位 内核、用户 32位linux系统上,进程的地址空间为4G,包括1G的内核地址空间-----内核,和3G的用户地址空间-----用户
  • 第十六章:线程栈

    2011-08-26 21:43:11
    1:线程初始化时,线程栈默认1M,所有页面都是PAGE_READWRITE属性,但只为前两个页面调拨了物理存储器,地址低的那个页面叫防护页面,为其指定了PAGE_GUARD属性 2:当线程访问到防护页面时,由于指定了PAGE_GUARD属性,系统...
  • 《windows核心编程系列》十五谈谈windows线程栈

    千次阅读 多人点赞 2011-11-21 22:51:06
    谈谈windows线程栈。 当系统创建线程时会为线程预订一块地址空间区域,注意仅仅是预订。默认情况下预定的这块区域的大小是1MB,虽然预订这么多,但是系统并不会给全部区域调拨物理存储器。默认情况下,仅仅为两个...
  • 关于多线程栈空间的若干问题

    千次阅读 2012-03-30 22:36:33
    2. 而创建了多线程,各个线程要求自己独立的,这些独立的是怎么分布的呢? 应当是一个动态地概念,只有在线程开始执行后,才为其分配资源.具体的分布是无法确定的.我觉得这就像 调用函数一样,当调用函数...
  • 2、ready:当线程有资格运行,但调度程序还没有把它选定为运行线程线程所处的状态。当start()方法调用时,线程首先进入ready状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到ready状态。 3、run...
  • 嵌入式系统中关于程序中线程栈空间的使用
  • 当系统创建线程时会为线程预订一块地址空间区域(每个线程自己的),并给区域调拨一些物理存储器。注意仅仅是预订。默认情况下预定的这块区域的大小是1MB,虽然预订这么多,但是系统并不会给全部区域调拨物理...
  • 线程
  • .NET中 类型,对象,线程栈,托管堆 在运行时的关系 The Relationship at Run Time between Types,Objects,A Thread's Stack,and The Managed Heap for .NET by 唐小崇 http://www.cnblogs.com/tangchong .NET中...
  • 在JAVA中线程到底起到什么作用!

    千次阅读 2018-03-27 14:54:19
    在JAVA中线程到底起到什么作用! 这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学.。 线程同步我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能...
  • 四个函数A、B、C、D,地址分别为100、200、300、400;... 函数B中又执行了Yield()函数(蓝色,Yield()的作用可以理解为切换线程),Yield()切换到地址300处的线程,执行线程2,同时将下一条指令的地址入栈(204)...
  • linux多线程默认大小和最大线程

    千次阅读 2015-04-16 12:04:38
    linux的线程栈大小可以使用ulimit -s查看,对于ubunt 2.6的内核线程栈的默认大小为8M,如下: shine@shine-bupt:~/Program/C$ ulimit -s 8192 32位的系统,虚拟内存空间是4G,其中1G是内核空间用户不...
  • 对于ubuntu 2.6的内核线程栈的默认大小为8M,如下:root@npti:/# ulimit -s819232位的系统,虚拟内存空间是4G,其中高1G是内核空间用户不可见,真正可以使用的内存空间大小是低3G的用户空间,具体参考下表:Linux的...
  • 其实我们都知道,计算机内存本来就是一...linux下一个进程中的所有线程共享该进程的地址空间,但它们各自独立的(私有的)(stack)。堆(heap)的分配与有所不同,一般是一个进程一个C运行时堆,这个堆为本进程中

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 171,448
精华内容 68,579
关键字:

线程栈有什么作用