您的位置:首页 > 运维架构 > Linux

linux线程栈的若干思考

2016-08-23 22:01 260 查看
        线程包含了表示其行环境必需的信息,其中包括进程中标示线程的线程ID,一组寄存器值,栈,调度优先级和策略, 信号屏蔽字,errno变量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本,程序的全局内存和堆内存,栈以及文件描述符,所以线程的mm_struct *mm指针变量和所属进程的mm指针变量相同。我们知道在linux系统中,线程栈的默认大小为8M,这可以通过ulimit
-s 命令查看,这个线程的大小也是可以更改的。下图的8192B就是8M.



下面让我们来在<<unix环境高级编程>>中涉及的几个关于线程栈的函数:

样例1:

#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include<iostream>
#include <unistd.h>
using namespace std;

void *thread_fun(void* args){
size_t stacksize;
void *addr;
int *p=(int *)args;
pthread_attr_t thread_attr;
pthread_attr_getstacksize(&thread_attr,&stacksize);
cout<<"the size of stack is: "<<stacksize<<endl;
return (void*)0;
}

int main(){
pthread_t pid[3];
int i=0;
int a[3]={1,2,3};
for(i=0;i<3;i++){
pthread_create(&pid[i],NULL,thread_fun,(void *)&a[i]);
cout<<"pthread id is: "<<pid[i]<<endl;
}
for(i=0;i<3;i++){
pthread_join(pid[i],NULL);

}
return 0;
}
输出结果:



我们可以看到其中有一个线程的堆栈大小为4001536字节约等于3.8M的大小,而其它两个大小为默认值8M,为什么会出现不想等的情况?????

样例2:

#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include<iostream>
#include <unistd.h>
using namespace std;

int main(){
pthread_t thread;
size_t stacksize;
pthread_attr_t thread_attr;
int ret;
void *addr;
pthread_attr_init(&thread_attr);
int new_size = 20480;
ret =  pthread_attr_getstack(&thread_attr,&addr,&stacksize);
if(ret != 0){
cout << "emError" << endl;
return -1;
}
cout << "stacksize=" << stacksize << endl<<"addr is: "<<addr<<endl;
cout << PTHREAD_STACK_MIN << endl;
ret = pthread_attr_setstacksize(&thread_attr,new_size);
if(ret != 0)return -1;
ret =  pthread_attr_getstack(&thread_attr,&addr,&stacksize);
if(ret != 0){
cout << "emError" << endl;
return -1;
}
cout << "after set stacksize=" << stacksize << endl;
cout<<"the address is: "<<addr<<endl;
ret = pthread_attr_destroy(&thread_attr);
if(ret != 0)return -1;
return 0;
}
测试结果:



从上面的例子可以看出,当我们使用pthread_attr_getstack的时候获得的线程栈的大小为0,且地址也为0,不明白???

当我们做如下修改的时候(也就是将pthread_attr_getstack变成pthread_attr_getsize):

int main(){
pthread_t thread;
size_t stacksize;
pthread_attr_t thread_attr;
int ret;
void *addr=NULL;
pthread_attr_init(&thread_attr);
int new_size = 20480;
ret =  pthread_attr_getstacksize(&thread_attr,&stacksize);
if(ret != 0){
cout << "emError" << endl;
return -1;
}
cout << "stacksize=" << stacksize << endl<<"addr is: "<<addr<<endl;
cout << PTHREAD_STACK_MIN << endl;
ret = pthread_attr_setstacksize(&thread_attr,new_size);
if(ret != 0)return -1;
ret =  pthread_attr_getstack(&thread_attr,&addr,&stacksize);
if(ret != 0){
cout << "emError" << endl;
return -1;
}
cout << "after set stacksize=" << stacksize << endl;
cout<<"the address is: "<<addr<<endl;
ret = pthread_attr_destroy(&thread_attr);
if(ret != 0)return -1;
return 0;
}



当我们更改函数的时候就可以得到线程栈的大小。当我们修改线程栈的大小之后再调用pthread_attr_getstack时,就可以获得线程栈的地址,但是我们可以看到,此时线程栈的地址在内核空间,这是为什么?????

样例3:

#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <errno.h>
using namespace std;

int main(void){
void *stackaddr;
unsigned int stacksize;
unsigned int guardsize;
pthread_attr_t attr;

if (pthread_attr_init(&attr) != 0){
printf("init attr failed.\n");
return -1;
}

if (pthread_attr_getstacksize(&attr,&stacksize) == 0){
printf("default stacksize:0x%x\n",stacksize);
}

if (pthread_attr_getstack(&attr,&stackaddr,&stacksize) == 0){
printf("default stacksize:0x%x,stack addr:%p\n",stacksize,stackaddr);
}
stacksize = 16*1024;
if (pthread_attr_setstacksize(&attr,stacksize) != 0){
printf("set thread stack size failed,error:%s\n",strerror(errno));
return -1;
}

if (pthread_attr_getstack(&attr,&stackaddr,&stacksize) == 0){
printf("after set stacksize,stacksize:0x%x,stack addr:%p\n",stacksize,stackaddr);
}
if (pthread_attr_setstack(&attr,(void *)0x80000,stacksize) !=  0){
printf("set stack error:%s\n",strerror(errno));
}
if (pthread_attr_getstack(&attr,&stackaddr,&stacksize) == 0){
printf("after setstack,stacksize:0x%x,stack addr:%p\n",stacksize,stackaddr);
}
return 0;
}
输出结果:



在样例3中我们指定了线程栈的大小和地址,所以输出的结果是0x4000和0x80000;这这个例子中,我们调用pthread_attr_getstack同样获得线程栈的大小为nil,也就是空。

所以总结上面的例子我的疑问有:

(1)为什么一开始调用pthread_attr_getstack不能获得线程栈的大小和线程栈的地址.

(2)在对线程栈进行设置之后(这里的设置仅仅限定栈的大小),利用pthread_attr_getstack能够输出线程栈的大小和线程栈的地址,但此时线程栈的地址在内核空间????

针对以上问题还希望大牛给出指导!!!!!!!!

补充知识:

用户栈和内核栈的切换:

  当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断,称为软中断。进程因为中断(软中断或硬件产生中断),使得CPU切换到特权工作模式,此时进程陷入内核态,进程进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆

栈指针寄存器的地址为内核栈地址,这样就完成了用户栈向内核栈的切换。
当进程从内核态切换到用户态时,最后把保存在内核栈中的用户栈地址恢复到CPU栈指针寄存器即可,这样就完成了内核栈向用户栈的切换。这里要理解一下内核堆栈。前面我们讲到,进程从用户态进入内核态时,需要在内核栈中保存用户栈的地址。那么进入内核态时,从哪里获得内核栈的栈指针呢?要解决这个问题,先要理解从用户态刚切换到内核态以后,进程的内核栈总是空的。这点很好理解,当进程在用户空间运行时,使用的是用户 栈;当进程在内核态运行时,内核栈中保存进程在内核态运行的相关信息,但是当进程完成了内核态的运行,重新回到用户态时,此时内核栈中保存的信息全部恢
复,也就是说,进程在内核态中的代码执行完成回到用户态时,内核栈是空的。理解了从用户态刚切换到内核态以后,进程的内核栈总是空的,那刚才这个问题就很好理解了,因为内核栈是空的,那当进程从用户态切换到内核态后,把内核栈的栈顶地址设置给CPU的栈指针寄存器就可以了。

在kernel-2.4内核里面,内核栈的实现是:

 Union task_union {

                   Struct task_struct task;

                   Unsigned long stack[INIT_STACK_SIZE/sizeof(long)];

 };

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

在kernel-2.6内核里面,内核栈的实现是:

union thread_union {

 struct thread_info thread_info;

 unsigned long stack[THREAD_SIZE/sizeof(long)];

};

其中,THREAD_SIZE的值取8192时,stack数组的大小为2048;THREAD_SIZE的值取4096时,stack数组的大小为1024。现在我们应该思考,为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)紧密的放在一起?最主要的原因就是内核可以很容易的通过esp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址;

struct thread_info {
struct task_struct  *task;      /* main task structure */
struct exec_domain  *exec_domain;   /* execution domain */
unsigned long       flags;      /* low level flags */
unsigned long       status;     /* thread-synchronous flags */
__u32           cpu;        /* current CPU */
int         preempt_count;  /* 0 => preemptable, <0 => BUG */
mm_segment_t        addr_limit; // thread address space: 0-0xBFFFFFFF for user-thead 0-0xFFFFFFFF for kernel-
      //thread*/
void            *sysenter_return;
struct restart_block    restart_block;
unsigned long           previous_esp;   /* ESP of the previous stack in  case of nested (IRQ) stacks */
__u8            supervisor_stack[0];
};
获取thread_info的地址(以下是内核栈的内存分配):



从上面的内存分布来看,我们知道内核栈的大小为两个页面的大小(8KB),esp指向的是内核堆栈的结尾,由于堆栈是向下增长的,esp和thread_info位于同一个8KB或者4KB的块当中。也就是thread_union的长度了。如果是8KB,屏蔽esp的低13位就可以得到thread_info的地址,也就是8KB块的开始位置。4KB的话,就屏蔽低12位.

下面是linux内核获得thread_info结构体地址的函数:

static inline struct thread_info *current_thread_info(void){
return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1));
} 这个函数直接返回thread_info指针,当前的堆栈指针current_stack_pointer,也就是esp,THREAD_SIZE为块的字节大小8192或者是4096,这里假设值8192,十六进制的表示是0x00002000,二进制的表示是00000000000000000010000000000000~(THREAD_SIZE-1)的结果刚好为1111111111111111111 0000000000000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息