天天看點

關于函數的可重入和遞歸

       今天在閱讀CM3 權威手冊的時候,看到這麼一段話:“所謂的重入,就是指某段子程式還沒有執行完,就因為中斷或者是多任務作業系統的排程原因,導緻該子程式在一個新的寄存器上下文中被執行(請不要把重入與遞歸混淆,它們有本質的差別)。這種情況常常會鬧出亂子,是以有“可重入性”的研究。”

        我仔細想了想關于單片機可重入與遞歸的概念,結果越來越糊塗,進而感悟到單片機中還有很多沒有弄明白的地方,于是在網上搜了搜上述的概念如下(摘自百度百科此條)

可重入函數:

   主要用于多任務環境中,一個可重入的函數簡單來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS排程下去執行另外一段代碼,而傳回控制時不會出現什麼錯誤;而不可重入的函數由于使用了一些系統資源,比如全局變量區,中斷向量表等,是以它如果被中斷的話,可能會出現問題,這類函數是不能運作在多任務環境下的。

  也可以這樣了解,重入即表示重複進入,首先它意味着這個函數可以被中斷,其次意味着它除了使用自己棧上的變量以外不依賴于任何環境(包括static),這樣的函數就是purecode(純代碼)可重入,可以允許有該函數的多個副本在運作,由于它們使用的是分離的棧,是以不會互相幹擾。如果确實需要通路全局變量(包括static),一定要注意實施互斥手段。可重入函數在并行運作環境中非常重要,但是一般要為通路全局變量付出一些性能代價。

  編寫可重入函數時,若使用全局變量,則應通過關中斷、信号量(即P、V操作)等手段對其加以保護。

  說明:若對所使用的全局變量不加以保護,則此函數就不具有可重入性,即當多個程序調用此函數時,很有可能使有關全局變量變為不可知狀态。

  示例:假設Exam是int型全局變量,函數Squre_Exam傳回Exam平方值。那麼如下函數不具有可重入性。

  unsigned int example( int para ) {

  unsigned int temp;

  Exam = para; // (**)

  temp = Square_Exam( );

  return temp;

  }

  此函數若被多個程序調用的話,其結果可能是未知的,因為當(**)語句剛執行完後,另外一個使用本函數的程序可能正好被激活,那麼當新激活的程序執行到此函數時,将使Exam賦與另一個不同的para值,是以當控制重新回到“temp = Square_Exam( )”後,計算出的temp很可能不是預想中的結果。此函數應如下改進。

  unsigned int example( int para ) {

  unsigned int temp;

  [申請信号量操作] //(1)

  Exam = para;

  temp = Square_Exam( );

  [釋放信号量操作]

  return temp;

  }

  (1)若申請不到“信号量”,說明另外的程序正處于給Exam指派并計算其平方過程中(即正在使用此信号),本程序必須等待其釋放信号後,才可繼續執行。若申請到信号,則可繼續執行,但其它程序必須等待本程序釋放信号量後,才能再使用本信号。

  保證函數的可重入性的方法:

  在寫函數時候盡量使用局部變量(例如寄存器、堆棧中的變量),對于要使用的全局變量要加以保護(如采取關中斷、信号量等方法),這樣構成的函數就一定是一個可重入的函數。

  VxWorks中采取的可重入的技術有:

  * 動态堆棧變量(各子函數有自己獨立的堆棧空間)

  * 受保護的全局變量和靜态變量

  * 任務變量

  在實時系統的設計中,經常會出現多個任務調用同一個函數的情況。如果這個函數不幸被設計成為不可重入的函數的話,那麼不同任務調用這個函數時可能修改其他任務調用這個函數的資料,進而導緻不可預料的後果。那麼什麼是可重入函數呢?所謂可重入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心資料是否會出錯。不可重入函數在實時系統設計中被視為不安全函數。滿足下列條件的函數多數是不可重入的:

  1) 函數體内使用了靜态的資料結構;

  2) 函數體内調用了malloc()或者free()函數;

  3) 函數體内調用了标準I/O函數。

  下面舉例加以說明。

  A. 可重入函數

  void strcpy(char *lpszDest, char *lpszSrc) {

  while(*lpszDest++=*lpszSrc++);

  *dest=0;

  }

  B. 不可重入函數1

  charcTemp;//全局變量

  void SwapChar1(char *lpcX, char *lpcY) {

  cTemp=*lpcX;

  *lpcX=*lpcY;

  lpcY=cTemp;//通路了全局變量

  }

  C. 不可重入函數2

  void SwapChar2(char *lpcX,char *lpcY) {

  static char cTemp;//靜态局部變量

  cTemp=*lpcX;

  *lpcX=*lpcY;

  lpcY=cTemp;//使用了靜态局部變量

  }

  問題1,如何編寫可重入的函數?

  答:在函數體内不通路那些全局變量,不使用靜态局部變量,堅持隻使用局部變量,寫出的函數就将是可重入的。如果必須通路全局變量,記住利用互斥信号量來保護全局變量。

  問題2,如何将一個不可重入的函數改寫成可重入的函數?

  答:把一個不可重入函數變成可重入的唯一方法是用可重入規則來重寫它。其實很簡單,隻要遵守了幾條很容易了解的規則,那麼寫出來的函數就是可重入的。

  1) 不要使用全局變量。因為别的代碼很可能覆寫這些變量值。

  2) 在和硬體發生互動的時候,切記執行類似disinterrupt()之類的操作,就是關閉硬體中斷。完成互動記得打開中斷,在有些系列上,這叫做“進入/退出核心”。

  3) 不能調用其它任何不可重入的函數。

  4) 謹慎使用堆棧。最好先在使用前先OS_ENTER_KERNAL。

  堆棧操作涉及記憶體配置設定,稍不留神就會造成益出導緻覆寫其他任務的資料,是以,請謹慎使用堆棧!最好别用!很多黑客程式就利用了這一點以便系統執行非法代碼進而輕松獲得系統控制權。還有一些規則,總之,時刻記住一句話:保證中斷是安全的!

  執行個體問題:曾經設計過如下一個函數,在代碼檢視的時候被提醒有bug,因為這個函數是不可重入的,為什麼?

  unsigned int sum_int( unsigned int base ) {

  unsigned int index;

  static unsigned int sum = 0; // 注意,是static類型

  for (index = 1; index <= base; index++)

  sum += index;

  return sum;

  }

  分析:所謂的函數是可重入的(也可以說是可預測的),即隻要輸入資料相同就應産生相同的輸出。這個函數之是以是不可預測的,就是因為函數中使用了static變量,因為static變量的特征,這樣的函數被稱為:帶“内部存儲器”功能的的函數。是以如果需要一個可重入的函數,一定要避免函數中使用static變量,這種函數中的static變量,使用原則是,能不用盡量不用。

  将上面的函數修改為可重入的函數,隻要将聲明sum變量中的static關鍵字去掉,變量sum即變為一個auto類型的變量,函數即變為一個可重入的函數。

  當然,有些時候,在函數中是必須要使用static變量的,比如當某函數的傳回值為指針類型時,則必須是static的局部變量的位址作為傳回值,若為auto類型,則傳回為錯指針。

遞歸函數

在數學上,關于遞歸函數的定義如下:

  對于某一函數f(x),其定義域是集合A,那麼若對于A集合中的某一個值X0,其函數值f(x0)由f(f(x0))決定,那麼就稱f(x)為遞歸函數。

  在程式設計語言中,把直接或間接地調用自身的函數稱為遞歸函數。函數的建構通常需要一個函數或者一個過程來完成。

以下是大俠們關于遞歸函數和遞歸調用的讨論:

“遞歸函數”是在函數體記憶體在對本函數的“調用”,而“遞歸調用”反映的是一種“循環反身”掉用的關系。“遞歸函數”必然存在“遞歸調用”,而且其調用關系相當的直接。但有時候,表面上看不出直接“遞歸調用”的關系,卻着實存在間接的“遞歸調用”關系,如A call B,B call C,C call A。此外,由于中斷的任意性,若在中斷中調用一個函數,而此函數又可以被其它途徑調用,則就有可能存在函數的“遞歸調用”。

“遞歸調用”涉及到一個函數重入的問題,其實如果函數局部變量如果能全部放在棧中,問題也就不存在了。但為了節省棧的空間,往往會把一些體積大的局部變量放在棧外,并可能作覆寫處理。是以必須告訴編譯,在處理具有重入特性的函數時,必須完全采用“棧”來存儲局部變量。當然如此一來,“棧”會長得很快,甚至會撐破記憶體。

最終總結。。。待續中 還望高人指點

上一篇: PC啟動過程