导航:首页 > 信息系统 > 为什么要看进程的堆栈信息

为什么要看进程的堆栈信息

发布时间:2023-04-03 06:51:50

1. linux应用中,在一个进程内如何获取本进程内其它线程的堆栈信息、

先用ps看目前存在的所有进程的进程号,然后可以对具体进程采用以下这些操作:(11345就是对应具体的进程尺梁号)

只查看该进程:ps -ef | grep 11345
查看该进程打开的文件:lsof -p 11345
查看内存分配:lcat /proc/11345/maps
查看堆栈:陵肆运pstack 11345
查看发出的系统调用:strace -p 11345
查看雹握调用库函数:ltrace -p 11345

2. 为什么线程调度要用到堆栈

我们知道,每一个线程都独立拥有一个栈,多个线程可以“同时”执行。CPU执行程序代码完全依靠各种寄存器。当一个线程将被挂起时,当前的各种寄存器的数值就被存储在了线程的栈中。当CPU重新执行此线程时,将从栈中取出寄存器的数值,接着运行,好像这个线程从来就没有被打断枯局过一样。正是因为每个线程都有一个独立的栈,使线程拥有了可以“闭门造车”的能力。只要将参数传递给线程的栈,CPU将担负起这块内存存储区的管理工作,并适时地执行线程函数代码对其进行操作。当系统在多个线程间切换时,CPU将执行相同的代码操作不同的栈。
下面举一个例子来加深理解。
随着面向对象编程方法的普及,我们很乐意将任何操作都包装成为一清胡个类。线程函数也不例外,以静态函数的形式将线程函数放在类中是C++编程普遍使用的一种方法。通常情况下对象包括属性(类变量)与方法(类函数)。属性指明对象自身的性质,方法用于操作对象,改变它的属性。现在有一个小问题要注意了。类的静态函数只能访问类的静态变量,而静态变量是不属于单个对象的,他存放在进程的全局数据存储区。一般情况下,我们希望每个对象能够“独立”,也就是说,多个对象能够各自干各自的工作,不要相互打扰。如果以通常的方法,以类(静态)变量存储对象的属性,可就要出问题了,因为类(静态)变量不属于单个对象。现在怎么办呢?如何继续保持每个对象的“独立性”。解决的方法就是使用栈,将参数传递给线程函数的局部变量(栈存储区),以单个对象管理每个线程,问题就解决了。当然了,解决方法是多种答败拦多样的,这里只是为了进一步解释多线程与对象的关系。

3. 堆(heap)和栈(Stack)的区别是什么为什么平时都把堆栈放在一起讲

将堆跟栈放在一起将是因为两者都是存储数据的方式。区别如下:

一、主体不枣扮同

1、堆:是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。

2、栈:又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。


二、特点不同

1、堆:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。

2、栈:是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。

三、作用不同

1、堆:堆是非线性数据结构,相当于一维数组,有两个直接后继。

2、栈:可以用来在函数调用的时候存储断点,做递归时要用到栈。


4. java语言中提及的“堆”主要有什么用“栈又有什么用”

Java把内存划分成两种:一种是栈内存,另一种是堆内存。在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。

堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。

这也是Java比较占内存的原因,实际上,栈中敬扰弊的变量指向堆内存中的变量,这就是 Java 中的指针!

java中内存分配策略及堆和栈的比较

1 内存分配策略

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

2 堆和栈的比较

上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:

从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:

在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时亮族间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时李羡,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).

3 JVM中的堆和栈

JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.

从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

Java 中的堆和栈

Java把内存划分成两种:一种是栈内存,一种是堆内存。

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

堆内存用来存放由new创建的对象和数组。

在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。

引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

具体的说:

栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。

栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

int a = 3;

int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

5. 为什么要用堆栈,什么是堆栈

粘帖一个:
堆(heap)和栈(stack)有什么区别??

简单的可以理解为:
heap:是由malloc之类函数分配的空间所在地。地址是由低向高增长的。
stack:是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少的。

预备知识—程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的睁历值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

二、例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

二、堆和栈的理论知识
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在袭派c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存拍早贺地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度, 也最灵活
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
2.6存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指edx中,在根据edx读取字符,显然慢了。
?

2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

堆和栈的区别主要分:
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因针值读

6. 内存分配中堆和栈的区各是指什么

简单地说,栈是属于进程管理的,大小相对固定,规模较小,一旦操作系统为程序分配了栈后就不管了,操作系统将其看做进程的一部分,栈的性质是先进后出,后进先出;堆属于系统维护的,进程可以申请的额外内存空间,访问方式是自由的(相对于栈的先进后出)。稍微深入点说,栈是由系统在加载程序时给进程分配的一块区域,提供存放栈数据,一般定义的变量都会存在栈区,函数调用以及数据传递和返回、递归、嵌套循环,文件夹等树状层次结构的遍历、表达式的解析都会用到栈区。栈的大小由编译器决定,也可以在IDE(集成开发环境,比如VC,VS,VB或任何编程工具)中设定,编译好的程序包含了栈空间大小的参数,当被操作系统加载时由操作系统一起分配给程序。当程序结束时栈区与进程空间一起被回收释放。所以,如果定义的数据超过栈的空间程序就会发生溢出而崩溃,编译器不负责检查,因此大容量数据不要分配在栈上。在C++中,应该使用new关键词,用new分配的对象或内存都是在堆上,堆是系统维护的内存空间,也可理解为操作差滚系统中看到的未使用的空间,当执行new的时候就是程序向操作系统申请额外空间,因此new也叫动态分配内存。系统会根据需求大小从未使用的空间中划一块给程序使用,并对该空间进行注册管理,以便当程序结束时释放该空间(假如程序没有主动申请释放)。所以用new创建的空间在使用完了后要及时申请释放(delete关键词),如果不释放,在程序运行期间如果不断的new大内存,最终也会将整个可用内存用拆并完,导致系统崩溃,当然,如今的操作系统比以前强壮得多,当发生内存用完导致崩溃时,操作系统会干预,直接down掉程序虚御余禁止运行下去并回收所有所占空间。

7. 如何查看进程堆栈

这个需帆局核要用调试器才可以看态掘到的。
linux平台,一般使用gdb
windows平台一般使用windbg
加载进程后,可以在堆栈窗口看到堆栈的内容腊银的。

8. 为什么进程在内核空间有堆栈,作用是什么

只要内存足够,不会产生空间不足!线程可以分配独立的空间也可访问共享空间

9. 如何在进程崩溃后打印堆栈并防止数据丢失

进程在运行过程中遇到逻辑错误, 比如除零, 空指针等等, 系统会触发一个软件中断.
这个中断会以信号的方式通知进程, 这些信号的默认处理方式是结束进程.
发生这烂猛种情况, 我们就认为进程崩溃了.

进程崩溃后, 我们会希望知道它是为嫌历坦何崩溃的, 是哪个函数, 哪行代码引起的错误.
另外, 在进程退出前, 我们还希望做一些善后处理, 比如把某些数据存入数据库, 等等.

下面, 我会介绍一些技术来达成这两个目标.

1. 在core文件中查看堆栈信息

如果进程崩溃时, 我们能看到当时的堆栈信息, 就能很快定位到错误的代码.
在 gcc 中加入 -g 选项, 可执行文件中便会包含调试信息. 进程崩溃后, 会生成一个 core 文件.
我们可以用 gdb 查看这个 core 文件, 从而知道进程崩溃时的环境.

在调试阶段, core文件能给我们带来很多便利. 但是在正式环境中, 它有很大的局限:
1. 包含调试信息的可执行文件会很大. 并且运行速度也会大幅降低.
2. 一个 core 文件常常很大, 如果进程频繁崩溃, 硬盘资源会变得很紧张.

所以, 在正式环境中运行的程序, 不会包含调试信息.
它的core文件的大小, 我们会把它设为0, 也就是不会输入core文件.
在这个前提下, 我们如何得到进程的堆栈信息呢?

2. 动态获取线程的堆栈

c 语言提供了 backtrace 函数, 通过这个函数可以动态的获取当前线程的堆栈.
要使用 backtrace 函数, 有两点要求:
1. 程序使用的是 ELF 二进制格式.
2. 程序连接时使用了 -rdynamic 选项.
-rdynamic可用来通知链接器将所有符号添加到动态符号表中, 这些信息比 -g 选项的信息要少得多.

下面是将要用到的函数说明:
#include <execinfo.h>

int backtrace(void **buffer,int size);
用于获取当前线程的调用堆栈, 获取的信息将会被存放在buffer中, 它是一个指针列表。
参数 size 用来指定buffer中可以保存多少个void* 元素。
函数返回值是实际获取的指针个数, 最大不超过size大小
注意: 某些编译器的优化选项对获取正确的调用堆栈有干扰,
另外内联函数没有堆栈框架; 删除框架指针也会导致无法正确解析堆栈内容;

char ** backtrace_symbols (void *const *buffer, int size)
把从backtrace函数获取的信息转化为一个字符串数组.
参数buffer应该是从backtrace函数获取的指针数组,
size是该数组中的元素个数(backtrace的返回值) ;
函数返回值是一个指向字符串数组的指针, 它的大小同buffer相同.
每个字符串包含了一个相对于buffer中对应元素的可打印信息.
它包括函数名,函数的偏移地址, 和实际的返回地址.
该函数的返回值是通过malloc函数申请的空间, 因此调用者必须使用free函数来释放指针.
注意: 如果不能为字符串获取足够的空间, 函数的返回值将会为NULL.

void backtrace_symbols_fd (void *const *buffer, int size, int fd)
与backtrace_symbols 函数具有相同的功能,
不同的是它不会给调用者返回字符串数组, 而是将结果写入文件描述符为fd的文件中,每个函数对应一行.

3. 捕捉信号

我们希望在进程崩溃时打印堆栈, 所以我们需要捕捉到相应的信号. 方法很简单.
#include <signal.h>
void (*signal(int signum,void(* handler)(int)))(int);
或者: typedef void(*sig_t) ( int );
sig_t signal(int signum,sig_t handler);
参数说明:
第一个参数signum指明了所要芹桐处理的信号类型,它可以是除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler描述了与信号关联的动作,它可以取以下三种值:
1. 一个返回值为正数的函数的地址, 也就是我们的信号处理函数.
这个函数应有如下形式的定义: int func(int sig); sig是传递给它的唯一参数。
执行了signal()调用后,进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行func()函数。
当func()函数执行结束后,控制权返回进程被中断的那一点继续执行。
2. SIGIGN, 忽略该信号.
3. SIGDFL, 恢复系统对信号的默认处理。
返回值: 返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

注意:
当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,
直到信号处理函数执行完毕再重新调用相应的处理函数。
如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。
在信号发生跳转到自定的handler处理函数执行后,系统会自动将此处理函数换回原来系统预设的处理方式,
如果要改变此操作请改用sigaction()。

4. 实例

下面我们实际编码, 看看具体如何在捕捉到信号后, 打印进程堆栈, 然后结束进程.

#include <iostream>
#include <time.h>
#include <signal.h>
#include <string.h>
#include <execinfo.h>
#include <fcntl.h>
#include <map>

using namespace std;

map<int, string> SIG_LIST;

#define SET_SIG(sig) SIG_LIST[sig] = #sig;

void SetSigList(){
SIG_LIST.clear();
SET_SIG(SIGILL)//非法指令
SET_SIG(SIGBUS)//总线错误
SET_SIG(SIGFPE)//浮点异常
SET_SIG(SIGABRT)//来自abort函数的终止信号
SET_SIG(SIGSEGV)//无效的存储器引用(段错误)
SET_SIG(SIGPIPE)//向一个没有读用户的管道做写操作
SET_SIG(SIGTERM)//软件终止信号
SET_SIG(SIGSTKFLT)//协处理器上的栈故障
SET_SIG(SIGXFSZ)//文件大小超出限制
SET_SIG(SIGTRAP)//跟踪陷阱
}

string& GetSigName(int sig){
return SIG_LIST[sig];
}

void SaveBackTrace(int sig){
//打开文件
time_t tSetTime;
time(&tSetTime);
tm* ptm = localtime(&tSetTime);
char fname[256] = {0};
sprintf(fname, "core.%d-%d-%d_%d_%d_%d",
ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday,
ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
FILE* f = fopen(fname, "a");
if (f == NULL){
exit(1);
}
int fd = fileno(f);

//锁定文件
flock fl;
fl.l_type = F_WRLCK;
fl.l_start = 0;
fl.l_whence = SEEK_SET;
fl.l_len = 0;
fl.l_pid = getpid();
fcntl(fd, F_SETLKW, &fl);

//输出程序的绝对路径
char buffer[4096];
memset(buffer, 0, sizeof(buffer));
int count = readlink("/proc/self/exe", buffer, sizeof(buffer));
if(count > 0){
buffer[count] = '\n';
buffer[count + 1] = 0;
fwrite(buffer, 1, count+1, f);
}

//输出信息的时间
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "Dump Time: %d-%d-%d %d:%d:%d\n",
ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday,
ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
fwrite(buffer, 1, strlen(buffer), f);

//线程和信号
sprintf(buffer, "Curr thread: %d, Catch signal:%s\n",
pthread_self(), GetSigName(sig).c_str());
fwrite(buffer, 1, strlen(buffer), f);

//堆栈
void* DumpArray[256];
int nSize = backtrace(DumpArray, 256);
sprintf(buffer, "backtrace rank = %d\n", nSize);
fwrite(buffer, 1, strlen(buffer), f);
if (nSize > 0){
char** symbols = backtrace_symbols(DumpArray, nSize);
if (symbols != NULL){
for (int i=0; i<nSize; i++){
fwrite(symbols[i], 1, strlen(symbols[i]), f);
fwrite("\n", 1, 1, f);
}
free(symbols);
}
}

//文件解锁后关闭, 最后终止进程
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);
fclose(f);
exit(1);
}

void SetSigCatchFun(){
map<int, string>::iterator it;
for (it=SIG_LIST.begin(); it!=SIG_LIST.end(); it++){
signal(it->first, SaveBackTrace);
}
}

void Fun(){
int a = 0;
int b = 1 / a;
}

static void* ThreadFun(void* arg){
Fun();
return NULL;
}

int main(){
SetSigList();
SetSigCatchFun();

printf("main thread id = %d\n", (pthread_t)pthread_self());
pthread_t pid;
if (pthread_create(&pid, NULL, ThreadFun, NULL)){
exit(1);
}
printf("fun thread id = %d\n", pid);

for(;;){
sleep(1);
}
return 0;
}

文件名为 bt.cpp
编译: g++ bt.cpp -rdynamic -I /usr/local/include -L /usr/local/lib -pthread -o bt

主线程创建了 fun 线程, fun 线程有一个除零错误, 系统抛出 SIGFPE 信号.
该信号使 fun 线程中断, 我们注册的 SaveBackTrace 函数捕获到这个信号, 打印相关信息, 然后终止进程.
在输出的core文件中, 我们可以看到简单的堆栈信息.

5. 善后处理

在上面的例子中, fun 线程被 SIGFPE 中断, 转而执行 SaveBackTrace 函数.
此时, main 线程仍然在正常运行.
如果我们把 SaveBackTrace 函数最后的 exit(1); 替换成 for(;;)sleep(1);
main 线程就可以一直正常的运行下去.
利用这个特点, 我们可以做很多其它事情.

游戏的服务器进程常常有这些线程:
网络线程, 数据库线程, 业务处理线程. 引发逻辑错误的代码常常位于业务处理线程.
而数据库线程由于功能稳定, 逻辑简单, 是十分强壮的.
那么, 如果业务处理线程有逻辑错误, 我们捕捉到信号后, 可以在信号处理函数的最后,
通知数据库线程保存游戏数据.
直到数据库线程把游戏信息全部存入数据库, 信号处理函数才返回.
这样, 服务器宕机不会导致回档, 损失被大大降低.

要实现这个机制, 要求数据库模块和业务处理模块具有低耦合度.
当然, 实际应用的时候, 还有许多细节要考虑.
比如, 业务处理线程正在处理玩家的数据, 由于发生不可预知的错误, 玩家的数据被损坏了, 这些玩家的数据就不应该被存入数据库.

阅读全文

与为什么要看进程的堆栈信息相关的资料

热点内容
手机微信博云学小程序怎么登录 浏览:791
口罩出口信息怎么看 浏览:858
产品防伪数码是什么意思啊 浏览:159
市场营销有哪些应用 浏览:315
花喜代理怎么加盟 浏览:38
信息管理人员经历了哪些阶段 浏览:967
仁化汽车配件代理加盟如何 浏览:1000
之江生物产品销量怎么样 浏览:670
宇花灵技术怎么用 浏览:600
想去泉州卖菜哪个菜市场人流大 浏览:411
沈阳雪花酒水怎么代理 浏览:125
rng秘密交易是什么意思 浏览:732
重庆红糖锅盔怎么代理赚钱吗 浏览:383
考察投资项目关注哪些数据 浏览:592
家纺家具都有什么产品 浏览:37
丘氏冰棒产品有哪些 浏览:414
程序员如何拉到业务 浏览:177
揭阳火车站到炮台市场怎么走 浏览:843
二线国企程序员怎么提升技能 浏览:154
蓝翔技术学院西点多少钱 浏览:787