使用C语言进行信号处理:从理论到实践的全面指南

devtools/2024/11/8 7:15:58/

在这里插入图片描述

1. 引言

在现代操作系统中,信号是一种进程间通信机制,它允许操作系统或其他进程向一个进程发送消息。信号可以用来通知进程发生了一些重要事件,如用户请求终止进程、硬件异常、定时器超时等。掌握信号处理技术对于开发健壮、高效的系统程序至关重要。本文将带你深入了解信号的基础知识,并通过一系列示例演示如何在C语言程序中实现信号处理

2. 信号概述

信号是由操作系统产生的软件中断,用于通知接收进程发生了某些类型的事件。信号可以分为两大类:

  • 不可忽略的信号:如SIGKILL和SIGSTOP,它们总是会被操作系统强制执行。
  • 可忽略的信号:如SIGINT和SIGTERM,接收进程可以选择忽略或者自定义处理。

常见的信号及其用途如下表所示:

信号编号描述
SIGINT2终端中断信号,通常由用户按下Ctrl+C触发。
SIGTERM15终止信号,通常用于请求程序优雅地停止运行。
SIGKILL9强制终止信号,无法被捕捉或忽略。
SIGALRM14定时信号,由alarm()函数设置的时间间隔到期时产生。
SIGHUP1挂断信号,当控制终端挂起或登录会话结束时产生。
SIGPIPE13管道破裂信号,当写入一个已经断开连接的管道时产生。
SIGUSR110用户定义信号1,用于进程间的通讯。
SIGUSR212用户定义信号2,用于进程间的通讯。

在这里插入图片描述

3. 信号处理基础

在C语言中,信号处理主要依赖于signal()函数。该函数允许用户注册一个信号处理函数,当指定的信号到达时,就会调用这个函数。然而,signal()函数存在一些限制,如不能传递额外参数给信号处理函数,且不是线程安全的。因此,在多线程程序中,更推荐使用sigaction()函数来替代。

3.1 使用signal()函数
#include <signal.h>
#include <stdio.h>void signal_handler(int signum) {printf("Received signal %d\n", signum);exit(signum);
}int main() {signal(SIGINT, signal_handler); // 注册信号处理函数while (1) {printf("Hello World!\n");sleep(1);}return 0;
}

上述代码注册了一个SIGINT信号处理函数signal_handler,当用户按下Ctrl+C时,程序将打印一条消息并退出。

3.2 使用sigaction()函数

sigaction()函数提供了更多的灵活性和安全性,可以设置信号掩码、指定信号处理方式(忽略、默认处理或自定义处理函数)等。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>void sigint_handler(int signum, siginfo_t *info, void *context) {printf("Caught signal %d\n", signum);exit(signum);
}int main() {struct sigaction sa;sa.sa_sigaction = sigint_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_SIGINFO;if (sigaction(SIGINT, &sa, NULL) == -1) {perror("sigaction");return 1;}while (1) {printf("Hello World!\n");sleep(1);}return 0;
}

在这个版本中,我们使用sigaction()函数注册了一个信号处理程序,并且启用了SA_SIGINFO标志,这允许我们的信号处理函数接受额外的参数。

在这里插入图片描述

4. 信号与线程

在多线程程序中,信号的处理需要特别注意。默认情况下,信号是针对整个进程而不是特定线程的。这意味着,如果一个线程捕获到了信号,所有线程都会受到影响。为了避免这种情况,可以使用pthread_sigmask()函数来设置线程的信号掩码,从而控制哪些信号可以被线程捕获。

4.1 设置线程信号掩码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>void *thread_func(void *arg) {int *num = (int *)arg;*num += 1;printf("Thread: %d\n", *num);pthread_exit(NULL);
}int main() {pthread_t thread_id;int num = 0;if (pthread_create(&thread_id, NULL, thread_func, &num) != 0) {perror("Failed to create thread");exit(EXIT_FAILURE);}sigset_t mask;sigfillset(&mask); // 设置信号掩码sigdelset(&mask, SIGINT); // 允许SIGINT信号if (pthread_sigmask(SIG_BLOCK, &mask, NULL) == -1) {perror("Failed to set signal mask");exit(EXIT_FAILURE);}while (1) {printf("Main thread: %d\n", num);sleep(1);}return 0;
}

在此示例中,我们创建了一个线程,并设置了信号掩码,使得只有SIGINT信号可以被线程捕获。这样即使在主线程中按下Ctrl+C,也不会影响到正在运行的线程。

5. 定时信号:alarm()sigtimedwait()

除了处理外部信号外,我们还可以通过alarm()函数来设置定时信号。当指定的时间过去之后,SIGALRM信号就会被发送给进程。此外,sigtimedwait()函数提供了一种等待信号的方式,并且可以指定一个超时时间。

5.1 使用alarm()函数
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void alarm_handler(int signum) {printf("Alarm signal received.\n");
}int main() {signal(SIGALRM, alarm_handler);alarm(5); // 设置5秒后发送SIGALRM信号while (1) {printf("Waiting...\n");sleep(1);}return 0;
}

此程序将在启动后五秒发出报警信号。

5.2 使用sigtimedwait()函数
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>int main() {sigset_t pending;sigemptyset(&pending);sigaddset(&pending, SIGALRM);alarm(5); // 设置5秒后发送SIGALRM信号struct timespec timeout = {1, 0}; // 超时时间为1秒siginfo_t info;while (1) {printf("Waiting for a signal...\n");if (sigtimedwait(&pending, &info, &timeout) != -1) {printf("Signal caught: %d\n", info.si_signo);}sleep(1);}return 0;
}

在这个例子中,我们使用sigtimedwait()函数来等待信号,如果在一秒钟内没有信号到来,则会继续循环。

在这里插入图片描述

6. 高级主题:信号队列与实时信号
6.1 信号队列

当一个信号被发送给一个进程时,如果该信号正在被处理或被阻止,则信号会被放入进程的信号队列中。每个进程都有一个信号队列,最多可以存储一个每个类型的信号。当信号队列已满时,再来的相同类型的信号将被丢弃。

信号队列的管理通常是由操作系统完成的,但作为程序员,我们需要知道信号队列的存在,并且在设计程序时考虑到这一点。例如,如果程序频繁地忽略或阻止某个信号,可能导致信号丢失,从而引发不可预期的行为。

6.2 实时信号

实时信号是一组特殊的信号,它们具有更高的优先级,并且可以携带额外的数据。使用sigqueue()函数可以发送实时信号,并附带一个用户定义的值。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>void real_time_signal_handler(int signum, siginfo_t *info, void *context) {printf("Real-time signal %d with value %d\n", signum, info->si_value.sival_int);
}int main() {struct sigaction sa;sa.sa_sigaction = real_time_signal_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_SIGINFO;if (sigaction(SIGRTMIN, &sa, NULL) == -1) {perror("sigaction");return 1;}// 发送带有整数值的实时信号union sigval value;value.sival_int = 1234;if (sigqueue(0, SIGRTMIN, value) == -1) {perror("sigqueue");return 1;}while (1) {printf("Waiting for a real-time signal...\n");sleep(1);}return 0;
}

这段代码演示了如何发送一个带有整数值的实时信号,并在信号处理函数中读取这个值。

7. 实战案例:实现一个简单的守护进程

守护进程(Daemon)是一种在后台运行的服务程序,它不与任何终端关联,并且通常会在系统启动时自动运行。下面我们将展示如何使用信号处理技术来创建一个简单的守护进程。

7.1 创建守护进程

创建守护进程的一般步骤如下:

  1. 第一次fork:创建一个子进程,然后让父进程退出。这是为了防止后续操作受到shell的影响。
  2. 成为会话领导者:通过调用setsid()函数,使进程脱离原来的会话和终端。
  3. 第二次fork:再次创建一个子进程,并让父进程退出。这是因为setsid()只能在一个没有控制终端的进程中调用,否则会失败。
  4. 更改工作目录:将当前工作目录改为根目录,防止进程删除其当前目录而导致进程无法正常工作。
  5. 关闭文件描述符:关闭标准输入、输出和错误文件描述符,防止守护进程占用不必要的资源。
  6. 设置信号处理程序:忽略某些信号,使守护进程更加稳定。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void daemonize() {pid_t pid = fork();if (pid < 0) {perror("Fork failed");exit(EXIT_FAILURE);} else if (pid > 0) {exit(EXIT_SUCCESS); // 父进程退出}// 成为会话领导者if (setsid() < 0) {perror("Setsid failed");exit(EXIT_FAILURE);}// 第二次forkpid = fork();if (pid < 0) {perror("Fork failed");exit(EXIT_FAILURE);} else if (pid > 0) {exit(EXIT_SUCCESS); // 父进程退出}// 更改工作目录if (chdir("/") < 0) {perror("Chdir failed");exit(EXIT_FAILURE);}// 关闭文件描述符umask(0);close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 设置信号处理程序signal(SIGHUP, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGQUIT, SIG_IGN);signal(SIGTERM, exit);
}int main() {daemonize();while (1) {printf("Daemon running...\n");sleep(10);}return 0;
}

这个简单的守护进程忽略了大多数信号,只对SIGTERM信号作出响应,即当接收到终止信号时退出。

在这里插入图片描述

8. 高级实战案例:守护进程与信号处理

让我们进一步扩展之前的守护进程示例,使其成为一个更加实用的服务程序。我们将添加日志记录功能,并且允许守护进程通过信号进行重启、停止等操作。

8.1 日志记录

在守护进程中添加日志记录功能可以帮助我们跟踪程序的状态和错误。我们可以将日志输出到一个文件中,这样即使程序崩溃,我们也能够查看到它最后的状态。

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>#define LOG_FILE "/var/log/mydaemon.log"void log(const char *message) {int fd = open(LOG_FILE, O_WRONLY | O_APPEND | O_CREAT, 0644);if (fd == -1) {perror("Open log file failed");return;}fprintf(fd, "%s\n", message);close(fd);
}void daemonize() {pid_t pid = fork();if (pid < 0) {perror("Fork failed");exit(EXIT_FAILURE);} else if (pid > 0) {exit(EXIT_SUCCESS); // 父进程退出}if (setsid() < 0) {perror("Setsid failed");exit(EXIT_FAILURE);}pid = fork();if (pid < 0) {perror("Fork failed");exit(EXIT_FAILURE);} else if (pid > 0) {exit(EXIT_SUCCESS); // 父进程退出}if (chdir("/") < 0) {perror("Chdir failed");exit(EXIT_FAILURE);}umask(0);close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);signal(SIGHUP, SIG_IGN);signal(SIGINT, SIG_IGN);signal(SIGQUIT, SIG_IGN);signal(SIGTERM, exit);
}void handle_signals(int signum) {switch (signum) {case SIGHUP:log("SIGHUP received, reloading configuration...");break;case SIGTERM:log("SIGTERM received, shutting down...");exit(EXIT_SUCCESS);default:log("Unknown signal received.");break;}
}int main() {daemonize();// 设置信号处理函数signal(SIGHUP, handle_signals);signal(SIGTERM, handle_signals);while (1) {log("Daemon running...");sleep(10);}return 0;
}

在这个版本中,我们添加了一个log()函数,用于将消息输出到日志文件中。同时,我们修改了信号处理函数handle_signals(),使其能够根据不同类型的信号采取不同的行动。

9. 总结与展望

通过本文,你不仅了解了信号的基本概念和用途,还学会了如何在C语言程序中使用信号处理技术。从简单的信号处理到复杂的守护进程创建,每一步都充满了挑战与乐趣。希望这些知识能够帮助你在未来的开发过程中更好地利用信号机制来提升程序的健壮性和可用性。


http://www.ppmy.cn/devtools/132239.html

相关文章

Docker 基础命令简介

目录 Docker 基础命令 1. Docker 版本信息 2. 获取 Docker 帮助 3. 列出所有运行中的容器 4. 运行一个新的容器 5. 查看容器日志 6. 停止容器 7. 启动已停止的容器 8. 删除容器 9. 列出所有镜像 10. 拉取镜像 11. 构建镜像 12. 删除镜像 13. 执行命令 14. 查看容…

MySQL_数据类型建表

复习&#xff1a; 我们昨天学习的知识都忘了嘛&#xff1f;如果忘了也不要担心&#xff0c;我来带大家来复习一遍吧&#xff01;&#xff01;&#xff01; 1.查看所有数据库 show databases;2.创建属于自己的数据库 create database 数据库名; 检查自己创建的数据库是…

Docker在CentOS上的安装与配置

前言 随着云计算和微服务架构的兴起&#xff0c;Docker作为一种轻量级的容器技术&#xff0c;已经成为现代软件开发和运维中的重要工具。本文旨在为初学者提供一份详尽的指南&#xff0c;帮助他们在CentOS系统上安装和配置Docker及相关组件&#xff0c;如Docker Compose和私有…

爬虫技术——小白入狱案例

知孤云出岫 目录 1. 案例概述2. 案例需求分析3. 实现步骤Step 1: 环境准备Step 2: 分析百度图片URL请求规律Step 3: 编写爬虫代码代码解析 4. 运行代码5. 注意事项6. 案例总结 要实现大批量爬取百度图片&#xff0c;可以使用Python编写一个网络爬虫&#xff0c;通过发送HTTP请求…

CSS Grid 布局在 IE 中不兼容的原因与解决方案

CSS Grid 布局在 IE 中不兼容的原因与解决方案 文章目录 CSS Grid 布局在 IE 中不兼容的原因与解决方案1. 引言2. CSS Grid 布局概述2.1 什么是CSS Grid布局&#xff1f;2.2 CSS Grid 与传统布局方法的区别 3. IE 对 CSS Grid 的支持情况3.1 IE11 对 CSS Grid 的支持3.2 其他IE…

大众汽车合肥社招入职笔试测评SHL题库:综合能力、性格问卷、英语口语真题考什么?

大众汽车合肥社招入职笔试测评包括综合能力测试、性格问卷和英语口语测试。以下是各部分的具体内容&#xff1a; 1. **综合能力测试**&#xff1a; - 这部分测试需要46分钟完成&#xff0c;建议准备计算器和纸笔。 - 测试内容涉及问题解决能力、数值计算能力和逻辑推理能力。 -…

Matplotlib 绘图艺术:从新手到高手的全面指南

引言 在数据科学和机器学习领域&#xff0c;数据可视化是一项至关重要的技能。一个优秀的可视化图表可以直观地展示数据的内在规律&#xff0c;帮助我们更好地理解数据&#xff0c;并做出更明智的决策。而在众多的绘图库中&#xff0c;Matplotlib 是 Python 中最强大、最灵活的…

并查集算法详解

文章目录 并查集概念并查集的常见操作构建并查集合并并查集和查找 关于find函数 并查集概念 并查集&#xff08;Union-Find&#xff09;是一种树型的数据结构&#xff0c;用于处理一些不交集的合并及查询问题。其主要应用是判断两个元素是否在同一个集合中&#xff0c;以及合并…