第10章 线程控制(4)_多线程下的信号

时间:2021-04-09 18:32:02

5. 线程和信号

5.1 多线程中的信号

(1)在Linux的多线程中使用信号机制与在进程中使用信号机制有着根本的区别。在进程环境中,对信号的处理是,先注册信号处理函数,当信号异步发生时,调用处理函数来处理信号。它完全是异步的。因此,信号处理函数里有时要考虑某些函数可重入或被异步信号中断后的处理。

(2)然而,多线程中处理信号的原则却完全不同,它的基本原则是将对信号的异步处理,转换成同步处理,也就是说用一个线程专门的来“同步等待”信号的到来,而其它的线程可以完全不被该信号中断/打断(interrupt)。这样就在相当程度上简化了在多线程环境中对信号的处理。而且可以保证其它的线程不受信号的影响。这样我们对信号就可以完全预测,因为它不再是异步的,而是同步的(我们完全知道信号会在哪个线程中的哪个执行点到来而被处理!)。而同步的编程模式总是比异步的编程模式简单。

5.2 相关的操作函数

(1)sigwait函数

头文件

#include <pthread.h>

函数

int sigwait(const sigset_t* set, int* signop);

功能

等待一个或者多个指定信号发生

返回值

成功返回0,否则返回错误编号

备注

①要确保sigwait的调用线程收到信号,就要屏蔽所有线程(含主线程和sigwait线程),否则该信号会被注册为sigaction的信号处理程序处理。

②在多线程代码中,总是使用sigwait、sigwaitinfo或者sigtimedwait等函数来处理信号。而不是signal或者sigaction等函数。

③sigwait的可以简化信号的处理,允许把异步产生的信号用同步的方式处理。

(2)pthread_sigmask函数

头文件

#include <signal.h>

函数

int pthread_sigmask(int how, const sigset_t* set, sigset_t oset);

功能

线程的信号屏蔽

返回值

成功返回0,否则返回错误编号

备注

①每个线程均有自己的信号屏蔽集(信号掩码),可以使用pthread_sigmask函数来屏蔽某个线程对某些信号的 响应处理,仅留下需要处理该信号的线程来处理指定的信号。实现方式是:利用线程信号屏蔽集的继承关系。

②注意:在主进程中对sigmask进行设置后,主进程创建出来的线程将继承主进程的掩码。

(3)pthread_kill函数

头文件

#include <signal.h>

函数

int pthread_kill(pthread_t thread, int signo);

功能

向指定的线程发送信号()

返回值

成功返回0,否则返回错误编号

备注

①当signo为0时,可以用来检查线程是否存在。

②如果信号的默认处理动作是终止进程,那把信号传递给某个线程仍然会杀掉整个进程。

③注意在多线程中 一般不使用kill函数发送信号,因为kill是对进程发送信号,结果是:正在运行的线程会处理该信号,如果该线程没有注册信号处理函数,那么会导致整个进程退出。

5.3 多线程信号的处理

信号发生者

信号处理者

处理方式

由异常(如程序错误:SIGPIPE、SIGEGV)产生的信号

由产生异常的线程自己处理

同步

由pthread_kill产生的信号

由pthread_kill参数中指定的目标线程处理

同步

外部使用kill命令产生的信号(如SIGINT、SIGHUG等)

会遍历所有线程、直到找到一个不屏蔽该信号的线程处理从最小的线程ID(主线程)开始找起

异步

【编程实验】多线程中的定时器1(本例由主线程处理alarm信号)

//pthread_alarm.c

#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
/*多线程下的alarm信号(本例中alarm信号由主线程处理)*/
void sig_handler(int signo)
{   
    if(signo == SIGALRM){
        printf("timeout: thread(%lx) call sig_handler:%d\n", pthread_self(), signo);
        alarm(2);//周期性定时(否则为一次性)
    }
}

void* th_fn(void* arg)
{
    //在子线程中注册信号处理函数,会作用于所有线程
    //由于alarm是进程资源,系统会遍历所有线程,直到找到一个不屏蔽该信号
    //的线程,并由其处理(一般从最小线程号(主线程)找起)。由于主线程
    //未屏蔽该信号,所以该信号会被主线程(而不是子线程)处理!
    if(signal(SIGALRM, sig_handler) == SIG_ERR){
        perror("signal sigalrm error");
    }

    //在子线程中设置定时器
    alarm(2);
    int i = 1;
    for(; i<=10; i++){
        printf("thread(%lx) i: %d\n", pthread_self(), i);
        sleep(1);
    }
    return NULL;
}

int main(void)
{
    int err = 0;
    pthread_t th;

    //以分离状态启动子线程
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if((err = pthread_create(&th, &attr, th_fn, NULL) != 0)){
        perror("pthread_create error");
    }
    
    int rc = 0;
    while(1){
        printf("main thread(%lx) is running\n", pthread_self());
        rc = sleep(10); //主线程睡眠期间可能会被alarm中断
        if(rc != 0){
            printf("main thread interrupted at %d second\n", 10 - rc);
        }
    }

    printf("main thread ends\n");
}
/*输出结果
 main thread(b77d26c0) is running
 thread(b77d1b70) i: 1
 thread(b77d1b70) i: 2
 timeout: thread(b77d26c0) call sig_handler:14  //alarm信号是在主线程被处理的!
 main thread interrupted at 2 second            //主线程被alarm信号中断,并没有睡足10秒!
 main thread(b77d26c0) is running
 thread(b77d1b70) i: 3
 thread(b77d1b70) i: 4
 timeout: thread(b77d26c0) call sig_handler:14
 main thread interrupted at 2 second
 main thread(b77d26c0) is running
 thread(b77d1b70) i: 5
 thread(b77d1b70) i: 6
 timeout: thread(b77d26c0) call sig_handler:14
 main thread interrupted at 2 second
 main thread(b77d26c0) is running
 thread(b77d1b70) i: 7
 ^C
 */

【编程实验】多线程中的定时器2(本例由子线程处理alarm信号)

//pthread_alarm2.c

#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

/*多线程下的alarm线程处理2(本例中alarm信号由子线程处理)*/

void sig_handler(int signo)
{   
    if(signo == SIGALRM){
        printf("timeout: thread(%lx) call sig_handler:%d\n", pthread_self(), signo);
        alarm(2);//周期性定时(否则为一次性)
    }
}

void* th_fn(void* arg)
{
    //在子线程中注册信号处理函数,会作用于所有线程
    //由于alarm是进程资源,系统会遍历所有线程,直到找到一个不屏蔽该信号
    //的线程,并由其处理(一般从最小线程号(主线程)找起)。由于主线程屏蔽了
    //ALARM信号,系统会找到未屏蔽该信号的子线程来处理该信号!
    if(signal(SIGALRM, sig_handler) == SIG_ERR){
        perror("signal sigalrm error");
    }

    //在子线程中设置定时器
    alarm(2);
    int i = 1;
    for(; i<=10; i++){
        printf("thread(%lx) i: %d\n", pthread_self(), i);
        sleep(1);
    }
    return NULL;
}

int main(void)
{
    int err = 0;
    pthread_t th;

    //以分离状态启动子线程
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if((err = pthread_create(&th, &attr, th_fn, NULL) != 0)){
        perror("pthread_create error");
    }

    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGALRM);
    pthread_sigmask(SIG_SETMASK, &mask, NULL);//主线程屏蔽ALARM信号

    int rc = 0;
    while(1){
        printf("main thread(%lx) is running\n", pthread_self());
        rc = sleep(10); //主线程睡眠期间可能会被alarm中断
        if(rc != 0){
            printf("main thread interrupted at %d second\n", 10 - rc);
        }
    }

    printf("main thread ends\n");
}
/*输出结果
 main thread(b77656c0) is running
 thread(b7764b70) i: 1
 thread(b7764b70) i: 2
 timeout: thread(b7764b70) call sig_handler:14 //alarm信号在子线程中处理
 thread(b7764b70) i: 3                         //注意,主线程因未收到alarm,会一直
 thread(b7764b70) i: 4                         //睡眠到超时为止!
 timeout: thread(b7764b70) call sig_handler:14
 thread(b7764b70) i: 5
 thread(b7764b70) i: 6
 timeout: thread(b7764b70) call sig_handler:14
 thread(b7764b70) i: 7
 ^C
 */

(1)异步处理的方式利用信号处理函数(由sigaction注册的函数)

(2)同步处理的方式sigwait将异步信号以同步的方式处理(步骤如下

  ①主线程设置信号屏蔽字,阻塞希望同步处理的信号

  ②主线程创建一个信号处理线程,该线程将希望同步处理的信号集作为 sigwait()的参数;

  ③主线程做其他工作,如创建其它工作线程。(注意主线程的信号屏蔽字会被其创建的新线程继承,因此这些线程没调用sigwait,所以将不会收到信号)。

(3)说明

  ①如果进程中同时注册了信号处理函数,同时又调用sigwait来等待同一个信号那么将由操作系统系统来决定以何种方式处理信号。在这种情况下,操作系统可以让sigwait返回通常优先级较高),也可以激活信号处理函数但这两种方式只能选其一,不会同时出现。因此,如果明确地让sigwait调用线程来处理信号那么所有的线程都应该屏蔽该信号含调用sigwait的线程因为只有被屏蔽信号才会被放入未决信号队列也只有被屏蔽,未决信号才不会传递给信号处理函数而sigwait会等待并从未决信号队列中取出信号来处理

  ②如果多个线程调用sigwait来等待同一个信号,这时会因线程的竞争而只会有一个线程被唤醒处理

  ③如果信号与硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV),该信号会发往引起该事件的线程,必须按照传统的异步方式使用signal/sigaction注册信号处理函数进行非阻塞处理。其它信号除非显式指定目标线程,否则通常会发往主线程。如果主线程屏蔽了该信号就会发往某个具有处理能力的线程。

【编程实验】同时注册信号处理函数和调用sigwait等待信号

//signal_both.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>

/*演示同时注册信号处理函数(异步)和用sigwait(同步)等待同一信号时,哪种方式会被优先选择
 *《UNIX环境高级编程》一书中指出这是一个未定义行为。本例中sigwait将被优先调用。
 */

//信号的异步处理方式
void sig_handler(int signo)
{
    printf("thread(%lu) call sig_handler: signal(%d)\n", pthread_self(), signo);
}

//信号的同步处理
void sigwait_handler(int signo)
{
    printf("sub thread(%lu) call sigwait_handler: signal(%d)\n",pthread_self(), signo);
}

//线程函数
void* th_fn(void* arg)
{
    int err =0;
    int signo = 0;

    //等待指定的信号
    sigset_t waitset,oldset;

    sigemptyset(&waitset);
    sigaddset(&waitset, SIGUSR1);
    sigaddset(&waitset, SIGTSTP); //ctrl-z

    if((err = pthread_sigmask(SIG_BLOCK, &waitset, &oldset)) !=0){
        perror("pthread_sigmask error");
        exit(err);
    }

    while(1){
        printf("sub thread(%lu) sigwait for signal...\n", pthread_self());
        err = sigwait(&waitset, &signo);
        if(err != 0){
            perror("sigwait error");
            exit(err);
        }
        sigwait_handler(signo);
    }

    return (void*)0;
}

int main(void)
{
    pthread_t th;
    sigset_t mask, oldmask;
    int err;
    
    printf("main thread(%lu) is running...\n", pthread_self());

    //注册异步信号处理函数
    if(signal(SIGUSR1, sig_handler)== SIG_ERR){
        perror("signal sigusr1 error");
    }
    if(signal(SIGTSTP, sig_handler)==SIG_ERR){
        perror("signal sigtstp error");
    }

    //主线程中同时屏蔽两个相同的信号(注意,信号屏蔽字会被子线程继承!)
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGTSTP);
    if((pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) !=0){//让sigwait起作用!
        perror("pthread_sigmask error");
        exit(err);
    }
    
    //创建子线程,用于以同步方式处理信号
    if((err = pthread_create(&th, NULL, th_fn, NULL)) !=0){
        perror("pthread_create error");
    }

    //当同时注册信号处理函数和用sigwait等待同一信号时,这时只有其中一种
    //处理方式会被调用。
    kill(getpid(), SIGUSR1);
    kill(getpid(), SIGTSTP);

    pthread_join(th, NULL); //等待子线程结束

    return 0;
}
/*输出结果
 main thread(3078055616) is running...
 sub thread(3078052720) sigwait for signal... //sigwait被优先调用
 sub thread(3078052720) call sigwait_handler: signal(10)
 sub thread(3078052720) sigwait for signal...
 sub thread(3078052720) call sigwait_handler: signal(20)
 sub thread(3078052720) sigwait for signal...
 ^C
 */

5.4 注意事项

(1)进程中的每个线程都有自己的信号屏蔽字。但是所有的线程共享sigaction注册的信号处理函数。这意味着:

  ①可以在线程中调用pthread_sigmask来决定本线程阻塞哪些信号。(注意:子线程会从父线程继承信号屏蔽字)。而被所有线程屏蔽的信号可以用sigwait来等待而不会被发送给信号处理函数

  ②如果某个线程调用sigaction修改了某个信号相关的处理行为以后,所有线程必须都会按同一种方式处理这个信号,也就是每个线程不能按自己的方式来处理信号。(如某线程选择忽略某个信号,而其他线程可以恢复为默认或设置成新的处理函数)

  ③一个信号只能被sigwait或sigaction两种方式之一处理,而不会同时出现。

(2)不要在线程信号屏蔽字中屏蔽和捕获不可忽略的信号,如(SIGKILL和SIGSTOP)。不要在线程中阻塞或等待SIGFPE/SIGSEGV/SIGBUS等硬件致命错误,而应捕获他们(即不使用sigwait,而是在信号处理函数中处理。)

(3)不要在多线程中调用sigwait等待同一个信号,应设置一个调用该函数的专用线程。

【编程实验】信号的处理顺序

//signal_sequence.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>

/*演示实时信号和非实时信号被处理的顺序*/

//信号处理程序
void sigwait_handler(int signo)
{
    printf("sigwait_handler receive signal(%d)\n", signo);
}

//线程函数
void* th_func(void* arg)
{
    sigset_t waitset; //要等待的信号集
    int sig;
    pthread_t pid = pthread_self();
    pthread_detach(pid);
    
    sigemptyset(&waitset);
    sigaddset(&waitset, SIGRTMIN);
    sigaddset(&waitset, SIGRTMIN + 2);
    sigaddset(&waitset, SIGRTMAX);
    sigaddset(&waitset, SIGUSR1);
    sigaddset(&waitset, SIGUSR2);

    while(1){
        //等待信号
        if(sigwait(&waitset, &sig) == 0){ //成功等到信号
            sigwait_handler(sig);
        }else{ //出错
            printf("sigwait() return err:%d; %s\n", errno, strerror(errno));
        }
    }

    return (void*)0;
}

int main(void)
{
    sigset_t bset, oset;
    pid_t pid = getpid();
    pthread_t th;

    sigemptyset(&bset);
    sigaddset(&bset, SIGRTMIN);
    sigaddset(&bset, SIGRTMIN+2);
    sigaddset(&bset, SIGRTMAX);
    sigaddset(&bset, SIGUSR1);
    sigaddset(&bset, SIGUSR2);
 
    //主线程设置信号屏蔽字(会被子线程继承),以使信号只能被
    //调用sigwait的线程接收,而不会被sigaction注册的信号处理函数接收。
    if(pthread_sigmask(SIG_BLOCK, &bset, &oset)!=0){
        perror("pthread_sigmask error");
    }
    
    //注意:
    //(1)发送信号的顺序,这里故意先发出实时信号,再发非实时信号
    //(2)信号数字的大小,
    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN); 
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR1);
    kill(pid, SIGUSR1);

    //创建子线程
    if(pthread_create(&th, NULL, th_func, NULL ) !=0){
        perror("pthread_create error");
    }

    sleep(10);

    return 0;
}
/*输出结果
sigwait_handler receive signal(10) //SIGUSR1,不进队列,只收到1个(非实时信号)
sigwait_handler receive signal(12) //SIGUSR2,不进队列,只收到1个(非实时信号)
sigwait_handler receive signal(34) //SIGRTMIN,会进队列,多次收到
sigwait_handler receive signal(34) 
sigwait_handler receive signal(36) //SIGRTMIN+2,会进队列,多次收到
sigwait_handler receive signal(36)
sigwait_handler receive signal(64) //SIGRTMAX,会进队列,多次收到。
sigwait_handler receive signal(64) 
//注意:
(1)如果既有多个实时信号和非实时信号,实时信号并不会先于非实时信号被取出,信号数字
小的会先被取出。如SIGUSR1(10)先于SIGUSR(2),SIGRTMIN(34)先于SIGRTMAX(64),非实时信号
因信号数字小而先于实时信号被取出。(注意这里的实时和非实时并不代表时间上的先后,而只表示
该信号会不会进队列以保证不会丢失,主要差别在于信号的可靠性)
(2)非实时信号,相同信号不能在信号队列中排列;实时信号,相同信号会进队列排队
*/