首先說一下什么是getpw系列函數,它主要是指這些函數:
這些函數根據一個用戶名(getpwnam和getpwnam_r兩個函數)或者一個用戶ID(getpwuid和getpwuid_r)來獲取這個用戶在/etc/passwd中相應的條目信息,并把這些信息存放在一個struct passwd的結構體里面,然后再把這個結構體的指針返回。問題就出在這個存儲用戶信息的結構體上面,它是由getpw函數在程序中自定義的一塊靜態存儲區,而且每調用一次getpw函數,這個靜態存儲區就會被重寫一次。比如下面這段代碼:
1 #include<errno.h> 2 #include<pwd.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<stdarg.h> 6 #include<stdio.h> 7 #include<sys/types.h> 8 9 #define BUFSIZE 51210 11 void err_exit(char *fmt,...);12 13 int main(int argc,char *argv[])14 {15 16 struct passwd *mistr;17 struct passwd *rootstr;18 19 if(NULL == (mistr=getpwnam("michael")))20 err_exit("[getpwnam]<mi>:");21 if(NULL == (rootstr=getpwnam("root")))22 err_exit("[getpwnam]<root>:");23 24 PRintf("name = %s/n",mistr->pw_name);25 printf("name = %s/n",rootstr->pw_name);26 27 return 0;28 }
我想要獲取michael用戶和root用戶的信息,然后我很異想天開得創建了兩個指針,一個指向michael用戶的信息,一個指向root用戶的信息,以為這樣就能分別保存兩個用戶的信息了,然后發現程序的運行結果如下圖:
發現兩次打印的信息,都是root,說明后面調用的getpwnam函數重寫了struct passwd這個結構體,把前面調用getpwnam函數時寫入的michael用戶的信息給覆蓋了。
這個就是getpwnam_r和getpwuid_r函數出現的原因,為了防止多線程程序因為競爭而產生錯誤。關于這兩個函數getpwnam的文檔中是這樣描述的:
這兩個函數可以將用戶信息存放在用戶定義的struct passwd結構體中,而不一定非得要放在函數定義的,可能被重寫的靜態區域中,這樣多線程程序運行的時候就可以避免競爭了。
接下來再來說我的一個新的發現,就是getpw函數調用的時候是有鎖的,也就是說同一個進程中,如果一個getpw函數正在運行,另一個getpw函數是無法運行的。
這個發現是我在運行《APUE》的程序清單10-2的時候發現的,《APUE》上面的代碼如下(不是完全相同,自己重寫了一下^_^):
1 /** 2 * 可重入函數的概念就是可以在被調用了一半的時候被打斷,然后再來從新調用,這里的getpwnam就是一個不可重入函數 3 * ,因為getpwnam函數會修改一個靜態變量來保存讀出的用戶信息,如果調用了一半再被重新調用,則有可能會讓原來的 4 * 信息被覆蓋掉 5 * 6 * 現在getpwname函數可能有鎖,信號處理函數再來調用這個函數會卡死 7 */ 8 9 #include<errno.h>10 #include<pwd.h>11 #include<signal.h>12 #include<string.h>13 #include<stdlib.h>14 #include<stdarg.h>15 #include<stdio.h>16 #include<sys/types.h>17 #include<unistd.h>18 19 #define BUFSIZE 51220 21 void err_exit(char *fmt,...);22 int err_dump(char *fmt,...);23 int err_ret(char *fmt,...);24 25 /**26 * 本函數用來處理SIGALRM信號27 */28 void sig_alrm(int signo)29 {30 struct passwd *rootstr;31 32 printf("In signal SIGALRM handler/n");33 34 if(NULL == (rootstr=getpwnam("root")))35 err_exit("[getpwnam]<root>:");36 else37 printf("pw_name = %s/n",rootstr->pw_name);38 39 alarm(1);40 }41 42 int main(int argc,char *argv[])43 {44 45 struct passwd *mistr;46 signal(SIGALRM,sig_alrm);47 48 /**49 * alarm函數用來在1秒鐘之后發送一個SIGALRM信號給本程序50 */51 alarm(1);52 53 for(;;)54 {55 if(NULL == (mistr=getpwnam("michael")))56 err_exit("[getpwnam]<mi>:");57 if(strcmp(mistr->pw_name,"michael"))58 printf("結果錯誤,讀出的用戶名是:%s/n",mistr->pw_name);59 }60 61 return 0;62 }
這個程序的流程是主函數調用getpwname獲取michael用戶的信息,同時通過alarm函數給自己發送SIGALRM信號,在SIGALRM的信號處理函數中,我們再來調用getpwnam函數獲取root用戶的信息,作者想要的結果是主函數不斷讀取michael用戶的信息,但是SIGALRM信號處理函數獲取了root用戶的信息,所以最后主函數最后打印就有可能是root用戶的信息,此時主函數就打印讀取出錯。
但是在程序運行的過程中我的電腦上卻會發生死鎖的情況,運行結果如下圖所示:
到此時程序就卡死了,不會再繼續下去了。
后來我又用gdb把這個程序調試了一下,首先在32行那里增加一個斷點,然后再用si命令一步一步地執行匯編指令,執行的過程如下:
si命令在調試時是會進入函數內部的,所以我在進入getpwnam函數之后又一步一步地深入,最后我發現我的函數卡在了__lll_lock_wait_private()這個地方,函數的調用棧如下:
前4行表示接受到了信號之后的處理過程,最后函數卡在了__lll_lock_wait_private()這個函數這里。我google了一下這個函數,在這里找到了它的實現。這個有點慚愧,看了半天愣是沒看懂那個實現代碼是是什么意思,不過我在那里看到了那個文件是一個線程庫,所以我猜getpwnam函數的執行過程應該是這樣的:
在getpwnam函數內部,具體還是要調用getpwnam_r函數來實現的(從上面函數棧截圖的5~8行可以看出來),在getpwnam函數內部調用getpwnam_r函數的時候,先要上一個線程鎖,調用完成之后,再來解鎖這個線程鎖,這樣來確保getpwnam函數每次只會被一個線程調用。
而在上面的代碼中,主函數中的getpwnam函數沒有解鎖,就被暫停運行了,而信號處理函數(sig_alrm)函數中的getpwnam再來調用的時候,就會等待主函數中的getpwnam解鎖,而主函數又是暫停的,這樣就會產生死鎖了!
OK,這就是我的分析了, 后面那個線程鎖那里我感覺還不是太了解,講的不是太好,還望見諒,如果哪位關于這部分有更深的了解,還望不吝賜教!
新聞熱點
疑難解答