线程栈学习总结

来源:互联网 发布:电子图书数据库 编辑:程序博客网 时间:2024/05/17 15:18

线程栈和进程栈 区别

http://blog.csdn.net/kickxxx/article/details/9278193


要搞清线程栈和进程栈的区别,首先要弄清线程和进程之间的关系。


线程和进程有很多类似的地方,人们习惯上把线程称为轻量级进程,这个所谓的轻量级是指线程并不拥有自己的系统资源,线程依附于创建自己的进程。


我们可以从l两个个方面来理解线程的轻量级


1. 调度


由于进程之间的线程共享同一个进程地址空间,因此在进程的线程之间做进程切换,并不会引起进程地址空间的切换,从而避免了昂贵的进程切换。当然不同进程组之间是需要进程切换的


2. 拥有资源


进程是操作系统中拥有资源的独立单位,在创建和撤销进程时,操作系统都会为进程分配和回收资源,资源包括地址空间,文件,IO,页表等。但是由于线程是依附与创建进程的,线程的代码段,数据段,打开文件,IO资源,地址空间,页表等都是和进程的所有线程共享的。


从上面我们看出线程并没有独立的地址空间,这就意味着隶属同一进程的所有线程栈,都在所属进程的地址空间中,他们的栈地址不同,但是如果操作栈时发生越界,是有可能破坏其他线程的栈空间的。


而进程实际上可以看作是主线程,它的栈和其它线程栈没有区别。


单线程只有一个栈,多线程则为每个线程都分配一个栈,并且这些栈的地址不同,可以通过如下方法验证这个结论


1. pslist输出系统进程以及他们的线程,在我的机器上得到如下结果


1889 gnome-session 1918 1926 1940 1969 1957 2282 2283 1971 1972 1973 1975 1998 2003 2010 2669 2691 2710 2776 2871  
1889是主线程,后面是这个进程创建的线程


2. 对每一个线程ID执行,cat /proc/threadID/maps


可以看到没个线程的stack地址范围各不相同,这也从侧面验证了每个线程的栈地址在同一进程地址空间的不同地址范围内。
========

线程堆栈

http://blog.csdn.net/nokianasty/article/details/7600321
线程堆栈!


       一个线程的开销包括:
      内核模式下的开销(内核堆栈,对象管理所需内存)
      用户模式下的开销(线程局部存储、线程环境块、堆栈、CRT、MFC、COM等等等等)


      通常,线程数目的瓶颈在于线程自己的堆栈。Visual C++编译器默认设置是每个线程的堆栈大小是1MB。当然,如果你在创建线程时指定较小的堆栈大小,你应该可以创建较多的线程。


      但是创建大量线程不是一个好的设计。每个线程创建和销毁的时候,Windows会调用已经加载的动态链接库的DLLMain,传递DLL_THREAD_ATTACH和DLL_THREAD_DETACH作为参数,除非动态库使用DisableThreadLibraryCalls禁用了这个通知。在创建大量线程的时候,这个开销是很大的。对于你这样的用后即弃的线程,你应该使用线程池。一个线程池示例可以在微软知识库找到。


     参数和局部变量的函数都存储在线程的堆栈。 如果声明局部变量具有大型值, 堆栈快速耗尽。 例如, 在以下代码示例函数要求堆栈来存储数组 1,200,000 个字节。


void func(void)
   {
     int i[300000];
     // Use 300,000 integers multiplied by 4 bytes per integer to store the array.
     return;
   }


要避免使用堆栈, 使用动态分配内存。 例如, 在以下代码示例函数动态分配内存。
void func(void)
   {
     int *i


     i = new int[400000];
     // More code goes here.
     return;
   }
   在此代码示例, 内存存储在堆栈代替。 因此, 函数不需要堆栈来存储数组 1,200,000 个字节。
由此也解开了为什么我在一个进程中创建大于2000个线程时导致程序异常退出,因为每个线程的默认堆
栈是1MB,2000*1MB = 2GB,my God!
可以在创建线程时指定线程堆栈大小,这样就能创建更多的线程了!但是前面提到,大量的创建线程并销
毁,会带来大量的系统开销,所以尽量避免创建大量线程,推荐使用线程池,但是偶现在还不会用,快去查查!
========

Linux 进程栈和线程栈的区别

http://www.cnblogs.com/jingzhishen/p/4433437.html
 
总结:线程栈的空间开辟在所属进程的堆区,线程与其所属的进程共享进程的用户空间,所以线程栈之间可以互访。线程栈的起始地址和大小存放在pthread_attr_t 中,栈的大小并不是用来判断栈是否越界,而是用来初始化避免栈溢出的缓冲区的大小(或者说安全间隙的大小)
 
进程内核栈、用户栈
 
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 *task;
                   struct exec_domain *exec_domain;
                   __u32 flags;
        __u32 status;
                   __u32 cpu;
                   …  ..
 };


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

多线程 - 你知道线程栈吗

http://www.cnblogs.com/snake-hand/p/3148191.html
问题
1. local 变量的压栈和出栈过程
void func1(){
    int a = 0;
    int b = 0;
}
系统中有一个栈顶指针,每次分配和回收local 变量时,其实就是移动栈指针。


2. static local变量的分配风险
void func2(){
    static int a = 0;
}
这个变量a可能会被分配多次,因为如果func2可能同时被多个线程调用,也就是函数在分配内存时是可能出现线程切换的。


问题:

void func3(){
int a;
int b;
}


void func4(){
int c;
int d;
}
假设,func3和func4分别被两个线程调用,并且func3先于func4执行,并且4个变量压栈的顺序分别是a、b、c、d。按照上面第1个说明,这个时候栈顶指针指向d。
如果,这个时候func3先执行完,那么这个时候,系统要回收b和a,但是b并不在栈顶,所以,无法移动栈顶指针,所以,b和a无法回收。最复杂的情况可能如下,压栈的顺序是a、c、d、b,这个时候b可以正常回收。当要回收a时,会不会误把d当作a给回收了?应该怎么解释这个问题呢。
显然,事实上并非上面所述,因为线程里有一个很重要的属性stacksize,它让我们隐约感觉到,线程是拥有私有的栈空间的,如果这样,abcd的压栈出栈就不会有问题了,因为他们并不保存在一起。


pthread线程栈
 
#include <stdio.h>
#include <pthread.h>


void* thread1(void* a)
{
char m[8388608];
printf("thread1\n");
}


int main(){
pthread_t pthread_id;
pthread_attr_t thread_attr;
int status;


status = pthread_attr_init(&thread_attr);
if(status != 0)
printf("init error\n");


size_t stacksize = 100;
status = pthread_attr_getstacksize(&thread_attr, &stacksize);
printf("stacksize(%d)\n", stacksize);
//printf("size(%d)\n", sizeof(int));


status = pthread_create(&pthread_id, NULL, thread1, NULL);
while(1)
{}
return 0;
}


运行结果:


stacksize(8388608)
段错误
分析


pthread_attr_getstacksize可以获得线程的私有栈的大小,我这个机器是8388608字节,为8M,也就是私有栈最大是8M,所以,创建的一个线程函数里有个局部数组长度为8M,显示段错误(虽然数组大小和私有栈一样大,但是私有栈除了分配局部变量外,还要保存一些管理信息,所以肯定要小于8M),如果将数组长度减小一定的值,就可以看到thread1函数的打印信息。


pthread线程内存布局


我们从图上可以看出,两个线程之间的栈是独立的,其他是共享的,所以,在操作共享区域的时候才有可能出现同步需要,操作栈不需要同步。


最后我们知道,pthread也提供了私有堆机制,关于私有堆机制在以后说明。


========
0 0