Linux C语言信号类型的捕获与忽略
信号的基本概念
在Linux系统中,信号(Signal)是一种异步通知机制,用于在进程间传递事件信息。信号可以由系统内核、其他进程或终端产生,用于通知目标进程发生了特定的事件。例如,当用户在终端按下 Ctrl+C
组合键时,系统会向当前前台进程发送一个 SIGINT
信号,通知该进程用户希望终止它。
信号有多种类型,每种类型都有特定的含义和默认处理动作。常见的信号类型包括:
- SIGINT:中断信号,通常由用户在终端按下
Ctrl+C
组合键产生,默认处理动作是终止进程。 - SIGTERM:终止信号,通常由系统管理员或其他进程发送,用于请求目标进程正常终止,默认处理动作也是终止进程。
- SIGKILL:强制终止信号,不能被捕获或忽略,一旦发送,目标进程将立即被终止。
- SIGSEGV:段错误信号,当进程访问非法内存地址时产生,默认处理动作是终止进程并产生核心转储文件(如果启用了核心转储功能)。
信号的处理方式
进程对信号的处理方式主要有以下三种:
- 默认处理:每个信号都有系统预设的默认处理动作,如前面提到的
SIGINT
和SIGTERM
的默认处理动作是终止进程,SIGSEGV
的默认处理动作是终止进程并产生核心转储文件。 - 忽略信号:进程可以选择忽略某些信号,即对该信号不做任何处理。例如,对于一些非关键的信号,进程可能选择忽略以避免不必要的干扰。
- 捕获信号:进程可以注册一个信号处理函数,当接收到特定信号时,系统会暂停当前进程的正常执行流程,转而执行信号处理函数。处理函数执行完毕后,进程恢复正常执行。
信号捕获
在C语言中,我们可以使用 signal
函数或 sigaction
函数来捕获信号。下面分别介绍这两个函数的使用方法。
signal函数
signal
函数是一个较老的用于处理信号的函数,其原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signum
:要捕获的信号类型,如SIGINT
、SIGTERM
等。handler
:指向信号处理函数的指针。如果handler
为SIG_IGN
,表示忽略该信号;如果handler
为SIG_DFL
,表示采用默认处理方式;否则,handler
指向一个自定义的信号处理函数。
下面是一个使用 signal
函数捕获 SIGINT
信号的示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void sigint_handler(int signum) {
printf("Received SIGINT signal. Exiting gracefully...\n");
// 在这里可以进行一些清理工作,如关闭文件、释放资源等
_exit(0);
}
int main() {
// 注册信号处理函数
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to send SIGINT signal.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在上述代码中,我们定义了一个 sigint_handler
函数作为 SIGINT
信号的处理函数。在 main
函数中,使用 signal
函数将 SIGINT
信号与 sigint_handler
函数关联起来。当用户在终端按下 Ctrl+C
时,进程会接收到 SIGINT
信号,然后执行 sigint_handler
函数,输出提示信息并优雅地退出。
sigaction函数
sigaction
函数是一个更强大、更灵活的用于处理信号的函数,其原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:要捕获的信号类型。act
:指向一个struct sigaction
结构体的指针,该结构体包含了信号处理的相关设置。oldact
:指向一个struct sigaction
结构体的指针,用于保存旧的信号处理设置。如果不需要保存旧设置,可以将其设置为NULL
。
struct 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
:指向信号处理函数的指针,与signal
函数中的handler
类似。sa_sigaction
:指向一个更复杂的信号处理函数的指针,用于处理需要更多信息的信号(如SIGCHLD
信号)。sa_mask
:一个信号集,用于指定在信号处理函数执行期间需要屏蔽的其他信号。sa_flags
:一些标志位,用于指定信号处理的行为,如SA_RESTART
表示系统调用被信号中断后自动重新启动。
下面是一个使用 sigaction
函数捕获 SIGINT
信号的示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void sigint_handler(int signum) {
printf("Received SIGINT signal. Exiting gracefully...\n");
// 在这里可以进行一些清理工作,如关闭文件、释放资源等
_exit(0);
}
int main() {
struct sigaction act;
// 初始化sigaction结构体
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 注册信号处理函数
sigaction(SIGINT, &act, NULL);
printf("Press Ctrl+C to send SIGINT signal.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在上述代码中,我们首先定义了一个 sigint_handler
信号处理函数。然后,在 main
函数中,初始化了一个 struct sigaction
结构体 act
,将 act.sa_handler
设置为 sigint_handler
,清空 act.sa_mask
,并设置 act.sa_flags
为 0。最后,使用 sigaction
函数将 SIGINT
信号与 act
结构体关联起来,从而实现信号的捕获。
信号忽略
在C语言中,忽略信号非常简单,只需将 signal
函数或 sigaction
函数中的 handler
参数设置为 SIG_IGN
即可。下面是使用 signal
函数忽略 SIGINT
信号的示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
// 忽略SIGINT信号
signal(SIGINT, SIG_IGN);
printf("Press Ctrl+C, nothing will happen.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在上述代码中,我们使用 signal(SIGINT, SIG_IGN)
语句将 SIGINT
信号设置为忽略状态。这样,当用户在终端按下 Ctrl+C
时,进程将不会对 SIGINT
信号做出任何响应,继续正常运行。
如果使用 sigaction
函数忽略信号,可以按照以下方式编写代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
struct sigaction act;
// 初始化sigaction结构体
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 忽略SIGINT信号
sigaction(SIGINT, &act, NULL);
printf("Press Ctrl+C, nothing will happen.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在这个示例中,我们同样初始化了一个 struct sigaction
结构体 act
,将 act.sa_handler
设置为 SIG_IGN
,然后使用 sigaction
函数将 SIGINT
信号与 act
结构体关联起来,从而实现对 SIGINT
信号的忽略。
信号屏蔽与解除屏蔽
在信号处理过程中,有时我们需要临时屏蔽某些信号,以防止在执行关键代码段时被信号打断。在C语言中,可以使用 sigprocmask
函数来设置信号屏蔽字。
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
:指向一个信号集的指针,用于指定要操作的信号。oldset
:指向一个信号集的指针,用于保存旧的信号屏蔽字。如果不需要保存旧设置,可以将其设置为NULL
。
下面是一个示例代码,展示了如何屏蔽和解除屏蔽 SIGINT
信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t set, oldset;
// 初始化信号集
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 屏蔽SIGINT信号
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("SIGINT signal is blocked. Press Ctrl+C, nothing will happen.\n");
sleep(5);
// 解除屏蔽SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal is unblocked. Press Ctrl+C to send SIGINT signal.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在上述代码中,我们首先初始化了一个信号集 set
,并将 SIGINT
信号添加到该信号集中。然后,使用 sigprocmask
函数将 SIGINT
信号屏蔽。在屏蔽期间,即使用户按下 Ctrl+C
,进程也不会接收到 SIGINT
信号。5 秒后,我们使用 sigprocmask
函数解除对 SIGINT
信号的屏蔽,此时用户按下 Ctrl+C
就可以正常向进程发送 SIGINT
信号了。
可重入函数与信号处理
在编写信号处理函数时,需要注意可重入性问题。可重入函数是指可以被多个线程或信号处理函数安全调用的函数,即使在调用过程中被中断并再次调用,也不会出现数据损坏或其他错误。
一些常见的不可重入函数包括:
- 标准I/O函数(如
printf
、fopen
等):这些函数通常使用静态数据结构来管理缓冲区,在信号处理函数中调用可能会导致数据损坏。 - 动态内存分配函数(如
malloc
、free
等):这些函数也可能使用静态数据结构,在信号处理函数中调用可能会引发问题。
为了确保信号处理函数的可重入性,应尽量使用可重入函数。如果确实需要在信号处理函数中执行一些不可重入的操作,可以考虑使用信号屏蔽和解除屏蔽的方法,在执行关键代码段时屏蔽相关信号,避免信号处理函数的干扰。
例如,下面是一个使用可重入函数 write
来替代不可重入函数 printf
的信号处理函数示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
// 信号处理函数
void sigint_handler(int signum) {
const char *msg = "Received SIGINT signal. Exiting gracefully...\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
// 在这里可以进行一些清理工作,如关闭文件、释放资源等
_exit(0);
}
int main() {
// 注册信号处理函数
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to send SIGINT signal.\n");
while (1) {
// 模拟进程的正常工作
sleep(1);
printf("Process is running...\n");
}
return 0;
}
在上述代码中,sigint_handler
信号处理函数使用 write
函数输出提示信息,从而确保了函数的可重入性。
信号与系统调用
当进程正在执行一个系统调用时,如果接收到一个信号,系统调用可能会被中断。例如,当进程正在执行 read
系统调用从文件中读取数据时,如果接收到一个 SIGINT
信号,read
系统调用可能会被中断,返回 -1
并设置 errno
为 EINTR
。
在早期的UNIX系统中,被信号中断的系统调用默认不会自动重新启动,这就要求程序员在编写代码时要检查系统调用的返回值,并在 errno
为 EINTR
时手动重新调用系统调用。例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFER_SIZE 1024
// 信号处理函数
void sigint_handler(int signum) {
printf("Received SIGINT signal.\n");
}
int main() {
int fd, n;
char buffer[BUFFER_SIZE];
// 注册信号处理函数
signal(SIGINT, sigint_handler);
// 打开文件
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
while (1) {
// 从文件中读取数据
n = read(fd, buffer, BUFFER_SIZE);
if (n == -1) {
if (errno == EINTR) {
// 系统调用被信号中断,重新调用read
continue;
} else {
perror("read");
close(fd);
return 1;
}
} else if (n == 0) {
// 读取到文件末尾,退出循环
break;
} else {
// 处理读取到的数据
write(STDOUT_FILENO, buffer, n);
}
}
close(fd);
return 0;
}
在上述代码中,当 read
系统调用被 SIGINT
信号中断时(errno
为 EINTR
),我们通过 continue
语句重新调用 read
系统调用,以确保数据读取的完整性。
在现代的UNIX系统(包括Linux)中,一些系统调用默认是可重启的,即被信号中断后会自动重新启动。例如,read
、write
、accept
等系统调用在默认情况下是可重启的。如果希望禁用系统调用的自动重启功能,可以在 sigaction
函数的 sa_flags
字段中设置 SA_RESTART
标志。例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFER_SIZE 1024
// 信号处理函数
void sigint_handler(int signum) {
printf("Received SIGINT signal.\n");
}
int main() {
int fd, n;
char buffer[BUFFER_SIZE];
struct sigaction act;
// 初始化sigaction结构体
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART; // 禁用系统调用的自动重启
// 注册信号处理函数
sigaction(SIGINT, &act, NULL);
// 打开文件
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
while (1) {
// 从文件中读取数据
n = read(fd, buffer, BUFFER_SIZE);
if (n == -1) {
if (errno == EINTR) {
// 系统调用被信号中断,手动重新调用read
continue;
} else {
perror("read");
close(fd);
return 1;
}
} else if (n == 0) {
// 读取到文件末尾,退出循环
break;
} else {
// 处理读取到的数据
write(STDOUT_FILENO, buffer, n);
}
}
close(fd);
return 0;
}
在上述代码中,我们通过设置 act.sa_flags = SA_RESTART
禁用了系统调用的自动重启功能,这样当 read
系统调用被 SIGINT
信号中断时,就需要我们手动重新调用 read
系统调用。
实时信号
到目前为止,我们讨论的信号大多是非实时信号,如 SIGINT
、SIGTERM
等。非实时信号有一些局限性,例如它们可能会丢失(如果在短时间内多次发送同一个非实时信号,进程可能只会接收到一次),并且它们的优先级是固定的。
为了克服这些局限性,Linux系统引入了实时信号。实时信号的编号从 SIGRTMIN
到 SIGRTMAX
,不同的系统可能有不同的 SIGRTMIN
和 SIGRTMAX
值,但通常 SIGRTMIN
为 34,SIGRTMAX
为 64。
实时信号具有以下特点:
- 可靠性:实时信号不会丢失,即使在短时间内多次发送同一个实时信号,进程也会接收到相应次数的信号。
- 优先级:实时信号具有不同的优先级,信号编号越大,优先级越高。
在使用实时信号时,通常需要使用 sigaction
函数来注册信号处理函数,并设置 sa_sigaction
字段为一个更复杂的信号处理函数,该函数可以获取更多关于信号的信息。例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 实时信号处理函数
void rt_sig_handler(int signum, siginfo_t *info, void *context) {
printf("Received real - time signal %d.\n", signum);
printf("Sender's PID: %d\n", info->si_pid);
}
int main() {
struct sigaction act;
// 初始化sigaction结构体
act.sa_sigaction = rt_sig_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
// 注册实时信号处理函数
sigaction(SIGRTMIN, &act, NULL);
printf("Sending real - time signal SIGRTMIN...\n");
// 发送实时信号
kill(getpid(), SIGRTMIN);
sleep(2);
return 0;
}
在上述代码中,我们定义了一个 rt_sig_handler
函数作为实时信号 SIGRTMIN
的处理函数。该函数通过 siginfo_t
结构体获取了发送信号的进程的PID。在 main
函数中,我们使用 sigaction
函数注册了 SIGRTMIN
信号的处理函数,并设置 sa_flags
为 SA_SIGINFO
,以启用获取信号详细信息的功能。然后,使用 kill
函数向自身发送了一个 SIGRTMIN
信号。
信号与多线程
在多线程程序中,信号的处理变得更加复杂。默认情况下,信号会发送到进程中的所有线程,并且只有一个线程可以处理信号。
为了在多线程程序中更好地处理信号,可以使用以下方法:
- 主线程捕获信号:可以在主线程中注册信号处理函数,然后通过线程间通信机制(如管道、共享内存等)将信号信息传递给其他线程进行处理。
- 线程特定信号处理:使用
pthread_sigmask
函数为每个线程设置不同的信号屏蔽字,使特定线程可以捕获特定信号。pthread_sigmask
函数的原型如下:
#include <pthread.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
其参数与 sigprocmask
函数类似,但作用于线程级别的信号屏蔽字。
下面是一个简单的示例,展示了如何在多线程程序中使用 pthread_sigmask
函数让特定线程捕获 SIGINT
信号:
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
// 线程函数
void* thread_func(void* arg) {
sigset_t set;
int signum;
// 初始化信号集
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 设置线程的信号屏蔽字,阻塞SIGINT信号
pthread_sigmask(SIG_BLOCK, &set, NULL);
// 等待SIGINT信号
sigwait(&set, &signum);
printf("Thread received SIGINT signal. Exiting...\n");
pthread_exit(NULL);
}
int main() {
pthread_t tid;
// 创建线程
if (pthread_create(&tid, NULL, thread_func, NULL) != 0) {
perror("pthread_create");
return 1;
}
printf("Press Ctrl+C to send SIGINT signal to the thread.\n");
sleep(10);
// 取消线程
pthread_cancel(tid);
pthread_join(tid, NULL);
return 0;
}
在上述代码中,我们创建了一个新线程 thread_func
。在 thread_func
中,首先使用 pthread_sigmask
函数阻塞了 SIGINT
信号,然后使用 sigwait
函数等待 SIGINT
信号。当主线程运行一段时间后,通过 pthread_cancel
函数取消线程。如果在这期间用户按下 Ctrl+C
,SIGINT
信号将被 thread_func
线程捕获并处理。
通过合理地使用信号处理机制,在多线程程序中可以实现更灵活、高效的异步事件处理。但需要注意的是,在多线程环境下处理信号时,要充分考虑线程安全问题,避免出现数据竞争和其他并发错误。
总结信号相关要点
- 信号概念:信号是Linux系统中的异步通知机制,用于进程间传递事件信息,每种信号有特定含义和默认处理动作。
- 处理方式:进程对信号可采用默认处理、忽略或捕获三种方式。捕获信号常用
signal
和sigaction
函数,sigaction
更灵活强大。 - 信号忽略:通过设置
handler
为SIG_IGN
可忽略信号,使用signal
或sigaction
函数均可实现。 - 信号屏蔽与解除屏蔽:利用
sigprocmask
函数可临时屏蔽或解除屏蔽信号,防止关键代码段被打断。 - 可重入函数:编写信号处理函数应使用可重入函数,避免使用不可重入函数导致数据损坏,如用
write
替代printf
。 - 信号与系统调用:系统调用可能被信号中断,现代系统部分系统调用默认可重启,也可通过设置禁用自动重启,需处理
errno
为EINTR
的情况。 - 实时信号:具有可靠性和不同优先级,编号从
SIGRTMIN
到SIGRTMAX
,使用sigaction
注册处理函数并设置sa_sigaction
获取更多信号信息。 - 信号与多线程:多线程中信号处理更复杂,可主线程捕获信号再通知其他线程,或用
pthread_sigmask
为特定线程设置信号屏蔽字捕获信号。