目录
信号(软中断信号),用于通知进程发生了异步事件(它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的)。
信号是进程间通信机制的唯一异步通信机制,一个进程不必通过任何操作来等待信号的到达。当进程接收到一个信号时,也会相应地采取一些行动。
生成:一个信号的产生。
捕获:进程接收到一个信号。
在linux系统中,信号可能是由于系统中某些错误而产生(如内存段冲突、浮点处理器错误或非法指令等,由shell和终端处理器生成并且引起中断),也可以是某个进程主动生成的一个信号(可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获到这个信号就会按照程序进行相应操作并且去处理它)。
无论何种情况,他们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。
进程间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
kill -l可查看系统支持的信号类型。

1~31信号值的信号属性为非实时信号(不可靠信号),34~64信号值的信号属性为实时信号(可靠信号),总共62个信号类型。具体信号类型意思可自行百度。
一般而言,信号的响应处理过程为:
如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。
如果该信号被捕获,那么将进一步判断捕获的类型(如果设置为响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。)最后才执行信号的默认处理。
非实时信号,主要是因为这类信号不支持排队,因此信号可能会丢失。比如发送多次相同的信号,进程只能收到一次,也只会处理一次,因此剩下的信号将被丢弃。
实时信号,主要是因为这类信号支持排队,发送了多少个信号给进程,进程就会处理多少次。
一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理(一般是在中断返回时,或常见是内核态返回用户态时)。
即使收到信号,进程也不一定会立即去处理它,因为系统不会为了处理一个信号而把当前正在运行的进程挂起,因为这样子系统的资源消耗过大。如果不是紧急信号,是不会立即处理的,所以系统一般都会选择在内核态切换回用户态时处理信号,比如有时候进程处于休眠状态,但是又收到一个信号,于是系统就得把信号存储在进程唯一的进程PCB中。
生成信号的事件:程序错误、外部事件、显式请求。
程序错误:零作除数、非法存储访问等,这种情况通常是由硬件而不是由Linux内核检测到的,但由内核向发生此错误的那个进程发送相应的信号。
外部事件:当用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时,内核会生成一个信号通知进程。
显式请求:使用kill()函数允许进程发送任何信号给其他进程或进程组。
信号的生成既可以是同步的,也可以是异步的。
同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。
异步信号是接收过程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生时去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)
无论是同步还是异步信号,当信号发生时,我们可以告诉linux内核采取以下3钟动作中的任意一种:
忽略信号。大部分信号都可以被忽略,但有两个除外:SIGSTOP和SIGKILL,因为是为了给超级用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。
捕获信号。这种处理是想要告诉Linux内核,当信号出现时调用专门提供的一个函数(信号处理函数,专门对产生信号的事件作出处理)。
让信号默认动作起作用。系统为每种信号规定了一个默认动作,这种动作由Linux内核来完成,有以下几种可能的默认动作:
终止进程并且生成内存转储文件,即写出进程的地址空间和寄存器上下文到进程当前目录下名为core的文件中。
终止进程但不生成core文件。
忽略信号
暂停进程
若进程为暂停状态,恢复进程,否则将忽略信号
当执行一个不断while循环的函数时,通过ctrl+c键,可以通过系统默认的处理方式去终止进程。很多时候使用信号只是向通知进程而不是终止进程,或者在终止前想进行某些收尾工作,因此需要去捕获这个信号,然后去处理它。
主要是捕获信号,可以改变进程中对信号的默认动作。
使用signal()函数时,需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数,或者设置忽略某个信号,才能改变信号的默认动作,这个过程称为“信号的捕获”。
可以重复进行对一个信号的捕获,不过signal()函数将会返回前一次设置的信号响应函数指针。
- #include
- typedef void (*sighandler_t)(int);
- sighandler_t signal(int signum, sighandler_t handler);
- /*
- signum:指定捕获的信号。如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL
- handler:函数指针,传递接收到的信号值。需要用户自定义处理信号的方式,也可以用宏
- SIG_IGN:忽略该信号
- SIG_DFL:采用系统默认方式处理信号
- */
准备捕获或忽略的信号由参数signum指出,接收到指定的信号后将要调用的函数由参数handler指出。
signal()函数会返回一个sighandler_t类型的函数指针,这是因为调用signal()函数修改了信号的动作,需要返回之前的信号处理动作。
signal()函数如果调用处理程序导致信号被阻塞,则从处理程序返回后,信号将被解除阻塞。无法捕获或忽略信号SIGKILL和SIGSTOP。
- #include
- #include
- #include
- #include
- #include
- #include
-
- void signal_handler(int sig)
- {
- printf("the signal number:%d\n", sig);
-
- if(sig == SIGINT){
- printf("get SIGINT!!!\n");
- // 回调中恢复该信号默认动作,回到进程继续往下执行
- signal(SIGINT, SIG_DFL);
- }
- }
-
- int main(void)
- {
- // 改变了CTRL+C的处理方式,不再是终止进程,而是进入回调做处理
- signal(SIGINT, signal_handler);
-
- while(1)
- {
- printf("waiting for the SIGINT signal, please enter : ctrl + c\n");
- sleep(1);
- }
-
- exit(0);
- }
照旧

- #include
- int sigaction(int signum, const struct sigaction *act, struct sigaction *lodact);
- /*
- signum:指定捕获的信号。如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL
- act:结构体。
- oldact:返回原有的信号处理参数,一般设置为NULL即可。
- */
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};sa_handler:函数指针,捕获信号后的处理函数,传入的是信号的值(int),这个函数就是标准的信号处理函数。
sa_sigaction:函数指针,扩展捕获信号后的处理函数,比sa_handler函数指针复杂。不用同时使用sa_handler和sa_sigaction,因为这两个处理函数有共同的联合体。
sa_mask:信号掩码,指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号,在进程响应信号期间被临时阻塞。除非使用SA_NODEFER标志,否则即使是当前正在处理的响应的信号再次到来时被阻塞。
sa_flags:指定一系列用于修改信号处理过程动作的标志.
SA_NOCLDSTOP:如果signum是SIGCHLD,则在子进程停止或恢复时,不会传入信号给调用sigaction()函数的进程。即当系统接收到SIGSTOP、SIGTSTO、SIGTTIN或SIGTTOU中的一种时或接收到SIGCONT(恢复)时,父进程不会收到通知。仅当为SIGCHLD建立信号处理程序时,此标志才有意义。
SA_NOCLDWAIT:表示父进程在它的子进程终止时不会收到SIGCHLD信号,这时子进程终止则不会称为僵尸进程。
SA_NODEFER:不用阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效,即在信号处理函数执行期间仍能接收到这个信号,仅当建立信号处理程序时,此标志才有意义。
SA_RESETHAND:信号处理后重新设置为默认的处理方式。
SA_SIGINFO:指示使用sa_sigaction成员而不是使用sa_handler成员作为信号处理函数。
当sa_flags指定SA_SIGINFO标志时,信号处理程序地址将通过sg_sigaction字段传递。该处理程序采用三个参数:
void handler(int sig, siginfo_t *info, void *ucontext){}
info指向siginfo_t的指针,它是一个包含有关信号的更多信息的结构。
siginfo_t { int si_signo; /* 信号数值 */ int si_errno; /* 错误值 */ int si_code; /* 信号代码 */ int si_trapno; /* 导致硬件生成信号的陷阱号,在大多数体系结构中未使用 */ pid_t si_pid; /* 发送信号的进程 ID */ uid_t si_uid; /* 发送信号的真实用户 ID */ int si_status; /* 退出值或信号状态 */ clock_t si_utime; /* 消耗的用户时间 */ clock_t si_stime; /* 消耗的系统时间 */ sigval_t si_value; /* 信号值 */ int si_int; /* POSIX.1b 信号 */ void *si_ptr; int si_overrun; /* 计时器溢出计数 */ int si_timerid; /* 计时器 ID */ void *si_addr; /* 导致故障的内存位置 */ long si_band; int si_fd; /* 文件描述符 */ short si_addr_lsb; /* 地址的最低有效位 (从 Linux 2.6.,→32 开始存在) */ void *si_lower; /* 地址冲突时的下限 */ void *si_upper; /* 地址冲突时的上限 (从 Linux 3.19开始存在) */ int si_pkey; /* 导致的 PTE 上的保护密钥 */ void *si_call_addr; /* 系统调用指令的地址 */ int si_syscall; /* 尝试的系统调用次数 */ unsigned int si_arch; /* 尝试的系统调用的体系结构 */ }成员变量绝大部分几乎用不到,因此如果只是对信号的简单处理,可以直接使用sa_handler处理,无需配置siginfo_t这些信息。
- #include
- #include
- #include
- #include
- #include
- #include
-
- void signal_handler(int sig)
- {
- printf("the signal number:%d\n", sig);
-
- if(sig == SIGINT){
- printf("get SIGINT!!!\n");
- // 信号自动恢复为默认处理函数
- }
- }
-
- int main(void)
- {
- struct sigaction act;
- act.sa_handler = signal_handler;
- // 清空进程屏蔽的信号集,即在信号处理时不会屏蔽任何信号
- sigemptyset(&act.sa_mask);
- // 在处理完信号后恢复默认信号处理
- act.sa_flags = SA_RESETHAND;
-
- sigaction(SIGINT, &act, NULL);
-
- while(1)
- {
- printf("waiting for the SIGINT signal, please enter : ctrl + c\n");
- sleep(1);
- }
-
- exit(0);
- }
照旧

不仅可以终止进程(SIGKILL信号),也可以向进程发送其他信号。
进程可以通过调用kill()函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用将失败,失败的常用原因是目标进程由另一个用户所拥有。因此想要发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。
kill()函数会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为INVAL)、发送进程权限不够(errno设置为EPERM)、目标进程不存在(errno设置为ESRCH)等情况。
- #include
- #include
- int kill(pid_t pid, int sig);
- /*
- pid:
- >1:将信号sig发送到进程ID值为pid指定的进程
- =0:信号被发送到所有和当前进程在同一进程组的进程
- =-1:将信号sig发送到系统中所有的进程,但进程1(init)除外
- <-1:将信号sig发送给进程组号为-pid(pid绝对值)的每一个进程
- sig:要发送的信号值
- 返回值:
- 0:发送成功
- -1:发送失败
- */
和kill()函数类似。区别在于,raise()函数只是进程向自身发送信号,而没有向其他进程发送信号。相当于kill(getpid(), sig)等同于raise(sig)。
- #include
- int raise(int sig);
- /*
- sig:发送的信号值
- 返回值:
- 0:发送成功
- -1:发送失败
- */
发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况。
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main(void)
- {
- pid_t pid;
- int ret;
-
- if((pid = fork()) < 0){
- printf("fork error!\n");
- exit(1);
- }else if(pid == 0){
- printf("the child pid:%d\n waiting for any signal\n", getpid());
- // 子进程停在这里
- raise(SIGSTOP);
- exit(0);
- }else{
- // 等待一下,确保子进程先执行
- sleep(1);
- // 父进程暂时停止执行,等待子进程发出信号或结束
- // WNOHANG:若指定的子进程还没有结束,返回0,不等待(不阻塞);若结束,返回子进程PID
- // 代码意思是如果子进程还没有结束,则使用kill()函数杀死子进程
- if((waitpid(pid, NULL, WNOHANG)) == 0){
- if((ret == kill(pid, SIGKILL)) == 0){
- printf("this is the parent, kill %d", pid);
- }
- }
- }
-
- waitpid(pid, NULL, 0);
-
- exit(0);
- }
照旧

闹钟函数,可以在进程中设置一个定时器,当定时器指定的时间seconds到来时,它就像进程发送SIGALARM信号。
- #include
- unsigned int alarm(unsigned int seconds);
如果在seconds秒内再次调用了alarm()函数设置了新的函数,则新的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代。
它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。
如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。
- #include
- #include
- #include
- #include
- #include
-
- int main(void)
- {
- // 设置5s的闹钟,让SIGALARM信号在5秒后传达给当前进程
- alarm(5);
-
- // 让进程休眠20秒
- sleep(20);
-
- // 当产生SIGALARM信号时,由于没有做捕获处理,系统会调用该信号的默认处理函数,即exit(0)并且自动打印Alarm clock或闹钟
- // 因此以下代码无法执行
- printf("end!!!\n");
- exit(0);
- }
照旧

- #include
- #include
- #include
- #include
- #include
-
- int main(void)
- {
- unsigned int seconds;
-
- // 设置20s的闹钟,让SIGALARM信号在20秒后传达给当前进程,没有上一个闹钟,返回0
- seconds = alarm(20);
- printf("last alarm seconds remaining is %d!\n", seconds);
-
- // 让进程休眠5秒
- sleep(5);
-
- // 覆盖上一个的闹钟,重新设置为5s的闹钟,让SIGALARM信号在5秒后传达给当前进程,并返回上一个闹钟的剩余秒数
- seconds = alarm(5);
- printf("last alarm seconds remaining is %d!\n", seconds);
-
- // 让进程休眠20秒
- sleep(20);
-
- // 当产生SIGALARM信号时,由于没有做捕获处理,系统会调用该信号的默认处理函数,即exit(0)并且自动打印Alarm clock
- // 因此以下代码无法执行
- printf("end!!!\n");
- exit(0);
- }
照旧
