欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
【轉】使用可重入函數進(jìn)行更安全的信號處理

何時(shí)如何利用可重入性避免代碼出現 bug

Dipak K. Jha (dipakjha@in.ibm.com), 軟件工程師, IBM 

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


在早期的編程中,不可重入性對程序員并不構成威脅;函數不會(huì )有并發(fā)訪(fǎng)問(wèn),也沒(méi)有中斷。在很多較老的 C 語(yǔ)言實(shí)現中,函數被認為是在單線(xiàn)程進(jìn)程的環(huán)境中運行。

不過(guò),現在,并發(fā)編程已普遍使用,您需要意識到這個(gè)缺陷。本文描述了在并行和并發(fā)程序設計中函數的不可重入性導致的一些潛在問(wèn)題。信號的生成和處理尤其增 加了額外的復雜性。由于信號在本質(zhì)上是異步的,所以難以找出當信號處理函數 觸發(fā)某個(gè)不可重入函數時(shí)導致的 bug。

本文:

  • 定義了可重入性,并包含一個(gè)可重入函數的 POSIX 清單。
  • 給出了示例,以說(shuō)明不可重入性所導致的問(wèn)題。
  • 指出了確保底層函數的可重入性的方法。
  • 討論了在編譯器層次上對可重入性的處理。

什么是可重入性?

可重入(reentrant)函數可以由多于一個(gè)任務(wù)并發(fā)使用,而不必擔心數據錯誤。相反, 不可重入(non-reentrant)函數不能由超過(guò)一個(gè)任務(wù)所共享,除非能確保函數的互斥 (或者使用信號量,或者在代碼的關(guān)鍵部分禁用中斷)??芍厝牒瘮悼梢栽谌我鈺r(shí)刻被中斷, 稍后再繼續運行,不會(huì )丟失數據??芍厝牒瘮狄词褂帽镜刈兞?,要么在使用全局變量時(shí) 保護自己的數據。

可重入函數:

  • 不為連續的調用持有靜態(tài)數據。
  • 不返回指向靜態(tài)數據的指針;所有數據都由函數的調用者提供。
  • 使用本地數據,或者通過(guò)制作全局數據的本地拷貝來(lái)保護全局數據。
  • 絕不調用任何不可重入函數。

不要混淆可重入與線(xiàn)程安全。在程序員看來(lái),這是兩個(gè)獨立的概念:函數可以是可重入的,是線(xiàn)程安全的,或者 二者皆是,或者二者皆非。不可重入的函數不能由多個(gè)線(xiàn)程使用。另外,或許不可能讓某個(gè) 不可重入的函數是線(xiàn)程安全的。

IEEE Std 1003.1 列出了 118 個(gè)可重入的 UNIX? 函數,在此沒(méi)有給出副本。參見(jiàn) 參 考資料 中指向 unix.org 上此列表的鏈接。

出于以下任意某個(gè)原因,其余函數是不可重入的:

  • 它們調用了 mallocfree。
  • 眾所周知它們使用了靜態(tài)數據結構體。
  • 它們是標準 I/O 程序庫的一部分。

信號和不可重入函數

信號(signal) 是軟件中斷。它使得程序員可以處理異步事件。為了向進(jìn)程發(fā)送一個(gè)信號, 內核在進(jìn)程表條目的信號域中設置一個(gè)位,對應于收到的信號的類(lèi)型。信號函數的 ANSI C 原型是:

void (*signal (int sigNum, void (*sigHandler)(int))) (int);

或者,另一種描述形式:

typedef void sigHandler(int);
SigHandler *signal(int, sigHandler *);

當進(jìn)程處理所捕獲的信號時(shí),正在執行的正常指令序列就會(huì )被信號處理器臨時(shí)中斷。然后進(jìn)程繼續執行, 但現在執行的是信號處理器中的指令。如果信號處理器返回,則進(jìn)程繼續執行信號被捕獲時(shí)正在執行的 正常的指令序列。

現在,在信號處理器中您并不知道信號被捕獲時(shí)進(jìn)程正在執行什么內容。如果當進(jìn)程正在使用 malloc 在它的堆上分配額外的內存時(shí),您通過(guò)信號處理器調用 malloc,那會(huì )怎樣?或者,調用了正在處理全局數據結構的某個(gè)函數,而 在信號處理器中又調用了同一個(gè)函數。如果是調用 malloc,則進(jìn)程會(huì ) 被嚴重破壞,因為 malloc 通常會(huì )為所有它所分配的區域維持一個(gè)鏈表,而它又 可能正在修改那個(gè)鏈表。

甚至可以在需要多個(gè)指令的 C 操作符開(kāi)始和結束之間發(fā)送中斷。在程序員看來(lái),指令可能似乎是原子的 (也就是說(shuō),不能被分割為更小的操作),但它可能實(shí)際上需要不止一個(gè)處理器指令才能完成操作。 例如,看這段 C 代碼:

temp += 1;

在 x86 處理器上,那個(gè)語(yǔ)句可能會(huì )被編譯為:

mov ax,[temp]
inc ax
mov [temp],ax

這顯然不是一個(gè)原子操作。

這個(gè)例子展示了在修改某個(gè)變量的過(guò)程中運行信號處理器可能會(huì )發(fā)生什么事情:


清單 1. 在修改某個(gè)變量的同時(shí)運行信號處理器
				
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", 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 信號 處理器每一秒打印一次當前內容(在處理器中調用 printf 是安全的,當信號發(fā)生時(shí) 它確實(shí)沒(méi)有在處理器外部被調用)。您預期這個(gè)程序會(huì )有怎樣的輸出?它應該打印 0,0 或者 1,1。但是實(shí)際的輸出 如下所示:

0, 0
1, 1
(Skipping some output...)
0, 1
1, 1
1, 0
1, 0
...

在大部分機器上,在 data 中存儲一個(gè)新值都需要若干個(gè)指令,每次存儲一個(gè)字。 如果在這些指令期間發(fā)出信號,則處理器可能發(fā)現 data.a 為 0 而 data.b 為 1,或者反之。另一方面,如果我們運行代碼的機器能夠在一個(gè) 不可中斷的指令中存儲一個(gè)對象的值,那么處理器將永遠打印 0,0 或 1,1。

使用信號的另一個(gè)新增的困難是,只通過(guò)運行測試用例不能夠確保代碼沒(méi)有信號 bug。這一困難的原因在于 信號生成本質(zhì)上異步的。


不可重入函數和靜態(tài)變量

假定信號處理器使用了不可重入的 gethostbyname。這個(gè)函數 將它的值返回到一個(gè)靜態(tài)對象中:

static struct hostent host; /* result stored here*/

它每次都重新使用同一個(gè)對象。在下面的例子中,如果信號剛好是在 main 中調用 gethostbyname 期間到達,或者甚至在調用之后到達,而程序仍然在使用那個(gè)值,則 它將破壞程序請求的值。


清單 2. gethostbyname 的危險用法
				
main(){
struct hostent *hostPtr;
...
signal(SIGALRM, sig_handler);
...
hostPtr = gethostbyname(hostNameOne);
...
}
void sig_handler(){
struct hostent *hostPtr;
...
/* call to gethostbyname may clobber the value stored during the call
inside the main() */
hostPtr = gethostbyname(hostNameTwo);
...
}

不過(guò),如果程序不使用 gethostbyname 或者任何其他在同一對象中返回信息 的函數,或者如果它每次使用時(shí)都會(huì )阻塞信號,那么就是安全的。

很多庫函數在固定的對象中返回值,總是使用同一對象,它們全都會(huì )導致相同的問(wèn)題。如果某個(gè)函數使用并修改了 您提供的某個(gè)對象,那它可能就是不可重入的;如果兩個(gè)調用使用同一對象,那么它們會(huì )相互干擾。

當使用流(stream)進(jìn)行 I/O 時(shí)會(huì )出現類(lèi)似的情況。假定信號處理器使用 fprintf 打印一條消息,而當信號發(fā)出時(shí)程序正在使用同一個(gè)流進(jìn)行 fprintf 調用。 信號處理器的消息和程序的數據都會(huì )被破壞,因為兩個(gè)調用操作了同一數據結構:流本身。

如果使用第三方程序庫,事情會(huì )變得更為復雜,因為您永遠不知道哪部分程序庫是可重入的,哪部分是不可重入的。 對標準程序庫而言,有很多程序庫函數在固定的對象中返回值,總是重復使用同一對象,這就使得那些函數 不可重入。

近來(lái)很多提供商已經(jīng)開(kāi)始提供標準 C 程序庫的可重入版本,這是一個(gè)好消息。對于任何給定程序庫,您都應該通讀它所提供 的文檔,以了解其原型和標準庫函數的用法是否有所變化。


確??芍厝胄缘慕?jīng)驗

理解這五條最好的經(jīng)驗將幫助您保持程序的可重入性。

經(jīng)驗 1

返回指向靜態(tài)數據的指針可能會(huì )導致函數不可重入。例如,將字符串轉換為大寫(xiě)的 strToUpper 函數可能被實(shí)現如下:


清單 3. strToUpper 的不可重入版本
				
char *strToUpper(char *str)
{
/*Returning pointer to static data makes it non-reentrant */
static char buffer[STRING_SIZE_LIMIT];
int index;
for (index = 0; str[index]; index++)
buffer[index] = toupper(str[index]);
buffer[index] = '\0';
return buffer;
}

通過(guò)修改函數的原型,您可以實(shí)現這個(gè)函數的可重入版本。下面的清單為輸出準備了存儲空間:


清單 4. strToUpper 的可重入版本
				
char *strToUpper_r(char *in_str, char *out_str)
{
int index;
for (index = 0; in_str[index] != '\0'; index++)
out_str[index] = toupper(in_str[index]);
out_str[index] = '\0';
return out_str;
}

由進(jìn)行調用的函數準備輸出存儲空間確保了函數的可重入性。注意,這里遵循了標準慣例,通過(guò)向函數名添加“_r”后綴來(lái) 命名可重入函數。

經(jīng)驗 2

記憶數據的狀態(tài)會(huì )使函數不可重入。不同的線(xiàn)程可能會(huì )先后調用那個(gè)函數,并且修改那些數據時(shí)不會(huì )通知其他 正在使用此數據的線(xiàn)程。如果函數需要在一系列調用期間維持某些數據的狀態(tài),比如工作緩存或指針,那么 調用者應該提供此數據。

在下面的例子中,函數返回某個(gè)字符串的連續小寫(xiě)字母。字符串只是在第一次調用時(shí)給出,如 strtok 子例程。當搜索到字符串末尾時(shí),函數返回 \0。函數可能如下實(shí)現:


清單 5. getLowercaseChar 的不可重入版本
				
char getLowercaseChar(char *str)
{
static char *buffer;
static int index;
char c = '\0';
/* stores the working string on first call only */
if (string != NULL) {
buffer = str;
index = 0;
}
/* searches a lowercase character */
while(c=buff[index]){
if(islower(c))
{
index++;
break;
}
index++;
}
return c;
}

這個(gè)函數是不可重入的,因為它存儲變量的狀態(tài)。為了讓它可重入,靜態(tài)數據,即 index, 需要由調用者來(lái)維護。此函數的可重入版本可能類(lèi)似如下實(shí)現:


清單 6. getLowercaseChar 的可重入版本
				
char getLowercaseChar_r(char *str, int *pIndex)
{
char c = '\0';
/* no initialization - the caller should have done it */
/* searches a lowercase character */
while(c=buff[*pIndex]){
if(islower(c))
{
(*pIndex)++; break;
}
(*pIndex)++;
}
return c;
}

經(jīng)驗 3

在大部分系統中,mallocfree 都不是可重入的, 因為它們使用靜態(tài)數據結構來(lái)記錄哪些內存塊是空閑的。實(shí)際上,任何分配或釋放內存的庫函數都是不可重入的。這也包括分配空間存儲結果的函數。

避免在處理器分配內存的最好方法是,為信號處理器預先分配要使用的內存。避免在處理器中釋放內存的最好方法是, 標記或記錄將要釋放的對象,讓程序不間斷地檢查是否有等待被釋放的內存。不過(guò)這必須要小心進(jìn)行,因為將一個(gè)對象 添加到一個(gè)鏈并不是原子操作,如果它被另一個(gè)做同樣動(dòng)作的信號處理器打斷,那么就會(huì )“丟失”一個(gè)對象。不過(guò), 如果您知道當信號可能到達時(shí),程序不可能使用處理器那個(gè)時(shí)刻所使用的流,那么就是安全的。如果程序使用的是某些其他流,那么也不會(huì )有任何問(wèn)題。

經(jīng)驗 4

為了編寫(xiě)沒(méi)有 bug 的代碼,要特別小心處理進(jìn)程范圍內的全局變量,如 errnoh_errno。 考慮下面的代碼:


清單 7. errno 的危險用法
				
if (close(fd) < 0) {
fprintf(stderr, "Error in close, errno: %d", errno);
exit(1);
}

假定信號在 close 系統調用設置 errno 變量 到其返回之前這一極小的時(shí)間片段內生成。這個(gè)生成的信號可能會(huì )改變 errno 的值,程序的行為會(huì )無(wú)法預計。

如下,在信號處理器內保存和恢復 errno 的值,可以解決這一問(wèn)題:


清單 8. 保存和恢復 errno 的值
				
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;
}

經(jīng)驗 5

如果底層的函數處于關(guān)鍵部分,并且生成并處理信號,那么這可能會(huì )導致函數不可重入。通過(guò)使用信號設置和 信號掩碼,代碼的關(guān)鍵區域可以被保護起來(lái)不受一組特定信號的影響,如下:

  1. 保存當前信號設置。
  2. 用不必要的信號屏蔽信號設置。
  3. 使代碼的關(guān)鍵部分完成其工作。
  4. 最后,重置信號設置。

下面是此方法的概述:


清單 9. 使用信號設置和信號掩碼
				
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, SIGALRM);
/* 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, &oldmask, NULL);
/* Continue with other parts of the code */

忽略 sigsuspend(&zeromask); 可能會(huì )引發(fā)問(wèn)題。從消除信號阻塞到進(jìn)程執行下一個(gè) 指令之間,必然會(huì )有時(shí)鐘周期間隙,任何在此時(shí)間窗口發(fā)生的信號都會(huì )丟掉。函數調用 sigsuspend 通過(guò)重置信號掩碼并使進(jìn)程休眠一個(gè)單一的原子操作來(lái)解決這一問(wèn)題。如果您能確保在此時(shí)間窗口中生成的信號不會(huì )有任何 負面影響,那么您可以忽略 sigsuspend 并直接重新設置信號。


在編譯器層次處理可重用性

我將提出一個(gè)在編譯器層次處理可重入函數的模型??梢詾楦呒壵Z(yǔ)言引入一個(gè)新的關(guān)鍵字: reentrant,函數可以被指定一個(gè) reentrant 標識符,以此確保函數可重入,比如:

reentrant int foo();

此指示符告知編譯器要專(zhuān)門(mén)處理那個(gè)特殊的函數。編譯器可以將這個(gè)指示符存儲在它的符號表中,并在中間代碼生成階段 使用這個(gè)指示符。為達到此目的,編譯器的前端設計需要有一些改變。此可重入指示符遵循這些準則:

  1. 不為連續的調用持有靜態(tài)數據。
  2. 通過(guò)制作全局數據的本地拷貝來(lái)保護全局數據。
  3. 絕對不調用不可重入的函數。
  4. 不返回對靜態(tài)數據的引用,所有數據都由函數的調用者提供。

準則 1 可以通過(guò)類(lèi)型檢查得到保證,如果在函數中有任何靜態(tài)存儲聲明,則拋出錯誤消息。這可以在編譯的語(yǔ)法分析 階段完成。

準則 2,全局數據的保護可以通過(guò)兩種方式得到保證?;镜姆椒ㄊ?,如果函數修改全局數據,則拋出一個(gè)錯誤 消息。一種更為復雜的技術(shù)是以全局數據不被破壞的方式生成中間代碼??梢栽诰幾g器層實(shí)現類(lèi)似于前面經(jīng)驗 4 的方法。 在進(jìn)入函數時(shí),編譯器可以使用編譯器生成的臨時(shí)名稱(chēng)存儲將要被操作的全局數據,然后在退出函數時(shí)恢復那些數據。 使用編譯器生成的臨時(shí)名稱(chēng)存儲數據對編譯器來(lái)說(shuō)是常用的方法。

確保準則 3 得到滿(mǎn)足,要求編譯器預先知道所有可重入函數,包括應用程序所使用的程序庫。這些關(guān)于函數的 附加信息可以存儲在符號表中。

最后,準則 4 已經(jīng)得到了準則 2 的保證。如果函數沒(méi)有靜態(tài)數據,那么也就不存在返回靜態(tài)數據的引用的問(wèn)題。

提出的這個(gè)模型將簡(jiǎn)化程序員遵循可重入函數準則的工作,而且使用此模型可以預防代碼出現無(wú)意的可重入性 bug。


參考資料

關(guān)于作者

Dipak 為分布式文件系統(Distributed File System,DFS)提供 Level 3 支持。他的工作包括,對內存轉儲和崩潰 進(jìn)行內核級和用戶(hù)級的調試,以及修復 AIX 和 Solaris 平臺上所報告的 bug。通過(guò) dipakjha@in.ibm.com 與 Dipak 聯(lián)系。

本站僅提供存儲服務(wù),所有內容均由用戶(hù)發(fā)布,如發(fā)現有害或侵權內容,請點(diǎn)擊舉報。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
可重入、異步信號安全和線(xiàn)程安全(一)
線(xiàn)程安全詳解及相關(guān)實(shí)用技巧
可重入函數與線(xiàn)程安全函數
多線(xiàn)程和多進(jìn)程的區別
函數的'可重入'與'線(xiàn)程安全'很重要!
線(xiàn)程安全和可重入
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久