Linux 多线程编程 与 信号处理


      原来在main函数中把几个子线程启动后就睡10分钟后开始清理子线程后退出。现在想改成子线程启动后主线程进入无限睡眠,直到收到SIGTERM或SIGINT。主程序如下:

其他头文件
#include <signal.h> //信号处理所需要的头文件
int main(int argc, char * argv[]){
  //其他所需要的变量声明  
  sigset_t sig_set,sig_pending;


  // 设置信号阻塞
  sigemptyset(&sig_set);
  sigaddset(&sig_set,SIGTERM);
  sigaddset(&sig_set,SIGINT);
  sigprocmask(SIG_BLOCK,&sig_set,NULL);


  启动几个子线程  
  ...........

  // 设置信号阻塞
  sigemptyset(&sig_set);
  sigaddset(&sig_set,SIGTERM);
  sigaddset(&sig_set,SIGINT);
  sigprocmask(SIG_BLOCK,&sig_set,NULL);
 
  //主线程进入睡眠,等待信号到达后跳出睡眠  
  while(1){
          sigpending(&sig_pending);
          if(sigismember(&sig_pending, SIGTERM)||
                    sigismember(&sig_pending,SIGINT)){
                break;
          }
          sleep(2);
  }

  //子线程退出情理
  ................
  return 0;

}

      程序运行后发现 当按下Ctrl+C后程序没有出现子线程退出时的信息而是立刻退出,非常奇怪。
仔细分析了一下,发现问题在于忽略了Linux下的多线程模型的特点。

      Linux下的线程实质上是轻量级进程(light weighted process), 线程生成时会生成对应的进程控制结构,只是该结构与父线程的进程控制结构共享了同一个进程内存空间。 同时新线程的进程控制结构将从父线程(进程)处复制得到同样的进程信息,如打开文件列表和信号阻塞掩码等

      由于我们是在子线程生成之后修改了信号阻塞掩码,此刻子线程使用的是主线程原有的进程信息,因此子线程仍然会对SIGINT和SIGTERM信号进行反应 因此当我们用Ctrl+C发出了SIGINT信号的时候,主进程不处理该信号,而子进程(线程)会进行默认处理,即退出
     
      子进程退出的同时会向父进程(线程)发送SIGCHLD信号,表示子进程退出,由于该信号没有被阻塞,因此会导致主进程(线程)也立刻退出,出现了前述的运行情况

因而该问题的一个解决方法是:
     1:在子线程生成前进行信号设置, 
     2:或在子线程内部进行信号设置。  
     
      由于子线程是往往是一个事务处理函数,因此我建议在简单的情况下采用前者,如果需要处理的信号比较复杂,那就必须使用后一种方法来处理。这样,以上的程序逻辑改为如下就可以了:

#include <signal.h> 
      //信号处理所需要的头文件
int main(int argc, char * argv[]){
  //其他所需要的变量声明  
  sigset_t sig_set,sig_pending;

  启动几个子线程  
  ...........
 
  //主线程进入睡眠,等待信号到达后跳出睡眠  
  while(1){
          sigpending(&sig_pending);
          if(sigismember(&sig_pending, SIGTERM)||
                    sigismember(&sig_pending,SIGINT)){
                break;
          }
          sleep(2);
  }

  //子线程退出情理
  ................
  return 0;
}

#################################

多线程下安全的信号处理

      最近项目上出现了一个小问题,今天把这个问题修补了一下。为此在博客上也做个记录。

      问题是有关端口绑定引起的,原来我们在做测试的时候,一般用ctrl+c退出server程序,这样退出有一个问题。 上次bind的端口仍然被占用着,如果马上重新执行server程序,那么会出现"bind listening sockey falure!"的情况,这样的话要等待一会儿,才能重新运行程序 。为此我修改了一下代码,在里面添加了一个专门用来捕获SIGINT信号的线程,用这个线程来处理信号,回收资源。具体的代码如下:

v_int_32 start()
{
    v_int_32 ret = 0;
    v_int_32 created_threads = 0;
   
    sigset_t bset, oset;
    sigemptyset(&bset);
    sigaddset(&bset, SIGINT);//建立一个信号集,将SIGINT添加进去
    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0) {//设置父线程的信号掩码,子线程会继承这信号掩码
         printf("set thread signal mask failrue!/n");
    }
    ret = pthread_create(&receive_thread_, NULL, receive_packet, (void*)analysis_queue_);
     if (ret != 0)
    {
          //error, destroy tc list lock
          pthread_rwlock_destroy(&tc_list_lock);
          pthread_rwlock_destroy(&ip_list_lock);
        return ERROR_THREAD_CREATE;
    }
    usleep(INIT_TIME);
    for (v_int_32 j=0; j<ANALYSE_THREAD_MAX; j++)
    {
        ret = pthread_create(&analyze_thread_[j], NULL, analysis_packet, (void*)analysis_queue_[j]);
        if (ret != 0)
        {
            break;
        }
        created_threads++;
        usleep(INIT_TIME);          //To ensure the binding to cpu is finished
    }
    ret = pthread_create(&signal_handle_thread_, NULL, handle_signal, NULL);
    if (ret != 0)
         {
          //error, destroy tc list lock
          pthread_rwlock_destroy(&tc_list_lock);
          pthread_rwlock_destroy(&ip_list_lock);
        return ERROR_THREAD_CREATE;
    }
    ret = pthread_create(&monitor_thread_, NULL, receive_monitor_command, NULL); 
     if (ret != 0)
    {
          //error, destroy tc list lock
          pthread_rwlock_destroy(&tc_list_lock);
          pthread_rwlock_destroy(&ip_list_lock);
        return ERROR_THREAD_CREATE;
    }
    
     usleep(INIT_TIME);
   
    if(created_threads != ANALYSE_THREAD_MAX)
     {
          //error, destroy tc list lock
          pthread_rwlock_destroy(&tc_list_lock);
          pthread_rwlock_destroy(&ip_list_lock);
         return ERROR_THREAD_CREATE;
    }
   
   
    pthread_join(receive_thread_, NULL);
    printf("receive thread %d canceled!/n/n", receive_thread_);
   
    for (v_int_32 k=0; k<ANALYSE_THREAD_MAX; k++)
    {
         if(analyze_thread_[k] != 0)
          {
              pthread_join(analyze_thread_[k], NULL);
               printf("analyse thread %d canceled!/n/n", analyze_thread_[k]);
          }
    }
   
   
    pthread_join(monitor_thread_, NULL);
    printf("monitor thread %d canceled!/n/n", monitor_thread_);
         
    pthread_join(signal_handle_thread_, NULL);
    printf("signal  thread %d canceled!/n/n", signal_handle_thread_);
    return NO_ERROR;
}


      上面这个是主函数,它创建了1个receive_thread_, MAX个analyse_thread_, 1个signal_handle_thread_和1个monitor_thread_,其中monitor_thread_在接受到一个连接的时候还会创建一个send_to_monitor_thread_。

      之所以要在父线程创建子线程之前,将SIGINT信号加入阻塞信号集,是因为子线程会继承父线程的信号掩码,即对于子线程来说,SIGINT信号也在自己的阻塞信号集之中当阻塞信号产生的时候,如果线程对该信号的动作是默认动作或者捕获信号,那么这个阻塞信号将会被挂起,知道线程解开信号的阻塞,或者改变动作为忽略。 

     对于我们这个例子来说,当所有的线程接受到SIGINT信号的时候,除了signal_handle_thread_线程之外的所有线程将继续执行。signal_thread等到SIGINT信号的时候,将会取消掉其他几个子线程,并且回收资源。signal_thread_是用来捕获SIGINT信号的线程,它的工作代码如下:


void* DpiPacketProcesser::handle_signal(void*)
{
     sigset_t waitset, oset;
     int sig;
    
     sigemptyset(&waitset);
     sigaddset(&waitset, SIGINT);      //将SIGINT信号加入等待的信号集当中
      sigwait(&waitset, &sig);            //在此阻塞,直到SIGINT信号到达
    
     if (send_to_monitor_thread_ != 0) {//如果有这个线程,则终止它
          //cancel send thread
          printf("/ncancel send thread!/n");
          pthread_cancel(send_to_monitor_thread_);

          printf("wait for send thread!/n");
          pthread_join(send_to_monitor_thread_, NULL);
          printf("send thread %d canceled!/n/n", send_to_monitor_thread_);
     }
    
     //cancel receive thread
     printf("cancel receive thread!/n");
     pthread_cancel(receive_thread_);//取消reveive_thread_
     printf("wait for receive thread!/n/n");
     
     //cancel analyse thread
     for (v_int_32 k=0; k<ANALYSE_THREAD_MAX; k++)
     {
         if(analyze_thread_[k] != 0)
          {
               printf("cancel the analyse thread!/n");
              pthread_cancel(analyze_thread_[k]);//取消analyse_thread_
              printf("wait for analyse thread!/n/n");
          }
     }
 
     //cancel monitor thread
     printf("cancel monitor thread!/n");
     pthread_cancel(monitor_thread_);//取消monitor_thread_
     printf("wait for monitor thread!/n/n");
    
     //close listening socket
     close(listen_sock);//关闭监听套接口
     printf("listen socket closed!/n");
     
     //destroy tc list lock
     pthread_rwlock_destroy(&tc_list_lock);
     printf("tc_list_lock destroyed!/n");
       
     //destroy ip list lock
     pthread_rwlock_destroy(&ip_list_lock);    
     printf("tc_list_lock destroyed!/n/n");
 
     return NULL;
}


############################################


linux多线程环境的信号处理 


      传统的信号处理方式是建立某个特定信号的信号处理程序,比如对SIGALRM的处理,可以通过signal(SIGALRM,alarm_handler)进行处理。其中alarm_handler的原型为:void func(void *);

      但是需要注意的是这种信号处理方式是异步的,不知道什么时候会发生,而且在信号处理函数里一般不建议执行那些耗时比较久的程序,另外,处理函数也不是可重入的,为安全起见,在函数里面只能调用少数一些可重入(reentrant)的函数

      为什么呢?首先,在信号处理中有可能又引起新的中断,如果处理函数中调用了某个需要同步的函数,而在调用过程中又发生了新的中断,新的中断处理或许也要取得资源锁,那么很容易造成死锁。总而言之,处理函数可能是多线程安全的,但不是可重入的

      在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知

      为了解决这种问题,可以使用同步处理异步信号的方法,由主线程派生一个新线程用于信号管理。posix标准中的sigwait方法用于监听阻塞信号集合,如果发现其中的某些信号达到,就可以调用自定义的信号处理方法。

例如下面的程序:
void sig_handler(int signum);
void *sig_handler_thread(void *arg);
void *worker_thread(void *arg);

int main(int argc,char **argv)
{
    //注意这里也定义了一个传统的信号处理函数,但实际上它并没有被调用到
    signal(SIGALRM,alarmHandler);
    pid_t pid=getpid();
    pthread_t ppid;
   //阻塞信号集合定义,bset为阻塞信号集,oset为旧的信号集
    sigset_t bset,oset;
    sigemptyset(&bset);
   //阻塞sigalrm信号
    sigaddset(&bset,SIGALRM);
  //阻塞sigint信号,不理会它的默认行为(退出),而是使用自己的信号处理函数
    sigaddset(&bset,SIGINT);
  //设置信号集合掩码
  //pthread_sigmask第一个参数是how,指明了处理方式是阻塞。
    if(pthread_sigmask(SIG_BLOCK,&bset,&oset)!=0){
        printf("pthread_sigmask error!\n");
    }
//启动信号处理线程
   pthread_create(&ppid,NULL,sig_handler_thread,NULL);
    alarm(1);
//启动工作线程
    pthread_t worker;
    pthread_create(&worker,NULL,worker_thread,NULL);
    while(1){
        sleep(1);
    }
    return 0;
}

void sig_handler(int signum){
    printf("receive signal:%d\n",signum);
    sleep(1);
//    alarm(1);
    pid_t pid=getpid();
    kill(pid,SIGALRM);

}

void* sig_handler_thread(void *arg){
    //信号处理线程中需要指定关注的信号,并将它们添加到waitset中
   //这样一来,当出现感兴趣的信号时,就会调用sig_handler函数。
   //这里只关注SIGALRM和SIGINT两个函数。
  //注意在主线程中已经指定了阻塞信号掩码集合,而sig_handler_thread是主线程生成的,所以
//它也继承了同样的阻塞信号掩码集合,如果在下面注释了sigaddset(&waitset,SIGINT),则在shell
中Ctrl+C时它将不调用sig_handler()函数。

    sigset_t waitset,oset;
    pthread_detach(pthread_self());
    int rv,sig;
    sigemptyset(&waitset);
    sigaddset(&waitset,SIGALRM);
    sigaddset(&waitset,SIGINT);
    while(1){
        rv=sigwait(&waitset,&sig);
        if(rv!=-1){
            sig_handler(sig);
        }else{
            printf("sigwait return error.%s\n",strerror(errno));
        }
    }
}

void *worker_thread(void *arg){
    pthread_detach(pthread_self());
    printf("i am worker.\n");
    pthread_exit(NULL);
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值