Linux内核中信号相关的系统调用

电子说

1.2w人已加入

描述

正如我们所知,运行在用户态下的程序可以发送和接收信号。这意味着必须定义一组系统调用来允许这类操作。不幸的是,由于历史原因,有些系统调用可能功能相同。 因此,其中一些系统调用永远不会被调用。例如,sys_sigaction()和sys_rt_sigaction()几乎相同,因此C库中包含的sigaction()包装函数最终会调用sys_rt_sigaction()而不是sys_sigaction()。

1 kill()

kill(pid,sig)系统调用用来给常规进程或多线程应用程序发送信号,相应的服务例程是sys_kill()。pid根据值的不同具有不同意义:

pid > 0:sig信号被发送给pid指定进程所属的线程组。

pid = 0:sig信号被发送给与调用进程同一进程组内所有进程的线程组。

pid = –1:sig信号被发送给所有进程,除了swapper(PID 0)、init(PID 1和current进程。

pid < –1:信号被发送给-pid进程组中所有进程的所属线程组。

sys_kill()为信号建立一个最小siginfo_t表,然后调用kill_something_info():

 

    info.si_signo = sig;
    info.si_errno = 0;
    info.si_code = SI_USER;
    info._sifields._kill._pid = current->tgid;
    info._sifields._kill._uid = current->uid;
    return kill_something_info(sig, &info, pid);

 

继而,kill_something_info()既可以调用kill_proc_info()(通过group_send_sig_info()发送信号给单个线程组),也可以调用kill_pg_info()(扫描目标进程组所有进程并为每个进程调用send_sig_info()),还可以为系统中的每个进程重复调用group_send_sig_info()(pid=-1)。

kill()能够发送任何信号,包括所谓的实时信号(32~64)。但是,正如我们在信号的产生过程一节中看到的,kill()系统调用不能确保将信号添加到目标进程的挂起信号队列中,因此可能会丢失多个挂起信号。实时信号应该通过诸如rt_sigqueueinfo()之类的系统调用来发送。

System V和BSD Unix变体也有一个killpg()系统调用,它能够显式地向一组进程发送信号。在Linux中,该函数是作为使用kill()系统调用的库函数实现的。另一种变体是raise(),它向当前进程(即执行函数的进程)发送信号。在Linux中,raise()是作为库函数实现的。

2 tkill()/tgkill()

tkill()和tgkill()向线程组中的特定进程发送信号。每个兼容POSIX的pthread库的pthread_kill()函数调用其中的一个来向特定的轻量级进程发送信号。

tkill()需要2个参数:pid,目标进程的PID;sig,信号编码。内核中的sys_tkill()服务例程填充siginfo表,获取进程描述符地址,进行一些权限检查,并调用specific_send_sig_info()发送信号。

tgkill()不同于tkill(),它需要第3个参数:tgid,线程组ID,该线程组包含信号的目标进程。sys_tgkill()服务例程执行与sys_tkill()完全相同的操作,但也检查信号的目标进程是否属于线程组tgid。这个额外的检查解决了当信号被发送到一个正在被终止的进程时发生的竞争条件:如果另一个多线程应用程序创建轻量级进程的速度足够快,那么信号可能会被传递给错误的进程。tgkill()解决了这个问题,因为线程组ID在多线程应用程序的生命周期内永远不会更改。

3 更改信号行为

sigaction(sig,act,oact)系统调用允许用户为信号指定动作;当然,如果没有定义信号动作,内核将执行信号相关联的默认动作。

相应的sys_sigaction()服务例程作用于2个参数:sig,信号值;act,类型为old_sigaction的表,用以指定新行为;oact,可选输出参数,用以获取信号之前的行为动作。(old_sigaction和sigaction具有相同的数据结构,但是成员顺序不一致)

该函数首先检查act地址是否有效。然后用*act的字段填充k_sigaction类型的new_ka局部变量的sa_handler、sa_flags和sa_mask字段:

 

    __get_user(new_ka.sa.sa_handler, &act->sa_handler);
    __get_user(new_ka.sa.sa_flags, &act->sa_flags);
    __get_user(mask, &act->sa_mask);
    siginitset(&new_ka.sa.sa_mask, mask);

 

函数调用do_sigaction()将新的new_ka表复制到current->sig->action表的第sig-1项中:

 

    k = ¤t->sig->action[sig-1];
    if (act) {
        *k = *act;
        sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
        if (k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler == SIG_DFL &&
                (sig==SIGCONT || sig==SIGCHLD || sig==SIGWINCH || sig==SIGURG))) {
            rm_from_queue(sigmask(sig), ¤t->signal->shared_pending);
            t = current;
            do {
                rm_from_queue(sigmask(sig), ¤t->pending);
                recalc_sigpending_tsk(t);
                t = next_thread(t);
            } while (t != current);
        }
    }

 

POSIX标准要求,当默认动作为ignore时,将信号动作设置为SIG_IGN或SIG_DFL会导致所有相同类型的挂起信号被丢弃。此外,请注意,无论信号处理程序请求的屏蔽信号是什么,SIGKILL和SIGSTOP都不会被屏蔽。

sigaction()系统调用还允许用户初始化sigaction表中的sa_flags字段。我们在前面曾经列出了该字段允许的值和相关含义。

旧的System V Unix变体提供了signal()系统调用,它仍然被程序员广泛使用。最近的C库通过rt_sigaction()实现了signal()。然而,Linux仍然支持旧的C库,并提供sys_signal()服务例程:

 

    new_sa.sa.sa_handler = handler;
    new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
    ret = do_sigaction(sig, &new_sa, &old_sa);
    return ret ? ret : (unsigned long)old_sa.sa.sa_handler;

 

4 检查挂起的阻塞信号

sigpending()系统调用允许进程检查挂起的阻塞信号集,例如那些在阻塞时产生的信号。对应的sys_sigpending()服务例程作用于单个参数set,用户变量的地址,在其中,位数组需要被拷贝:

 

    sigorsets(&pending, ¤t->pending.signal,
            ¤t->signal->shared_pending.signal);
    sigandsets(&pending, ¤t->blocked, &pending);
    copy_to_user(set, &pending, 4);

 

5 修改阻塞信号集

sigprocmask()系统调用允许进程修改阻塞信号集; 它仅适用于常规(非实时)信号。 相应的sys_sigprocmask()服务例程作用于3个参数:

oset: 进程地址空间中,指向存储先前位掩码的位数组的指针。

set: 进程地址空间中,指向存储新位掩码的位数组的指针。

how:标志,可取的值如下所示:

SIG_BLOCK:set指向的位掩码数组必须被添加阻塞信号的位掩码数组中。

SIG_UNBLOCK:set指向的位掩码数组必须从阻塞信号的位掩码数组中移除。

SIG_SETMASK:set指向的位掩码数组设定为新的阻塞信号的位掩码数组。

该函数调用copy_from_user()将set指向的值拷贝到new_set这个局部变量中,并将current进程的阻塞的标准信号的位掩码数组拷贝到old_set这个局部变量中。然后根据how标志设置这两个变量:

 

    if (copy_from_user(&new_set, set, sizeof(*set)))
        return -EFAULT;
    new_set &= ~(sigmask(SIGKILL)|sigmask(SIGSTOP));
    old_set = current->blocked.sig[0];
    if (how == SIG_BLOCK)
        sigaddsetmask(¤t->blocked, new_set);
    else if (how == SIG_UNBLOCK)
        sigdelsetmask(¤t->blocked, new_set);
    else if (how == SIG_SETMASK)
        current->blocked.sig[0] = new_set;
    else
        return -EINVAL;
    recalc_sigpending(current);
    if (oset && copy_to_user(oset, &old_set, sizeof(*oset)))
        return -EFAULT;
    return 0;

 

6 挂起进程

在阻塞了由mask参数对应的标准信号之后,sigsuspend()系统调用将进程置于TASK_INTERRUPTIBLE状态。只有当一个非忽略、非阻塞的信号被发送给进程时,进程才会被唤醒。

相应的sys_sigsuspend()服务例程执行以下内容:

 

    mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
    saveset = current->blocked;
    siginitset(¤t->blocked, mask);
    recalc_sigpending(current);
    regs->eax = -EINTR;
    while (1) {
        current->state = TASK_INTERRUPTIBLE;
        schedule();
        if (do_signal(regs, &saveset))
            return -EINTR;
    }

 

将进程设置为可中断的挂起状态后,调用schedule()函数选择其它进程来运行。 当发出sigsuspend()系统调用的进程再次执行时,sys_sigsuspend()调用do_signal()函数来传递唤醒进程的信号。 如果该函数返回值1,则不会忽略该信号。因此,系统调用通过返回错误代码-EINTR来终止。

其实,这个功能完全可以通过组合sigprocmask()和sleep()来实现。但是,sigsuspend()解决了一个竞态问题:因为进程随时会交叉执行,先通过系统调用执行A动作,然后通过系统调用执行B动作,并不等价于通过单个系统调用直接执行A和B两个动作。

这种情况下,sigprocmask()可能会在调用sleep()之前对信号解除阻塞。如果这个发生,因为唤醒信号已经传递过,从而进程得不到唤醒而永远留在TASK_INTERRUPTIBLE状态。相反,sigsuspend()不允许在解除阻塞之后和schedule()调用之前发送信号,因为其它进程在这个时间段不可能抢占CPU时间。

7 实时信号的系统调用

前面介绍的系统调用都是针对标准信号的,对于实时信号有专门的的系统调用。

实时信号的系统调用,如rt_sigaction()、rt_sigpending()、rt_sigprocmask()、rt_sigsuspend()与前面的标准信号对应的系统调用类似,不再赘述。简单介绍一下实时信号队列相关的两个系统调用:

rt_sigqueueinfo()

发送实时信号,以便将其添加目标进程的共享挂起信号队列中。通常通过标准库函数中的sigqueue()实现。

rt_sigtimedwait()

将阻塞的挂起信号从队列中取出而不发送它,并将信号值返回给调用者;如果没有阻塞信号挂起,则将当前进程挂起一段固定的时间。通常通过标准库函数sigwaitinfo()和sigtimedwait()调用。

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分