版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
1、使用可重入函數(shù)進(jìn)行更安全的信號(hào)處理使用可重入函數(shù)進(jìn)行更安全的信號(hào)處理2010年06月08日星期二16:48如果要對(duì)函數(shù)進(jìn)行并發(fā)訪問(wèn),不管是通過(guò)線程還是通過(guò)進(jìn)程,您都可能會(huì)遇到函數(shù)不可重入所導(dǎo)致的問(wèn)題。在本文中,通過(guò)示例代碼了解如果可重入性不能得到保證會(huì)產(chǎn)生何種異常,尤其要注意信號(hào)。引入了五條可取的編程經(jīng)驗(yàn),并對(duì)提出的編譯器模型進(jìn)行了討論,在這個(gè)模型中,可重入性由編譯器前端處理。在早期的編程中,不可重入性對(duì)程序員并不構(gòu)成威脅;函數(shù)不會(huì)有并發(fā)訪問(wèn),也沒(méi)有中斷。在很多較老的C語(yǔ)言實(shí)現(xiàn)中,函數(shù)被認(rèn)為是在單線程進(jìn)程的環(huán)境中運(yùn)行。不過(guò),現(xiàn)在,并發(fā)編程已普遍使用,您需要意識(shí)到這個(gè)缺陷。本文描述了在并行和并發(fā)
2、程序設(shè)計(jì)中函數(shù)的不可重入性導(dǎo)致的一些潛在問(wèn)題。信號(hào)的生成和處理尤其增加了額外的復(fù)雜性。由于信號(hào)在本質(zhì)上是異步的,所以難以找出當(dāng)信號(hào)處理函數(shù)觸發(fā)某個(gè)不可重入函數(shù)時(shí)導(dǎo)致的bug。本文:定義了可重入性,并包含一個(gè)可重入函數(shù)的POSIX清單。給出了示例,以說(shuō)明不可重入性所導(dǎo)致的問(wèn)題。指出了確保底層函數(shù)的可重入性的方法。討論了在編譯器層次上對(duì)可重入性的處理??芍厝?reentrant)函數(shù)可以由多于一個(gè)任務(wù)并發(fā)使用,而不必?fù)?dān)心數(shù)據(jù)錯(cuò)誤。相反,不可重入(non-reentrant)函數(shù)不能由超過(guò)一個(gè)任務(wù)所共享,除非能確保函數(shù)的互斥(或者使用信號(hào)量,或者在代碼的關(guān)鍵部分禁用中斷)??芍厝牒瘮?shù)可以在任意時(shí)刻被
3、中斷,稍后再繼續(xù)運(yùn)行,不會(huì)丟失數(shù)據(jù)??芍厝牒瘮?shù)要么使用本地變量,要么在使用全局變量時(shí)保護(hù)自己的數(shù)據(jù)??芍厝牒瘮?shù):不為連續(xù)的調(diào)用持有靜態(tài)數(shù)據(jù)。不返回指向靜態(tài)數(shù)據(jù)的指針;所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供。使用本地?cái)?shù)據(jù),或者通過(guò)制作全局?jǐn)?shù)據(jù)的本地拷貝來(lái)保護(hù)全局?jǐn)?shù)據(jù)。絕不調(diào)用任何不可重入函數(shù)。不要混淆可重入與線程安全。在程序員看來(lái),這是兩個(gè)獨(dú)立的概念:函數(shù)可以是可重入的,是線程安全的,或者二者皆是,或者二者皆非。不可重入的函數(shù)不能由多個(gè)線程使用。另外,或許不可能讓某個(gè)不可重入的函數(shù)是線程安全的。IEEE Std 1003.1列出了118個(gè)可重入的UNIX函數(shù),在此沒(méi)有給出副本。參見(jiàn)中指向上
4、此列表的鏈接。出于以下任意某個(gè)原因,其余函數(shù)是不可重入的:它們調(diào)用了malloc或free。眾所周知它們使用了靜態(tài)數(shù)據(jù)結(jié)構(gòu)體。它們是標(biāo)準(zhǔn)I/O程序庫(kù)的一部分。信號(hào)(signal)是軟件中斷。它使得程序員可以處理異步事件。為了向進(jìn)程發(fā)送一個(gè)信號(hào),內(nèi)核在進(jìn)程表?xiàng)l目的信號(hào)域中設(shè)置一個(gè)位,對(duì)應(yīng)于收到的信號(hào)的類型。信號(hào)函數(shù)的ANSI C原型是:void(*signal(int sigNum,void(*sigHandler)(int)(int);或者,另一種描述形式:typedef void sigHandler(int);SigHandler*signal(int,sigHandler*);當(dāng)進(jìn)程處理
5、所捕獲的信號(hào)時(shí),正在執(zhí)行的正常指令序列就會(huì)被信號(hào)處理器臨時(shí)中斷。然后進(jìn)程繼續(xù)執(zhí)行,但現(xiàn)在執(zhí)行的是信號(hào)處理器中的指令。如果信號(hào)處理器返回,則進(jìn)程繼續(xù)執(zhí)行信號(hào)被捕獲時(shí)正在執(zhí)行的正常的指令序列。現(xiàn)在,在信號(hào)處理器中您并不知道信號(hào)被捕獲時(shí)進(jìn)程正在執(zhí)行什么內(nèi)容。如果當(dāng)進(jìn)程正在使用malloc在它的堆上分配額外的內(nèi)存時(shí),您通過(guò)信號(hào)處理器調(diào)用malloc,那會(huì)怎樣?或者,調(diào)用了正在處理全局?jǐn)?shù)據(jù)結(jié)構(gòu)的某個(gè)函數(shù),而在信號(hào)處理器中又調(diào)用了同一個(gè)函數(shù)。如果是調(diào)用malloc,則進(jìn)程會(huì)被嚴(yán)重破壞,因?yàn)閙alloc通常會(huì)為所有它所分配的區(qū)域維持一個(gè)鏈表,而它又可能正在修改那個(gè)鏈表。甚至可以在需要多個(gè)指令的C操作符開(kāi)始和
6、結(jié)束之間發(fā)送中斷。在程序員看來(lái),指令可能似乎是原子的(也就是說(shuō),不能被分割為更小的操作),但它可能實(shí)際上需要不止一個(gè)處理器指令才能完成操作。例如,看這段C代碼:temp+=1;在x86處理器上,那個(gè)語(yǔ)句可能會(huì)被編譯為:mov ax,tempinc ax movtemp,ax這顯然不是一個(gè)原子操作。這個(gè)例子展示了在修改某個(gè)變量的過(guò)程中運(yùn)行信號(hào)處理器可能會(huì)發(fā)生什么事情:#include signal.h#include stdio.h struct two_intint a,b;data;void signal_handler(int signum)printf("%d,%dn"
7、;,data.a,data.b);alarm(1);int main(void)static struct two_int zeros=0,0,ones=1,1;signal(SIGALRM,signal_handler);data=zeros;alarm(1);while(1)data=zeros;data=ones;這個(gè)程序向data填充0,1,0,1,一直交替進(jìn)行。同時(shí),alarm信號(hào)處理器每一秒打印一次當(dāng)前內(nèi)容(在處理器中調(diào)用printf是安全的,當(dāng)信號(hào)發(fā)生時(shí)它確實(shí)沒(méi)有在處理器外部被調(diào)用)。您預(yù)期這個(gè)程序會(huì)有怎樣的輸出?它應(yīng)該打印0,0或者1,1。但是實(shí)際的輸出如下所示:0,0 1,1
8、(Skipping some output.)0,1 1,1 1,0 1,0在大部分機(jī)器上,在data中存儲(chǔ)一個(gè)新值都需要若干個(gè)指令,每次存儲(chǔ)一個(gè)字。如果在這些指令期間發(fā)出信號(hào),則處理器可能發(fā)現(xiàn)data.a為0而data.b為1,或者反之。另一方面,如果我們運(yùn)行代碼的機(jī)器能夠在一個(gè)不可中斷的指令中存儲(chǔ)一個(gè)對(duì)象的值,那么處理器將永遠(yuǎn)打印0,0或1,1。使用信號(hào)的另一個(gè)新增的困難是,只通過(guò)運(yùn)行測(cè)試用例不能夠確保代碼沒(méi)有信號(hào)bug。這一困難的原因在于信號(hào)生成本質(zhì)上異步的。假定信號(hào)處理器使用了不可重入的gethostbyname。這個(gè)函數(shù)將它的值返回到一個(gè)靜態(tài)對(duì)象中:static struct hos
9、tent host;/*result stored here*/它每次都重新使用同一個(gè)對(duì)象。在下面的例子中,如果信號(hào)剛好是在main中調(diào)用gethostbyname期間到達(dá),或者甚至在調(diào)用之后到達(dá),而程序仍然在使用那個(gè)值,則它將破壞程序請(qǐng)求的值。main()struct hostent*hostPtr;signal(SIGALRM,sig_handler);hostPtr=gethostbyname(hostNameOne);void sig_handler()struct hostent*hostPtr;/*call to gethostbyname may clobber the valu
10、e stored during the call inside the main()*/hostPtr=gethostbyname(hostNameTwo);不過(guò),如果程序不使用gethostbyname或者任何其他在同一對(duì)象中返回信息的函數(shù),或者如果它每次使用時(shí)都會(huì)阻塞信號(hào),那么就是安全的。很多庫(kù)函數(shù)在固定的對(duì)象中返回值,總是使用同一對(duì)象,它們?nèi)紩?huì)導(dǎo)致相同的問(wèn)題。如果某個(gè)函數(shù)使用并修改了您提供的某個(gè)對(duì)象,那它可能就是不可重入的;如果兩個(gè)調(diào)用使用同一對(duì)象,那么它們會(huì)相互干擾。當(dāng)使用流(stream)進(jìn)行I/O時(shí)會(huì)出現(xiàn)類似的情況。假定信號(hào)處理器使用fprintf打印一條消息,而當(dāng)信號(hào)發(fā)出時(shí)程序
11、正在使用同一個(gè)流進(jìn)行fprintf調(diào)用。信號(hào)處理器的消息和程序的數(shù)據(jù)都會(huì)被破壞,因?yàn)閮蓚€(gè)調(diào)用操作了同一數(shù)據(jù)結(jié)構(gòu):流本身。如果使用第三方程序庫(kù),事情會(huì)變得更為復(fù)雜,因?yàn)槟肋h(yuǎn)不知道哪部分程序庫(kù)是可重入的,哪部分是不可重入的。對(duì)標(biāo)準(zhǔn)程序庫(kù)而言,有很多程序庫(kù)函數(shù)在固定的對(duì)象中返回值,總是重復(fù)使用同一對(duì)象,這就使得那些函數(shù)不可重入。近來(lái)很多提供商已經(jīng)開(kāi)始提供標(biāo)準(zhǔn)C程序庫(kù)的可重入版本,這是一個(gè)好消息。對(duì)于任何給定程序庫(kù),您都應(yīng)該通讀它所提供的文檔,以了解其原型和標(biāo)準(zhǔn)庫(kù)函數(shù)的用法是否有所變化。理解這五條最好的經(jīng)驗(yàn)將幫助您保持程序的可重入性。返回指向靜態(tài)數(shù)據(jù)的指針可能會(huì)導(dǎo)致函數(shù)不可重入。例如,將字符串轉(zhuǎn)換為
12、大寫(xiě)的strToUpper函數(shù)可能被實(shí)現(xiàn)如下:char*strToUpper(char*str)/*Returning pointer to static data makes it non-reentrant*/static char bufferSTRING_SIZE_LIMIT;int index;for(index=0;strindex;index+)bufferindex=toupper(strindex);bufferindex='message';return buffer;通過(guò)修改函數(shù)的原型,您可以實(shí)現(xiàn)這個(gè)函數(shù)的可重入版本。下面的清單為輸出準(zhǔn)備了存儲(chǔ)空間:cha
13、r*strToUpper_r(char*in_str,char*out_str)int index;for(index=0;in_strindex!='message';index+)out_strindex=toupper(in_strindex);out_strindex='message';return out_str;由進(jìn)行調(diào)用的函數(shù)準(zhǔn)備輸出存儲(chǔ)空間確保了函數(shù)的可重入性。注意,這里遵循了標(biāo)準(zhǔn)慣例,通過(guò)向函數(shù)名添加"_r"后綴來(lái)命名可重入函數(shù)。記憶數(shù)據(jù)的狀態(tài)會(huì)使函數(shù)不可重入。不同的線程可能會(huì)先后調(diào)用那個(gè)函數(shù),并且修改那些數(shù)據(jù)時(shí)不會(huì)通知其
14、他正在使用此數(shù)據(jù)的線程。如果函數(shù)需要在一系列調(diào)用期間維持某些數(shù)據(jù)的狀態(tài),比如工作緩存或指針,那么調(diào)用者應(yīng)該提供此數(shù)據(jù)。在下面的例子中,函數(shù)返回某個(gè)字符串的連續(xù)小寫(xiě)字母。字符串只是在第一次調(diào)用時(shí)給出,如strtok子例程。當(dāng)搜索到字符串末尾時(shí),函數(shù)返回message。函數(shù)可能如下實(shí)現(xiàn):char getLowercaseChar(char*str)static char*buffer;static int index;char c='message';/*stores the working string on first call only*/if(string!=NULL)bu
15、ffer=str;index=0;/*searches alowercase character*/while(c=buffindex)if(islower(c)index+;break;index+;return c;這個(gè)函數(shù)是不可重入的,因?yàn)樗鎯?chǔ)變量的狀態(tài)。為了讓它可重入,靜態(tài)數(shù)據(jù),即index,需要由調(diào)用者來(lái)維護(hù)。此函數(shù)的可重入版本可能類似如下實(shí)現(xiàn):char getLowercaseChar_r(char*str,int*pIndex)char c='message';/*no initialization-the caller should have done it*
16、/*searches alowercase character*/while(c=buff*pIndex)if(islower(c)(*pIndex)+;break;(*pIndex)+;return c;在大部分系統(tǒng)中,malloc和free都不是可重入的,因?yàn)樗鼈兪褂渺o態(tài)數(shù)據(jù)結(jié)構(gòu)來(lái)記錄哪些內(nèi)存塊是空閑的。實(shí)際上,任何分配或釋放內(nèi)存的庫(kù)函數(shù)都是不可重入的。這也包括分配空間存儲(chǔ)結(jié)果的函數(shù)。避免在處理器分配內(nèi)存的最好方法是,為信號(hào)處理器預(yù)先分配要使用的內(nèi)存。避免在處理器中釋放內(nèi)存的最好方法是,標(biāo)記或記錄將要釋放的對(duì)象,讓程序不間斷地檢查是否有等待被釋放的內(nèi)存。不過(guò)這必須要小心進(jìn)行,因?yàn)閷⒁粋€(gè)對(duì)象
17、添加到一個(gè)鏈并不是原子操作,如果它被另一個(gè)做同樣動(dòng)作的信號(hào)處理器打斷,那么就會(huì)"丟失"一個(gè)對(duì)象。不過(guò),如果您知道當(dāng)信號(hào)可能到達(dá)時(shí),程序不可能使用處理器那個(gè)時(shí)刻所使用的流,那么就是安全的。如果程序使用的是某些其他流,那么也不會(huì)有任何問(wèn)題。為了編寫(xiě)沒(méi)有bug的代碼,要特別小心處理進(jìn)程范圍內(nèi)的全局變量,如errno和h_errno。考慮下面的代碼:if(close(fd)0)fprintf(stderr,"Error in close,errno:%d",errno);exit(1);假定信號(hào)在close系統(tǒng)調(diào)用設(shè)置errno變量到其返回之前這一極小的時(shí)間片段
18、內(nèi)生成。這個(gè)生成的信號(hào)可能會(huì)改變errno的值,程序的行為會(huì)無(wú)法預(yù)計(jì)。如下,在信號(hào)處理器內(nèi)保存和恢復(fù)errno的值,可以解決這一問(wèn)題:void signalHandler(int signo)int errno_saved;/*Save the error no.*/errno_saved=errno;/*Let the signal handler complete its job*/*Restore the errno*/errno=errno_saved;如果底層的函數(shù)處于關(guān)鍵部分,并且生成并處理信號(hào),那么這可能會(huì)導(dǎo)致函數(shù)不可重入。通過(guò)使用信號(hào)設(shè)置和信號(hào)掩碼,代碼的關(guān)鍵區(qū)域可以被保護(hù)起來(lái)
19、不受一組特定信號(hào)的影響,如下:保存當(dāng)前信號(hào)設(shè)置。用不必要的信號(hào)屏蔽信號(hào)設(shè)置。使代碼的關(guān)鍵部分完成其工作。最后,重置信號(hào)設(shè)置。下面是此方法的概述:sigset_t newmask,oldmask,zeromask;/*Register the signal handler*/signal(SIGALRM,sig_handler);/*Initialize the signal sets*/sigemtyset(&newmask);sigemtyset(&zeromask);/*Add the signal to the set*/sigaddset(&newmask,SI
20、GALRM);/*Block SIGALRM and save current signal mask in set variable'oldmask'*/sigprocmask(SIG_BLOCK,&newmask,&oldmask);/*The protected code goes here*/*Now allow all signals and pause*/sigsuspend(&zeromask);/*Resume to the original signal mask*/sigprocmask(SIG_SETMASK,&oldmas
21、k,NULL);/*Continue with other parts of the code*/忽略sigsuspend(&zeromask);可能會(huì)引發(fā)問(wèn)題。從消除信號(hào)阻塞到進(jìn)程執(zhí)行下一個(gè)指令之間,必然會(huì)有時(shí)鐘周期間隙,任何在此時(shí)間窗口發(fā)生的信號(hào)都會(huì)丟掉。函數(shù)調(diào)用sigsuspend通過(guò)重置信號(hào)掩碼并使進(jìn)程休眠一個(gè)單一的原子操作來(lái)解決這一問(wèn)題。如果您能確保在此時(shí)間窗口中生成的信號(hào)不會(huì)有任何負(fù)面影響,那么您可以忽略sigsuspend并直接重新設(shè)置信號(hào)。我將提出一個(gè)在編譯器層次處理可重入函數(shù)的模型??梢詾楦呒?jí)語(yǔ)言引入一個(gè)新的關(guān)鍵字:reentrant,函數(shù)可以被指定一個(gè)reentra
22、nt標(biāo)識(shí)符,以此確保函數(shù)可重入,比如:reentrant int foo();此指示符告知編譯器要專門處理那個(gè)特殊的函數(shù)。編譯器可以將這個(gè)指示符存儲(chǔ)在它的符號(hào)表中,并在中間代碼生成階段使用這個(gè)指示符。為達(dá)到此目的,編譯器的前端設(shè)計(jì)需要有一些改變。此可重入指示符遵循這些準(zhǔn)則:不為連續(xù)的調(diào)用持有靜態(tài)數(shù)據(jù)。通過(guò)制作全局?jǐn)?shù)據(jù)的本地拷貝來(lái)保護(hù)全局?jǐn)?shù)據(jù)。絕對(duì)不調(diào)用不可重入的函數(shù)。不返回對(duì)靜態(tài)數(shù)據(jù)的引用,所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供。準(zhǔn)則1可以通過(guò)類型檢查得到保證,如果在函數(shù)中有任何靜態(tài)存儲(chǔ)聲明,則拋出錯(cuò)誤消息。這可以在編譯的語(yǔ)法分析階段完成。準(zhǔn)則2,全局?jǐn)?shù)據(jù)的保護(hù)可以通過(guò)兩種方式得到保證?;镜姆椒ㄊ?,如果函數(shù)修改全局?jǐn)?shù)據(jù),則拋出一個(gè)錯(cuò)誤消息。一種更為復(fù)雜的技術(shù)是以全局?jǐn)?shù)據(jù)不被破壞的方式生成中間代碼。可以在編譯器層實(shí)現(xiàn)類似于前面經(jīng)驗(yàn)4的方法。在進(jìn)入函數(shù)時(shí),編譯器可以使用編譯器生成的臨時(shí)名稱存儲(chǔ)將要被操作
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 【正版授權(quán)】 ISO/IEC TR 11801-9906:2025 EN Information technology - Generic cabling for customer premises - Part 9906: Balanced 1-pair cabling channels up to 600 MHz for single pair Ethe
- 二零二五版煤炭開(kāi)采權(quán)轉(zhuǎn)讓及安全生產(chǎn)保障服務(wù)合同3篇
- 二零二五年度高速公路交通安全警示標(biāo)志制作合同樣本2篇
- 二零二五版餐飲業(yè)店長(zhǎng)任期管理與聘用合同3篇
- 二零二五版自來(lái)水廠自動(dòng)化控制系統(tǒng)升級(jí)合同3篇
- 二零二五版地鐵停車場(chǎng)車位租賃及公共交通服務(wù)合同2篇
- 二零二五版法院判決引導(dǎo)下的債務(wù)償還與追加借款合同3篇
- 二零二五版地下室出租合同(含倉(cāng)儲(chǔ)物流)3篇
- 二零二五版深基坑降水井施工勞務(wù)分包合同2篇
- 二零二五年果園廢棄物資源化利用合同2篇
- (正式版)QC∕T 1206.1-2024 電動(dòng)汽車動(dòng)力蓄電池?zé)峁芾硐到y(tǒng) 第1部分:通 用要求
- 《煤礦地質(zhì)工作細(xì)則》礦安﹝2024﹞192號(hào)
- 平面向量及其應(yīng)用試題及答案
- 2024高考復(fù)習(xí)必背英語(yǔ)詞匯3500單詞
- 消防控制室值班服務(wù)人員培訓(xùn)方案
- 《貴州旅游介紹》課件2
- 2024年中職單招(護(hù)理)專業(yè)綜合知識(shí)考試題庫(kù)(含答案)
- 無(wú)人機(jī)應(yīng)用平臺(tái)實(shí)施方案
- 挪用公款還款協(xié)議書(shū)范本
- 事業(yè)單位工作人員年度考核登記表(醫(yī)生個(gè)人總結(jié))
- 盾構(gòu)隧道施工數(shù)字化與智能化系統(tǒng)集成
評(píng)論
0/150
提交評(píng)論