linux用户空间 - 线程的可重入
- 作者: 雪山肥鱼
- 时间:20210727 13:27
- 目的:从信号,多线程角度谈线程的可重入性
# 典型场景分析
## 信号打断举例
# 多线程编程的风险
## 多线程重入 - 小结
# 不可重入举例与修正
# 异步信号同步化 举例
# 如何在信号处理函数中打印
# 不可重入函数群
典型场景分析:
4根线程.png- 多CPU 下
4根 线程在4个CPU 中, T1跑的同时 T2 也在跑。线程之间互相倒腾来 倒腾去,互相抢CPU的。 - 单核
由于是操作系统抢占调度或者时间片调度,T1跑的时候 T2 就进来的,T2跑的时候T3 就进来了。不会是同步的。要么基于优先级在抢,要么基于时间片在轮。这些线程都有可能访问同一块资源。
线程之间会访问同一块资源,信号也有相同的属性。但是信号是线程内部的
信号打断线程,也有可能会访问这根线程的同一资源。所以也会涉及安全问题。但信号是一根线程内部的,并非线程之间的。
信号打断举例
#include <stdio.h>
struct tow_long {long a, b; } data;
void singal_handler(int signum) {
printf("%d\n, %d\n", data.a, data..b);
alarm(1);
}
int main(void) {
static struct two_long zero = {0, 0}, ones = {1,1};
signal(SIGALRM, signal_handler);
data = zeros;
alarm(1);
while(1);
{data = zeros; data = ones;}
}
交替性给data赋值。 理论上每次都是 0, 0, 或者每次都是 1,1
但是实际上是交替的:
交替.png
信号不是线程,是在打断这个线程的时候跑的,是属于这个线程的。
有个想法就是加锁
void signal_handler(int signum) {
lock;
printf();
unlock
}
{pthread_mutex_lock data = zeros; unlock; lock data = ones; unlock}
加锁其实从理论概念上就是错误的,因为锁是针对线程之间的,而信号处理函数是线程内部的。
多线程编程的风险
-
线程风险
malloc
free
printf -
不访问全局资源的函数
-
访问全局资源但是加上 mutex的函数
在使用这些函数的时候,我们根本不用加锁,因为内部已经帮我们做好了。
malloc 和 free 面对的场景其实很复杂,
多线程申请内存的时候,面对的堆其实只有一个,这是有风险的。但malloc 和 free 不会出问题。 -
信号风险
- 不访问全局资源的函数
- 访问全局资源但是保存和回复函数
- 不能用mutex等
但是 malloc 和 free 并不是信号安全的。因为信号进到线程后,也去malloc或者free这块堆,但是对被操作的堆来说,是无法用锁去保护的,因为锁是针对线程之间的。
-
可重入的定义应从两方面出发
- 信号安全定义
对于linux来说 中断就是信号,中断的人依然可能调同一个函数, 从信号函数出来后,数据的统一性。强调的是异步安全。 - 线程安全定义
几个线程 操作同一个函数。
- 信号安全定义
线程不安全的,通常不可重入
可重入的通常线程安全(极少数除外)
各种举例见如下网站:
https://deadbeef.me/2017/09/reentrant-threadsafe
其实就是两种打断,不会造成冲突。
信号打断:一次性执行完成,进去后执行完再出来。信号函数牛逼呀。
线程打断:竞争关系,是互相打的。
多线程可重入 - 小结
可重入函数满足两条件:
- 函数是线程安全的
- 函数是可中断的,对于linux而言,异步的信号,执行了中断处理例程后,再回过头来继续执行函数,结果仍然正确。
函数分类:
图片.png
举例 与 修正
一个简单的大小写转换的代码:
char * toupper(char * lower) {
static char buffer[1000];
lower -> buffer
return buffer
}
T1: hello world
T2: world hello
如果这个时候,有两个线程操作这个函数。很明显会对static 数据 进行破坏。这个函数定是线程不安全的函数。
可怕之处:99% 都是正确的。则一会正常一会不正常。
#include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <sys/type.h>
char * strtoupper(char * string) {
static char buffer[1000];
int index;
for(index = 0; string[index]; index++)
buffer[index] = touppfer(string[index]);
buffer[index] = 0;
return buffer;
}
void * thread_fun(void * param) {
while(1) {
unsleep(100);
printf("%s\n", strtoupper((char*)param));
}
}
int main(int argc ,char ** argv)
{
pthread_t tid1, tid2;
int ret;
printf("main pid:%d, tid:lu\n", getpid(), pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, "hello world");
if(ret == -1) {
perror("can not create new thread");
return 1;
}
ret = pthread_create(&tid2, NULL, thread_fun, "world hello") ;
if(ret == -1) {
perror("can not create new thread");
return 1;
}
if(pthread_join(tid1, NULL) != 0) {
perror("call pthread_join fail")
return 1;
}
if(pthread_join(tid2, NULL) != 0) {
perror("call pthread_join fail")
return 1;
}
return 0;
}
查看真实输出:
//搜索非HELLO 的
./a.out |grep -v HELLO
结果明显会有冲突。可能会到处 HELLWO WELLO 等情况
- 如果一个函数用到了全局或者静态变量,那么它不是线程安全的,也不是可重入的。
- 改进:访问全局变量或者静态变量时,使用互斥量或者信号量等方式加锁,则时线程安全的。
- 但这种改进方式,仅是线程之间安全的,仍然是不可重入的,因为通常加锁方式是针对不同线程访问,而对统一线程可能依旧出现问题。(信号打断)
- 如果将函数中的全局或者静态变量去掉,改成函数参数等其他形式,则有可能使函数编程线程安全,又可重入。
信号函数中不要调用 malloc printf free 这些函数。因为都是 信号层面不可重入的函数。否则堆会坏,打印会乱。
修改如下:
char * strtoupper(char * string, char * buffer) {
int index;
for(index = 0; string[index];index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0;
return buffer;
}
void * thread_fun(void * param) {
while(1) {
char buf[1000];
uslepp(100);
strtoupper((char*param, buff);
printf("%s\n", buff);
}
}
两个函数都调用的 thread_fun, 但是每个线程申请了自己的栈。所以是安全的
异步信号同化 举例
将可重入问题弱化成线程安全问题,因为信号是突然跳进来的东东,是异步的。即 把异步的东西 同步 化。
增加 signal manager 线程来同步等信号。而不是让信号异步的跳进来。
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
// used to show which thread the signal is handled in.
if (signum == SIGUSR1) {
printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
//SIGRTMIN should not be considered constants from userland,
//there is compile error when use switch case
} else if (signum == SIGRTMIN) {
printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}
void* worker_thread()
{
pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {
printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}
void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();
pthread_detach(ppid);
sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);
while (1) {
rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {
printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}
int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;
// Block SIGRTMIN and SIGUSR1 which will be handled in
//dedicated thread sigmgr_thread()
// Newly created threads will inherit the pthread mask from its creator
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
//A new thread inherits a copy of its creator's signal mask.
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
// Create the dedicated thread sigmgr_thread() which will handle
// SIGUSR1 and SIGRTMIN synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);
// Create 5 worker threads, which will inherit the thread mask of
// the creator main thread
for (i = 0; i < 5; i++) {
pthread_create(&ppid, NULL, worker_thread, NULL);
}
// send out 50 SIGUSR1 and SIGRTMIN signals
for (i = 0; i < 50; i++) {
kill(pid, SIGUSR1);
printf("main thread, send SIGUSR1 No. %d\n", i);
kill(pid, SIGRTMIN);
printf("main thread, send SIGRTMIN No. %d\n", i);
sleep(10);
}
exit (0);
}
- 所有线程屏蔽掉 SIGTMIN 和 SIGUSR1
- 创建一个线程,专门同步的等待这两个信号
sigwaitinfo() 同步的等待信号的到来 - 创建子线程
- 主线程 kill 去给进程发信号
此时某根线程在运行
每隔10s输出一次结果:
同步处理异步信号.png
Tid: 385738496 专门处理信号的线程
如何在信号处理函数中打印
signal_handler() {
//printf ->
write
}
printf 直接 调用 write
printf 是线程安全的,但并非信号安全。
printf内部是有锁的,
信号处理函数内部是不能有锁的
因为自己线程如果拿了锁,又被信号中断,在printf里又拿了个锁,很容易造成死锁。 不仅仅是printf 自身打印出问题。程序可能会hang住。
可以采用wirte, 直接用系统调用。
不可重入函数:
图片.png举例:
char * asctime(const struct tm *tm);
char * asctime_r(const struct tm * tm, char* buf);
- 不可重入版本:转好的数据在哪里呢?一定是存在某个全局变量中
- 可重入版本:传入一个参数,结果保存在这个buffer 里面即可。