admin 管理员组

文章数量: 1184232


2024年3月13日发(作者:using namespace srd)

Linux 多线程编程问题

1 重入问题

传统的UNIX没有太多考虑线程问题,库函数里过多使用了全局和静态数据,导致严重

的线程重入问题。

1.1 –D_REENTRANT /-pthread和errno的重入问题。

所先UNIX的系统调用被设计为出错返回-1,把错误码放在errno中(更简单而直

接的方法应该是程序直接返回错误码,或者通过几个参数指针来返回)。由于线程

共享所有的数据区,而errno是一个全局的变量,这里产生了最糟糕的线程重入问

题。比如:

do {

bytes = recv(netfd, recvbuf, buflen, 0);

} while (bytes != -1 && errno != EINTR);

在上面的处理recv被信号打断的程序里。如果这时连接被关闭,此时errno应该不

等于EINTR,如果别的线程正好设置errno为EINTR,这时程序就可能进入死循环。

其它的错误码处理也可能进入不可预测的分支。

在线程需求刚开始时,很多方面技术和标准(TLS)还不够成熟,所以在 为 了

解决这个重入问题引入了一个解决方案,把errno定义为一个宏:

extern int *__errno_location (void);

#define errno (*__errno_location())

在上面的方案里,访问errno之前先调用__errno_location()函数,线程库提供这个

函数,不同线程返回各自errno的地址,从而解决这个重入问题。在编译时加

-D_REENTRANT就是启用上面的宏,避免errno重入。另外 -D_REENTRANT

还影响一些stdio的函数。在较高版本的gcc里,有很多嵌入函数的优化,比如把

printf(“Hellon”);

优化为

puts(“hellon”);

之类的,有些优化在多线程下有问题。所以gcc引入了 –pthread 参数,这个

参数出了-D_REENTRANT外,还校正一些针对多线程的优化。

因为宏是编译时确定的,所以没有加-D_REENTRANT编译的程序和库都有errno

重入问题,原则上都不能在线程环境下使用。不过在一般实现上主线程是直接使用

全局errno变量的,也就是 __errno_location()返回值为全局&errno,所以那些没加

-D_REENTRANT编译的库可以在主线程里使用。这里仅限于主线程,有其它且只

有一个固定子线程使用也不行,因为子线程使用的errno地址不是全局errno变量

地址。

对于一个纯算法的库,不涉及到errno和stdio等等,有时不加_REENTRANT也是

安全的,比如一个纯粹的加密/解谜函数库。比较简单的判断一个库是否有errno问

题是看看这个库是使用了errno还是__errno_location():

readelf -s | grep errno

另外一个和errno类似的变量是DNS解析里用到的h_errno变量,这个变量的重入

和处理与errno一样。这个h_errno用于gethostbyXX这个系列的函数。

1.2 库函数重入

早期很多unix函数设计成返回静态buffer。这些函数都是不能重入的。识别这

些函数有几个简单的规则:

1.2.1 stdio函数是可以重入的。这是因为stdio函数入口都会调用flockfile()锁定

FILE。另外stdio也提供不锁定(非重入)的函数,这些函数以_unlock结尾,

具体参见man unlocked_stdio。利用这些特性可以做到多个stdio的互斥操作。

如:

flockfile(fp);

fwrite_unlocked(rec1, reclen1, 1, fp);

fwrite_unlocked(rec2, reclen2, 1, fp);

funlockfile(fp);

1.2.2 返回动态分配数据的函数,这些一般是可以重入的。这些函数的特点是返回

的指针需要显式释放,用free或者配对的释放函数。如:

getaddrinfo /freeaddrinfo

malloc/strdup/calloc/free

fopen/fdopen/popen/fclose

get_current_dir_name/free

asprintf/vasprintf/free

getline/getdelim/free

regcomp/regfree

1.2.3 函数返回一个和输入参数无关的数据,而且不需要free的大部分情况下是不

可重入的。如gmtime, ntoa, gethostbyname…

1.2.4 函数依赖一个全局数据,在多次或者多个函数间维护状态的函数是不可重入

的。如getpwent, rand…

1.2.5 带有_r变体的函数都是不可重入的。这些函数大部分是上面两类的。这些变

体函数是可重入的代替版本。可以用下面命令查看glibc有多少这种函数:

readelf -s /lib/.6 | grep _r@

这些函数名有很大一部分是

getXXbyYY, getXXid, getXXent, getXXnam

1.2.6 rand,lrand48系列随机数不可重入的原因在于这些函数使用一个全局的状

态,并且都有相应的_r变体。重入这些非线程安全的函数不会有稳定性问题,

不过会导致随机数不随机(可预测)。在要求比较严格的随机数应用里,建

议用/dev/random和/dev/urandom,这两个设备的不同在于前者读出的数据理

论上是绝对随机的,在系统无法提供足够随机数据时读会阻塞。后者只是提

供尽量随即的数据,随机度不够时用算法生成伪随机数来代替,所以不会阻

塞。

1.2.7 不可重入函数处理。对大部分不可重入函数可以使用对应的_r变体。有些函

数可能没有对应_r变体,可以选用类似功能的函数替换。如:

inet_ntoa  inet_ntop


本文标签: 函数 返回 数据 使用 重入