pthread_cancel如何实现及相关信号

一、杀死线程
这个名字并不像中文"杀死"对应的那样暴力,而是使用了一个相对比较糖衣炮弹的名字,pthread_cancel。事实上,这个中文对应的pthread_kill有另外专门的作用,就是向指定特殊线程发送信号。这里比较感兴趣的是pthread_cancel是如何实现的,它发送的信号是什么信号,为什么不使用SIGKILL来取消指定线程,而是使用了一个自己定义的特殊信号。
二、线程取消实现
对于posix库,线程的取消是通过pthread_cancel接口来实现的,这个接口是posix的标准接口,但是不同的系统对这个接口的实现方法可能并不相同,这里只是看一下Linux/GLIBC对这个接口的实现方式。
glibc-2.7 ptlpthread_cancel.c中对于这个函数实现的核心在于对指定的线程发送特定信号:
val = INTERNAL_SYSCALL (tkill, err, 2, pd->tid, SIGCANCEL);
其中的tkill对应内核中的sys_tkill,该函数位于linux-2.6.21kernelsignal.c文件中sys_tkill,也就是对通过CLONE_THREAD创建的特定线程发送信号(相对于对线程组中所有线程都可见这个信号),这是一个更加精确制导的信号投递,是Linux内核完善线程支持的一个重要基础支撑函数。其中发送的信号就是SIGCANCEL,这个是posix库Linux下NPTL线程库使用的一个内部信号,但是和IP地址一样,信号也是一个比较有限的紧缺资源,所以要对所有的实时和非实时信号进行跟踪和调度。而我们这里使用的SIGCANCEL具体值为
glibc-2.7 ptlpthreadP.h
/* The signal used for asynchronous cancelation.  */
#define SIGCANCEL    __SIGRTMIN
也即是最小的一个实时信号,这个值定义为32。其定义位于glibc-2.7sysdepsunixsysvlinuxitssignum.h文件中

#define SIGRTMIN        (__libc_current_sigrtmin ())
#define SIGRTMAX        (__libc_current_sigrtmax ())
/* These are the hard limits of the kernel.  These values should not be
   used directly at user level.  */
#define __SIGRTMIN    32
但是通过kill -l 看一下系统中定义的信号,是没有这个32信号的,例如在我的Fedora Core系统中:
[root@Harry fwritebuff]# uname -a
Linux Harry 2.6.31.5-127.fc12.i686.PAE #1 SMP Sat Nov 7 21:25:57 EST 2009 i686 athlon i386 GNU/Linux
[root@Harry fwritebuff]# kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3  这里出现了非连续,缺少了32和33两个信号
不过所幸的是SIGRTMIN的定义和__SIGRTMIN在同一个文件中,该变量的定义为一个函数
__libc_current_sigrtmin ()
这个函数其实很简单,就是直接返回了变量current_rtmin(glibc-2.7 ptlsysdepsunixsysvlinuxallocrtsig.c)

static int current_rtmin = __SIGRTMIN + 2;这里故意跳跃了2个信号,也就是32和33两个信号,SIGRTMIN从34开始,这里的32就作为SIGCANCEL
static int current_rtmax = __SIGRTMAX;    而接下来的33信号则用来作为SIGSETXID,这个信号的定义在pthreadP.h中为(__SIGRTMIN + 1)
/* We reserve __SIGRTMIN for use as the cancelation signal.  This
   signal is used internally.  */
int
__libc_current_sigrtmin (void)
{
  return current_rtmin;
}
三、信号处理函数的注册
glibc-2.7 ptlinit.c中__pthread_initialize_minimal_internal (void)函数
 struct sigaction sa;
  sa.sa_sigaction = sigcancel_handler;
  sa.sa_flags = SA_SIGINFO;
  __sigemptyset (&sa.sa_mask);

  (void) __libc_sigaction (SIGCANCEL, &sa, NULL);这里为SIGCANCEL信号的相关处理

  /* Install the handle to change the threads' uid/gid.  */
  sa.sa_sigaction = sighandler_setxid;
  sa.sa_flags = SA_SIGINFO | SA_RESTART;

  (void) __libc_sigaction (SIGSETXID, &sa, NULL);这里为SIGSETXID的信号相关处理,不过这里咱不关心。
这个函数在什么时候执行呢?从名字上看,它是位于一个init.c中,所以这个函数中的代码同样将会在init中被调用,这个init也就是位于可执行文件的“.init”节中的代码,有C库保证在main函数执行之前执行这个代码段中的代码,所以这个__pthread_initialize_minimal_internal 函数将会在main函数之前注册,当然前提是可执行文件生成的时候链接了pthread库。
四、信号处理函数的执行
sigcancel_handler函数中最为重要的就是调用了__do_cancel函数,不要小看这个函数,对于一个健壮的系统,这是一相当重要的函数,因为pthread_exit也会调用这个函数,看一下pthread_exit函数的实现:
void
__pthread_exit (value)
     void *value;
{
  THREAD_SETMEM (THREAD_SELF, result, value);

  __do_cancel ();
}
可以看到,该函数的实质性操作就是执行这个__do_cancel函数,所以,在信号处理函数中执行这个函数相当于迫使目标线程主动执行pthread_exit函数。那么这个函数执行什么东西呢?它执行的主体为
  __pthread_unwind ((__pthread_unwind_buf_t *)
            THREAD_GETMEM (self, cleanup_jmp_buf));
,这里的cleanup_jmp_buf引导了一个线程通过pthread_cleanup_push注册的所有的清理函数,这些函数可能包含一些互斥锁的解锁操作。假设一个线程获得了互斥锁,然后正在欢乐的执行自己的代码,或者说它在幸运的获得了多个互斥锁之后,不幸的在下一个互斥锁的获得中被挂起来,此时如果该线程被pthread_cancel杀死,那么这个线程获得的所有的锁没有被释放,这样其它的线程将会进入无限等待。
这里解决的办法一方面禁止该信号(当然不是直接操作__SIGRTMIN,而是通过pthread_setcancelstate接口),另一方面就是在获得互斥锁之前通过pthread_cleanup_push将解锁操作注册进去,从而当自己被cancel的时候可以由信号处理函数调用pthread_unwind来完成自己的遗嘱。
五、和SIGKILL的比较
事实上,用户态的线程是无法处理SIGKILL的,可以认为这个信号是内核中的尚方宝剑,所有用户态线程将会被这个信号无条件杀死,当然只要你有发送这个信号的权限,例如root用户。看一下内核对于信号的处理函数位于get_signal_to_deliver中:
ka = &current->sighand->action[signr-1];
        if (ka->sa.sa_handler == SIG_IGN) /* Do nothing.  */如果忽略了该信号,可以直接返回
            continue;
        if (ka->sa.sa_handler != SIG_DFL) {如果注册了信号处理函数,则执行信号处理函数
            /* Run the handler.  */
            *return_ka = *ka;

            if (ka->sa.sa_flags & SA_ONESHOT)
                ka->sa.sa_handler = SIG_DFL;

            break; /* will return non-zero "signr" value */
        }
……
        /*
         * Death signals, no core dump.
         */
        do_group_exit(signr);执行到这里,说明信号处理函数一定是SIG_DEF了
        /* NOTREACHED */
既然如此,我们是不是可以将信号注册为SIG_IGN或者屏蔽这个SIGKILL信号,从而逃过一劫,相当于孙悟空修改生死簿来长生不老一样?
用户态修改信号处理函数一般是通过sigaction,这个函数对应的内核函数为
sys_rt_sigaction->>>do_sigaction
    if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
        return -EINVAL;
这里如果SIGKILL将会满足上面最后的一个判断条件(act && sig_kernel_only(sig)),从而返回参数错误而导致这次设置修改无效。
另一个是屏蔽该信号,拒敌于国门之外,这个接口为
sys_rt_sigprocmask
sigdelsetmask(&new_set, sigmask(SIGKILL)|sigmask(SIGSTOP));
这里同样是删除了用户提供的信号集中的SIGKILL和SIGSTOP,而这两个信号和上面的sig_kernel_only是相同的判断。这里内核严防死守,算是断绝了用户态线程屏蔽自己的SIGKILL处理函数的途径。
六、内核态线程为何躲过此劫
看一下内核态线程的处理函数
[root@Harry fwritebuff]# cat /proc/2/status 
……
SigBlk:    0000000000000000
SigIgn:    ffffffffffffffff
SigCgt:    0000000000000000
这里我使用的内核和看到的内容并不一致,不知道是不是我使用的fedora core发行版本微调了内核的这个地方,因为我使用我自己编译的2.6.21内核不是这个样子,显示的信号处理状况是SigBlk全部为f,而SigIgn为0.从效果上看,目标线程都不会被信号唤醒,更别说执行信号了。但是SigBlk的信号在sigprocmask打开这些信号使能之后,这些信号可以被再次处理;而如果是SigIgn类型的信号处理函数,那么这些信号在发送的时候会被直接丢弃。
对于内核线程通过
daemonize
    /* Block and flush all signals */
    sigfillset(&blocked);
    sigprocmask(SIG_BLOCK, &blocked, NULL);
来屏蔽自己所有的信号,所以用户态使用SIGKILL也无法杀死内核态线程,当然,如果有些线程比较自觉的检测自己的信号并进行主动退出的话,那也是可以的。
也就是说,虽然系统调用无法修改信号处理函数,但是内核线程本身就在内核中,所以它可以不通过系统调用而直接操作自己的进程描述符中的数据结构,从而屏蔽某些信号。

原文地址:https://www.cnblogs.com/tsecer/p/10486118.html