Linux应用开发5 信号(软中断,处理异步请求,进程间通讯)

发布于:2022-11-27 ⋅ 阅读:(742) ⋅ 点赞:(0)
        信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够 打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号的目的是用来通信的  
一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC )的原始形式
硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为 0 、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT ),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT ),通过这个方法可以暂停当前前台运行的进程。
进程调用 kill() 系统调用可将任意信号发送给另一个进程或进程组。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是 root 超级用户。
用户可以通过 kill 命令将信号发送给其它进程。 kill 命令想必大家都会使用,通常我们会通过 kill命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx" 来杀死 PID xxx 的进程。 kill命令其内部的实现原理便是通过 kill() 系统调用来完成的。
发生了软件事件,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为 0 、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)。
信号由谁处理、怎么处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是 SIGKILL SIGSTOP ,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理, Linux 系统提供了 signal() 系统调用可用于注册信号的处理函数,将会在后面向大家介绍。
执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,8.3 小节中对此有进行介绍。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
信号本质上是 int 类型数字编号
信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号
但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名 (也就是宏定义)

信号的分类

从可靠性方面将信号分为可靠信号与不可靠信号;

一开始的信号,都属于不可靠信号(信号值小于 SIGRTMIN 34 的信号都是不可靠信号),因为可能会发生信号丢失,或者信号使用错误“必须加 signal()”,但原来的又不好改动,因此后续新添加了很多信号,这些信号称为可靠信号。 (SIGRTMIN~SIGRTMAX)
在 Linux 系统下使用"kill -l"命令可查看到所有信号

从实时性方面将信号分为实时信号与非实时信号; 

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。

常见信号

进程对信号的处理

当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作
signal() 函数
signal() 函数是 Linux 系统下设置 信号处理方式 最简单的接口,可将信号的处理方式设置为捕获信号忽略信号以及系统默认操作
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
使用该函数需要包含头文件 <signal.h>

signum 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN SIG_DFL SIG_IGN 表示此进程需要忽略该信号, SIG_DFL 则表示设置为系统默认操作。 sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    sig_t ret = NULL;

    ret = signal(SIGINT, (sig_t)sig_handler);
    if (SIG_ERR == ret) {
        perror("signal error");
        exit(-1);
}
    /* 死循环 */
    for ( ; ; ) { }
    exit(0);
/*自定义了一个信号处理函数 sig_handler,并将信号SIGINT(ctrl+c)绑定到该信号处理函数上*/
}

sigaction()函数

推荐大家使用sigaction()函数,sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
使用该函数需要包含头文件<signal.h>。
 

signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
oldact: oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数oldact 不为 NULL, 则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值: 成功返回 0;失败将返回-1,并设置 errno

 

⚫ sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,稍后介绍该数据结构; sa_handler 和sa_sigaction 是互斥的,不能同时设置, 对于标准信号来说, 使用 sa_handler 就可以了,可通过SA_SIGINFO 标志进行选择。
⚫ sa_mask: 参数 sa_mask 定义了一组信号, 当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。 当进程在执行信号处理函数期间, 可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现, 如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。 在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号
时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成,信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。

⚫ sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志 (多个标志使用位或" | "组合):
SA_NOCLDSTOP
如果signum为SIGCHLD,则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU 中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT
如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程。
SA_NODEFER
不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞。
SA_RESETHAND
执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。
SA_RESTART
被信号中断的系统调用,在信号处理完成之后将自动重新发起。
SA_SIGINFO
如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler,关于 sa_sigaction信号处理函数的参数信息。  
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
 printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 //注意此处与signal的不同之处在于这里是 取地址
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 /* 死循环 */
 for ( ; ; ) { }
 exit(0);
}
一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

向进程发送信号

进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户 root 进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID
kill() 函数
kill() 系统调用可将信号发送给指定的进程或进程组中的每一个进程
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
使用该函数需要包含头文件 <sys/types.h> <signal.h>
pid 参数 pid 为正数的情况下,用于指定接收此信号的进程 pid ;除此之外,参数 pid 也可设置为 0 或 -1 以及小于 -1 等不同值,稍后给说明。
如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
如果 pid 等于 0 ,则将 sig 发送到当前进程的进程组中的每个进程
如果 pid 等于 -1 ,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1 init )除外。
如果 pid 小于 -1 ,则将 sig 发送到 ID -pid 的进程组中的每个进程。
sig 参数 sig 指定需要发送的信号,也可设置为 0 ,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值: 成功返回 0 ;失败将返回 -1 ,并设置 errno
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
 int pid;
 /* 判断传参个数 */
 if (2 > argc)
 exit(-1);
 /* 将传入的字符串转为整形数字 */
 pid = atoi(argv[1]);
 printf("pid: %d\n", pid);
 /* 向 pid 指定的进程发送信号 */
 if (-1 == kill(pid, SIGINT)) {
 perror("kill error");
 exit(-1);
 }
 exit(0);
}
//与signational例程配合

raise()函数

有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为 C 库函数):

#include <signal.h>
int raise(int sig);
使用该函数需要包含头文件 <signal.h>
sig 需要发送的信号。
返回值: 成功返回 0 ;失败将返回非零值。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
 printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 for ( ; ; ) {
 /* 向自身发送 SIGINT 信号 */
 if (0 != raise(SIGINT)) {
 printf("raise error\n");
 exit(-1);
 }
 sleep(3); // 每隔 3 秒发送一次
 }
 exit(0);
}

alarm()函数

使用 alarm() 函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM 信号,其函数原型如下所示:
每个进程只能设置一个 alarm 闹钟
需要注意的是 alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm() 函数设置定时器。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds 设置定时时间,以秒为单位;如果参数 seconds 等于 0 ,则表示取消之前设置的 alarm 闹钟。
返回值: 如果在调用 alarm() 时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm() 函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
 puts("Alarm timeout");
 exit(0);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int second;
 /* 检验传参个数
    argc 为传入main函数的参数个数 */
 if (2 > argc)
 exit(-1);
 /* 为 SIGALRM 信号绑定处理函数 */
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
 exit(-1);
 }
 /* 启动 alarm 定时器 */
 second = atoi(argv[1]);
 printf("定时时长: %d 秒\n", second);
 alarm(second);
 /* 循环 */
 for ( ; ; )
 sleep(1);
 exit(0);
}

pause()函数

pause() 系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause() 才返回,在这种情况下, pause() 返回 -1 ,并且将 errno 设置为 EINTR
#include <unistd.h>
int pause(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
 puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int second;
 /* 检验传参个数 */
 if (2 > argc)
 exit(-1);
 /* 为 SIGALRM 信号绑定处理函数 */
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 if (-1 == sigaction(SIGALRM, &sig, NULL)) {
 perror("sigaction error");
 exit(-1);
 }
 /* 启动 alarm 定时器 */
 second = atoi(argv[1]);
 printf("定时时长: %d 秒\n", second);
 alarm(second);
 /* 进入休眠状态 */
 pause();
 puts("休眠结束");
 exit(0);
}

信号集 

通常我们需要有一个能表示多个信号(一组信号)的数据类型 --- 信号集( signalset ),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction() 函数、 sigprocmask() 函数、 sigpending() 函数等。
初始化信号集
sigemptyset() sigfillset() 用于初始化信号集。
sigemptyset() 初始化信号集,使其不包含任何信号;
sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号)
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
使用这些函数需要包含头文件 <signal.h>
set 指向需要进行初始化的信号集变量。
返回值: 成功返回 0 ;失败将返回 -1 ,并设置 errno
使用示例
初始化为空信号集:
sigset_t sig_set;
sigemptyset(&sig_set);
初始化信号集,使其包含所有信号:
sigset_t sig_set;
sigfillset(&sig_set);

 向信号集中添加/删除信号

分别使用 sigaddset() sigdelset() 函数向信号集中添加或移除一个信号
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
函数参数和返回值含义如下:
set 指向信号集。
signum 需要添加 / 删除的信号。
返回值: 成功返回 0 ;失败将返回 -1 ,并设置 errno
使用示例
向信号集中添加信号:
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
从信号集中移除信号:
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);

测试信号是否在信号集中

使用 sigismember()函数可以测试某一个信号是否在指定的信号集中

#include <signal.h>
int sigismember(const sigset_t *set, int signum);
set 指定信号集。
signum 需要进行测试的信号。
返回值: 如果信号 signum 在信号集 set 中,则返回 1 ;如果不在信号集 set 中,则返回 0 ;失败则返回 -1,并设置 errno
使用示例
判断 SIGINT 信号是否在 sig_set 信号集中:
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts(" 信号集中包含 SIGINT 信号 ");
else if (!sigismember(&sig_set, SIGINT))
puts(" 信号集中不包含 SIGINT 信号 ");
获取信号的描述信息
Linux 下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于 sys_siglist 数组中
从图中打印信息可知,这个描述信息其实非常简洁,没什么太多的信息。
使用 sys_siglist 数组需要包含<signal.h>头文件。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
 printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
 printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
 exit(0);
}

strsignal() 函数
除了直接使用 sys_siglist 数组获取描述信息之外,还可以使用 strsignal() 函数。较之于直接引用 sys_siglist 数组,更推荐使用 strsignal() 函数
#include <string.h>
char *strsignal(int sig);
使用 strsignal() 函数需要包含头文件 <string.h> ,这是一个库函数。
调用 strsignal() 函数将会获取到参数 sig 指定的信号对应的描述信息,返回该描述信息字符串的指针;
函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回 "Unknown signal" 信息
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main ( void )
{
printf ( "SIGINT 描述信息 : %s\n" , strsignal ( SIGINT ));
printf ( "SIGQUIT 描述信息 : %s\n" , strsignal ( SIGQUIT ));
printf ( "SIGBUS 描述信息 : %s\n" , strsignal ( SIGBUS ));
printf ( " 编号为 1000 的描述信息 : %s\n" , strsignal ( 1000 ));
exit ( 0 );
}
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到