linux用户空间 - 线程的可重入

2021-07-31  本文已影响0人  404Not_Found
# 典型场景分析
  ## 信号打断举例
# 多线程编程的风险
  ## 多线程重入 - 小结
# 不可重入举例与修正
# 异步信号同步化 举例
# 如何在信号处理函数中打印
# 不可重入函数群

典型场景分析:

4根线程.png

线程之间会访问同一块资源,信号也有相同的属性。但是信号是线程内部的
信号打断线程,也有可能会访问这根线程的同一资源。所以也会涉及安全问题。但信号是一根线程内部的,并非线程之间的。

信号打断举例

#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}

加锁其实从理论概念上就是错误的,因为锁是针对线程之间的,而信号处理函数是线程内部的。

多线程编程的风险

线程不安全的,通常不可重入
可重入的通常线程安全(极少数除外)

各种举例见如下网站:
https://deadbeef.me/2017/09/reentrant-threadsafe

其实就是两种打断,不会造成冲突。
信号打断:一次性执行完成,进去后执行完再出来。信号函数牛逼呀。
线程打断:竞争关系,是互相打的。

多线程可重入 - 小结

可重入函数满足两条件:

  1. 函数是线程安全的
  2. 函数是可中断的,对于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);
}


  1. 所有线程屏蔽掉 SIGTMIN 和 SIGUSR1
  2. 创建一个线程,专门同步的等待这两个信号
    sigwaitinfo() 同步的等待信号的到来
  3. 创建子线程
  4. 主线程 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);
上一篇下一篇

猜你喜欢

热点阅读