1. 程式人生 > 實用技巧 >Centos7硬碟LVM擴容

Centos7硬碟LVM擴容

開發的時候碰到如下的錯誤(PHP-FPM+apache),所以想好好理解下Segmentation fault.

參考了文章Segmentation fault到底是何方妖孽

維基百科的解釋如下:

儲存器區塊錯誤英語:Segmentation fault,經常被縮寫為segfault),又譯為儲存器段錯誤,也稱訪問許可權衝突(access violation),是一種程式錯誤。

它會出現在當程式企圖訪問CPU無法定址的儲存器區塊時。當錯誤發生時,硬體會通知作業系統產生了儲存器訪問許可權衝突的狀況。作業系統通常會產生核心轉儲(core dump)以方便程式設計師進行除錯。通常該錯誤是由於呼叫一個地址,而該地址為空(NULL)所造成的,例如

連結串列中呼叫一個未分配地址的空連結串列單元的元素。陣列訪問越界也可能產生這個錯誤。

網友總結的Linux開發中可能遇到Segmentation fault的情況:

1 使用非法的記憶體地址(指標),包括使用未經初始化及已經釋放的指標、不存在的地址、受系統保護的地址,只讀的地址等,這一類也是最常見和最好解決的段錯誤問題,使用GDB print一下即可知道原因。

2 記憶體讀/寫越界。包括陣列訪問越界,或在使用一些寫記憶體的函式時,長度指定不正確或者這些函式本身不能指定長度,典型的函式有strcpy(strncpy),sprintf(snprint)等等。

3 對於C++物件,應該通過相應類的介面來去記憶體進行操作,禁止通過其返回的指標對記憶體進行寫操作,典型的如string類的c_str()介面,如果你強制往其返回的指標進行寫操作肯定會段錯誤的,因為其返回的地址是隻讀的。

4 函式不要返回其中區域性物件的引用或地址,當函式返回時,函式棧彈出,區域性物件的地址將失效,改寫或讀這些地址都會造成未知的後果。

5 避免在棧中定義過大的陣列,否則可能導致程序的棧空間不足,此時也會出現段錯誤,同樣的,在建立程序/執行緒時如果不知道此執行緒/程序最大需要多少棧空間時最好不要在程式碼中指定棧大小,應該使用系統預設的,這樣問題比較好查,ulimit一下即可知道。這類問題也是為什麼我的程式在其他平臺跑得好好的,為什麼一移植到這個平臺就段錯誤了。

6 作業系統的相關限制,如:程序可以分配的最大記憶體,程序可以開啟的最大檔案描述符個數等,在Linux下這些需要通過ulimit、setrlimit、sysctl等來解除相關的限制,這類段錯誤問題在系統移植中也經常發現,以前我們移植Linux的程式到VxWorks下時經常遇到(VxWorks要改核心配置來解決)。

7 多執行緒的程式,涉及到多個執行緒同時操作一塊記憶體時必須進行互斥,否則記憶體中的內容將不可預料。

8 在多執行緒環境下使用非執行緒安全的函式呼叫,例如 strerror 函式等。

9 在有訊號的環境中,使用不可重入函式呼叫,而這些函式內部會讀或寫某片記憶體區,當訊號中斷時,記憶體寫操作將被打斷,而下次進入時將無法避免地出錯。

10 跨程序傳遞某個地址,傳遞的都是經過對映的虛擬地址,對另外一個程序是不通用的。

11 某些有特殊要求的系統呼叫,例如epool_wait,正常情況下使用close關閉一個套接字後,epool會不再返回這個socket上的事件,但是如果你使用dup或dup2操作,將導致epool無法進行移除操作,此時再進行讀寫操作肯定是段錯誤的。

舉例說明:

  眾所周知Linux中可執行檔案的格式是ELF,其實編譯過程中的中間檔案*.o檔案、動態共享庫*.so檔案也是ELF格式的。在連結器看來,當它通過*.o或者配合*.so檔案來生成可執行檔案時,它對ELF格式的檔案以連結檢視(Linking View)進行看待。也就是說連結器以Section的形式來對待和處理ELF檔案,諸如我們常見說的程式碼段(.text)、資料段(.data和.bss)等待概念。當程式最終需要被裝載成程序時,裝載器就出場了,裝載器將可執行檔案以裝載檢視(Executive View)進行看待。裝載器將以Segment的形式來處理ELF檔案。網上很多教程也是這樣說的,大家可能還是理解的不是很明白,後面我們通過例項的方式將進一步向大家來澄清這兩者的區別。

readelf –h命令能夠可以檢視一個EFL檔案的頭部資訊。因為viewobj.o是編譯時的中間臨時檔案,所以它的“Start of pgrogram headers”和“Number of program headers”都為0,說明他不是一個可執行檔案。取而代之的是它有9個section,所以它有“Start of section headers”和“Number of section headers”都有資料。

再看一下動態共享庫:(這塊不怎麼理解 容後。。)

在Linux下動態共享庫被當作可執行檔案來處理,雖然它不能單獨執行,但某些應用程式的執行離不了它。
最後是可執行檔案,這個就不用多說了,看圖:


所以,我們可以得到這樣一個結論:一個具體的ELF檔案,其檔案頭部中的某些屬性值,指明瞭它到底是可執行檔案還是可重定位檔案(*o和*.so的統稱)。這樣,連結器和裝載器通過分析ELF檔案頭部就可以知道它該怎麼處理該檔案了。用比較直觀的、方便理解的圖來表示它們的區別就是:


也就是說連結的時候Program Header Table是可選的,但Section Header Table是必須有的。例如*.o就沒有Program Header Table,而*.so就有。裝載的時候Program Header Table必須有,但Section Header Table是可選的,但即使有Section Header Table,裝載器也不會鳥它。

那麼,裝載器為什麼要採取和連結器不同的處理策略呢?最主要的原因是為了提高記憶體的利用率。現代作業系統在裝載程式時都充分利用程式的區域性性原理,那就是,當程序執行時,並不需要一下子將程式的所有程式碼和資料都裝載到記憶體裡,而是先裝載程式的一部分到記憶體裡執行。當程序將要執行的指令不在記憶體裡的話,CPU便會觸發一個缺頁異常,作業系統捕獲到這樣的異常後便接管程序,然後將需要的指令“弄”到記憶體裡,再將執行許可權還給程序。

程序執行的時候,它虛擬地址空間的佈局和它所佔用的實體記憶體到底是什麼樣子呢?虛擬地址空間我們還比較好理解,可實際實體地址並不是我們能直接訪問到的。一般是通過一個整合在CPU內部的叫做MMU的記憶體管理單元完成了從程序虛擬地址到實體地址之間的對映。對這個對映過程感興趣的童鞋可以去拜讀Bean_lee兄的“Linux 從虛擬地址到實體地址”文章,那是相當之精彩。如果看不懂,就隨時諮詢他老人家。不過據我所知,他最近有點忙,忙得不亦樂乎,呵呵。OK,回到我們的話題上來。既然程序虛擬地址空間的任何地址,在使用前都必須通過MMU將其對映到實體記憶體上一個實實在在的儲存單元上。那麼對於任何沒有經過MMU對映過的虛擬空間的地址,不管程序是執行寫操作還是讀操作,作業系統都會捕捉到這個錯誤的非法訪問,然後輸出一個“Segmetation Fault”的錯誤提示資訊並強行終止程序。

換句話說,一個程序虛擬空間裡的任何地址,在程序訪問它之前必須要經過MMU轉換,將它對映到實體記憶體的某個具體的儲存位置上才是合法有效的,不然作業系統就會用“Segmetation Fault”對你的程序進行宣判,然後將其kill掉。那麼,問題又來了,到底哪些地址才是合法有效的呢?看一個簡單的程序虛擬地址空間的佈局:


上圖是很多資料上說的Linux程序虛擬地址空間的佈局結構圖,其中0x0804800為程序執行時的地址入口。注意,這裡的入口地址是指你的程式的第一條指令的入口地址,但是當程序執行時,程序環境空間的初始化工作,包括建立程式虛擬地址空間和實體記憶體的對映、載入動態庫等等操作都已經完成了。當所有準備工作就緒之後才會跳到這個地址執行我們程式裡的第一條指令。這個0x0804800一般由連結器在生成可執行檔案時就已經固定了,通常無需我們來更改。如果你對連結的過程和原理了如指掌,那麼你肯定也知道如何修改它了。上圖中,當用戶的程式直接訪問0x084800以前的地址、0xC0000000以後的地址或者free空間裡的地址都會觸發“Segmetation Fault”。原因如下:

1、0x084800以前的地址、0xC0000000以後的地址:由於許可權的問題,不允許程序直接訪問,作業系統對其進行保護。所以使用者程序如何訪問它們的話就會觸發“Segmetation Fault”的錯誤。前面幾篇博文有如何訪問0xC0000000以後地址的博文,也就是使用者空間和核心空間的通訊問題。

2、free地址段的空間就是前面說的,由於沒有經過MMU將其對映到實體記憶體的實際儲存單元上,當程式訪問System break(也就是常說的brk)之後的地址就出引發段錯誤。brk一般是程序堆空間結束的地方。那麼,我們如何知道當前程序的brk在什麼地方呢?答案就是通過一個C庫函式sbrk()來獲取。另外還有一個系統呼叫brk()用來設定System break的位置,其實sbrk()也可以設定,它只不是對brk()系統呼叫的一個封裝而已。關於這兩個函式的更多用法可以參考man手冊。

為了不影響我們的測試效果,我們需要將核心的隨機地址保護模式關掉。為了方式溢位攻擊,現代很多作業系統都做了這樣的隨機地址保護。就是,當程式執行時,程式碼段、堆疊段的裝載起始地址並不是固定不變的,而是每次執行程序時都會加上一個隨機的偏移量,這會影響我們的測試效果。關閉它的方法很簡單:

[root@localhost ~]#echo "0" > /proc/sys/kernel/randomize_va_space

如果/proc/sys/kernel/randomize_va_space為0則表示,程序每次啟動執行時,其虛擬地址空間裡的值就是它在ELF檔案裡所指定的值;如果為1,則每次啟動時只有棧的裝載地址做隨機保護;如果為2,表示程序每次啟動時,程序的裝載地址、brk和堆疊地址都會隨機變化。看個例子,這是網上流傳比較多的一段程式碼,很具有代表性,這裡我又站在前人的肩膀上了:

  由於全域性變數bssvar未初始化,所以當程式執行時它會被放置在.bss段,佔4位元組。sbrk(0)會返回當前brk的值。為了便於觀察,我們用了sleep(8)。下面用readelf看一下可執行檔案被裝載時,Segement的情況將會是什麼樣子:

  另一方面,記憶體分配時是以頁為單位,一般頁大小為4096位元組,所以從0x08048000開始是程式碼段,共佔記憶體0x00628,即1576個位元組,不足一個頁,但必須以頁為單位,所以下一個頁,也就是資料頁必須從0x0804900開始。但上面顯示卻說資料頁從0x08049628開始,但注意最後一列Allign,指明瞭對其方式,正好是4096位元組。驗證一下:

   這裡我們看到作業系統確實是以頁(4096位元組)為單位進行記憶體分配。有些人可能覺得奇怪,既然stack都已經有了,為什麼沒有heap呢?原因是,預設情況,.bss段結束地址就是heap的開始地址。當原始碼中沒有諸如malloc()之類的動態記憶體分配函式時,在檢視程序的記憶體對映時是看不到heap的。此時的程序空間的佈局應該如下:

我們可以知道,當程式訪問0x0848000~0x0849FFF之間的所有資料都是OK的,當訪問到0x084A000及其之後的地址就會報“Segmetation Fault”,因為我們的brk剛好到這裡。不信??好吧,把上面程式簡單調整一下:

  1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int bssvar; int main(int argc,char** argv) { void *ptr; printf("main start = %p\n",main); printf("bss end = %p\n",(long)&bssvar+4); ptr=sbrk(0); printf("current brk = %p\n",(long*)ptr); sleep(8); int i=0x08049628; for(;;i++) printf("At:0x%x-0x%x\n",i,*((char*)i)); }

      

重新編譯執行memlayout,最後出現“Segmetation Fault”時應該是下面這個樣子:

  當你的原始碼中有用到諸如malloc()之類的動態記憶體申請函式時,brk的值會被相應的往高階記憶體的位置進行調整,這樣調整出來的一段記憶體就被所謂的記憶體管理器,也就是著名的buddy system納入管理範圍了。這樣當我們再訪問這些地址時,就不會報“Segmetation Fault”了。其實如果你看過Glibc原始碼你就會驚奇的發現,malloc()最終也是通過呼叫brk()

系統掉用來實現堆的管理。所以,如果我們把上述程式碼再做一下簡單修改:

點選(此處)摺疊或開啟

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int bssvar; int main(int argc,char** argv) { void *ptr; printf("main start = %p\n",main); printf("bss end = %p\n",(long)&bssvar+4); ptr=sbrk(0); printf("current brk = %p\n",(long*)ptr); sleep(8); int i=0x08049628; brk((char*)0x804A123); //注意這行程式碼 for(;;i++) printf("At:0x%x-0x%x\n",i,*((char*)i)); }

  

我們用brk()系統呼叫,手動把brk調整到0x804A123處,再編譯執行,你就會得到下面這樣的結果:

至於是為什麼不在0x804A123處報“Segmetation Fault”而是要跑到0x804B000處才報,原因已經不止一次的強調了,腦袋犯迷糊的童鞋還是從頭再認真看一遍吧。

又到了該總結的時候了,可能有些童鞋都忘了這篇博文是要討論什麼話題了:
程式之所以會時不時的出現“Segmetation Fault”的根本原因是程序訪問到了沒有訪問許可權的地方,諸如核心區域或者其0x08048000之前的地方,或者由於要訪問的記憶體沒有經MMU進行對映所導致。而這種問題比較多的是出在malloc()之類的動態記憶體申請函式申請完記憶體,釋放後,沒有將指標設定為NULL,而其他地方在繼續用先前申請的那塊記憶體時,由於記憶體管理系統已經將其收回,所以才會出現這樣的問題。良好的關於指標的使用習慣是,使用之前先判斷其是否為NULL,所有已經歸還給作業系統的記憶體,其訪問指標都要及時置為NULL,防止所謂的“野指標”到處飛的情況,不然在大型專案裡,光是圍剿“Segmetation Fault”就要耗費不少兵力。