【Linux系统】信号产生 从 Ctrl+C 到 core dump
学习时的第一个信号实验很短。程序每隔一秒打印一次,终端里按下 Ctrl+C,它马上退出。给 SIGINT 安装处理函数以后再按一次,终端只多出一行收到信号的提示,程序还活着。
刚看到这里时,我把信号记成了几个按键和一张编号表。等我把 kill、raise、alarm、abort 和非法内存访问放进同一个程序里,信号产生的过程才连了起来。进程原本在执行自己的代码,某个事件发生后,内核给它递交一条通知,原来的控制流随时可能因此改变。
信号来之前,处理办法已经在那里了
我现在把一次信号过程先分成下面三段。
产生信号 → 信号等待递达 → 执行信号的处置动作
进程收到信号以后,会采用默认、忽略或自定义处理函数中的一种处置。处理办法往往早已设置好,信号出现时才决定采用哪一种。SIGKILL 和 SIGSTOP 是两个例外,它们不能被捕捉、阻塞或忽略,否则一个失控进程可能连内核强制控制它的手段也屏蔽掉。
信号产生也不等于处理函数在产生位置立刻执行。中间有时间差。外部信号可能先处于未决状态,等到合适的时机再递达。这两个阶段混在一起以后,后面学习信号屏蔽字时很容易把 pending 和 blocked 当成一回事。
课件把信号称为异步通知,这对 Ctrl+C、其他进程调用 kill、定时器到期都很好理解。进程无法从当前这行 C++ 代码预测它们在哪一刻出现。非法指令和非法内存访问稍有不同,它们由当前正在执行的指令触发,通常归在同步故障一类。两类事件最后都能借助 Linux 信号机制影响进程,产生原因并不完全相同。
平时可以用 kill -l 查看当前系统的信号名称。代码里更适合写 SIGINT、SIGALRM、SIGSEGV 这些宏,不要把某台机器上的数字写死。文末实测输出中的 2、6、10、11、14 来自这次 WSL 环境,换到别的平台时应当以系统头文件为准。
Ctrl+C 这一小段比我原先想得长
参考文章把键盘输入解释成操作系统收到硬件中断,再把组合键变成信号。这个方向能帮助理解,不过从程序能观察到的边界来看,更关键的一层是终端驱动。
在常见终端设置中,Ctrl+C 对应控制字符 VINTR,Ctrl+\ 对应 VQUIT,Ctrl+Z 对应 VSUSP。终端的 ISIG 标志打开时,这些字符会分别产生 SIGINT、SIGQUIT 和 SIGTSTP。它们通常不会作为普通字符交给程序的 read。
还有一处很容易写成“发给当前进程”。终端控制字符面向的是前台进程组。同一条管道命令中的多个前台进程可以属于同一个进程组,按下 Ctrl+C 时,它们都有机会收到 SIGINT。Shell 会启动可执行文件,也要维护会话、进程组和前后台作业。
Ctrl+C
↓
终端驱动识别 VINTR
↓
内核产生 SIGINT
↓
递交给终端的前台进程组
文末程序的 keyboard 模式捕捉了这三个信号。手动运行后按一次组合键,处理函数打印固定字符串并返回,主流程看到 g_last_signal 已经改变后正常结束。
./signal_lab keyboard
这里捕捉的是 SIGTSTP,所以按 Ctrl+Z 后程序不会采用默认的暂停动作。SIGTSTP 与 SIGSTOP 名字相近,能力却有差别。前者可以捕捉,后者始终由内核保留。
kill、raise 和 abort 需要分开记
kill 这个名字容易让我先想到“杀进程”,它实际可以向目标进程或进程组发送指定信号。发送 SIGUSR1 时,目标只会按照 SIGUSR1 的处置方式行动,不会因为调用者用了 kill 就必然退出。
int kill(pid_t pid, int sig);
pid 大于 0 时指向一个具体进程。它取 0 或负数时还能表示进程组,具体范围要连同调用者权限一起判断。返回 0 表示至少成功发送给一个目标,返回 -1 时再检查 errno。常见错误包括目标不存在的 ESRCH、权限不足的 EPERM 和信号编号无效的 EINVAL。
我在实验里先调用了下面这一句。
kill(child, 0);
信号编号 0 不会递交真实信号。它仍然检查进程是否存在,也会检查当前用户有没有发送权限。这个办法适合做轻量探测,不过检查通过之后目标也可能马上退出,因此它不能消除并发变化。
随后父进程发送 SIGUSR1,子进程的处理函数被调用,最后正常退出。为了避免父进程发得太早,代码用一根匿名管道传递就绪字节。子进程先安装处理函数,再通知父进程。少了这一步,父进程可能在子进程完成 sigaction 之前就发送 SIGUSR1,子进程会执行默认终止动作,实验结果也会时好时坏。
raise 的目标固定为调用者。在单线程程序中,可以把它理解成 kill(getpid(), sig)。多线程程序里它针对调用线程,Linux 的 C 库通常会落到线程定向的实现上。raise 属于 C 标准库接口,把它直接称为系统调用不够准确。
我的运行结果里,raise(SIGUSR1) 先进入处理函数,处理函数返回以后,raise 才回到主流程。这个顺序很适合验证“给自己发信号”并不等于新开一条执行流。
abort 也属于库函数,而且它比 raise 更强硬。它会解除对 SIGABRT 的阻塞,再向调用者产生 SIGABRT。即使进程忽略了 SIGABRT,或者处理函数执行后正常返回,abort 仍会恢复默认处置并让进程异常终止。
这次我让子进程安装一个会返回的 SIGABRT 处理函数,输出中先出现 handler 的消息,父进程随后仍然观察到子进程由 6 号信号终止。这正好补上只看一次 handler 输出时容易漏掉的后半段。
alarm 到点以后,内核补上一记 SIGALRM
alarm 对我理解“软件条件产生信号”最有帮助。调用者给内核设置一个以秒为单位的闹钟,时间到后,内核向这个进程产生 SIGALRM。这个信号的默认动作是终止进程。
unsigned int alarm(unsigned int seconds);
每个进程只有一个这样的闹钟。再次调用 alarm 会取消旧设置并换成新设置,返回值是旧闹钟剩余的秒数。传入 0 会取消当前闹钟,同时仍返回原先剩余的时间。
实验先设置 3 秒闹钟,经过约 1.1 秒后改成 1 秒。第二次 alarm 返回 2,随后只收到一次 SIGALRM。处理函数里没有再次调用 alarm,所以它不会自动形成周期定时器。
参考代码常在 handler 中使用 cout 或 printf。普通演示有时看不出问题,异步信号到达的时机却无法预测。主程序可能正改到标准 IO 的内部缓冲区,handler 又进入同一套非可重入状态,结果没有可靠保证。POSIX 给出了一组异步信号安全函数,write 在其中。文末处理函数只用 write 输出预先准备好的固定文本,并把信号编号记录进 volatile sig_atomic_t。格式化输出和流程判断都放回主程序完成。
还有一个实际边界值得记住。alarm 与 setitimer 会共享计时资源,一些 sleep 实现也可能依赖 SIGALRM。把它们混在同一段程序里会相互干扰。实验用 nanosleep 等待,并把 EINTR 当成正常的信号打断情况处理。
硬件异常、退出状态和 core dump
进程访问没有映射或没有权限的地址时,处理器产生异常并进入内核。Linux 可以把这类故障转换成发给当前进程的 SIGSEGV。整数除零常见的现象是 SIGFPE,这个名字里虽然有 floating point,它覆盖的情况并不限于浮点运算。
参考文章里的 4/0 是编译期就能看见的常量表达式。C++ 对整数除零没有定义,编译器可以警告、拒绝某种写法或做出优化,不能把某一次运行现象当成语言标准保证。用 raise(SIGFPE) 又只能证明软件可以主动产生同名信号,无法证明处理器执行除法时发生了异常。
文末实验选择在子进程中写空指针。空指针解引用同样属于 C++ 未定义行为,这次 WSL 和编译选项下观察到了 SIGSEGV,这个结论只描述 Linux 实测。把危险操作留在子进程中,父进程还能继续运行并读取它的终止原因。
这类故障不适合随手捕捉后继续执行。假如处理函数返回,而引发异常的指令和地址都没有修复,进程很可能回到原指令再次故障,handler 便会反复进入。实验保留 SIGSEGV 的默认处置,让子进程终止,再由父进程分析状态。
父进程没有直接拆 status 的二进制位,而是按顺序使用这些宏。
if (WIFSIGNALED(status)) {
int signo = WTERMSIG(status);
#ifdef WCOREDUMP
bool dumped = WCOREDUMP(status);
#endif
}
只有 WIFSIGNALED 为真时,WTERMSIG 和 WCOREDUMP 的含义才成立。这样写也比 status & 0x7F 更容易看出代码要判断正常退出、信号终止还是其他状态变化。
man 7 signal 中的 Term 与 Core 都会终止进程,Core 还带有生成核心转储的默认动作。看到 Segmentation fault (core dumped) 这一行还不够。当前目录未必出现名为 core 的文件,先要看进程的 RLIMIT_CORE,目录权限、磁盘空间和同名文件也会影响写入。/proc/sys/kernel/core_pattern 还可能改名或转交转储,有些系统会把它交给 systemd-coredump 一类服务。
core 文件还可能包含进程内存中的口令、令牌和业务数据,生产环境不能只为了调试方便就无限打开。实验子进程主动把 RLIMIT_CORE 设为 0,只验证终止信号,不在工作区落下 core 文件。所以这次 abort 和空指针实验的 core-flag 都是 0。若要练习事后调试,应当在专门目录中单独调整限制,再核对 core_pattern 和系统的转储服务。
一次跑完后的记录
源码在 WSL 中使用下面的选项编译,没有产生编译警告。
g++ -std=c++11 -Wall -Wextra -Wpedantic signal_lab.cc -o signal_lab
./signal_lab all
这次实际输出如下。PID 每次都会变化,信号宏对应的数字也应当以运行平台为准。
[kill] parent pid=417
[kill] kill(pid, 0) passed existence and permission checks
[kill] sending SIGUSR1 to child pid=556
[handler] received SIGUSR1
[parent] kill-demo child pid=556 exited, code=0
[raise] caller pid=417, raising SIGUSR1 for itself
[handler] received SIGUSR1
[raise] raise returned after the handler, observed signal=10
[alarm] scheduling an alarm for 3 seconds
[alarm] replaced it with a 1-second alarm, old remaining=2
[handler] received SIGALRM
[alarm] one SIGALRM arrived, current alarm(0) returned=0
[abort] child installs a SIGABRT handler that returns
[abort-child] calling abort()
[handler] received SIGABRT and will return
[parent] abort-demo child pid=557 terminated by signal=6, core-flag=0
[segv] child writes through a null pointer
[segv-child] about to access address 0
[parent] segv-demo child pid=558 terminated by signal=11, core-flag=0
交互模式也用 SIGINT 补测过一次,handler 返回后主流程读到了信号编号 2。
[keyboard] pid=635, press Ctrl+C, Ctrl+\, or Ctrl+Z once
[handler] received SIGINT
[keyboard] handler returned, main observed signal=2
完整源代码
下面三个文件就是这次编译和运行使用的全部源码。run_demo.sh 会先重新编译,再依次运行不需要手工按键的五组实验。
signal_lab.cc
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
namespace {
volatile sig_atomic_t g_last_signal = 0;
void SignalHandler(int signo)
{
g_last_signal = signo;
const char *message = "[handler] received an unexpected signal\n";
std::size_t length = sizeof("[handler] received an unexpected signal\n") - 1;
switch (signo)
{
case SIGINT:
message = "[handler] received SIGINT\n";
length = sizeof("[handler] received SIGINT\n") - 1;
break;
case SIGQUIT:
message = "[handler] received SIGQUIT\n";
length = sizeof("[handler] received SIGQUIT\n") - 1;
break;
case SIGTSTP:
message = "[handler] received SIGTSTP\n";
length = sizeof("[handler] received SIGTSTP\n") - 1;
break;
case SIGUSR1:
message = "[handler] received SIGUSR1\n";
length = sizeof("[handler] received SIGUSR1\n") - 1;
break;
case SIGALRM:
message = "[handler] received SIGALRM\n";
length = sizeof("[handler] received SIGALRM\n") - 1;
break;
case SIGABRT:
message = "[handler] received SIGABRT and will return\n";
length = sizeof("[handler] received SIGABRT and will return\n") - 1;
break;
default:
break;
}
const ssize_t ignored = write(STDOUT_FILENO, message, length);
(void)ignored;
}
bool InstallHandler(int signo)
{
struct sigaction action;
std::memset(&action, 0, sizeof(action));
action.sa_handler = SignalHandler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
if (sigaction(signo, &action, nullptr) == -1)
{
std::cerr << "sigaction failed: " << std::strerror(errno) << std::endl;
return false;
}
return true;
}
bool SleepFor(long seconds, long nanoseconds)
{
struct timespec request = {seconds, nanoseconds};
struct timespec remaining = {0, 0};
while (nanosleep(&request, &remaining) == -1)
{
if (errno != EINTR)
{
std::cerr << "nanosleep failed: " << std::strerror(errno) << std::endl;
return false;
}
request = remaining;
}
return true;
}
bool WaitForSignalOrTimeout(int signo, int timeout_seconds)
{
struct timespec request = {0, 100 * 1000 * 1000};
for (int elapsed = 0; elapsed < timeout_seconds * 10; ++elapsed)
{
if (g_last_signal == signo)
{
return true;
}
struct timespec remaining = {0, 0};
while (nanosleep(&request, &remaining) == -1 && errno == EINTR)
{
if (g_last_signal == signo)
{
return true;
}
request = remaining;
}
request = {0, 100 * 1000 * 1000};
}
return g_last_signal == signo;
}
int WaitAndReport(pid_t child, const char *label)
{
int status = 0;
pid_t result = -1;
do
{
result = waitpid(child, &status, 0);
} while (result == -1 && errno == EINTR);
if (result == -1)
{
std::cerr << "waitpid failed: " << std::strerror(errno) << std::endl;
return 1;
}
std::cout << "[parent] " << label << " child pid=" << child;
if (WIFEXITED(status))
{
std::cout << " exited, code=" << WEXITSTATUS(status);
}
else if (WIFSIGNALED(status))
{
std::cout << " terminated by signal=" << WTERMSIG(status);
#ifdef WCOREDUMP
std::cout << ", core-flag=" << (WCOREDUMP(status) ? 1 : 0);
#endif
}
else
{
std::cout << " changed state, raw-status=" << status;
}
std::cout << std::endl;
return 0;
}
bool DisableCoreDump()
{
const struct rlimit limit = {0, 0};
return setrlimit(RLIMIT_CORE, &limit) == 0;
}
int RunKeyboardDemo()
{
g_last_signal = 0;
if (!InstallHandler(SIGINT) || !InstallHandler(SIGQUIT) || !InstallHandler(SIGTSTP))
{
return 1;
}
std::cout << "[keyboard] pid=" << getpid()
<< ", press Ctrl+C, Ctrl+\\, or Ctrl+Z once" << std::endl;
while (g_last_signal == 0)
{
if (!SleepFor(1, 0))
{
return 1;
}
}
std::cout << "[keyboard] handler returned, main observed signal="
<< g_last_signal << std::endl;
return 0;
}
int RunKillDemo()
{
int ready_pipe[2];
if (pipe(ready_pipe) == -1)
{
std::cerr << "pipe failed: " << std::strerror(errno) << std::endl;
return 1;
}
std::cout << "\n[kill] parent pid=" << getpid() << std::endl;
const pid_t child = fork();
if (child == -1)
{
std::cerr << "fork failed: " << std::strerror(errno) << std::endl;
close(ready_pipe[0]);
close(ready_pipe[1]);
return 1;
}
if (child == 0)
{
close(ready_pipe[0]);
g_last_signal = 0;
if (!InstallHandler(SIGUSR1))
{
_exit(2);
}
const char ready = 'R';
if (write(ready_pipe[1], &ready, 1) != 1)
{
_exit(3);
}
close(ready_pipe[1]);
while (g_last_signal != SIGUSR1)
{
pause();
}
_exit(0);
}
close(ready_pipe[1]);
char ready = 0;
ssize_t read_result = -1;
do
{
read_result = read(ready_pipe[0], &ready, 1);
} while (read_result == -1 && errno == EINTR);
close(ready_pipe[0]);
if (read_result != 1 || ready != 'R')
{
std::cerr << "child did not become ready" << std::endl;
kill(child, SIGKILL);
return WaitAndReport(child, "kill-demo");
}
if (kill(child, 0) == 0)
{
std::cout << "[kill] kill(pid, 0) passed existence and permission checks"
<< std::endl;
}
else
{
std::cerr << "kill(pid, 0) failed: " << std::strerror(errno) << std::endl;
kill(child, SIGKILL);
return WaitAndReport(child, "kill-demo");
}
std::cout << "[kill] sending SIGUSR1 to child pid=" << child << std::endl;
if (kill(child, SIGUSR1) == -1)
{
std::cerr << "kill(SIGUSR1) failed: " << std::strerror(errno) << std::endl;
kill(child, SIGKILL);
return WaitAndReport(child, "kill-demo");
}
return WaitAndReport(child, "kill-demo");
}
int RunRaiseDemo()
{
g_last_signal = 0;
if (!InstallHandler(SIGUSR1))
{
return 1;
}
std::cout << "\n[raise] caller pid=" << getpid()
<< ", raising SIGUSR1 for itself" << std::endl;
if (raise(SIGUSR1) != 0)
{
std::cerr << "raise failed" << std::endl;
return 1;
}
std::cout << "[raise] raise returned after the handler, observed signal="
<< g_last_signal << std::endl;
return g_last_signal == SIGUSR1 ? 0 : 1;
}
int RunAlarmDemo()
{
g_last_signal = 0;
if (!InstallHandler(SIGALRM))
{
return 1;
}
std::cout << "\n[alarm] scheduling an alarm for 3 seconds" << std::endl;
alarm(3);
if (!SleepFor(1, 100 * 1000 * 1000))
{
alarm(0);
return 1;
}
const unsigned int remaining = alarm(1);
std::cout << "[alarm] replaced it with a 1-second alarm, old remaining="
<< remaining << std::endl;
if (!WaitForSignalOrTimeout(SIGALRM, 3))
{
std::cerr << "SIGALRM did not arrive in time" << std::endl;
alarm(0);
return 1;
}
std::cout << "[alarm] one SIGALRM arrived, current alarm(0) returned="
<< alarm(0) << std::endl;
return 0;
}
int RunAbortDemo()
{
std::cout << "\n[abort] child installs a SIGABRT handler that returns"
<< std::endl;
const pid_t child = fork();
if (child == -1)
{
std::cerr << "fork failed: " << std::strerror(errno) << std::endl;
return 1;
}
if (child == 0)
{
if (!DisableCoreDump())
{
_exit(2);
}
if (!InstallHandler(SIGABRT))
{
_exit(3);
}
const char message[] = "[abort-child] calling abort()\n";
(void)write(STDOUT_FILENO, message, sizeof(message) - 1);
abort();
}
return WaitAndReport(child, "abort-demo");
}
int RunSegvDemo()
{
std::cout << "\n[segv] child writes through a null pointer" << std::endl;
const pid_t child = fork();
if (child == -1)
{
std::cerr << "fork failed: " << std::strerror(errno) << std::endl;
return 1;
}
if (child == 0)
{
if (!DisableCoreDump())
{
_exit(2);
}
const char message[] = "[segv-child] about to access address 0\n";
(void)write(STDOUT_FILENO, message, sizeof(message) - 1);
volatile int *bad_address = nullptr;
*bad_address = 7;
_exit(0);
}
return WaitAndReport(child, "segv-demo");
}
void PrintUsage(const char *program)
{
std::cerr << "Usage: " << program
<< " {keyboard|kill|raise|alarm|abort|segv|all}" << std::endl;
}
} // namespace
int main(int argc, char *argv[])
{
if (argc != 2)
{
PrintUsage(argv[0]);
return 1;
}
const std::string mode = argv[1];
if (mode == "keyboard")
{
return RunKeyboardDemo();
}
if (mode == "kill")
{
return RunKillDemo();
}
if (mode == "raise")
{
return RunRaiseDemo();
}
if (mode == "alarm")
{
return RunAlarmDemo();
}
if (mode == "abort")
{
return RunAbortDemo();
}
if (mode == "segv")
{
return RunSegvDemo();
}
if (mode == "all")
{
int result = 0;
result |= RunKillDemo();
result |= RunRaiseDemo();
result |= RunAlarmDemo();
result |= RunAbortDemo();
result |= RunSegvDemo();
return result;
}
PrintUsage(argv[0]);
return 1;
}
Makefile
CXX ?= g++
CXXFLAGS ?= -std=c++11 -Wall -Wextra -Wpedantic
TARGET := signal_lab
.PHONY: all clean run
all: $(TARGET)
$(TARGET): signal_lab.cc
$(CXX) $(CXXFLAGS) $< -o $@
run: $(TARGET)
./$(TARGET) all
clean:
rm -f $(TARGET)
run_demo.sh
#!/usr/bin/env bash
set -eu
make clean
make
./signal_lab all
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)