setjmp與longjmp學習筆記

 

setjmplongjmp學習筆記

摘自

http://hi.baidu.com/yeqiwei/blog/item/abd187017297b4d6277fb58e.html

http://blog.codingnow.com/2010/05/setjmp.html

 

一、基礎介紹

    頭文件:#include<setjmp.h>

    原型:  int setjmp(jmp_buf envbuf)

    宏函數setjmp()在緩衝區envbuf中保存系統堆棧裏的內容,供longjmp()以後使用,setjmp()必須使用頭文件setjmp.h。首次調用setjmp()宏時,返回值爲0,然而longjmp()把一個變原傳遞給setjmp(),該值(恆不爲0)就是調用longjmp()後出現的setjmp()的值。

void longjmp(jmp_buf envbuf,int status);

    函數longjmp()使程序在最近一次調用setjmp()處重新執行。

    setjmp()longjmp()提供了一種在函數間調轉的手段。函數longjmp()通過把堆棧復位成envbuf中描述的狀態進行操作,envbuf的設置是由預先調用setjmp()生成的。這樣使程序的執行在setjmp()調用後的下一個語句從新開始,使計算機認爲從未離開調用setjmp()的函數。從效果上看,longjmp()函數似乎過了時間和空間(內存)回到程序的原點,不必執行正常的函數返回過程。

    緩衝區envbuf具有<setjmp.h>中定義的buf_jmp類型,它必須調用longjmp()前通過調用setjmp()來設置好,而值status變成setjmp()的返回值,由此確定長調轉的來處。不允許的唯一值是00是程序直接調用函數setjmp()時由該函數返回的,不是間接通過執行函數longjmp()返回的。longjmp()函數最常用於在一個錯誤發生時,從一組深層嵌套的實用程序中返回。

    簡單實例:

 

程序輸出: 1 2 3

 

    在linux內核中,異常大多使用goto來處理。實際上goto語句是面向過程與面向結構化程序語言中,進行異常處理編程的最原始的支持形式。後來爲了更好地、更方便地支持異常處理編程機制,使得程序員在C語言開發的程序中,能寫出更高效、更友善的帶有異常處理機制的代碼模塊來。於是,C語言中出現了一種更優雅的異常處理機制,那就是setjmp()函數與longjmp()函數。

    實際上,這種異常處理的機制不是C語言中自身的一部分,而是在C標準庫中實現的兩個非常有技巧的庫函數,也許大多數C程序員朋友們對它都很熟悉,而且,通過使用setjmp()函數與longjmp()函數組合後,而提供的對程序的異常處理機制,以被廣泛運用到許多C語言開發的庫系統中,如jpg解析庫,加密解密庫等等。

    也許C語言中的這種異常處理機制,較goto語句相比較,它纔是真正意義上的、概念上比較徹底的,一種異常處理機制。作風一向比較嚴謹、喜歡刨根問底的主人公阿愚當然不會放棄對這種異常處理機制進行全面而深入的研究。下面一起來看看。

 

二、setjmp的使用分析

    前面剛說了,setjmpC標準庫中提供的一個函數,它的作用是保存程序當前運行的一些狀態。

    它的函數原型如下:int setjmp( jmp_buf env );

  這是MSDN中對它的評論,如下:

   setjmp函數用於保存程序的運行時的堆棧環境,接下來的其它地方,你可以通過調用longjmp函數來恢復先前被保存的程序堆棧環境。當setjmplongjmp組合一起使用時,它們能提供一種在程序中實現非本地局部跳轉"non-local goto")的機制。並且這種機制常常被用於來實現,把程序的控制流傳遞到錯誤處理模塊之中;或者程序中不採用正常的返回(return)語句,或函數的正常調用等方法,而使程序能被恢復到先前的一個調用例程(也即函數)中。

  對setjmp函數的調用時,會保存程序當前的堆棧環境到env參數中;接下來調用longjmp時,會根據這個曾經保存的變量來恢復先前的環境,並且當前的程序控制流,會因此而返回到先前調用setjmp時的程序執行點。此時,在接下來的控制流的例程中,所能訪問的所有的變量(除寄存器類型的變量以外),包含了longjmp函數調用時,所擁有的變量。

  setjmplongjmp並不能很好地支持C++中面向對象的語義。因此在C++程序中,請使用C++提供的異常處理機制。

 

三、longjmp的使用分析

    同樣,longjmp也是C標準庫中提供的一個函數,它的作用是用於恢復程序執行的堆棧環境,

    它的函數原型如下: void longjmp( jmp_buf env, int value );

  這是MSDN中對它的評論,如下:

  longjmp函數用於恢復先前程序中調用的setjmp函數時所保存的堆棧環境。setjmplongjmp組合一起使用時,它們能提供一種在程序中實現非本地局部跳轉"non-local goto")的機制。並且這種機制常常被用於來實現,把程序的控制流傳遞到錯誤處理模塊,或者不採用正常的返回(return)語句,或函數的正常調用等方法,使程序能被恢復到先前的一個調用例程(也即函數)中。

  對setjmp函數的調用時,會保存程序當前的堆棧環境到env參數中;接下來調用longjmp時,會根據這個曾經保存的變量來恢復先前的環境,並且因此當前的程序控制流,會返回到先前調用setjmp時的執行點。此時,value參數值會被setjmp函數所返回,程序繼續得以執行。並且,在接下來的控制流的例程中,它所能夠訪問到的所有的變量(除寄存器類型的變量以外),包含了longjmp函數調用時,所擁有的變量;而寄存器類型的變量將不可預料。setjmp函數返回的值必須是非零值,如果longjmp傳送的value參數值爲0,那麼實際上被setjmp返回的值是1

  在調用setjmp的函數返回之前,調用longjmp,否則結果不可預料。

在使用longjmp時,請遵守以下規則或限制:
  不要假象寄存器類型的變量將總會保持不變。在調用longjmp之後,通過setjmp所返回的控制流中,例程中寄存器類型的變量將不會被恢復。

      ②不要使用longjmp函數,來實現把控制流,從一箇中斷處理例程中傳出,除非被捕獲的異常是一個浮點數異常。在後一種情況下,如果程序通過調用_fpreset函數,來首先初始化浮點數包後,它是可以通過longjmp來實現從中斷處理例程中返回。

      ③C++程序中,小心對setjmplongjmp的使用,應爲setjmplongjmp並不能很好地支持C++中面向對象的語義。因此在C++程序中,使用C++提供的異常處理機制將會更加安全。

      把setjmplongjmp組合起來,原來它這麼厲害!

 

 

四、深入分析 

      setjmp C 語言解決 exception 的標準方案。

      setjmp/longjmp 這組 api 的名字沒有取好,導致了許多誤解。名字體現的是其行爲:跳轉,卻沒能反映其功能:exception 的拋出和捕獲。

      longjmp 從名字上看,叫做長距離跳轉。實際上它能做的事情比名字上看起來的要少得多。跳轉並非從靜止狀態的代碼段的某個點跳轉到另一個位置(類似在彙編層次的 jmp 指令做的那樣),而是在運行態中向前跳轉。C 語言的運行控制模型,是一個基於棧結構的指令執行序列。表示出來就是 call / return :調用一個函數,然後用 return 指令從一個函數返回。setjmp/longjmp 實際上是完成的另一種調用返回的模型。setjmp 相當於 call longjmp 則是 return

      重要的區別在於setjmp 不具備函數調用那樣靈活的入口點定義;而 return 不具備 longjmp 那樣可以靈活的選擇返回點。其次,第一、setjmp 並不負責維護調用棧的數據結構,即,你不必保證運行過程中 setjmp longjmp 層次上配對。如果需要這種層次,則需要程序員自己維護一個調用棧。這個調用棧往往是一個 jmp_buf 的序列;第二、它也不提供調用參數傳遞的功能,如果你需要,也得自己來實現。

      以庫形式提供的 setjmp/longjmp 和以語言關鍵字 return 提供的兩套平行的運行流控制放在一起,大大拓展了 C 語言的能力。把 setjmp/longjmp 嵌在單個函數中使用,可以模擬 pascal 中嵌套函數定義:即在函數中定義一個局部函數ps. GNUC 擴展了 C

言,也在語法上支持這種定義方法。這種用法可以讓幾個局部函數有訪問和共享 upvalue 的能力。

      把 setjmp/longjmp 放在大框架上,則多用來模擬 exception 機制。setjmp 也可以用來模擬 coroutine 。但是會遇到一個難以逾越的難點:正確的 coroutine 實現需要爲每個 coroutine 配備一個獨立的數據棧,這是 setjmp 無法做到的。雖然有一些 C coroutine 庫用 setjmp/longjmp 實現。但使用起來都會有一定隱患。多半是在單一棧上預留一塊空間,然後給另一個 coroutine 運行時覆蓋使用。當數據棧溢出時,程序會發生許多怪異的現象,很難排除這種溢出 bug 。要正確的實現 coroutine ,還需要 setcontext ,這已經不是 C 語言的標準庫了。

      在使用 setjmp 時,最常見的一個錯誤用法就是對 setjmp 做封裝,用一個函數去調用它。比如:

int try(breakpoint bp)

{

      return setjmp(bp->jb);

}

void throw(breakpoint bp)

{

      longjmp(bp->jb,1);

}

      setjmp 不應該封裝在一個函數中。這樣寫並不諱引起編譯錯誤。但十有八九會引起運行期錯誤。錯誤的起源在於 longjmp 的跳轉返回點,必須在運行流經過並有效的位置。而如果對 setjmp 做過一層函數調用的封裝後。上例中的 setjmp 設置的返回點經過 try 的調用返回後,已經無效。如果要必要封裝的話,應該使用宏。

      setjmp/longjmp 對於大多數 C 程序員來說比較陌生。正是在於它的定義含糊不清,不太容易弄清楚。使用上容易出問題,運用場合也就變的很狹窄,多用於規模較大的庫或框架中。和 C++ 語言提供的 execption 機制一樣,很少有構架師願意把它暴露到外面,那需要對二次開發的程序員有足夠清晰的頭腦,並充分理解其概念纔不會用錯。這往往是不可能的。另外,setjmp/longjmp 的理念和 C++ 本身的 RAII 相沖突。雖然許多編譯器爲防止 C++ 程序員錯誤使用 setjmp 都對其做了一定的改進。讓它可以正確工作。但大多數情況下,還是在文檔中直接聲明不推薦在 C++ 程序中使用這個東西。

 

一個MSDN的實例

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章