一、前言

        前面我们学习了共享内存的相关知识,今天我们来学习另一个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函数中:

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

        此时子进程已经被完美回收了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐