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(“Hello\n”);
优化为
puts(“hello\n”);
之类的,有些优化在多线程下有问题。所以gcc引入了 –pthread 参数,这个 参数出了-D_REENTRANT外,还校正一些针对多线程的优化。
因为宏是编译时确定的,所以没有加-D_REENTRANT编译的程序和库都有errno 重入问题,原则上都不能在线程环境下使用。不过在一般实现上主线程是直接使用 全局errno变量的,也就是 __errno_location()返回值为全局&errno,所以那些没加 -D_REENTRANT编译的库可以在主线程里使用。这里仅限于主线程,有其它且只 有一个固定子线程使用也不行,因为子线程使用的errno地址不是全局errno变量 地址。
对于一个纯算法的库,不涉及到errno和stdio等等,有时不加_REENTRANT也是 安全的,比如一个纯粹的加密/解谜函数库。比较简单的判断一个库是否有errno问 题是看看这个库是使用了errno还是__errno_location():
readelf -s libxxx.so | 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/libc.so.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
ctime strftime, asctime, localtime_r+sprintf
gethostbyname, getservbyname getaddrinfo
1.2.8 用其它代码/逻辑替换不可重入代码
1.2.9 有些库有两个版本,带和不带_r/_mt/th等后缀的,多线程一般用带后缀的版本的库。
1.3 应用程序的线程安全
1.3.1 全局量/共享资源互斥访问
1.3.2 相关数据原子操作
1.3.3 操作顺序
2 互斥逻辑
同步逻辑不仅仅是多线程程序的问题,在多进程环境里也经常使用。同步逻辑有很多种,其中最常用的就是互斥逻辑,也就是锁。由于历史原因,LINUX下产生了好多锁定API,下面列个简单的表格:
Fcntl文件锁 | Flock文件锁 | SYSV semaphore | Mutex | rwlock | |
类别 | 读写锁 | 读写锁 | 信号量 | 互斥锁 | 读写锁 |
对象 | 进程/文件/范围 | 句柄 | 信号值 | 内存 | 内存 |
非竞争开销 | 高 | 高 | 中 | 很低 | 低 |
竞争开销 | 高 | 高 | 中 | 很高/linuxthread 较低/NPTL | 很高/linuxthread 低/NPTL |
资源消耗 | recv函数 句柄 | 句柄 | 信号ID | 24 byte内存 | 32 byte内存 |
进程 | 支持 | 支持 | 支持 | 不支持/linuxthread 支持/NPTL | 不支持/linuxthread 支持/NPTL |
线程 | 支持(2.4) 不支持(2.6) | 支持 | 支持 | 支持 | 支持 |
Crash解锁 | 是 | 是 | 需要UNDO | 否 | 否 |
2.1 Fcntl文件锁
2.1.1 支持偏移量。也就是可以用一个文件模拟许多互斥锁。
2.1.2 进程锁非线程锁。也就是线程之间无法互斥。老的2.4 kernel没有支持这个POSIX标准,所以可以跨线程使用。
2.1.3 相关句柄关闭导致文件解锁。这个锁是按进程+文件定位的,也就是同一进程打开多次文件使用相同的锁定关系。即使只关闭其中一个句柄导致解锁。在2.4 kernel下也有这个问题,任何线程关闭对应文件句柄,不是导致该线程解锁,而是导致所有线程解锁。
2.1.4 逻辑死锁检测。
2.2 Flock文件锁
2.2.1 按句柄锁定
2.2.2 进程的句柄继承
2.3 SYSV semaphore
2.3.1 信号量。
2.3.2 性能比文件锁要好。
2.3.3 可以同时对多个信号量进行复合操作
2.3.4 /proc/sys/kernel/sem: SEMMSL SEMMNS SEMOPM SEMMNI
2.3.4.1 SEMMSL, 每个信号量ID里的最大信号量数
2.3.4.2 SEMMNS, 系统总信量灯数,小于SEMMSL x SEMMNI
2.3.4.3 SEMOPM, 每次semop最大操作个数
2.3.4.4 SEMMNI, 信号量ID数
2.3.5 高版kernel有等待超时机制
#include <unistd.h>
#include <asm/unistd.h>
#ifndef SEMTIMEDOP
#define SEMTIMEDOP 4
#endif
static inline
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout)
{
return syscall(__NR_ipc, SEMTIMEDOP, semid, nsops, 0, sops, timeout);
}
2.4 Mutex和rwlock
2.4.1 在非竞争下性能最好
2.4.2 NPTL使用futex实现,竞争条件下性能也不错。Linuxthread在竞争时由管理线程仲裁,开销较大。
2.4.3 无crash自动解锁机制
2.4.4 有等待超时机制
2.5 内存原子操作
2.5.1 内存原子操作是多CPU系统里最基本的互斥操作。所有的其它逻辑都是建立这之上的。
2.5.2 整数操作,操作书为一个int类型。有些非x86的CPU只支持到24位值。
#include <asm/atomic.h>
atomic_t value;
int v;
v=atomic_read(&value);
atomic_set(&value, v);
atomic_add(v, &value);
atomic_sub(v, &value);
atomic_sub_and_test(v, &value); /*返回结果是否为0*/
atomic_inc(&value);
atomic_dec(&value);
atomic_dec_and_test(&value); /*返回结果是否为0*/
atomic_inc_and_test(&value); /*返回结果是否为0*/
atomic_set_mask(mask, &value);
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论