浅谈Linux中的信号机制(二)

首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇《浅谈Linux中的信号机制(一)》的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉。本人自知功力不够,尚且不能对着Linux内核源码评头论足。以后的路还很长,我还是一步一个脚印的慢慢走着吧,Linux内核这座山,我才刚刚抵达山脚下。

好了,言归正传,我接着昨天写下去。如有错误还请各位看官指正,先此谢过。

上篇末尾,我们看到了这样的现象:send进程总共发送了500次SIGINT信号给rcv进程,但是实际过程中rcv只接受/处理了13次SIGINT的信号处理函数(signal-handler function)。究竟是rcv进程接受了500次SIGINT信号只执行了13次信号处理函数,还是rcv进程只接受了13次SIGINT信号然后执行了13次信号处理函数呢。我们不禁要问:信号去了哪儿呢?要搞清这个问题之前,我们还需了解一个叫做做信号集和信号屏蔽的知识点。

信号集

在处理信号相关的函数时,我们时常需要一种的特殊的数据结构来表示一组信号的集合,这样的集合我们称之为信号集,其数据类型表示为sigset_t,通常是用位掩码的形式来实现的。我的环境是CentOS7,其定义在/usr/include/bits/sigset.h中,具体如下:

/* A `sigset_t‘ has a bit for each signal. */

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

在sigset.h同时也提供了一组函数(实际上用宏来实现的,感兴趣可以查阅sigset.h),用以实现对sigset_t类型数据的操作。其原型如下:

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

除此之外Glibc还提供了另外三个非标准规定的函数:

int sigisemptyset(const sigset_t* set);

int sigandset(sigset_t* dest,sigset_t* left,sigset_t* right);

int sigorset(sigset_t* dest,sigset_t* left,sigset_t* right);

基本上看了原型之后这些函数的用法也就一目了然了,不需要浪费篇幅了。除此之外,我觉得的这些函数的实现还是值得一读的,是C语言中位运算学习的一个不错的demo。

信号屏蔽

在了解了信号集的基本概念之后,我们就可以知道继续了解其他与信号集相关的概念了,首先是信号屏蔽字。它定义了要阻塞递送到当前进程的信号集,每一个进程都有一个信号屏蔽字(signal mask)。如果你知道什么是权限屏蔽(umask)那么信号屏蔽字也很好理解。sigprocmask()函数可以检测和更改当前进程的信号屏蔽字。其原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

当oldset是一个非空指针的话,调用sigprocmask之后,oldset便返回了之前的信号屏蔽字。set参数会结合how参数对当前的信号屏蔽字做出修改。(和之前一节提到过的一样有两个特殊的信号,你不可以屏蔽它们是:SIGKILL和SIGSTOP)具体规则是:

how 行为
SIG_BLOCK 设置进程的信号屏蔽字为当期信号屏蔽字和set的并集。set是新增的要屏蔽的信号集。
SIG_UNBLOCK 设置当前进程的信号屏蔽字为当前信号屏蔽字和set补集的交集,也就是当前信号屏蔽字减去set中的要解除屏蔽的信号集。set中是要解除屏蔽的信号集。
SIG_SETMASK 设置当前进程的信号屏蔽字为set信号集。

然而当set指向一个NULL时,那么how也就没有作用了。通常我们让set设置为NULL时,通过oldset获取当前的信号屏蔽字。

如果某个或多个信号在进程屏蔽了该信号的期间来到过一次或者多次,我们称这样的信号叫做未决的(pending)信号。那么在调用sigprocmask()解除这个信号屏蔽之后,该信号会在sigprocmask ()返回之前,递送给(SUSv3 规定至少传递一个信号)当前进程

进程维护了一个数据结构来保存未决的信号,我们可以通过sigpending()来获取哪些信号是未决的:

int sigpending(sigset_t *set);//return 0 on success,or -1 on error

set参数返回的便是未决的信号集。之后便可以通过使用sigismember()来判断,set中包含哪些信号。

到这里我们就可以解释上一篇末尾的问题了。因为Linux上signal()注册的信号处理函数在执行时,会自动的将当前的信号添加到进程的信号屏蔽字当中。当信号处理函数返回时,会恢复之前的信号屏蔽字。这意味着,当信号处理函数执行时,它不会递归的中断自身。

实时信号

早期Unix系统只定义了32种信号。POSIX.1b定义了一组额外的实时信号(为了兼容之前的应用,而不是修改以前的传统信号)。实时信号的特点,《Linux系统编程手册》上有一段总结的很是全面:

  • Realtime signals provide an increased range of signals that can be used for application-defined purposes. Only two standard signals are freely available for application-defined purposes: SIGUSR1 and SIGUSR2.
  • Realtime signals are queued. If multiple instances of a realtime signal are sent to a process, then the signal is delivered multiple times. By contrast, if we send further instances of a standard signal that is already pending for a process, that signal is delivered only once.
  • When sending a realtime signal, it is possible to specify data (an integer or pointer value) that accompanies the signal. The signal handler in the receiving process can retrieve this data.
  • The order of delivery of different realtime signals is guaranteed. If multiple different realtime signals are pending, then the lowest-numbered signal is delivered first. In other words, signals are prioritized, with lower-numbered signals having higher priority. When multiple signals of the same type are queued, they are delivere—along with their accompanying data—in the order in which they were sent.

根据第二点,我们可以将上篇的博客末尾的SIGINT改成SIGRTMIN+5(当然这里随意,只要是实时信号,Linux上kill()也是可以发送实时信号的),然后重复昨天的测试,我们会惊喜的发现,rcv进程“不出意外”地接受并处理了500次信号处理函数。

那么如何通过发送实时信号时传递数据呢?别着急,还得掌握一个系统调用sigaction()。

sigaction()系统调用

之前我们已经解除了signal()函数,sigaction()是另外一种选择,它功能更加强大,兼容性更好,任何时候我们都应优先考虑使用sigaction(),即使signal()更加简单灵活。其函数原型:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error

与sigprocmask类似地,oldact返回之前的信号设置,act用来设置新的信号处理。signum自然不用解释,这是要处理的信号。这个函数的关键之处就是struct sigaction这个和函数同名的结构体。当然要使用sigaction()还是得从struct sigaction入手,它的定义:

struct sigaction {

union {
        void (*sa_handler)(int);                                 
        void (*sa_sigaction)(int, siginfo_t *, void *);

}__sigaction_handler;                                  //Address of handler
    sigset_t sa_mask;                                        //Signals blocked during the handler invocation
    int sa_flags;                                                //Flags controlling handler invocation
    void (*sa_restorer)(void);                             //Restore,not use
};

sa_mask是一组信号集,当调用信号处理函数之前会将这组信号集添加到进程的信号屏蔽字中,直到信号处理函数返回。利用sa_mask参数,我们可以指定一组信号,让我们的信号处理函数不被这些信号打断。与前面的signal()一样,默认还是会把引发信号处理函数的信号,自动的添加到进程的信号屏蔽字中的。sa_flags参数,如果有经验的话,我们不难猜到这肯定是一组选项,毕竟身经百战了嘛。那我们就来看看这组选项是什么意思:

sa_flags 说明
SA_INTERRUPT 由此信号中断的系统调用不会自动重启。
SA_NOCLDSTOP
当signum为SIGCHLD时,当因接受一信号的子进程停止或者恢复时,将不会产生此信号(有点绕).但是子进程终止时,仍会产生此信号。

(If sig is SIGCHLD, don’t generate this signal when a child process is stopped or resumed as a consequence of receiving a signal.)

 SA_NOCLDWAIT 当signum为SIGCHLD时,子进程终止时不会转化为僵尸进程。此时调用wait(),则阻塞到所有子进程都终止,才返回-1,errno被视之为ECHILD。 
 SA_NODEFER 捕获该信号的时候,不会在执行信号处理函数之前将该信号自动添加到进程的信号屏蔽字中。 
 SA_ONSTACK 调用信号处理函数时,使用sigaltstack()安装的备用栈。 
 SA_RESETHAND  当捕获该信号时,会在调用信号处理函数之前将信号处理函数设置为默认值SIG_DFL,并清除SA_SIGINFO标志。
 SA_RESTART  被此信号中断的系统调用,会自动重启。
SA_SIGINFO 调用信号处理函数时附带了额外的数据要处理,具体见下文。

sa_restorer和名字一样为保留参数,不需要使用。最后我们要看的是__sigaction_handler,这是一个联合体(当然啦,这是废话)。sa_handler和sa_sigaction都是信号处理函数的指针,所以一次只能选择两者中的一个。如果sa_mask中设置了SA_SIGINFO位那么就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函数调用信号处理函数,否则使用 void (*sa_handler)(int)这样的函数。下面我们再来看一看sa_sigaction这个函数:

void sa_sigaction(int signum, siginfo_t* info, void* context);

siginfo_t是一个结构体,其结构和实现相关,我的CentOS7系统上是这样的:

siginfo_t {
    int si_signo; /* Signal number */
    int si_errno; /* An errno value */
    int si_code; /* Signal code */
    int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */
    pid_t si_pid; /* Sending process ID */
    uid_t si_uid; /* Real user ID of sending process */
    int si_status; /* Exit value or signal */
    clock_t si_utime; /* User time consumed */
    clock_t si_stime; /* System time consumed */
    sigval_t si_value; /* Signal value */
    int si_int; /* POSIX.1b signal */
    void *si_ptr; /* POSIX.1b signal */
    int si_overrun; /* Timer overrun count; POSIX.1b timers */
    int si_timerid; /* Timer ID; POSIX.1b timers */
    void *si_addr; /* Memory location which caused fault */
    long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
    int si_fd; /* File descriptor */
    short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
}

每个字段的含义后边都加了清晰的注释,但是还有一个参数使我们需要特别注意的,其中si_value字段用来接收伴随着信号发送过来的数据,其类型是一个sigval_t的联合体,其定义(我的系统是在路径/usr/include/bits/siginfo.h 上):

# define __have_sigval_t 1

/* Type for data associated with a signal. */
typedef union sigval
{
    int     sival_int;
    void* sival_ptr;
} sigval_t;
#endif

在实际编程中,到底选择sival_int还是sival_ptr字段,还是取决于你的应用程序。但是由于指针的作用范围只能在进程的内部,如果发送一个指针到另一个进程一般没有什么实际的意义。

基本上写到这里,我们就可以使用sigaction()进行信号处理的demo了,但是这里我们先不急着写,留到下一节一并写了。

使用sigqueue()

之前我们提到了发送实时信号时可以附带数据,kill(),raise()等函数的参数注定他们无法附带更多的数据,这里我们要认识一个新的函数sigqueue()专门用于在发送信号的时候,附加传递额外的数据。

int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error

前两个参数和kill()一致,但是不同于kill(),这里不能将pid只能是单个进程,而不像kill()那样丰富的用法。value的类型便是在上边提及的sigval_t,于是就清晰了:发送进程在这里发送的value在接受进程中通过信号处理函数sa_sigaction中的siginfo_t info参数就可以拿到了。

一个处理实时信号信号简单的demo,处理信号端代码catch.c:

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

void sighandler(int sig,siginfo_t* info,void* context)
{
    printf("Send process pid = %ld,receive a data :%d\n",info->si_pid,info->si_value.sival_int);
}

int main()
{
    printf("pid = %ld\n",(long)getpid());
    struct sigaction act;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = sighandler;
    if(sigaction(SIGRTMIN+5,&act,0) == -1)
        exit(-1);
    pause();
}

发送信号端send.c:

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>

int main(int argc,char* argv[])
{
    printf("Send process pid = %ld\n",(long)getpid());
    union sigval value;
    value.sival_int = 5435620;
    pid_t pid = (pid_t)atol(argv[1]);
    sigqueue(pid,SIGRTMIN+5,value);
}

  运行结果如图所示,在sa_sigaction中成功拿到了发送进程的进程id以及传送的数据:

当然由于夜深了,这个demo写的还是比较简单的,基本我们使用已经没有任何障碍了。

准备把有关信号的知识点总结完的,一写出来,才发现信号这部分的知识点真是多,而且牵扯到好多细节方面的东西,看来这个任务今晚完不成了,明天继续吧。

如果您发现我的博文有错误之处,烦请您指正,我先在此谢过!联系邮箱[email protected]。

时间: 2024-10-12 12:14:31

浅谈Linux中的信号机制(二)的相关文章

浅谈linux中shell变量$#,[email&#160;protected],$0,$1,$2,$?的含义解释

浅谈linux中shell变量$#,[email protected],$0,$1,$2,$?的含义解释 下面小编就为大家带来一篇浅谈linux中shell变量$#,[email protected],$0,$1,$2的含义解释.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 摘抄自:ABS_GUIDE 下载地址:http://www.tldp.org/LDP/abs/abs-guide.pdf linux中shell变量$#,[email protected],$

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理 转自:https://www.jianshu.com/p/2b71ea919d49 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ 浅谈 Linux

浅析Linux中的信号机制(一)

有好些日子没有写博客了,自己想想还是不要荒废了时间,写点儿东西记录自己的成长还是百利无一害的.今天是9月17号,暑假在某家游戏公司实习了一段时间,做的事情是在Windows上用c++写一些游戏英雄技能的逻辑实现.虽然时间不算长,但是也算学了一点东西,对团队项目开发流程也有了一个直观的感受,项目里c++11新特性也有用到不少,特别是lambda表达式,STL的一些容器和算法也终于有了可以实践的地方.由于自己比较喜欢Linux C,也就没有做留下的打算,现在回到了学校,好好复习一段时间,准备一下校招

浅谈Linux的内存管理机制

转至:http://ixdba.blog.51cto.com/2895551/541355 一 物理内存和虚拟内存          我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念.物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的

转:浅谈Linux的内存管理机制

一 物理内存和虚拟内存          我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念. 物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space).          作为物理内存的扩展,l

浅谈linux中的grub

什么是GRUB GRUB 是一个多重操作系统启动管理器.既可以用来引导window也可以引导linux,是一个非常强大的bootloader,linux运维必用神器. GRUB版本   grub 0.x  grub legacy Centos 5,6中广泛使用 stage1 位于mbr的前446字节,其主要作用是定位stage2,由于不能识别文件系统,则需要位于紧跟mbr之后的stage1_5帮助其识别文件系统. stage2 位于磁盘分区上,为内核提供菜单,交互式接口,以及菜单保护机制 gru

浅谈linux中umask值及其对应的文件权限

最近刚好系统学习到linux文件权限这一块,在"umask值及其对应的文件权限"这一个问题上,找过视频看(老男孩老师的视频,强烈推荐),也看过鸟哥的书,当然也在网上找了找,发现计算方法都是多种多样的,但是个人觉得掌握一种就好了,看了各种方法,觉得其实大多数都是在找规律,不是找规律的又实在是太复杂,因此个人感觉不太好记,时间一长可能就忘记了,于是就根据自己的理解,做各种测试,发现有了如下更好的方法.(从理解和记忆的层次上更好的方法)  1.问题引出:设置umask为004与005时,创建

A1—浅谈JavaScript中的原型(二)

原型是什么?想要了解这个问题,我们就必须要知道对象. 对象 根据w3cschool上的介绍:对象只是带有属性和方法的特殊数据类型. 我们知道,数组是用来描述数据的.其实呢,对象也是用来描述数据的.只不过有一点点的区别,那就是数组的下标只能是数字.所以,数组最好只用来装同样意义的内容.比如说[1,2,3,4,5]或者["a","b","c","d","e"]等.因为他们的表达的含义是一样的.那么如何表达含义不

辛星浅谈Linux中的环境变量

全局变量和本地环境变量: bash  shell使用一种称为环境变量的特性来存储关于shell会话和工作环境的信息,这也是环境变量的名字的由来.该特性还允许我们把数据存储在内存中,以便于在shell中运行的程序或者脚本中访问它们.bash  shell中共有两种类型的环境变量:全局变量和本地变量. 全局环境变量在shell会话和该shell产生的任何子进程中都可见.而本地变量则只在创建它们的shell中可见.在产生需要父进程信息的子进程的应用程序中,这便体现了全局变量的重要作用.系统环境变量使用