linux信号详解

发布于:2025-06-04 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

Linux 信号详解

一、信号是什么

1、是什么

2、查看信号

3、信号处理流程 

二、发出信号的方式(信号的产生)

1、kill 函数

2、raise 函数

3、abort 函数

4、alarm 函数(闹钟函数)(一个进程只能设置一个闹钟)

三、捕捉信号的方式

用户态和内核态详解:

四、处理信号的几种方式(信号的处理-处理)

1、前提 

 2、signal 函数

3、sigaction 函数和 sigaction 结构体(配合信号集使用)

五、保存信号的方式(信号的处理-待处理/阻塞)

1、前提介绍:

​编辑

2、信号阻塞和信号忽略

3、sigemptyset 相关函数

 (1)、sigemptyset函数  

(2)、sigfillset函数 

(3)、 sigaddset函数

(4)、sigdelset函数

(5)、sigismember函数

4、sigprocmask 函数(用于设置block图,配合信号集使用)

5、sigpending 函数(用于查看待处理/阻塞信号的位图)

6、pause函数

总结:

1、如何理解信号处理

2、如何记录信号

3、如何执行信号

补充——前台程序和后台程序

1、前台程序

2、后台程序


Linux 信号详解

在 Linux 系统中,信号是一种用于进程间通信的异步通知机制,它可以用于通知进程发生了某种事件,进程可以根据信号的类型做出相应的处理。下面我们将从多个方面对 Linux 信号进        行详细的讲解。

一、信号是什么

1、是什么

信 号是进程之间事件异步通知的⼀种⽅式,属于软中断

2、查看信号

3、信号处理流程 

二、发出信号的方式(信号的产生)

1、终端按键产生信号:比如按下 Ctrl+C 产生 SIGINT 信号,按下 Ctrl+\ 产生 SIGQUIT 信号,用于强制终止进程。

2、系统函数调用:通过kill、raise、abort等函数向进程发送信号。

3、软件条件产生:例如,当进程执行除 0 操作时,会产生 SIGFPE(浮点异常)信号;当进程访问非法内存地址时,会产生 SIGSEGV(段错误)信号。

4、硬件异常产生:如内存越界、除零操作等硬件错误会引发相应的信号。

下面介绍系统函数调用来产生信号 

1、kill 函数

#incldue<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);

pid:指定接收信号的进程 ID。如果pid > 0,信号将发送给指定 ID 的进程;

        如果pid = 0,信号将发送给与当前进程同组的所有进程;

        如果pid < 0,信号将发送给进程组 ID 为-pid的所有进程;

        如果pid = -1,信号将发送给系统内的所有进程(除了进程 1 和一些特殊进程)。 

sig:指定要发送的信号编号或宏定义名称。

返回值:成功时返回 0,出错时返回 - 1。

用例 一个父进程用 kill 函数杀死子进程

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        printf("Child process is running, pid: %d\n", getpid());
        while (1) {
            sleep(1);
        }
    } else if (child_pid > 0) {
        // 父进程
        sleep(3);
        // 向子进程发送SIGTERM信号
        if (kill(child_pid, SIGTERM) == 0) {
            printf("Sent SIGTERM signal to child process\n");
        } else {
            perror("kill");
        }
    } else {
        perror("fork");
    }
    return 0;
}

在这个例子中,父进程通过fork创建子进程,然后在 3 秒后使用kill函数向子进程发送 SIGTERM 信号,子进程收到信号后会按照默认方式终止。

2、raise 函数

#include<signal.h>

int raise(int sig);
  1. 参数说明:sig指定要发送的信号编号或宏定义名称,该函数用于杀死自己
  2. 返回值:成功时返回 0,出错时返回非零值。

用例: 

#include <stdio.h>
#include <signal.h>

int main() {
    // 向自身发送SIGINT信号
    raise(SIGINT);
    return 0;
}

在这个例子中,我们先设置了 SIGINT 信号的处理函数,然后使用raise函数向自身发送 SIGINT 信号,来用SIGINT信号来杀死自己。

3、abort 函数

#inlude<stdlib.h>
void abort(void);

功能说明:该函数用于向当前进程发送 SIGABRT 信号,使进程异常终止,并产生核心转储文件(如果系统允许)。

  1. 用例
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Before abort...\n");
    // 向自身发送SIGABRT信号
    abort();
    printf("This line will not be executed.\n");
    return 0;
}

在这个例子中,调用abort函数后,程序会收到 SIGABRT 信号,按照默认处理方式异常终止,后面的打印语句不会被执行。

4、alarm 函数(闹钟函数)(一个进程只能设置一个闹钟)

#inlude<stdlib.h>
void abort(void);

参数说明:seconds指定经过多少秒后向当前进程发送 SIGALRM 信号。如果seconds为 0,则取消之前设置的闹钟。

返回值:返回之前设置的闹钟剩余的秒数,如果之前没有设置闹钟,则返回 0。

用例

#include <stdio.h>
#include <unistd.h>

void handler(int signum) {
    printf("Received SIGALRM signal, time's up!\n");
    exit(0);
}

int main() {
    signal(SIGALRM, handler);
    // 设置5秒后发送SIGALRM信号
    alarm(5);
    printf("Waiting for 5 seconds...\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

在这个例子中,我们设置了 5 秒后发送 SIGALRM 信号,并自定义了该信号的处理函数。5 秒后,程序会收到 SIGALRM 信号,执行处理函数后退出。

三、捕捉信号的方式

流程

如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下: 

⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler 。

 当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。

 在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 • 内核决定返回⽤⼾态后不是恢复 数, sighandler 和 SIGQUIT 递达。 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个 独⽴的控制流程。

s ighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。

如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了。

简化图 

用户态和内核态详解:

用户态(User Mode)和内核态(Kernel Mode)是操作系统中两种不同的CPU运行状态,用于区分程序执行的权限级别和资源访问能力12。

一、用户态和内核态是什么​

在 Linux 系统中,为了保护操作系统的关键资源和确保系统稳定运行,将处理器的执行模式划分为用户态和内核态 。​

  • 用户态:是应用程序运行的模式。处于用户态的程序受到诸多限制,只能访问有限的内存空间,并且不能直接执行一些敏感的操作,如访问硬件设备、修改系统关键数据等。应用程序在用户态下执行的指令集是经过筛选的,不具备直接控制硬件和操作系统核心功能的能力。例如,我们日常使用的文本编辑器、浏览器等应用程序,在运行时都处于用户态。​
  • 内核态:是操作系统内核运行的模式。在内核态下,程序拥有最高的权限,可以访问系统的所有资源,包括硬件设备、内存的所有区域,并且能够执行任何指令,如控制 CPU、磁盘、网卡等硬件设备,进行进程调度、内存管理等核心操作。内核态下的代码直接与硬件交互,负责处理系统的各种底层事务,保障系统的正常运行。​

二、用户态和内核态的切换方式​

用户态和内核态之间的切换是 Linux 系统实现资源管理和保护的重要机制,主要通过以下几种方式实现:​

  • 系统调用:这是应用程序主动从用户态切换到内核态的最常见方式。当应用程序需要使用操作系统提供的服务,如文件读写、网络通信、进程创建等功能时,会通过系统调用陷入内核态。例如,当应用程序调用open函数打开一个文件时,会触发系统调用,CPU 会切换到内核态,由内核中的文件系统模块来处理文件打开的具体操作,完成后再返回用户态并将结果返回给应用程序。系统调用通过软中断(在 x86 架构上通常是int 0x80或更现代的syscall指令)来实现,它会保存用户态的当前执行环境(如寄存器值、程序计数器等),然后跳转到内核态的系统调用处理函数执行相应操作。​
  • 异常:当 CPU 在执行用户态程序时遇到一些异常情况,如除零错误、内存访问越界、非法指令等,会自动触发异常机制,导致 CPU 切换到内核态。内核会根据异常的类型进行相应的处理,例如,当发生内存访问越界异常时,内核可能会终止该进程或者向进程发送一个SIGSEGV信号。在处理完异常后,根据具体情况决定是恢复用户态程序的执行还是终止程序。​
  • 外部中断:外部设备(如键盘、鼠标、网卡等)产生的中断信号也会使 CPU 从用户态切换到内核态。当外部设备完成某项操作(如键盘按键按下、网卡接收到数据)时,会向 CPU 发送一个中断请求,CPU 在响应中断时,会暂停当前用户态程序的执行,保存现场,切换到内核态执行中断处理程序。中断处理程序会处理设备的请求,如读取键盘输入的数据、接收网卡数据等,处理完成后恢复用户态程序的执行。​

三、用户态和内核态的数据交互​

用户态和内核态之间的数据交互是实现系统功能的重要环节,主要有以下几种方式:​

  • 参数传递:在进行系统调用时,应用程序需要将相关的参数传递给内核。这些参数可以通过寄存器传递,也可以在用户空间和内核空间之间共享内存区域来传递。例如,在调用write函数向文件写入数据时,应用程序会将文件描述符、要写入的数据缓冲区地址以及数据长度等参数传递给内核,内核根据这些参数完成数据写入操作。​
  • 共享内存:为了提高数据传输的效率,用户态和内核态之间可以通过共享内存的方式进行数据交互。内核可以分配一段物理内存,并将其映射到用户空间和内核空间,使得用户态程序和内核态代码都可以访问该内存区域。这种方式常用于需要频繁进行大量数据传输的场景,如某些高性能的网络应用程序与内核网络模块之间的数据交互。​
  • 内核缓冲区:内核通常会维护一些缓冲区,用于暂存数据。例如,在文件系统中,内核会使用缓冲区来缓存从磁盘读取的数据或准备写入磁盘的数据。当用户态程序进行文件读写操作时,数据会先在用户态缓冲区和内核缓冲区之间进行传输,然后再由内核根据具体情况进行磁盘 I/O 操作。

四、处理信号的几种方式(信号的处理-处理)

1、前提 

默认处理:每个信号都有一个默认的处理方式,如终止进程、忽略信号、产生核心转储文件等。例如,SIGKILL 信号的默认处理方式是立即终止进程,且该信号不能被捕获和忽略。

忽略信号:进程可以选择忽略某些信号,使其不产生任何效果。其中忽略也是一种动作

自定义处理:进程可以自定义信号处理函数,当收到特定信号时,执行自定义的处理逻辑。

 2、signal 函数

#include<signal.h>

sighandler_t signal(int signum, sighandler_t handler);

参数说明

signum:指定要处理的信号编号或宏定义名称。

handler:指定信号的处理方式,可以是SIG_IGN(忽略信号)、SIG_DFL(使用默认处理方式)或给他传一个自定义函数指针。

返回值:成功时返回上一次该信号的处理函数指针,出错时返回SIG_ERR。

用例

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int signum) {
    printf("hello\n");
    exit(0);
}

int main() {
    // 设置SIGINT信号的自定义处理函数
    signal(SIGINT, sigint_handler);
    while (1) {
        sleep(1);
    }
    return 0;
}

在这个例子中,我们通过signal函数自定义了 SIGINT 信号的处理函数sigint_handler,当程序收到 SIGINT 信号(按下 Ctrl+C)时,会执行sigint_handler函数,打印提示信息后退出程序。

3、sigaction 函数和 sigaction 结构体(配合信号集使用)

sigaction函数用途于signal函数类似,但它提供了比传统 signal 函数更强大、更灵活的信号处理机制。

sigaction函数(函数的主要作用是‌检查或修改与指定信号相关联的处理动作

#include<signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数说明

signum:指定要设置处理方式的信号编号或宏定义名称。

act:指向struct sigaction结构体的指针,用于指定新的信号处理方式。(输出型参数)

oldact:如果不为 NULL,用于保存原来的信号处理方式。(输出型参数)

返回值:成功时返回 0,出错时为-1

sigaction 结构体

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。

               用 sigemptyset() 函数清空sa_mask,清空阻塞集

               将sa_flags赋值为 0

五、保存信号的方式(信号的处理-待处理/阻塞)

1、前提介绍:

在 Linux 系统中,信号的保存涉及到几个重要的位图:

  • pending 位图:用于记录当前进程收到的所有信号,每一位对应一个信号,若该位为 1,表示进程收到了相应的信号。
  • block 位图:用于记录当前被阻塞的信号,被阻塞的信号不会立即被处理,而是处于挂起状态,直到该信号的阻塞被解除。
  • handler 位图:记录了每个信号对应的处理方式(默认、忽略、自定义处理函数)。

2、信号阻塞和信号忽略

信号阻塞:是指进程可以将某些信号设置为阻塞状态,被阻塞的信号即使到达进程也不会立即被处理,而是处于挂起状态。可以使用sigprocmask函数来设置信号的阻塞状态。

信号忽略:是指进程明确表示不处理某些信号,使其不产生任何效果。可以通过将信号的处理方式设置为SIG_IGN来实现信号忽略,例如signal(SIGINT, SIG_IGN);。

3、sigemptyset 相关函数

sigset_t是一个信号集,是一个位图,下面是用来改变此位图的相关函数

 (1)、sigemptyset函数  

#include<signal.h>
int sigemptyset(sigset_t *set);

参数说明:set是指向sigset_t类型变量的指针,该函数用于清空信号集set,即将其中所有信号的对应位都设置为 0。 

返回值:成功时返回 0,出错时返回 - 1。 

(2)、sigfillset函数 

#include<signal.h>
int sigfillset(sigset_t *set);

参数说明:set是指向sigset_t类型变量的指针,该函数用于将信号集set中的所有位都设置为 1,即包含所有信号。 

 返回值:成功时返回 0,出错时返回 - 1。

(3)、 sigaddset函数

#include<signal.h>
int sigaddset(sigset_t *set, int signum);

参数说明:set是指向sigset_t类型变量的指针,signum指定要添加到信号集中的信号编号或宏定义名称。 

返回值:成功时返回 0,出错时返回 - 1。 

(4)、sigdelset函数

#include<signal.h>
int sigdelset(sigset_t *set, int signum);

参数说明:set是指向sigset_t类型变量的指针,signum指定要删除此信号集中的信号编号或宏定义名称。 

返回值:成功时返回 0,出错时返回 - 1。 

(5)、sigismember函数

#include<signal.h>
int sigismember(sigset_t *set, int signum);

参数说明:set是指向sigset_t类型变量的指针,signum是一个信号编号或者宏定义名称,

该函数用于检查signum信号是否在信号集中。

返回值:成功时返回 0,出错时返回 - 1。 

4、sigprocmask 函数(用于设置block图,配合信号集使用)

#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:指向要操作的信号集。

oldset:如果不为 NULL,用于保存原来的信号屏蔽字(输出型参数)。

返回值:成功时返回 0,出错时返回 - 1。

用例

#include <stdio.h>
#include <unistd.h>

void sigalrm_handler(int signum) {
    printf("Received SIGALRM signal, time's up!\n");
    exit(0);
}

int main() {
    signal(SIGALRM, sigalrm_handler);
    // 设置5秒后发送SIGALRM信号
    alarm(5);
    printf("Waiting for 5 seconds...\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

在这个例子中,我们先阻塞了 SIGINT 信号,此时按下 Ctrl+C 不会立即终止程序,10 秒后解除对 SIGINT 信号的阻塞,再次按下 Ctrl+C 就会执行自定义的信号处理函数。

5、sigpending 函数(用于查看待处理/阻塞信号的位图)

#include<signal.h>

int sigpending(sigset_t *set);

参数说明:set是指向sigset_t类型变量的指针,用于保存当前进程中处于挂起状态(已收到但未处理)的信号集合。是一个输出型参数

返回值:成功时返回 0,出错时返回 - 1。

用例

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t pending_set;
    // 获取当前进程中处于挂起状态的信号集合
    if (sigpending(&pending_set) == 0) {
        if (sigismember(&pending_set, SIGINT)) {
            printf("SIGINT signal is pending.\n");
        } else {
            printf("SIGINT signal is not pending.\n");
        }
    } else {
        perror("sigpending");
    }
    return 0;
}

在这个例子中,我们使用sigpending函数获取当前进程中处于挂起状态的信号集合,并检查 SIGINT 信号是否在其中。

6、pause函数

pause 函数的主要作用是:

  • 使调用进程进入睡眠状态,直到捕获到一个信号
  • 在信号处理程序执行完毕后,pause 才会返回
  • 如果信号导致进程终止,则 pause 不会返回

总结:

1、如何理解信号处理

信号处理是进程对收到的信号做出响应的过程。当进程收到一个信号时,系统会根据信号的处理方式进行相应的操作。

如果是默认处理,系统会按照预定义的行为处理信号;

 如果是忽略信号,系统会直接丢弃该信号;

 如果是捕捉信号,系统会暂停当前进程的正常执行流程,转而执行用户自定义的信号处理函数,  处理完信号后,再回到原来被中断的地方继续执行。

2、如何记录信号

前面提到的 pending 位图就是用于记录信号的一种方式。当进程收到一个信号时,系统会将 pending 位图中对应的位置为 1,表示该信号已收到。此外,系统还会维护一些与信号相关的内核数据结构,用于记录信号的详细信息,如信号的来源、发送时间等。

3、如何执行信号

当信号的阻塞被解除且信号处于 pending 状态时,系统会根据信号的处理方式执行相应的操作。如果是默认处理或忽略信号,系统会直接按照预定义的规则执行;如果是捕捉信号,系统会调用用户自定义的信号处理函数来执行相应的逻辑。

补充——前台程序和后台程序

1、前台程序

前台程序是指在终端中当前正在运行,并且占据终端输入输出的程序。当终端有输入时,输入会被发送到前台程序;

我们平时运行的一般为前台程序,可以通过ctrl+c来终止。

2、后台程序

后台程序是指在终端中启动后,不占据终端输入输出,可以在后台继续运行的程序。通常可以通过在命令后加上&符号将程序放入后台运行。

 此时运行的程序为后台程序,后台程序无法通过ctrl+c来杀死。后台程序默认不会收到一些终端产生的信号,但可以通过kill命令向其发送信号。

-----------------------------------------------------------------------------------------------------------------------------


网站公告

今日签到

点亮在社区的每一天
去签到