【APUE】Chapter12
只有两个小节,但是断断续续看了几次,争取用不同的例子尽量多理解这两部分内容。
有理解的不深刻,不正确的地方,以后再求改正。
12.8 Threads and Signals
作者提醒大家,“信号+进程”本身就已经比较复杂了,如果再跟多线程搅和在一起就更加作死了。
有几个基本的outline:
(1)每个线程都有各自的signal mask(可以设定当前线程屏蔽哪些信号)
(2)但是signal处理函数,在进程内是各个线程共享的(某个signal handler改了 其余的都受到影响)
(3)signal一般是发给单独线程的;如果是hardware fault相关的内容,哪个线程引起的就发给哪个线程
(4)给线程设置signal mask需要用pthread_sigmask函数,这个函数与sigprocmask几乎相同,区别在于pthread_sigmask如果执行错误,会返回错误码;而sigprocmask执行错误会重置errno的值,并且返回-1
两个重要的函数:
1. pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
三个参数的具体含义与sigprocmask类似,不再追溯。
2. sigwait(const sigset_t *restrict set, int *restrict signop);
set : 表示要等哪些signal
signop: 用于记录等来的是哪个signal (因为再set中可能等好几个signal,其中任何一个signal的到来都可以让sigwai停止阻塞,因此需要记录到底是哪个signal来的)
作者还特意强调,为了避免“erroneous behavior”,在调用sigwait之前,先要把需要等待的信号block了;在调用sigwait之后,会自动将set中的signal都unblock,然后再等signal到来
这亮个函数的好处在于,可以单独开一个线程,异步处理特定的信号。具体的做法如下:
(1)在不需要接收到特定信号的线程中,用pthread_sigmask在当前线程中屏蔽这些信号
(2)然后在用一个线程专门等着处理这个信号,sigwait就派上用场了
看一个例子:
#include <pthread.h> #include "apue.h" int quitflay; /*set nonzero by thread*/ sigset_t mask; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER; void * thr_fn(void *arg) { int err, signo; for(; ;) { err = sigwait(&mask, &signo); /*mask是要等着的信号集合 signo存放等来的信号*/ if (err!=0) err_exit(err, "sigwait failed"); switch (signo) { case SIGINT: printf("\ninterrupt\n"); break; case SIGQUIT: pthread_mutex_lock(&lock); quitflay = 1; pthread_mutex_unlock(&lock); pthread_cond_signal(&waitloc); return 0; default: printf("unexpected signal %d\n", signo); exit(1); } } } int main() { int err; sigset_t oldmask; pthread_t tid; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); pthread_sigmask(SIG_BLOCK, &mask, &oldmask); pthread_create(&tid, NULL, thr_fn, 0); pthread_mutex_lock(&lock); while (quitflay==0) pthread_cond_wait(&waitloc, &lock); pthread_mutex_unlock(&lock); quitflay = 0; sigprocmask(SIG_SETMASK, &oldmask, NULL); exit(0); }
代码执行结果:
分析如下:
(1)在main的主线程中先堵住SIGINT和SIGQUIT信号
(2)堵完了之后,再用pthread_create创建新的线程,实际上就是注册了一个信号处理的线程
(3)这个新线程的signal mask继承了创建其的main线程中的signal mask(留到最后再说这个)
(3)判断quitflag的值来判断是否往下执行(这里用“条件变量+锁”的形式来保持对全局变量mash读写的同步动作)
(4)那么,有没有可能出现信号空等待的情况呢?假象一种情况:如果main函数中,pthread_create和while之间的time window中来了一个SIGINT或者SIGQUIT信号,会怎么处理?其实也没事,如果来的是SIGINT,没关系,正常进入while循环等着;如果来但是SIGQUIT,此时quitflag已经被置为1了,while循环不会执行等待。所以,经过分析,这种信号处理模式是等待安全的。
总结一下上述的代码,pthread_sigmask屏蔽的目的是让不该接收到该信号的线程收不到;而某个线程中的sigwait起到的作用是“瞬间unblock开set中的那些信号 → 接收信号,保存到signo里面 → 恢复信号mask → 处理signo这个信号”。而在这期间,只有sigwait的时候是瞬间unblock的,其余的时间都是继承main中对信号的屏蔽的。
对上述代码修改如下:
#include <pthread.h> #include "apue.h" int quitflay; /*set nonzero by thread*/ sigset_t mask; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER; void * thr_fn(void *arg) { int err, signo; sigset_t curr; sigprocmask(SIG_BLOCK, NULL, &curr); if ( sigismember(&curr, SIGINT) ) printf("blocking SIGINT\n"); if ( sigismember(&curr, SIGQUIT) ) printf("blocking SIGQUIT\n"); for(; ;) { err = sigwait(&mask, &signo); /*mask是要等着的信号集合 signo存放等来的信号*/ if ( sigismember(&curr, SIGINT) ) printf("blocking SIGINT\n"); if ( sigismember(&curr, SIGQUIT) ) printf("blocking SIGQUIT\n"); if (err!=0) err_exit(err, "sigwait failed"); switch (signo) { case SIGINT: printf("\ninterrupt\n"); break; case SIGQUIT: pthread_mutex_lock(&lock); quitflay = 1; pthread_mutex_unlock(&lock); pthread_cond_signal(&waitloc); return 0; default: printf("unexpected signal %d\n", signo); exit(1); } } } int main() { int err; sigset_t oldmask; pthread_t tid; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); pthread_sigmask(SIG_BLOCK, &mask, &oldmask); //sigprocmask(SIG_BLOCK, &mask, &oldmask); pthread_create(&tid, NULL, thr_fn, 0); pthread_mutex_lock(&lock); while (quitflay==0) pthread_cond_wait(&waitloc, &lock); pthread_mutex_unlock(&lock); quitflay = 0; sigprocmask(SIG_SETMASK, &oldmask, NULL); exit(0); }
再次运行结果如下:
上述代码的修改就是在信号处理线程中sigwait前后均获取当前线程是否阻塞某些信号。可以看到,在信号处理线程中sigwait前后都屏蔽了信号,但是还能够处理这些信号;也就是说,只有sigwait这个函数执行的瞬间,SIGINT和SIGQUIT是unblock的,之前和之后都与main中信号mask相同。
还有一点需要总结,不是说各个线程间的signal mask可以是独立的么?下面写一个例子,直观帮助理解,代码如下:
#include <pthread.h> #include "apue.h" int quitflay; /*set nonzero by thread*/ sigset_t mask; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER; void signal_check() { sigset_t curr; sigprocmask(SIG_BLOCK, NULL, &curr); if ( sigismember(&curr, SIGINT) ) printf("blocking SIGINT\n"); if ( sigismember(&curr, SIGQUIT) ) printf("blocking SIGQUIT\n"); if ( sigismember(&curr, SIGUSR1) ) printf("blocking SIGUSR1\n"); } void * thr_fn(void *arg) { int err, signo; printf("befor setting mask in signal handling thread...\n"); signal_check(); sigset_t extra; sigaddset(&extra, SIGUSR1); sigprocmask(SIG_BLOCK, &extra, NULL); printf("after setting mask in signal handling thread...\n"); signal_check(); for(; ;) { err = sigwait(&mask, &signo); /*mask是要等着的信号集合 signo存放等来的信号*/ if (err!=0) err_exit(err, "sigwait failed"); switch (signo) { case SIGINT: printf("\ninterrupt\n"); break; case SIGQUIT: pthread_mutex_lock(&lock); quitflay = 1; pthread_mutex_unlock(&lock); pthread_cond_signal(&waitloc); return 0; default: printf("unexpected signal %d\n", signo); exit(1); } } } int main() { int err; sigset_t oldmask; pthread_t tid; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); pthread_sigmask(SIG_BLOCK, &mask, &oldmask); //sigprocmask(SIG_BLOCK, &mask, &oldmask); pthread_create(&tid, NULL, thr_fn, 0); sleep(2); printf("check signal in main after pthread_create...\n"); signal_check(); pthread_mutex_lock(&lock); while (quitflay==0) pthread_cond_wait(&waitloc, &lock); pthread_mutex_unlock(&lock); quitflay = 0; sigprocmask(SIG_SETMASK, &oldmask, NULL); exit(0); }
运行结果如下:
上述代码为了验证各个线程间signal mask的独立性,专门在thr_fn中多屏蔽了一个SIGUSR1,看看main线程是否会收到影响。结果是main线程的signal mask并没有收到影响。再总结一下:
(1)从main中pthread_create()这地方开始,产生了一个新的线程,同时新的线程继承了main线程中的signal mask;这也就是为什么thr_fn上来就已经把SIGINT和SIGQUIT给屏蔽了
(2)紧接着,在thr_fn中再把SIGUSR1给屏蔽了,马上thr_fn中就体现出来屏蔽SIGUSR1了;随后再检查main线程中的屏蔽信号,还是SIGINT和SIGQUIT两个,thr_fn中屏蔽的SIGUSR1并没有影响到main线程中对SIGUSR1的处理方式。
结合以上两点,可以直观理解每个线程的signal mask都是独立的。
还想继续探究,当pthread_create()执行时,有关信号处理的方面到底发生了什么?查阅了如下的资料:
更具体的参见:http://man7.org/linux/man-pages/man3/pthread_create.3.html
12.9 Threads and fork
这里的核心在于如何在多线程+多进程环境下处理锁的问题。
首先需要回顾一下8.3节的fork函数,尤其是copy-on-write的优化的fork实现方法:
(1)fork出一个child process后,parent process和child process的地址空间并非完全独立
(2)为了提高效率,如果parent process中的变量虽然copy给了child process,但只是对这个变量有“读"的操作是不会在child process中给这个变量再内存中开辟一个独立的新的地址的;只有对这个变量有“写”的操作了,这时候才会给这个变量在child process中完全独立开辟内存空间
以上是copy-on-write的fork的背景知识。
再说一下多线程状态下的fork:
(1)fork之后,copy到child process中只有一个线程,parent process中哪个线程fork的,就copy那个线程
(2)如果copy到child process中的那个线程中,有被lock住的mutex,则这种lock的状态也会被child process所继承
这里的问题就在于,很可能真正lock住mutex的那个线程并没有被copy到child process中,这样就容易出现死锁的问题(因为child process中没有那个对mutex实际锁住的线程,并且mutex是独立的,也就永远不会获得那个线程中解锁的操作了)。
书上给出的是一种比较暴力的方法:在fork的时候,把各种需要打开的锁都unlock了。
为了解决上述的问题,给出了相关的系统函数:
pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
这个函数就是一个注册用的函数,在执行fork的时候被触发。几个参数的意义如下:
(1)prepare : 在fork出child之前用;获取所有的locks
(2)parent : 在fork出child之后,但fork return之前用;在父进程中解锁
(3)child : 在fork出child之后,但fork return之前用;在子进程中解锁
先看一个例子:
#include <pthread.h> #include "apue.h" pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; void prepare(void) { int err; printf("preparing locks...\n"); /*养成这种容错编程的习惯 否则出错了都不知道是哪出的错误*/ if ((err=pthread_mutex_lock(&lock1))!=0) { err_cont(err, "can't lock lock1 in prepare handler"); } if ((err=pthread_mutex_lock(&lock2))!=0) { err_cont(err, "can't lock lock2 in prepare handler"); } } void parent(void) { int err; printf("parent unlocking locks...\n"); if ((err=pthread_mutex_unlock(&lock1))!=0) { err_cont(err, "can't unlock lock1 in parent handler"); } if ((err=pthread_mutex_unlock(&lock2))!=0) { err_cont(err, "can't unlock lock2 in parent handler"); } } void child(void) { int err; printf("child unlocking locks...\n"); if ((err=pthread_mutex_unlock(&lock1))!=0) { err_cont(err, "can't unlock lock1 in child handler"); } if ((err=pthread_mutex_unlock(&lock2))!=0) { err_cont(err, "can't unlock lock2 in child handler"); } } void * thr_fn(void *arg) { printf("thread started...\n"); pause(); return 0; } int main() { int err; pid_t pid; pthread_t tid; if ((err=pthread_atfork(prepare, parent, child))!=0) { err_exit(err, "can't install fork handlers"); } if ((err=pthread_create(&tid, NULL, thr_fn, 0))!=0) { err_exit(err, "can't create thread"); } sleep(2); printf("parent about to fork...\n"); if ((pid=fork())<0) { err_quit("fork failed"); } else if (pid==0) { printf("child returned from fork\n"); } else { printf("parent returned from fork\n"); } exit(0); }
程序运行结果如下:
分析如下:
(1)prepare最先执行,而且是在fork出child之前执行的。为什么要有这么一个prepare?因为,如果敢在多线程条件下用fork,就必须保证fork中涉及的各个mutex的状态都得是可控的(即不能被其他的线程lock住)。这也就解决了之前提到的一个问题,“如果copy到child process的线程中有mutex被原来父进程中的某个线程锁住了怎么办?”;因为,如果一旦prepare对所有相关的mutex都获得控制权了,自然就可以避免上面的问题了。
(2)parent handler在fork出child之后,但是fork return之前执行;child handler在fork出child之后,但是fork return之前执行。这里还有一个问题:为什么给人感觉prepare加锁只有一次,但是parent handler和child handler却解锁了两次?这个问题书上说的很清楚,这是由于copy-on-write的fork实现策略给人造成的错觉。的确,在prepare获得mutex控制权的时候,lock1和lock2在内存中只有一份的;但是一旦parent handler或child handler对lock1和lock2有写操作了,马上就会先给child process在内存中独立分配出一套新的lock1和lock2变量。因此,parent handler中的解锁是针对原来的lock1和lock2,而child handler中的解锁是针对新的lock1和lock2。这样的操作是没有问题的。
书上还给出了多次调用pthread_atfork的分析,但这部分没有例子。
我的理解就是,这是针对locking hierarchy的操作,强调多个pthread_atfork的注册顺序和执行顺序。
背景:A模块调用B模块,两个模块都有各自的锁(A中有lock1 lock2,B中有lock3 lock4);现在要fork,该怎么处理?
方法:这种情况下,A的锁再外面,B的锁在里面;因此针对B的pthread_atfork一定要在A前面注册
执行顺序:parent handler和child handler按照注册顺序去调用;而prepare按照与注册相反的顺序去调用
这里需要理解两个问题:
(1)为什么prepare的执行顺序要与注册顺序相反(注意,这里B的pthread_atfork是先于A的pthread_atfork的)?原因就是,为了在获得锁的控制权的时候,依然保持原来的hierarchy;如果prepare获得了B中的锁,此时如果父进程中的A模块正等着B中的锁才能往下进行呢?于是就憋住了,形成死锁了。反之,如果能够获得A的锁,就从源头保住了不会出现A等着B的锁才能往下进行的死锁情况。这个内容,后面最好写一个例子。
(2)为什么parent handler和child handler的执行顺序与注册顺序相同了呢?我理解就是,加锁顺序是lock1 lock2 lock3 lokc4,那么解锁的顺序就应该是lock4 lock3 lock2 lock1。由于B中的pthread_atfork是先于A中的pthread_atfork注册的,所以parent handler和child handler就是按照注册的顺序执行,就符合解锁顺序了,不会造成死锁问题。
完毕。