Linux学习日记15:信号
本文介绍了Linux系统中的信号机制,主要内容包括:信号的基本概念和分类(不可靠信号1-31和可靠信号32-64);常见信号的作用(如SIGINT、SIGKILL、SIGTERM等);信号的生命周期和处理方式(默认处理、忽略和捕获);相关系统调用(kill、raise、alarm、pause、signal等)。通过多个示例演示了信号的使用方法,包括父子进程间通过信号通信,以及使用wait函数避免僵
一、前言
前面我们学习了共享内存的相关知识,今天我们来学习另一个IPC通信机制——信号的相关知识。
二、信号
在 Linux 操作系统中,信号是一种核心的进程间通信(IPC)和系统事件通知机制,用于向进程传递 “异步事件” 的通知(如用户中断、硬件异常、进程间协作等)。它允许内核或其他进程 “打断” 目标进程的执行流程,触发特定的处理逻辑。
2.1、信号的基本概念
信号是 Linux 内核向进程发送的 “事件通知”,本质是一个整数标识 (如SIGINT对应数字2)。进程收到信号后,会根据预设的 “信号处理规则” 执行操作(如终止、暂停、执行自定义函数等)。
2.2、信号的分类
Linux 信号分为 “不可靠信号”(1~31)和“可靠信号”(32~64),也可按来源和作用分类。
2.2.1、按可靠性分类
1、不可靠信号(1~31)
早期 UNIX 信号的实现,存在信号丢失问题(多次发送同一信号可能只触发一次)。例如SIGINT(中断)、SIGTERM(终止)。
2、可靠信号(32~64)
实时信号,支持信号排队(多次发送不会丢失),且可携带额外数据。例如SIGRTMIN(32)、SIGRTMAX(64)。
2.2.2、按来源分类
1、硬件信号
由硬件事件触发,如SIGFPE(浮点异常,如除零)、SIGSEGV(段错误,非法内存访问)。
2、软件信号
由软件操作触发,如SIGINT(用户按Ctrl+C)、SIGKILL(强制终止进程)、SIGUSR1/SIGUSR2(用户自定义信号)。
2.3、常见信号及其作用
这类信号用于终止进程,是日常管理进程的核心信号:
| 信号编号 | 信号名称 | 触发场景 | 默认动作 | 关键特点 |
|---|---|---|---|---|
| 2 | SIGINT |
按下终端 Ctrl+C |
终止进程 | 可被进程捕获 / 忽略(常用于实现 “优雅退出”) |
| 9 | SIGKILL |
执行 kill -9 <PID> |
强制终止进程 | 不可被捕获 / 忽略,是 “终极杀进程” 手段 |
| 15 | SIGTERM |
执行 kill <PID>(默认信号) |
终止进程 | 可被捕获 / 忽略,是 “优雅终止” 的默认选择(进程可在退出前清理资源) |
用于控制进程的运行状态(暂停 / 继续):
| 信号编号 | 信号名称 | 触发场景 | 默认动作 | 关键特点 |
|---|---|---|---|---|
| 18 | SIGCONT |
执行 kill -18 <PID> |
恢复暂停的进程 | 与暂停信号配合使用,唯一能恢复 T 状态进程的信号 |
| 19 | SIGSTOP |
执行 kill -19 <PID> |
强制暂停进程 | 不可被捕获 / 忽略,进程直接进入 T 状态 |
| 20 | SIGTSTP |
按下终端 Ctrl+Z |
暂停进程 | 可被捕获 / 忽略,默认进入 T 状态(你之前代码中用 raise(SIGTSTP) 触发的就是它) |
这类信号对应常见的系统事件或异常场景:
| 信号编号 | 信号名称 | 触发场景 | 默认动作 | 常见场景 |
|---|---|---|---|---|
| 1 | SIGHUP |
终端断开(如 SSH 连接关闭) | 终止进程 | 很多守护进程(如 Nginx)会捕获它,用于重新加载配置(无需重启进程) |
| 11 | SIGSEGV |
内存访问违规(如越界读写) | 终止并生成 core 文件 |
俗称 “段错误”,是程序崩溃的常见原因之一 |
| 13 | SIGPIPE |
向已关闭的管道 / 套接字写数据 | 终止进程 | 网络 / 管道编程中常见(如客户端断开后服务端继续写) |
| 14 | SIGALRM |
调用 alarm(n) 后超时 |
终止进程 | 用于实现 “超时控制”(如进程执行超过 N 秒则终止) |
| 6 | SIGABRT |
调用 abort() 函数 |
终止并生成 core 文件 |
程序主动触发的 “异常终止”(常用于调试断言失败) |
2.4、信号的生命周期
信号从 “产生” 到 “处理” 需经历以下阶段:
1、信号产生:由硬件(如键盘中断)或软件(如kill命令)触发。
2、信号注册:内核将信号标记为 “待处理”,记录在进程的 “信号掩码” 中。
3、信号等待:进程在执行时会检查 “待处理信号”,若存在则触发处理逻辑。
4、信号处理:根据进程对该信号的 “处理动作” 执行(捕获、忽略、默认行为)。
5、信号清理:处理完成后,内核清除该信号的 “待处理” 标记。
2.5、信号的处理方式
2.5.1、默认处理
内核为每个信号预设的处理逻辑,常见动作包括:终止:如SIGINT,SIGTERM;终止并生成核心转储:如SIGSEGV(用于调试);暂停:如SIGSTOP;忽略:如SIGCHLD(子进程退出时的通知,默认忽略)。
2.5.2、忽略信号
进程显式声明 “忽略该信号”,内核收到信号后不执行任何操作。
示例:忽略SIGINT(避免Ctrl+C终止进程)
#include <signal.h>
#include <stdio.h>
int main() {
// 忽略SIGINT信号
signal(SIGINT, SIG_IGN);//SIG_IGN是一个宏,表示忽略信号
printf("进程已忽略SIGINT,按Ctrl+C不会终止\n");
while (1) {} // 无限循环
return 0;
}
2.5.3、捕获信号
进程注册自定义函数,信号触发时执行该函数(替代默认行为)。
示例:捕获SIGINT并打印提示后退出:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void sigint_handler(int signo) {
printf("\n捕获到SIGINT,即将退出...\n");
exit(0);
}
int main() {
// 注册自定义处理函数
signal(SIGINT, sigint_handler);
printf("按Ctrl+C触发自定义处理\n");
while (1) {}
return 0;
}
2.6、信号相关的核心系统调用
2.6.1、kill函数(向进程发送信号)
用于向指定进程发送信号,原型:
int kill(pid_t pid, int sig);
pid:进程 ID(>0指定进程;=0发送给同进程组;-1发送给所有有权限的进程;<-1发送给进程组-pid);
sig:信号编号;
返回值:成功返回0,失败返回-1。
示例:向进程1234发送SIGTERM:
kill(1234, SIGTERM);
2.6.2、 raise函数
进程向自己发送信号,原型:
int raise(int sig);
示例:进程向自身发送SIGUSR1:
raise(SIGUSR1);
2.6.3、alarm函数
alarm 只会发送SIGALARM信号,原型:
#include <unistd.h>
// 参数:定时秒数;返回值:之前未到期的定时剩余秒数(无则返回0)
unsigned int alarm(unsigned int seconds);
参数:seconds:指定秒数。
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。出错返回‐1。
注:alarm 会让内核定时一段时间之后发送信号, raise会让内核立刻发信号。
2.6.4、pause函数
pause:使得进程状态为S(休眠):原型如下:
函数原型 int pause(void);
返回值:成功:0,出错:‐1 。
2.6.5、signal函数
用于注册信号处理函数,原型:
void (*signal(int signum, void (*handler)(int)))(int);
signum:信号编号(如SIGINT);
handler:处理函数(SIG_IGN表示忽略,SIG_DFL表示默认行为)或者自定义函数。
返回值:成功:返回该信号之前的处理函数指针。失败:返回-1;
三、典型示例
3.1、kill
首先随意创建一个文件xxx.c:
![]()
接着输入以下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("endless loop ing...\n");
sleep(2);
}
return 0;
}
接着使用gcc编辑器进行编译,运行结果如下:
可见2s一个死循环,这时我们可以直接使用键盘发送信号Ctrl+C来终止该进程:
当然,我们也可以使用kill函数来进行终止,这里我们打开另一个终端,输入ps aux来查看运行文件的pid:
可见其pid为3124,然后使用kill -9 3124 这条指令终止该进程:
可以看到成功杀死了进程。
3.2、raise
首先创建一个文件raise.c:
#include <stdio.h>
#include <signal.h>
int main()
{
printf("before sig\n");
raise(9);//终止自己
printf("after sig\n");
return 0;
}
使用gcc编译器进行编译,结果如下:
可见该进程终止了自己。
接下来使用父子进程来进一步体现,输入以下代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid;
pid = fork();
if(pid >0)
{
sleep(8);
while(1);
}
if(pid == 0)
{
printf("before sig\n");
raise(SIGTSTP);
}
return 0;
}
使用gcc编译器进行编译,然后运行,打开另一个终端查看父子进程的状态:

可见8s前,父进程的状态是休眠,子进程的状态是暂停,8s后再次查看:
可见父进程的状态变为了运行态,子进程依旧处于暂停态。 接下来引入waitpid函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
pid_t wpid;
if(pid >0)
{
sleep(8);
if(wpid =(waitpid(pid,NULL,WNOHANG) == 0))
{
kill(pid,9);
}
while(1);
}
if(pid == 0)
{
printf("before sig\n");
raise(SIGTSTP);
}
return 0;
}
使用gcc编译器进行编译,然后运行,再打开另一个终端,输入ps aux指令:

等8s后再次输入ps aux指令:

此时父进程仍处在死循环中,没有回收子进程,所以子进程会处于僵尸状态,直到父进程被外部杀死。
3.3、alarm和pause
首先创建一个alarm.c文件,输入以下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
int i=0;
printf("before sig\n");
alarm(7);
printf("after sig\n");
while(1)
{
if(i<10)
{
i++;
sleep(1);
printf("process %d\n",i);
}
}
return 0;
}
使用gcc编译器进行编译,运行结果如下:

可以看到在第七秒的时候,闹钟开始"响",终止了该进程。
然后将alarm换成pause():
#include <stdio.h>
#include <unistd.h>
int main()
{
int i=0;
printf("before sig\n");
pause();
printf("after sig\n");
while(1)
{
if(i<10)
{
i++;
sleep(1);
printf("process %d\n",i);
}
}
return 0;
}
编译并运行,打开另一个终端查看其状态:

可以看到此时的进程状态为睡眠态,此时我们可以使用Ctrl+C来终止或者使用Ctrl+Z来使进程状态为暂停态。
3.4、signal
首先创建一个signal.c文件,并输入以下代码:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void myfun(int signum)
{
int i=0;
while(i<10)
{
printf("process signed = %d i=%d\n",signum,i);
sleep(1);
i++;
}
}
int main()
{
int i=0;
signal(14,myfun);
printf("before sig\n");
alarm(7);
printf("after sig\n");
while(1)
{
if(i<10)
{
i++;
sleep(1);
printf("process %d\n",i);
}
}
return 0;
}
使用gcc编译器进行编译,然后运行,结果如下:
可以看到执行完了alarm函数后开始执行myfun函数,myfun函数执行完了以后开始继续执行后面的循环。看到这里可能不免有些疑问,为什么alarm执行完后不会终止进程呢?这是因为signal函数中设置了一个myfun自定义函数,把信号编号为14的alarm给捕获了,然后执行了myfun函数里的内容。
3.5、信号父子进程间通讯
首先创建一个signal2.c文件,并输入以下代码:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void myfun(int signum)
{
int i=0;
while(i<5)
{
printf("receive signal is %d,i = %d\n",signum,i);
sleep(1);
i++;
}
}
int main()
{
pid_t pid;
pid = fork();
if(pid >0)
{
int i =0;
signal(10,myfun);
while(1)
{
printf("process %d\n",i);
sleep(1);
i++;
}
}
if(pid ==0)
{
sleep(10);
kill(getppid(),10);//SIGUSR1
sleep(10);
}
return 0;
}
使用gcc编译器进行编译,运行结果如下:

可见父进程先进入10s循环,然后子进程醒来后给父进程发送信号10(SIGUSR1——用户自定义信号),然后使用siganal函数进入myfun函数中再次循环,循环完后父进程再次进入死循环。
接着打开另一个终端,使用ps aux指令查看父子进程的状态:

不出意外的,因为父进程一直处于死循环状态,受sleep函数的影响,处于可中断休眠状态,无法回收子进程,所以子进程处于僵尸态。那怎么正确回收子进程呢?我们使用exit函数试试:

再打开另一个终端查看:

发现还是不行,这是因为exit函数本质也是一个信号,为SIGCHLD,编号为17:
![]()
我们可以验证一下exit是否为信号:


编译并运行:

验证成功,那如何将子进程进行回收呢,那就是使用之前讲过的wait函数,放入myfun2函数中:

这个时候我们再次打开终端查看状态:

此时子进程已经被完美回收了。
更多推荐



所有评论(0)