什么是信号?

信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

“中断”在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断”了,我去签收快递( 处理中断 ),处理完成后,再继续玩我的游戏。这里我们学习的“信号”就是属于这么一种“中断”。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。

Linux 可使用命令:kill -l("l" 为字母),查看相应的信号。

列表中,编号为 1 ~ 31 的信号为传统 UNIX 支持的信号,是不可靠信号(非实时的),编号为 32 ~ 63 的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。非可靠信号一般都有确定的用途及含义,  可靠信号则可以让用户自定义使用。更多详情,请看《Linux信号列表》。

信号的产生方式

1)当用户按某些终端键时,将产生信号。

终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT,终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT,终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。

2)硬件异常将产生信号。

除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。

3)软件异常将产生信号。

当检测到某种软件条件已发生,并将其通知有关进程时,产生信号。

4)调用 kill() 函数将发送信号。

注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

5)运行 kill 命令将发送信号。

此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

信号的常用操作

发送信号

所需头文件:

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int signum);

功能:

给指定进程发送信号。

注意:使用 kill() 函数发送信号,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户。

参数:

pid: 取值有 4 种情况:

pid > 0: 将信号传送给进程 ID 为pid的进程。

pid = 0: 将信号传送给当前进程所在进程组中的所有进程。

pid = -1: 将信号传送给系统内所有的进程。

pid < -1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。

signum: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

返回值:

成功:0

失败:-1

下面为测试代码,本来父子进程各自每隔一秒打印一句话,3 秒后,父进程通过 kill() 函数给子进程发送一个中断信号 SIGINT( 2 号信号),最终,子进程结束,剩下父进程在打印信息:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main(int argc, char *argv[])
{pid_t pid;int i = 0;pid = fork(); // 创建进程if( pid < 0 ){ // 出错perror("fork");}if(pid == 0){ // 子进程while(1){printf("I am son\n");sleep(1);}}else if(pid > 0){ // 父进程while(1){printf("I am father\n");sleep(1);i++;if(3 == i){// 3秒后kill(pid, SIGINT); // 给子进程 pid ,发送中断信号 SIGINT// kill(pid, 2); // 等级于kill(pid, SIGINT);}}}return 0;
}

运行结果如下:

等待信号

所需头文件:

#include <unistd.h>

int pause(void);

功能:

等待信号的到来(此函数会阻塞)。将调用进程挂起直至捕捉到信号为止,此函数通常用于判断信号是否已到。

参数:

无。

返回值:

直到捕获到信号才返回 -1,且 errno 被设置成 EINTR。

测试代码如下:

#include <unistd.h>
#include <stdio.h>int main(int argc, char *argv[])
{printf("in pause function\n");pause();return 0;
}

没有产生信号前,进程一直阻塞在 pause() 不会往下执行,假如,我们按“Ctrl+c”,pause() 会捕获到此信号,中断当前进程。

处理信号

一个进程收到一个信号的时候,可以用如下方法进行处理:

1)执行系统默认动作

对大多数信号来说,系统默认动作是用来终止该进程。

2)忽略此信号

接收到此信号后没有任何动作。

3)执行自定义信号处理函数

用用户定义的信号处理函数处理该信号。

注意:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

产生一个信号,我们可以让其执行自定义信号处理函数。假如有函数 A, B, C,我们如何确定信号产生后只调用函数 A,而不是函数 B 或 C。这时候,我们需要一种规则规定,信号产生后就调用函数 A,就像交通规则一样,红灯走绿灯行,信号注册函数 signal() 就是做这样的事情

所需头文件:

#include <signal.h>

typedef void (*sighandler_t)(int);// 回调函数的声明
sighandler_t signal(int signum,sighandler_t handler);

功能:

注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞

参数:

signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

handler: 取值有 3 种情况:

SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数,如:fun

回调函数的定义如下:

void fun(int signo)

{

// signo 为触发的信号,为 signal() 第一个参数的值

}

注意信号处理函数应该为可重入函数,关于可重入函数的更多详情,请《浅谈可重入函数与不可重入函数》。

返回值:

成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型

失败:返回 SIG_ERR

示例一:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 信号处理函数
void signal_handler(int signo)
{if(signo == SIGINT){printf("recv SIGINT\n");}else if(signo == SIGQUIT){printf("recv SIGQUIT\n");}
}int main(int argc, char *argv[])
{printf("wait for SIGINT OR SIGQUIT\n");/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */// 信号注册函数signal(SIGINT, signal_handler);signal(SIGQUIT, signal_handler);// 等待信号pause();pause();return 0;
}

在终端里敲“Ctrl+c”或“Ctrl+\”,自动调用其指定好的回调函数 signal_handler():

示例二:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 回调函数的声明
typedef void (*sighandler_t)(int);void fun1(int signo)
{printf("in fun1\n");
}void fun2(int signo)
{printf("in fun2\n");
}int main(int argc, char *argv[])
{sighandler_t previous = NULL;// 第一次返回 NULLprevious = signal(SIGINT,fun1); if(previous == NULL){printf("return fun addr is NULL\n");}// 下一次返回此信号上一次注册的信号处理函数的地址。previous = signal(SIGINT, fun2);if(previous == fun1){printf("return fun addr is fun1\n");}// 还是返回 NULL,因为处理的信号变了previous = signal(SIGQUIT,fun1);if(previous == NULL){printf("return fun addr is NULL\n");}return 0;
}

运行结果如下:

信号集与信号阻塞集

信号集

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集(信号的集合)。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友

信号集是用来表示多个信号的数据类型(sigset_t),其定义路径为:/usr/include/i386-linux-gnu/bits/sigset.h。

信号集相关的操作主要有如下几个函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigismember(const sigset_t *set, int signum);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

以上几个函数的用法都是比较简单,这里就不一一介绍,我们通过一个例子来学习其用法:

#include <signal.h>
#include <stdio.h>int main(int argc, char *argv[])
{sigset_t set;  // 定义一个信号集变量int ret = 0;sigemptyset(&set); // 清空信号集的内容// 判断 SIGINT 是否在信号集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGINT);if(ret == 0){printf("SIGINT is not a member of set \nret = %d\n", ret);}sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 setsigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set// 判断 SIGINT 是否在信号集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGINT);if(ret == 1){printf("SIGINT is a member of set \nret = %d\n", ret);}sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除// 判断 SIGQUIT 是否在信号集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGQUIT);if(ret == 0){printf("SIGQUIT is not a member of set \nret = %d\n", ret);}return 0;
}

运行结果如下:

信号阻塞集(屏蔽集、掩码)

信号阻塞集也称信号屏蔽集、信号掩码。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。

所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。

我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。

所需头文件:

#include <signal.h>

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

功能:

检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。

参数:

how: 信号阻塞集合的修改方法,有 3 种情况:

SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。

set: 要操作的信号集地址。

若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。

oldset: 保存原先信号阻塞集地址

返回值:

成功:0,

失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。

注意:不能阻塞 SIGKILL 和 SIGSTOP 等信号,但是当 set 参数包含这些信号时 sigprocmask() 不返回错误,只是忽略它们。另外,阻塞 SIGFPE 这样的信号可能导致不可挽回的结果,因为这些信号是由程序错误产生的,忽略它们只能导致程序无法执行而被终止。

示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>int main(int argc, char *argv[])
{sigset_t set; // 信号集合int i = 0;sigemptyset(&set); // 清空信号集合sigaddset(&set, SIGINT); // SIGINT 加入 set 集合while(1){// set 集合加入阻塞集,在没有移除前,SIGINT 会被阻塞sigprocmask(SIG_BLOCK, &set, NULL);for(i=0; i<5; i++){printf("SIGINT signal is blocked\n");sleep(1);}// set 集合从阻塞集中移除// 假如 SIGINT 信号在被阻塞时发生了// 此刻,SIGINT 信号立马生效,中断当前进程sigprocmask(SIG_UNBLOCK, &set, NULL);for(i=0; i<5; i++){printf("SIGINT signal unblocked\n");sleep(1);}}return 0;
}

可靠信号的操作

从 UNIX 系统继承过来的信号(SIGHUP~SIGSYS,前 32 个)都是不可靠信号,不支持排队(多次发送相同的信号, 进程可能只能收到一次,可能会丢失)。

SIGRTMIN 至 SIGRTMAX 的信号支持排队(发多少次, 就可以收到多少次, 不会丢失),故称为可靠信号。

可靠信号就是实时信号,非可靠信号就是非实时信号。

signal() 函数只能提供简单的信号安装操作,使用 signal() 函数处理信号比较简单,只要把要处理的信号和处理函数列出即可。

signal() 函数主要用于前面 32 种不可靠、非实时信号的处理,并且不支持信号传递信息。

Linux 提供了功能更强大的 sigaction() 函数,此函数可以用来检查和更改信号处理操作,可以支持可靠、实时信号的处理,并且支持信号传递信息。

下面我们一起学习其相关函数的使用。

所需头文件:

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);

功能:

给指定进程发送信号。

参数:

pid: 进程号。

sig: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

value: 通过信号传递的参数。

union sigval 类型如下:

union sigval
{int   sival_int;void *sival_ptr;
};

返回值:

成功:0

失败:-1

int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact );

功能:

检查或修改指定信号的设置(或同时执行这两种操作)。

参数:

signum:要操作的信号。

act:   要设置的对信号的新处理方式(设置)。

oldact:原来对信号的处理方式(设置)。

如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式(设置)存入 oldact。

返回值:

成功:0

失败:-1

信号设置结构体:

struct sigaction
{/*旧的信号处理函数指针*/void (*sa_handler)(int signum) ;/*新的信号处理函数指针*/void (*sa_sigaction)(int signum, siginfo_t *info, void *context);sigset_t sa_mask;/*信号阻塞集*/int sa_flags;/*信号处理的方式*/
};

sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:

SIG_IGN:忽略该信号

SIG_DFL:执行系统默认动作

处理函数名:自定义信号处理函数

sa_mask:信号阻塞集

sa_flags:用于指定信号处理的行为,它可以是一下值的“按位或”组合:

SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)

SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。

SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。

SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。

SA_RESETHAND:信号处理之后重新设置为默认的处理方式。

SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

信号处理函数:
void (*sa_sigaction)( int signum,  siginfo_t *info,  void *context );
参数说明:

signum:信号的编号。
info:记录信号发送进程信息的结构体,进程信息结构体路径:/usr/include/i386-linux-gnu/bits/siginfo.h,其结构体详情请点此链接。
context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文,其结构体详情点此链接。

下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。

发送信号示例代码如下:

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>/*******************************************************
*功能:     发 SIGINT 信号及信号携带的值给指定的进程
*参数:     argv[1]:进程号argv[2]:待发送的值(默认为100)
*返回值:    0
********************************************************/
int main(int argc, char *argv[])
{if(argc >= 2){pid_t pid,pid_self;union sigval tmp;pid = atoi(argv[1]); // 进程号if( argc >= 3 ){tmp.sival_int = atoi(argv[2]);}else{tmp.sival_int = 100;}// 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去sigqueue(pid, SIGINT, tmp);pid_self = getpid(); // 进程号printf("pid = %d, pid_self = %d\n", pid, pid_self);}return 0;
}

接收信号示例代码如下:

#include <signal.h>
#include <stdio.h>// 信号处理回电函数
void signal_handler(int signum, siginfo_t *info, void *ptr)
{printf("signum = %d\n", signum); // 信号编号printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}int main(int argc, char *argv[])
{struct sigaction act, oact;act.sa_sigaction = signal_handler; //指定信号处理回调函数sigemptyset(&act.sa_mask); // 阻塞集为空act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler// 注册信号 SIGINTsigaction(SIGINT, &act, &oact);while(1){printf("pid is %d\n", getpid()); // 进程号pause(); // 捕获信号,此函数会阻塞}return 0;
}

两个终端分别编译代码,一个进程接收,一个进程发送,运行结果如下:

【linux系统编程】进程间通信:信号中断处理相关推荐

  1. linux系统发送信号的系统调用是,linux系统编程之信号:信号发送函数sigqueue和信号安装函数sigaction...

    信号发送函数sigqueue和信号安装函数sigaction sigaction函数用于改变进程接收到特定信号后的行为. sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然 ...

  2. 【Linux系统编程】信号 (下)

    00. 目录 文章目录 00. 目录 01. 信号集 02. 信号阻塞集 03. sigaction函数 04. 附录 01. 信号集 为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处 ...

  3. 【Linux系统编程】信号 (上)

    00. 目录 文章目录 00. 目录 01. 信号概述 02. 信号编号 03. 信号产生方式 04. kill发送信号 05. pause等待信号 06. 信号处理方式 07. 信号处理函数 08. ...

  4. linux系统编程之信号(四):信号的捕捉与sigaction函数

    一.内核如何实现信号的捕捉 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号.由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 1. 用户程序注册了SI ...

  5. linux系统编程之信号(一):信号基本概述

    一.为了理解信号,先从我们最熟悉的场景说起: 1. 用户输入命令,在Shell下启动一个前台进程. 2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断. 3. 如果CPU当前正在执行这个进程的代 ...

  6. Linux系统编程(二)–信号

    文章目录 1 Hello signal 1.1 hello signal 程序 1.2 向 hello signal 发信号 1.3 招待你的"客人" 1.3.1 signal 函 ...

  7. Linux系统编程—进程间通信—信号量

    信号量 信号量是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用.在进入一个关键代码段之前,线程必须获取一个信号量:一旦该关键代码段完成了,那么该线程必须释放信号量.其它想 ...

  8. linux系统编程之信号(signal)的使用方法案例

    信号处理过程: 中断源->中断屏蔽->保护现场->中断处理程序->中断恢复 信号称为软中断 //kill -l 查看所有signal信号 共计64个信号 //man 7 sig ...

  9. Linux系统编程47 信号 - setitimer(),优先使用setitimer()计时

    NAMEgetitimer, setitimer - get or set value of an interval timerSYNOPSIS#include <sys/time.h>i ...

  10. Linux系统编程——进程间通信:命名管道(FIFO)

    命名管道的概述 无名管道,由于没有名字,只能用于亲缘关系的进程间通信(更多详情,请看<无名管道>).为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道.FIFO 文件. 命名管道( ...

最新文章

  1. PHP教程:WebService最常用的两种方法
  2. 【无标题】服务机器人---SLAM算法之gmapping
  3. 注册域名的时候一定要注意的事项
  4. vmware + centos6设置桥接模式
  5. wechat code miniprogram 没有找到可以构建的 NPM 包
  6. java反序列化异常接不到_由Java对象反序列化异常想到的
  7. directx和opengl 电子白板
  8. FreeRTOS内核实现07(完):支持时间片
  9. doT js模板入门
  10. Palm应用开发之一开发环境搭建
  11. Ps 初学者教程,如何用文字增强您的照片?
  12. 求一个任意实数的算术平方根的算法设计_感恩!面试必刷100道算法题
  13. MessageCenterUI.exe - 无法找到入口 无法定位程序输入点
  14. 将unity地形转换为mesh
  15. IT帮2019年2月线下活动【定义工作,解读自我】之站桩练习
  16. [拼搏到底之龟兔赛跑--Day 16]一套兽医作家写的小说--“大地”系列
  17. c语言海明校验码编码,海明校验码的编码规则有哪些?
  18. 2022年ccpc威海站
  19. 一维数组、二维数组的大小、长度与偏移
  20. django 实现显示图片功能

热门文章

  1. 思科pix防火墙配置实例大全
  2. 新闻视频 36:整合首页 用到 Repeater 主要用gridview /gridview去掉边框用到 BorderWidth=”0” inner join和 left...
  3. 安卓开发要学多久_华为发布鸿蒙2.0,和安卓相似度达80%「西瓜视频」
  4. 手机和工业计算机运算能力对比,手机CPU跟电脑CPU到底差多少?
  5. linux script $,linux – 这在shell脚本中是什么意思SCRIPTNAME =“${0 ## * /}”?
  6. linux 跨IP拷贝命令 scp
  7. numpy---one
  8. linux系统下开机启动流程
  9. [转]Github 中被 Fork 最多的库
  10. 谢瑛姿(帮别人名字作诗)