可重入函數(shù)和不可重入函數(shù)_第1頁
可重入函數(shù)和不可重入函數(shù)_第2頁
可重入函數(shù)和不可重入函數(shù)_第3頁
可重入函數(shù)和不可重入函數(shù)_第4頁
可重入函數(shù)和不可重入函數(shù)_第5頁
已閱讀5頁,還剩6頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

使用可重入函數(shù)進行更安全的信號處理何時如何利用可重入性避免代碼出現(xiàn)bug\o""DipakK.Jha(dipakjha@),軟件工程師,IBM簡介:

如果要對函數(shù)進行并發(fā)訪問,不管是通過線程還是通過進程,您都可能會遇到函數(shù)不可重入所導(dǎo)致的問題。在本文中,通過示例代碼了解如果可重入性不能得到保證會產(chǎn)生何種異常,尤其要注意信號。引入了五條可取的編程經(jīng)驗,并對提出的編譯器模型進行了討論,在這個模型中,可重入性由編譯器前端處理。標記本文!發(fā)布日期:

2005年2月20日

級別:

初級

訪問情況

1603次瀏覽

建議:

0

(添加評論)平均分(共3個評分)在早期的編程中,不可重入性對程序員并不構(gòu)成威脅;函數(shù)不會有并發(fā)訪問,也沒有中斷。在很多較老的C語言實現(xiàn)中,函數(shù)被認為是在單線程進程的環(huán)境中運行。不過,現(xiàn)在,并發(fā)編程已普遍使用,您需要意識到這個缺陷。本文描述了在并行和并發(fā)程序設(shè)計中函數(shù)的不可重入性導(dǎo)致的一些潛在問題。信號的生成和處理尤其增加了額外的復(fù)雜性。由于信號在本質(zhì)上是異步的,所以難以找出當(dāng)信號處理函數(shù)觸發(fā)某個不可重入函數(shù)時導(dǎo)致的bug。本文:定義了可重入性,并包含一個可重入函數(shù)的POSIX清單。給出了示例,以說明不可重入性所導(dǎo)致的問題。指出了確保底層函數(shù)的可重入性的方法。討論了在編譯器層次上對可重入性的處理。什么是可重入性?可重入(reentrant)函數(shù)可以由多于一個任務(wù)并發(fā)使用,而不必擔(dān)心數(shù)據(jù)錯誤。相反,不可重入(non-reentrant)函數(shù)不能由超過一個任務(wù)所共享,除非能確保函數(shù)的互斥(或者使用信號量,或者在代碼的關(guān)鍵部分禁用中斷)??芍厝牒瘮?shù)可以在任意時刻被中斷,稍后再繼續(xù)運行,不會丟失數(shù)據(jù)。可重入函數(shù)要么使用本地變量,要么在使用全局變量時保護自己的數(shù)據(jù)??芍厝牒瘮?shù):不為連續(xù)的調(diào)用持有靜態(tài)數(shù)據(jù)。不返回指向靜態(tài)數(shù)據(jù)的指針;所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供。使用本地數(shù)據(jù),或者通過制作全局數(shù)據(jù)的本地拷貝來保護全局數(shù)據(jù)。絕不調(diào)用任何不可重入函數(shù)。不要混淆可重入與線程安全。在程序員看來,這是兩個獨立的概念:函數(shù)可以是可重入的,是線程安全的,或者二者皆是,或者二者皆非。不可重入的函數(shù)不能由多個線程使用。另外,或許不可能讓某個不可重入的函數(shù)是線程安全的。IEEEStd1003.1列出了118個可重入的UNIX?函數(shù),在此沒有給出副本。參見參考資料中指向上此列表的鏈接。出于以下任意某個原因,其余函數(shù)是不可重入的:它們調(diào)用了malloc或free。眾所周知它們使用了靜態(tài)數(shù)據(jù)結(jié)構(gòu)體。它們是標準I/O程序庫的一部分?;仨撌仔盘柡筒豢芍厝牒瘮?shù)信號(signal)是軟件中斷。它使得程序員可以處理異步事件。為了向進程發(fā)送一個信號,內(nèi)核在進程表條目的信號域中設(shè)置一個位,對應(yīng)于收到的信號的類型。信號函數(shù)的ANSIC原型是:void(*signal(intsigNum,void(*sigHandler)(int)))(int);或者,另一種描述形式:typedefvoidsigHandler(int);SigHandler*signal(int,sigHandler*);當(dāng)進程處理所捕獲的信號時,正在執(zhí)行的正常指令序列就會被信號處理器臨時中斷。然后進程繼續(xù)執(zhí)行,但現(xiàn)在執(zhí)行的是信號處理器中的指令。如果信號處理器返回,則進程繼續(xù)執(zhí)行信號被捕獲時正在執(zhí)行的正常的指令序列?,F(xiàn)在,在信號處理器中您并不知道信號被捕獲時進程正在執(zhí)行什么內(nèi)容。如果當(dāng)進程正在使用malloc在它的堆上分配額外的內(nèi)存時,您通過信號處理器調(diào)用malloc,那會怎樣?或者,調(diào)用了正在處理全局數(shù)據(jù)結(jié)構(gòu)的某個函數(shù),而在信號處理器中又調(diào)用了同一個函數(shù)。如果是調(diào)用malloc,則進程會被嚴重破壞,因為malloc通常會為所有它所分配的區(qū)域維持一個鏈表,而它又可能正在修改那個鏈表。甚至可以在需要多個指令的C操作符開始和結(jié)束之間發(fā)送中斷。在程序員看來,指令可能似乎是原子的(也就是說,不能被分割為更小的操作),但它可能實際上需要不止一個處理器指令才能完成操作。例如,看這段C代碼:temp+=1;在x86處理器上,那個語句可能會被編譯為:movax,[temp]incaxmov[temp],ax這顯然不是一個原子操作。這個例子展示了在修改某個變量的過程中運行信號處理器可能會發(fā)生什么事情:

清單1.在修改某個變量的同時運行信號處理器 #include<signal.h>#include<stdio.h>structtwo_int{inta,b;}data;voidsignal_handler(intsignum){printf("%d,%d\n",data.a,data.b);alarm(1);}intmain(void){staticstructtwo_intzeros={0,0},ones={1,1};signal(SIGALRM,signal_handler);data=zeros;alarm(1);while(1){data=zeros;data=ones;}}這個程序向data填充0,1,0,1,一直交替進行。同時,alarm信號處理器每一秒打印一次當(dāng)前內(nèi)容(在處理器中調(diào)用printf是安全的,當(dāng)信號發(fā)生時它確實沒有在處理器外部被調(diào)用)。您預(yù)期這個程序會有怎樣的輸出?它應(yīng)該打印0,0或者1,1。但是實際的輸出如下所示:0,01,1(Skippingsomeoutput...)0,11,11,01,0...在大部分機器上,在data中存儲一個新值都需要若干個指令,每次存儲一個字。如果在這些指令期間發(fā)出信號,則處理器可能發(fā)現(xiàn)data.a為0而data.b為1,或者反之。另一方面,如果我們運行代碼的機器能夠在一個不可中斷的指令中存儲一個對象的值,那么處理器將永遠打印0,0或1,1。使用信號的另一個新增的困難是,只通過運行測試用例不能夠確保代碼沒有信號bug。這一困難的原因在于信號生成本質(zhì)上異步的。回頁首不可重入函數(shù)和靜態(tài)變量假定信號處理器使用了不可重入的gethostbyname。這個函數(shù)將它的值返回到一個靜態(tài)對象中:staticstructhostenthost;/*resultstoredhere*/它每次都重新使用同一個對象。在下面的例子中,如果信號剛好是在main中調(diào)用gethostbyname期間到達,或者甚至在調(diào)用之后到達,而程序仍然在使用那個值,則它將破壞程序請求的值。

清單2.gethostbyname的危險用法 main(){structhostent*hostPtr;...signal(SIGALRM,sig_handler);...hostPtr=gethostbyname(hostNameOne);...}voidsig_handler(){structhostent*hostPtr;.../*calltogethostbynamemayclobberthevaluestoredduringthecallinsidethemain()*/hostPtr=gethostbyname(hostNameTwo);...}不過,如果程序不使用gethostbyname或者任何其他在同一對象中返回信息的函數(shù),或者如果它每次使用時都會阻塞信號,那么就是安全的。很多庫函數(shù)在固定的對象中返回值,總是使用同一對象,它們?nèi)紩?dǎo)致相同的問題。如果某個函數(shù)使用并修改了您提供的某個對象,那它可能就是不可重入的;如果兩個調(diào)用使用同一對象,那么它們會相互干擾。當(dāng)使用流(stream)進行I/O時會出現(xiàn)類似的情況。假定信號處理器使用fprintf打印一條消息,而當(dāng)信號發(fā)出時程序正在使用同一個流進行fprintf調(diào)用。信號處理器的消息和程序的數(shù)據(jù)都會被破壞,因為兩個調(diào)用操作了同一數(shù)據(jù)結(jié)構(gòu):流本身。如果使用第三方程序庫,事情會變得更為復(fù)雜,因為您永遠不知道哪部分程序庫是可重入的,哪部分是不可重入的。對標準程序庫而言,有很多程序庫函數(shù)在固定的對象中返回值,總是重復(fù)使用同一對象,這就使得那些函數(shù)不可重入。近來很多提供商已經(jīng)開始提供標準C程序庫的可重入版本,這是一個好消息。對于任何給定程序庫,您都應(yīng)該通讀它所提供的文檔,以了解其原型和標準庫函數(shù)的用法是否有所變化?;仨撌状_??芍厝胄缘慕?jīng)驗理解這五條最好的經(jīng)驗將幫助您保持程序的可重入性。經(jīng)驗1返回指向靜態(tài)數(shù)據(jù)的指針可能會導(dǎo)致函數(shù)不可重入。例如,將字符串轉(zhuǎn)換為大寫的strToUpper函數(shù)可能被實現(xiàn)如下:

清單3.strToUpper的不可重入版本 char*strToUpper(char*str){/*Returningpointertostaticdatamakesitnon-reentrant*/staticcharbuffer[STRING_SIZE_LIMIT];intindex;for(index=0;str[index];index++)buffer[index]=toupper(str[index]);buffer[index]='\0';returnbuffer;}通過修改函數(shù)的原型,您可以實現(xiàn)這個函數(shù)的可重入版本。下面的清單為輸出準備了存儲空間:

清單4.strToUpper的可重入版本 char*strToUpper_r(char*in_str,char*out_str){intindex;for(index=0;in_str[index]!='\0';index++)out_str[index]=toupper(in_str[index]);out_str[index]='\0';returnout_str;}由進行調(diào)用的函數(shù)準備輸出存儲空間確保了函數(shù)的可重入性。注意,這里遵循了標準慣例,通過向函數(shù)名添加“_r”后綴來命名可重入函數(shù)。經(jīng)驗2記憶數(shù)據(jù)的狀態(tài)會使函數(shù)不可重入。不同的線程可能會先后調(diào)用那個函數(shù),并且修改那些數(shù)據(jù)時不會通知其他正在使用此數(shù)據(jù)的線程。如果函數(shù)需要在一系列調(diào)用期間維持某些數(shù)據(jù)的狀態(tài),比如工作緩存或指針,那么調(diào)用者應(yīng)該提供此數(shù)據(jù)。在下面的例子中,函數(shù)返回某個字符串的連續(xù)小寫字母。字符串只是在第一次調(diào)用時給出,如strtok子例程。當(dāng)搜索到字符串末尾時,函數(shù)返回\0。函數(shù)可能如下實現(xiàn):

清單5.getLowercaseChar的不可重入版本 chargetLowercaseChar(char*str){staticchar*buffer;staticintindex;charc='\0';/*storestheworkingstringonfirstcallonly*/if(string!=NULL){buffer=str;index=0;}/*searchesalowercasecharacter*/while(c=buff[index]){if(islower(c)){index++;break;}index++;}returnc;}這個函數(shù)是不可重入的,因為它存儲變量的狀態(tài)。為了讓它可重入,靜態(tài)數(shù)據(jù),即index,需要由調(diào)用者來維護。此函數(shù)的可重入版本可能類似如下實現(xiàn):

清單6.getLowercaseChar的可重入版本 chargetLowercaseChar_r(char*str,int*pIndex){charc='\0';/*noinitialization-thecallershouldhavedoneit*//*searchesalowercasecharacter*/while(c=buff[*pIndex]){if(islower(c)){(*pIndex)++;break;}(*pIndex)++;}returnc;}經(jīng)驗3在大部分系統(tǒng)中,malloc和free都不是可重入的,因為它們使用靜態(tài)數(shù)據(jù)結(jié)構(gòu)來記錄哪些內(nèi)存塊是空閑的。實際上,任何分配或釋放內(nèi)存的庫函數(shù)都是不可重入的。這也包括分配空間存儲結(jié)果的函數(shù)。避免在處理器分配內(nèi)存的最好方法是,為信號處理器預(yù)先分配要使用的內(nèi)存。避免在處理器中釋放內(nèi)存的最好方法是,標記或記錄將要釋放的對象,讓程序不間斷地檢查是否有等待被釋放的內(nèi)存。不過這必須要小心進行,因為將一個對象添加到一個鏈并不是原子操作,如果它被另一個做同樣動作的信號處理器打斷,那么就會“丟失”一個對象。不過,如果您知道當(dāng)信號可能到達時,程序不可能使用處理器那個時刻所使用的流,那么就是安全的。如果程序使用的是某些其他流,那么也不會有任何問題。經(jīng)驗4為了編寫沒有bug的代碼,要特別小心處理進程范圍內(nèi)的全局變量,如errno和h_errno??紤]下面的代碼:

清單7.errno的危險用法 if(close(fd)<0){fprintf(stderr,"Errorinclose,errno:%d",errno);exit(1);}假定信號在close系統(tǒng)調(diào)用設(shè)置errno變量到其返回之前這一極小的時間片段內(nèi)生成。這個生成的信號可能會改變errno的值,程序的行為會無法預(yù)計。如下,在信號處理器內(nèi)保存和恢復(fù)errno的值,可以解決這一問題:

清單8.保存和恢復(fù)errno的值 voidsignalHandler(intsigno){interrno_saved;/*Savetheerrorno.*/errno_saved=errno;/*Letthesignalhandlercompleteitsjob*/....../*Restoretheerrno*/errno=errno_saved;}經(jīng)驗5如果底層的函數(shù)處于關(guān)鍵部分,并且生成并處理信號,那么這可能會導(dǎo)致函數(shù)不可重入。通過使用信號設(shè)置和信號掩碼,代碼的關(guān)鍵區(qū)域可以被保護起來不受一組特定信號的影響,如下:保存當(dāng)前信號設(shè)置。用不必要的信號屏蔽信號設(shè)置。使代碼的關(guān)鍵部分完成其工作。最后,重置信號設(shè)置。下面是此方法的概述:

清單9.使用信號設(shè)置和信號掩碼 sigset_tnewmask,oldmask,zeromask;.../*Registerthesignalhandler*/signal(SIGALRM,sig_handler);/*Initializethesignalsets*/sigemtyset(&newmask);sigemtyset(&zeromask);/*Addthesignaltotheset*/sigaddset(&newmask,SIGALRM);/*BlockSIGALRMandsavecurrentsignalmaskinsetvariable'oldmask'*/sigprocmask(SIG_BLOCK,&newmask,&oldmask);/*Theprotectedcodegoeshere......*//*Nowallowallsignalsandpause*/sigsuspend(&zeromask);/*Resumetotheoriginalsignalmask*/sigprocmask(SIG_SETMASK,&oldmask,NULL);/*Continuewithotherpartsofthecode*/忽略sigsuspend(&zeromask);可能會引發(fā)問題。從消除信號阻塞到進程執(zhí)行下一個指令之間,必然會有時鐘周期間隙,任何在此時間窗口發(fā)生的信號都會丟掉。函數(shù)調(diào)用sigsuspend通過重置信號掩碼并使進程休眠一個單一的原子操作來解決這一問題。如果您能確保在此時間窗口中生成的信號不會有任何負面影響,那么您可以忽略sigsuspend并直接重新設(shè)置信號。回頁首在編譯器層次處理可重用性我將提出一個在編譯器層次處理可重入函數(shù)的模型。可以為高級語言引入一個新的關(guān)鍵字:reentrant,函數(shù)可以被指定一個reentrant標識符,以此確保函數(shù)可重入,比如:reentrantintfoo();此指示符告知編譯器要專門處理那個特殊的函數(shù)。編譯器可以將這個指示符存儲在它的符號表中,并在中間代碼生成階段使用這個指示符。為達到此目的,編譯器的前端設(shè)計需要有一些改變。此可重入指示符遵循這些準則:不為連續(xù)的調(diào)用持有靜態(tài)數(shù)據(jù)。通過制作全局數(shù)據(jù)的本地拷貝來保護全局數(shù)據(jù)。絕對不調(diào)用不可重入的函數(shù)。不返回對靜態(tài)數(shù)據(jù)的引用,所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供。準則1可以通過類型檢查得到保證,如果在函數(shù)中有任何靜態(tài)存儲聲明,則拋出錯誤消息。這可以在編譯的語法分析階段完成。準則2,全局數(shù)據(jù)的保護可以通過兩種方式得到保證?;镜姆椒ㄊ?,如果函數(shù)修改全局數(shù)據(jù),則拋出一個錯誤消息。一種更為復(fù)雜的技術(shù)是以全局數(shù)據(jù)不被破壞的方式生成中間代碼。可以在編譯器層實現(xiàn)類似于前面經(jīng)驗4的方法。在進入函數(shù)時,編譯器可以使用編譯器生成的臨時名稱存儲將要被操作的全局數(shù)據(jù),然后在退出函數(shù)時恢復(fù)那些數(shù)據(jù)。使用編譯器生成的臨時名稱存儲數(shù)據(jù)對編譯器來說是常用的方法。確保準則3得到滿足,要求編譯器預(yù)先知道所有可重入函數(shù),包括應(yīng)用程序所使用的程序庫。這些關(guān)于函數(shù)的附加信息可以存儲在符號表中。最后,準則4已經(jīng)得到了準則2的保證。如果函數(shù)沒有靜態(tài)數(shù)據(jù),那么也就不存在返回靜態(tài)數(shù)據(jù)的引用的問題。提出的這個模型將簡化程序員遵循可重入函數(shù)準則的工作,而且使用此模型可以預(yù)防代碼出現(xiàn)無意的可重入性bug。參考資料您可以參閱本文在developerWorks全

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論