信号的概念

信号再我们的生活中随处可见,如:古代战争中摔杯为号;现在战争中的信号弹;体育比赛中使用的信号强等,它们有相同的共性:

  1. 简单
  2. 不能携带大量信息
  3. 满足某个设定条件才发送
  4. 信号是信息的载体,Linux/UNIX环境下古老的通信方式,线下依然是主要的通信手段。

UNIX早期版本就提供了信号机制,但不可靠,信号可能丢失,Berkley和AT&T都对信号模型做了更改,增加了可靠信号的机制,但彼此不兼容,POSIX.1对可靠信号进行可标准化。

信号的机制

A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,取处理信号,处理完毕再继续执行,与硬件和中断类似(异步模式),但信号是软件层面上实现的中断,早期被称为"软中断"。

由于信号是通过软件方法实现的,其实现手段导致信号有很强的延时性,但对用户来说这个延时非常短,不易察觉。

每个进程收到的所有信号都是由内核负责发送的,并且由 内核处理。

与信号相关的事件和状态

产生信号

  1. 按键产生:Ctrl + c、Ctrl + z、Ctrl + \
  2. 系统调用产生:kill、raise、abort
  3. 软件条件产生:定时器、alarm
  4. 硬件异常产生:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
  5. 命令产生:kill命令

递达

  • 信号产生之后传递并且到达进程

未决

  • 产生和递达之间的状态,由于信号的屏蔽或阻塞导致的该状态

信号的处理方式

  1. 执行默认动作
  2. 忽略(丢弃)
  3. 捕捉(调用用户编写的信号畜栏里函数)

Linux内核的进程控制块PCB是一个结构体,除了包含进程id、状态、工作目录、用户id、组id、文件按描述符,还包含了信号相关的信息,主要是指阻塞信号集和未决信号集。

未决信号集

当信号产生后,未决信号集中描述该信号的为立刻反转为1,表示该信号处于未决状态,这一时刻非常短暂,当信号被处理后该位对应的位立刻反转为0。

信号产生后由于阻塞或屏蔽导致信号不能递达,这类信号的集合被称为未决信号集,再解除屏蔽前,信号会一直处理未决状态。

阻塞信号信号集

将某些信号加入该集合,就会对该信号进行屏蔽,当该信号被屏蔽后,再次收到该信号时,对该信号的处理将会推迟(直到该信号解除屏蔽)

查看信号

再Linux操作系统下,可以使用 kill -l 指令查看当前系统可以使用的信号有哪些

Linux系统编程之信号以及相关API函数详解_Linux系统编程

信号的四要素

  1. 编号:每个信号都有一个唯一的编号,用于在系统中标识该信号。例如,SIGHUP的编号是1,SIGINT的编号是2。
  2. 名称:每个信号都有一个对应的名称,用于在编程或命令行中引用该信号。例如,SIGHUP的名称是SIGHUP,SIGINT的名称是SIGINT。
  3. 事件:每个信号都与一个特定的事件相关联。当发生该事件时,操作系统会向相应的进程发送对应的信号。例如,SIGINT与用户在终端上按下Ctrl+C键相关联。
  4. 默认处理动作:每个信号都有一个默认的处理动作,即操作系统在收到信号时所采取的默认行为。默认处理动作可以是终止进程、忽略信号、产生核心转储文件等。可以通过编程或命令行来修改信号的默认处理动作。

信号4要素对于理解和处理信号非常重要。在编程中,可以使用信号处理函数来捕获和处理特定的信号,以便根据需要采取适当的操作。

常用信号介绍

SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。
SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
SIGPWR:关机。默认动作为终止进程。
SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

信号相关的API函数

kill()

功能
	向指定进程发送信号。可以通过指定进程ID将特定信号发送给目标进程
头文件
	#include <sys/types.h>
  #include <signal.h>
原型
	int kill(pid_t pid, int signum)
参数
	pid			要发送信号的进程ID。
	signum	要发送的信号编号,不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值
	成功 0
  失败 -1
  
	pid > 0:  发送信号给指定的进程。
	pid = 0:  发送信号给 与调用kill函数进程属于同一进程组的所有进程。
	pid < 0:  取|pid|发给对应进程组。
	pid = -1:发送给进程有权限发送的系统中所有进程。

alarm()

功能
	设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
头文件
	#include <unistd.h>
原型
	unsigned int alarm(unsigned int seconds)
参数
	seconds	要设置的定时器的时间,单位为秒
返回值
	返回之前设置的定时器剩余的时间,如果之前没有设置定时器,则返回0
  
alarm函数定时与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。

setitimer()

功能
	设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
头文件
	#include <sys/time.h>
原型
	int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value)
参数
  which	指定要设置的定时器类型,可以是ITIMER_REAL、ITIMER_VIRTUAL或ITIMER_PROF之一。
  new_value	指向一个itimerval结构的指针,用于设置新的定时器值。
  old_value	指向一个itimerval结构的指针,用于获取之前的定时器值。
返回值
	成功 0
  失败 -1

sigemptyset()

功能
	将指定的信号集清空,即将所有信号位都置为0
头文件
	#include <signal.h>
原型
	int sigemptyset(sigset_t *set)
参数
	set	一个指向sigset_t类型的指针,用于表示要清空的信号集
返回值
	成功 0
  失败 -1

sigfillset()

功能
	将指定的信号集置为1,即将所有信号位都置为1
头文件
	#include <signal.h>
原型
	int sigfillset(sigset_t *set);
参数
	set	一个指向sigset_t类型的指针,用于表示要置1的信号集
返回值
	成功 0
  失败 -1

sigaddset()

功能
	将指定的信号添加到信号集中,即将对应信号位置为1
头文件
	#include <signal.h>
原型
	int sigaddset(sigset_t *set, int signum);
参数
	set	一个指向sigset_t类型的指针,用于表示要添加信号的信号集;
  signum	要添加的信号编号。
返回值
成功	0
失败	-1

sigdelset()

功能
	将指定的信号从信号集中删除,即将对应信号位置为0
头文件
	#include <signal.h>
原型
	int sigdelset(sigset_t *set, int signum);
参数
	set		一个指向sigset_t类型的指针,用于表示要从中删除信号的信号集;
  signum	要删除的信号编号。
返回值
	成功0
  失败-1

sigismember()

功能
	判断指定的信号是否在信号集中,即判断对应信号位是否为1。
头文件
	#include <signal.h>
原型
	int sigismember(const sigset_t *set, int signum);
参数
	set		一个指向sigset_t类型的指针,用于表示要判断的信号集;
  signum	是要判断的信号编号。
返回值
  在集合中返回1
  不在集合中返回0
  出错返回-1。

sigprocmask()

功能
	设置或修改进程的信号屏蔽字,即设置进程的信号屏蔽集。
头文件
	#include <signal.h>
原型
	int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
参数
	how	指定如何修改信号屏蔽字,可以是以下几种取值之一:
      SIG_BLOCK:将set中的信号添加到当前信号屏蔽字中。
      SIG_UNBLOCK:将set中的信号从当前信号屏蔽字中移除。
      SIG_SETMASK:将当前信号屏蔽字设置为set中的值。
	set	指向sigset_t类型的指针,表示要设置的信号屏蔽集。
	oldset	指向sigset_t类型的指针,用于获取之前的信号屏蔽集。
返回值
	成功 0
  失败 -1

sigpending()

功能
	读取当前进程的未决信号集
头文件
	#include <signal.h>
原型
	int sigpending(sigset_t *set)
参数
	set	传出参数,未决信号集
返回值
	成功 0
  失败 -1

signal()

功能
	设置信号的处理函数,即指定在接收到某个信号时要执行的处理函数
头文件
	#include <signal.h>
原型
	void (*signal(int signum, void (*handler)(int)))(int);
参数
	ignum:要设置处理函数的信号编号,可以是以下几种取值之一:
        SIGABRT:程序异常终止。
        SIGFPE:算术异常。
        SIGILL:非法指令。
        SIGINT:中断信号。
        SIGSEGV:段错误。
        SIGTERM:终止信号。
  handler:指向处理函数的指针,可以是以下几种取值之一:
        SIG_DFL:默认处理函数。
        SIG_IGN:忽略信号。
自定义的信号处理函数。
返回值
	成功 指向之前信号处理函数的指针
  失败 返回SIG_ERR
  
  signal函数在设置信号处理函数时,会返回指向之前信号处理函数的指针,以便在需要恢复之前的处理函数时使用

sigaction()

功能
	设置信号的处理函数和处理方式,相比于signal函数,它提供了更多的功能和灵活性。
头文件
	#include <signal.h>
原型
	int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数
	signum:要设置处理函数的信号编号,可以是以下几种取值之一,也可以是自定义的信号编号。
	act:指向struct sigaction类型的指针,表示要设置的信号处理方式。
	oldact:指向struct sigaction类型的指针,用于获取之前的信号处理方式。
返回值
	成功 0
  失败 -1
  
struct sigaction {
    void (*sa_handler)(int); // 处理函数的指针
    void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展的处理函数的指针
    sigset_t sa_mask; // 用于阻塞的信号集
    int sa_flags; // 标志位
    void (*sa_restorer)(void); // 未使用
};

信号的捕捉性

信号捕捉特性是指在接收到信号时,进程可以通过设置信号处理函数来对信号进行处理。这种特性允许进程在接收到特定信号时执行自定义的操作,例如处理异常情况、优雅地终止进程等。

信号捕捉特性的实现依赖于操作系统提供的信号机制。当进程接收到一个信号时,操作系统会中断当前的执行流程,调用相应的信号处理函数来处理该信号。

在信号处理函数中,可以执行一些特定的操作,例如打印日志、发送通知、保存数据等。处理函数的具体逻辑由程序员根据实际需求自行编写。

在设置信号处理函数时,可以使用signal函数或sigaction函数。signal函数是较早的信号处理函数,功能相对简单,只能指定处理函数的指针。而sigaction函数提供了更多的功能和灵活性,可以指定处理函数的指针、阻塞信号集、处理方式等。

需要注意的是,在信号处理函数中,应尽量避免执行耗时操作和非可重入函数,以免影响程序的正常执行。另外,某些信号是不可捕捉的,例如SIGKILL和SIGSTOP,它们的处理函数无法被设置或修改。

信号捕捉特性在很多场景下都有重要的作用,例如:

  • 处理异常情况:当程序发生异常时,可以通过捕捉相应的信号来进行错误处理,例如打印错误信息、记录日志、进行资源清理等。
  • 优雅地终止进程:通过捕捉SIGINT信号(通常由Ctrl+C发送),可以在用户希望终止进程时进行一些清理工作,例如保存数据、关闭文件、释放资源等。
  • 进程间通信:信号可以用于进程间的简单通信,例如通过信号来触发另一个进程执行某个操作。
  • 定时任务:通过捕捉定时器信号(如SIGALRM),可以实现定时任务的功能,例如定时执行某个操作、定时检查某个状态等。

总之,信号捕捉特性为进程提供了一种灵活的机制,可以在特定的时刻对信号进行处理,从而实现更多的功能和逻辑。但在使用信号捕捉特性时,需要注意处理函数的安全性和可靠性,以及避免滥用信号机制导致不可预料的问题。

内核实现信号捕捉的过程:

Linux系统编程之信号以及相关API函数详解_Linux系统编程_02

信号的注意事项

  1. 信号处理函数的安全性:信号处理函数应该是线程安全的,因为它可能会在任何时候被调用,甚至在关键代码段中断执行。因此,应避免在信号处理函数中使用不可重入函数、共享数据、非线程安全的库函数等。
  2. 信号处理函数的执行时间:由于信号处理函数会中断当前的执行流程,因此应尽量保持信号处理函数的执行时间尽可能短。长时间的处理函数可能会导致延迟和性能问题。
  3. 信号的排队和丢失:在某些情况下,多个相同类型的信号可能会在处理函数执行之前被合并为一个信号,这可能导致信号的丢失。为了避免信号丢失,可以使用信号屏蔽集或实时信号。
  4. 不可捕捉的信号:有一些信号是不可捕捉的,例如SIGKILL和SIGSTOP。这些信号的处理函数无法被设置或修改。
  5. 信号的默认处理方式:每个信号都有一个默认的处理方式,例如终止进程、忽略信号、产生核心转储等。在设置信号处理函数之前,应该了解信号的默认处理方式,并根据需要选择合适的处理方式。
  6. 信号的可重入性:某些信号处理函数是不可重入的,即不能在信号处理函数中再次调用该信号的处理函数。这是因为信号处理函数可能会被递归调用,导致不可预料的问题。为了避免这种情况,可以使用sigaction函数的SA_RESTART标志来避免信号中断系统调用。
  7. 信号的阻塞和解除阻塞:可以使用sigprocmask函数来设置信号屏蔽集,阻塞或解除阻塞指定的信号。这可以用于控制信号在某些关键代码段的中断执行。
  8. 信号的发送和接收:可以使用kill函数向指定的进程发送信号,也可以使用raise函数向自身发送信号。在接收信号时,可以使用signal函数或sigaction函数来设置信号处理函数。

使用信号时需要注意处理函数的安全性、执行时间和可重入性,同时要了解信号的默认处理方式和不可捕捉的信号。合理地使用信号处理机制可以帮助我们实现一些重要的功能和逻辑。