首先说一下什么是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 512 10 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 512 20 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 else 37 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,这就是我的分析了, 后面那个线程锁那里我感觉还不是太了解,讲的不是太好,还望见谅,如果哪位关于这部分有更深的了解,还望不吝赐教!