当前位置: 首页 > 科技观察

Linux应用程序编程:使用便捷的方式获取线程栈的使用信息

时间:2023-03-12 05:52:07 科技观察

线程栈空间面临的问题,相信大家对线程栈空间并不陌生。它具有以下特点:固定空间由操作系统分配;堆栈寄存器用于保存实时位置;后进先出。今天,我们不谈操作系统层面栈的管理。我们只从应用的角度来看如何实时获取栈的使用情况。在一般的单片机/嵌入式程序开发过程中,在创建线程(或称为任务)时,可以指定线程分配多少堆栈空间。然后在调试的时候,周期性的打印出栈区的使用情况:消耗了多少空间,还剩下多少空间。这样,在运行完每一个测试用例之后,就可以得到一个粗略的统计数据,最终决定:这个线程需要分配多少栈空间。例如:ucOS系统中,提供了函数NT8UOSTaskStkChk(INT8Uprio,OS_STK_DATA*p_stk_data)来获取任务的堆栈使用信息。但是在linux系统中,并没有这种直接获取栈使用信息的类似函数。因此,要想获得本线程已用和空闲的栈空间,必须通过其他方式获得。下面,我们提供两种方案:正规军法和杂牌军法!在正规军方法中,在Linux系统中创建线程时,可以通过线程属性来设置:为这个线程分配多少栈(stack)空间。如果应用程序未指定它,则操作系统将其设置为默认值。线程创建后,操作系统会在内核空间记录该线程的所有信息,包括分配给它的栈空间信息。为了让应用层获得这些信息,操作系统也提供了相应的系统函数。代码如下:pthread_attr_tattr;void*stack_addr;intstack_size;memset(&attr,0,sizeof(pthread_attr_t));pthread_getattr_np(pthread_self(),&attr);pthread_attr_getstack(&attr,&stack_addr,&stack_size);pthread_attr_destroy(&attr)=%p\n",stack_addr);printf("stackbottom=%p\n",stack_addr+stack_size);从上面的代码可以看出,它只能得到栈空间的起始地址和总空间大小,还是不知道当前栈空间的实际使用情况!我找了相关的系统调用,linux好像没有提供相关的函数。怎么办?只能迂回操作。我们知道在linuxx86上platform,寄存器ESP是用来存放栈指针的,对于一个满减类型的栈来说,这个寄存器中的值代表的是当前栈最后一次使用的栈空间的地址。因此,只要我们可以得到ESP寄存器中的值,就相当于知道了当前栈使用了多少空间。所以如何获取ESP寄存器的值?既然是寄存器,那肯定要用汇编代码。非常简单,只有1行:size_tesp_val;asm("movl%%esp,%0":"=m"(esp_val):);对不起我错了!应该是2行代码,忘记定义变量了。不熟悉汇编代码的朋友可以参考之前总结的一篇文章:内联汇编可怕吗?看完这篇文章,结束吧!找到第四个例子,直接复制。好吧,有了上面的所有信息,你就可以计算出堆栈的已用空间和可用空间的大小:将上面的代码放在一起:#include#include#include#include#include#includevoidprint_stack1(){size_tused,avail;pthread_attr_tattr;void*stack_addr;intstack_size;//获取当前栈寄存器ESP值size_tesp_val;asm("movl%%esp,%0":"=m"(esp_val):);//通过线程属性,获取栈区起始地址和空间总大小memset(&attr,0,sizeof(pthread_attr_t));pthread_getattr_np(pthread_self(),&attr);pthread_attr_getstack(&attr,&stack_addr,&stack_size);pthread_attr_destroy(&attr);printf("espVal=%p\n",esp_val);printf("statcktop=%p\n",stack_addr);printf("stackbottom=%p\n",stack_addr+stack_size);avail=esp_val-(size_t)stack_addr;used=stack_size-avail;printf("print_stack1:used=%d,avail=%d,total=%d\n",used,avail,stack_size);}intmain(intargc,char*agv[]){print_stack1();return0;}重新上面的gulararmy方法主要是通过系统函数获取线程的属性信息,从而获取栈区的起始地址和栈的总空间为了获取这两个值,调用了三个函数,有点繁琐!不知道大家还记得吗:Linux操作系统会提供一些关于应用程序限制的信息,包括堆栈相关信息。信息。这样,我们就可以得到一个线程的栈空间总大小。此时,还有最后一个未知的变量:栈区的起始地址!我们来分析一下:当一个线程刚开始执行时,栈区可以认为是空的,也就是说此时ESP寄存器的值可以认为是指向栈区的起始地址!是不是有种豁然开朗的感觉?!不过这个还是需要通过调用汇编代码来获取。再想想,既然此时栈区可以认为是空的,如果在线程的第一个函数中定义一个局部变量,然后获取这个局部变量的地址,就相当于获取了栈区的地址是起始地址吗?如下图所示:我们可以将这个局部变量的地址记录在一个全局变量中。那么在应用程序的其他代码中,可以用它来表示栈的起始地址。知道了需要的3个变量,就可以计算出栈空间的使用情况了://用来存放栈区的起始地址size_ttop_stack;voidprint_stack2(){size_tused,available;size_tesp_val;asm("movl%%esp,%0":"=m"(esp_val):);printf("esp_val=%p\n",esp_val);used=top_stack-esp_val;structrlimitlimit;getrlimit(RLIMIT_STACK,&limit);avail=limit.rlim_cur-used;printf("print_stack2:used=%d,avail=%d,total=%d\n",used,avail,used+avail);}intmain(intargc,char*agv[]){intx=0;//记录栈区起始地址(近似值)top_stack=(size_t)&x;print_stack2();return0;}比较讨人喜欢的方式上面两种方法中,获取栈当前指针位置的方法是通过汇编代码,获取寄存器ESP中的值。是不是可以继续使用刚才的技巧:通过定义一个局部变量来间接获取ESP寄存器的值?voidprint_stack3(){intx=0;size_tused,avail;//局部变量的地址可以近似看成是ESP寄存器的值size_ttmp=(size_t)&x;used=top_stack-tmp;structrlimitlimit;getrlimit(RLIMIT_STACK,&limit);avail=limit.rlim_cur-used;printf("print_stack3:used=%d,avail=%d,total=%d\n",used,avail,used+avail);}intmain(intargc,char*agv[]){intx=0;top_stack=(size_t)&x;print_stack3();return0;}以上方法总结,各有优缺点。我们把上面3个打印栈使用情况的函数放在一起,然后在main函数中依次调用3个测试函数,每个函数定义一个整型数组(占用4K栈空间),然后通过以下方式查看打印输出信息://测试代码(3个打印函数就不贴了)voidprint_stack1(){...}voidprint_stack2(){...}voidprint_stack3(){...}voidfunc3(){intnum[1024];print_stack1();printf("\n\n**********\n");print_stack2();printf("\n\n***********\n");print_stack3();}voidfunc2(){intnum[1024];func3();}voidfunc1(){intnum[1024];func2();}intmain(intargc,char*agv[]){intx=0;top_stack=(size_t)&x;func1();return0;}打印输出信息:espVal=0xffe8c980statcktop=0xff693000stackbottom=0xffe90000print_stack1:used=13952,avail=8362368,total=8376320*********esp_val=0xffe8c9a0print_stack2:used=12456,avail=8376152,total=8388608********print_stack3:used=12452,avail=8376156,total=8388608镇”,可关注下方二维码,转载请见谅请联系IOTTown公众号。