linux 线程退出 signal,Linux signal 那些事儿 (3)
這篇博客,想集中在signal 與線程的關系上,順帶介紹內核signal相關的結構。如何組織我其實并沒想好,想到哪就寫到哪里吧。主題一定會落在signal之內而不跑題。
提到signal與thread的關系,就得先提POSIX標準。POSIX標準決定了Linux為何將signal如此實現:
1 信號處理函數必須在多線程應用的所有線程之間共享,但是,每個線程要有自己的掛起信號掩碼和阻塞信號掩碼。
2 POSIX 函數kill/sigqueue必須面向所有的多線程應用而不是某個特殊的線程。
3 每個發給多線程應用的信號僅傳送給1個線程,這個線程是由內核從不會阻塞該信號的線程中隨意選出。
4 如果發送一個致命信號到多線程,那么內核將殺死該應用的所有線程,而不僅僅是接收信號的那個線程。
上面是POSIX標準,也就是提出來的要求,Linux要遵循POSIX標準,那Linux是怎么做到的呢?
到了此處,我們需要理清一些基本的概念:
struct task_struct {
pid_t pid;
pid_t tgid
.....
struct task_struct *group_leader;????/* threadgroup leader */
......
struct list_head thread_group;
....
}
從字面意思上看 pid,是process id,其則不然,pid是thread id。從字面意思上看,tgid是thread group id,其則是真正的pid。
有點繞是不是?對于一個多線程的程序,無論是哪個線程執行getpid,結果都是一樣的,最終返回的同一個值 tgid。如果我們實現了gettid(很不幸的是glibc沒有這個函數,所以我們要用syscall),我們就會發現,各個線程返回的值不同,此時,返回的值是內核task_struct中的pid。對于多線程應用/proc/pid/task可以看到的,就是線程的thread id,也就是task_struct中的pid。
我在我的博文Linux線程之線程 線程組 進程 輕量級進程(LWP)提到了這個問題。我不想多浪費筆墨贅述。
group leader字段,指向線程組的第一個線程。對于我們自己的程序而言,main函數所在的線程,也就是線程組的第一個線程,所以group leader就會他自己。一旦用pthread_create創建了線程,那么main所在的線程,還有創建出來的線程,隸屬于同一個線程組,線程的group leader還是main函數所在的線程id。
thread_group,同一線程組的所有線程的隊列。對于group_leader,這是一個隊列頭,對于同一線程組的其他線程,通過這個字段掛入隊列。可以根據這個隊列,遍歷線程組的所有線程。
是時候看看內核代碼了,下面的代碼屬于do_fork函數及copy_process函數的一些代碼。
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)//創建線程,tgid等于當前線程的
p->tgid = current->tgid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
if (clone_flags & CLONE_THREAD) {?//線程處理部分,group_leader都是第一個線程。同時掛入隊列
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
atomic_inc(¤t->signal->sigcnt);
p->group_leader=current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
代碼表明,第一個線程呢,pid和tgid相同,都是分配的那個pid,group_leader也是自己。后面第二個線程,pid是自己的,但是tgid 等于創建者的tgid,group_leader指向第一個線程的task_struct. 后面創建的所有的線程,都會掛入隊列,方便遍線程組的所有線程。
有了線程組的概念,我們就可以進一步解釋signal相關的內容了。
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask;????/* restored if set_restore_sigmask() was used */
struct sigpending pending;
線程組里面的所有成員共享一個signal_struct類型結構,同一線程組的多線程的task_struct 中的signal指針都是指向同一個signal_struct。sighand成員變量也是如此,統一個線程組的多個線程指向同一個signalhand_struct結構。
static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
{
struct signal_struct *sig;
if (clone_flags & CLONE_THREAD) //線程,直接返回,表明同一線程組共享
return 0;
sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
tsk->signal = sig;
if (!sig)
return -ENOMEM;
sig->nr_threads = 1;
atomic_set(&sig->live, 1);
atomic_set(&sig->sigcnt, 1);
init_waitqueue_head(&sig->wait_chldexit);
sig->curr_target = tsk;
。。。。
}
static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
{
struct sighand_struct *sig;
if (clone_flags & CLONE_SIGHAND) {
atomic_inc(¤t->sighand->count); //如果發現是線程,直接講引用計數++,無需分配sighand_struct結構
return 0;
}
sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
rcu_assign_pointer(tsk->sighand, sig);
if (!sig)
return -ENOMEM;
atomic_set(&sig->count, 1);
memcpy(sig->action, current->sighand->action, sizeof(sig->action));
return 0;
}
這就基本實現了多線程應用中,信號處理程序是共享的,因為他們共用一個signalhand_struct。
上一篇博文提到,signal->shared_pending 和pending兩個掛起信號相關的數據結構,此處我們可以具體講解了。signal是線程組共享的結構,自然下屬的shared_pending也是線程組共享的。就像POSIX提到的,kill/sigqueue發送信號,發送的對象并不是線程組某個特定的線程,而是整個線程組。自然,如果kernel會將信號記錄在全線程組共享的signal->shared_pending,表示,線程組收到信號X一枚。
有筒子說了,我就要給某個特定的線程發信號,有沒有辦法,內核怎么辦?這是個好問題。
int tkill(int tid, int sig);
int tgkill(int tgid, int tid, int sig)
這兩個API是給線程組特定線程發信號的,毫不意外,內核會將信號記錄在線程自己的結構pending中。
pending = group ? &t->signal->shared_pending : &t->pending;
對于kill/sigqueue,__send_signal傳進來的是group是true,對于tkill/tgkill傳進來的是false。會將信號寫入相應的掛起信號位圖。
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
assert_spin_locked(&t->sighand->siglock);
result = TRACE_SIGNAL_IGNORED;
if (!prepare_signal(sig, t,
from_ancestor_ns || (info == SEND_SIG_FORCED)))
goto ret;
pending=group? &t->signal->shared_pending: &t->pending; //?tkill用的自己的pending,
//?kill/sigqueue用的線程組共享的signal->shared_pending
/*
* Short-circuit ignored signals and support queuing
* exactly one non-rt signal, so that we can get more
* detailed information about the cause of the signal.
*/
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;
result = TRACE_SIGNAL_DELIVERED;
/*
* fast-pathed signals for kernel-internal things like SIGSTOP
* or SIGKILL.
*/
if (info == SEND_SIG_FORCED)
goto out_set;
/*
* Real-time signals must be queued if sent by sigqueue, or
* some other real-time mechanism. It is implementation
* defined whether kill() does so. We attempt to do so, on
* the principle of least surprise, but since kill is not
* allowed to fail with EAGAIN when low on memory we just
* make sure at least one signal gets delivered and don't
* pass on the info struct.
*/
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;
q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
override_rlimit);
if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
break;
case (unsigned long) SEND_SIG_PRIV:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info.si_pid = 0;
q->info.si_uid = 0;
break;
default:
copy_siginfo(&q->info, info);
if (from_ancestor_ns)
q->info.si_pid = 0;
break;
}
userns_fixup_signal_uid(&q->info, t);
} else if (!is_si_special(info)) {
if (sig >= SIGRTMIN && info->si_code != SI_USER) {
/*
* Queue overflow, abort. We may abort if the
* signal was rt and sent by user using something
* other than kill().
*/
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
} else {
/*
* This is a silent loss of information. We still
* send the signal, but the *info bits are lost.
*/
result = TRACE_SIGNAL_LOSE_INFO;
}
}
out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal,sig);//修改位圖,表明該信號存在掛起信號。
complete_signal(sig, t, group);
ret:
trace_signal_generate(sig, info, t, group, result);
return ret;
}
線程存在一個很讓人迷惑的問題,如何讓線程組的所有線程一起退出。我們都知道,多線程的程序有一個線程訪問了非法地址,引發段錯誤,會造成所有線程一起退出。這也是多線程程序脆弱的地方。但是如何做到的呢?
do_signal--->get_signal_to_deliver中,會選擇信號,如果發現需要退出,會執行do_group_exit。這個名字顧名思義了,線程組退出。
void
do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
BUG_ON(exit_code & 0x80); /* core dumps don't get here */
if (signal_group_exit(sig))
exit_code = sig->group_exit_code;
else if (!thread_group_empty(current)) {
struct sighand_struct *const sighand = current->sighand;
spin_lock_irq(&sighand->siglock);
if (signal_group_exit(sig))
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code;
else {
sig->group_exit_code = exit_code;
sig->flags = SIGNAL_GROUP_EXIT;
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
}
do_exit(exit_code);
/* NOTREACHED */
}
如果是多線程,會走入到else中,主要的操作都在zap_other_threads函數中:
/*
* Nuke all other threads in the group.
*/
int zap_other_threads(struct task_struct *p)
{
struct task_struct *t = p;
int count = 0;
p->signal->group_stop_count = 0;
while_each_thread(p, t) {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
count++;
/* Don't bother with already dead threads */
if (t->exit_state)
continue;
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
return count;
}
不多說了,就是給每一個線程都掛上一個SIGKILL的信號,當CPU選擇線程執行時候的時候,自然會處理這個信號,而對SIGKILL的處理,會再次調用do_group_exit。這一次會調用do_exit退出。當線程組所有進程都執行過之后,整個線程組就消亡了。
講完這些,需要講block了。我第一篇就講到,我們有時候需要阻塞某些信號。POSIX說了多線程中每個線程要有自己的阻塞信號。不必說,task_struct中的blocked就是阻塞信號位圖。我們的glibc的sigprocmask函數,就是設置進程的blocked。
那些block的信號為何不能傳遞,內核是怎么做到的?
int next_signal(struct sigpending *pending, sigset_t *mask)
{
unsigned long i, *s, *m, x;
int sig = 0;
s = pending->signal.sig;
m = mask->sig;
/*
* Handle the first word specially: it contains the
* synchronous signals that need to be dequeued first.
*/
x = *s &~ *m;
if (x) {
if (x & SYNCHRONOUS_MASK)
x &= SYNCHRONOUS_MASK;
sig = ffz(~x) + 1;
return sig;
}
switch (_NSIG_WORDS) {
default:
for (i = 1; i < _NSIG_WORDS; ++i) {
x = *++s &~ *++m;
if (!x)
continue;
sig = ffz(~x) + i*_NSIG_BPW + 1;
break;
}
break;
case 2:
x = s[1] &~ m[1];
if (!x)
break;
sig = ffz(~x) + _NSIG_BPW + 1;
break;
case 1:
/* Nothing to do */
break;
}
return sig;
}
m就是task_struct中的blocked,阻塞的信號就不會不會被取出傳遞了。很有意思的一點是信號傳遞的順序。在Linux programming interface一書中提到小signo優先的策略,比如SIGINT(2)和SIGQUIT(3)同時存在,SIGINT(2)?先deliver,然后才是SIGQUIT(3).我們看代碼,很有意思的是有同步信號:
#define SYNCHRONOUS_MASK \
(sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \
sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))
有SIGSEGV SIGBUS SIGILL SIGTRAP SIGFPE SIGSYS,那么這幾個信號優先。沒有這幾個信號,按照小信號優先。當然了,這些是Linux kernel的實現,畢竟不是POSIX標準,不可依賴這種順序。
另外,dequeue很有意思,先去task_struct中的pending中取,取不到再去整個線程組共享的shered_pending位圖去取。
int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
{
int signr;
/* We only dequeue private signals from ourselves, we don't let
* signalfd steal them
*/
signr = __dequeue_signal(&tsk->pending, mask, info);
if (!signr) {
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info);
。。。。
}
參考文獻:
1
總結
以上是生活随笔為你收集整理的linux 线程退出 signal,Linux signal 那些事儿 (3)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全民小视频审核多久(全民k歌下载最新版本
- 下一篇: linux 中文文件名不能下载不了,li