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 線程就可以一直正常的運行下去.
利用這個特點, 我們可以做很多其它事情.
游戲的伺服器進程常常有這些線程:
網路線程, 資料庫線程, 業務處理線程. 引發邏輯錯誤的代碼常常位於業務處理線程.
而資料庫線程由於功能穩定, 邏輯簡單, 是十分強壯的.
那麼, 如果業務處理線程有邏輯錯誤, 我們捕捉到信號後, 可以在信號處理函數的最後,
通知資料庫線程保存游戲數據.
直到資料庫線程把游戲信息全部存入資料庫, 信號處理函數才返回.
這樣, 伺服器宕機不會導致回檔, 損失被大大降低.
要實現這個機制, 要求資料庫模塊和業務處理模塊具有低耦合度.
當然, 實際應用的時候, 還有許多細節要考慮.
比如, 業務處理線程正在處理玩家的數據, 由於發生不可預知的錯誤, 玩家的數據被損壞了, 這些玩家的數據就不應該被存入資料庫.