1. 知识回顾
前文已介绍 Ctrl+C 可以杀死前台进程,本文详解 Ctrl+C 是如何变成信号的。
Linux 信号机制通过硬件中断与软件模拟实现进程间异步通知。本文详解 Ctrl+C 触发 SIGINT 的硬件流程,以及 kill、raise、abort 等系统调用发送信号的原理。涵盖系统调用号管理数据结构及信号处理示例代码。

前文已介绍 Ctrl+C 可以杀死前台进程,本文详解 Ctrl+C 是如何变成信号的。
操作系统为了保证安全,进程无法直接读取键盘数据。基于 Linux'一切皆文件'的思想,键盘也是文件,操作系统会先读取键盘缓冲区文件。
操作系统如何知道键盘上有数据要读取?CPU 上的针脚和外设间接相连,键盘有数据时会向 CPU 发送硬件中断,由 CPU 告诉操作系统将键盘数据拷贝到键盘文件缓冲区。
计算机外设众多,每个外设均可向 CPU 发送中断。如何确保是键盘发送的中断?每个中断都有唯一编号,称为中断号。CPU 根据中断号区分不同中断。
外设向 CPU 发送中断,CPU 解释出中断号。保护模式下,CPU 根据中断号到中断描述符表找出对应中断例程的地址,然后跳转执行,结束后通过 iret 返回。
该中断例程可以是操作系统将键盘外设上的数据拷贝到键盘文件缓冲区。若多个硬件同时发送中断,操作系统会串行处理。
参考《Linux 内核设计与实现》:内核负责管理硬件设备,提供中断机制。当硬件设备和系统通信时,发出异步中断信号打断处理器执行。内核通过中断号查找相应的中断服务程序并调用响应和处理。例如敲击键盘时,键盘控制器发送中断信号告知系统缓冲区有数据到来。内核调用相应服务程序处理数据,通知控制器继续输入。为保证同步,内核可停用中止。
上方讲的是硬件中断,信号是用软件的方式模拟硬件中断。
用户按下 Ctrl+C,键盘输入产生硬件中断,被操作系统获取,解释成信号,发送给目标前台进程。前台进程收到信号,引起进程退出。
正常情况下,从键盘输入的数据会显示到显示器上,这是因为操作系统将键盘缓冲区的数据拷贝到显示器的缓冲区。
注:图中的'键盘文件'和'显示器文件'是根据 Linux'一切皆文件'的思想,将硬件的内容抽象为文件。
不回显的含义:键盘缓冲区的数据不拷贝到显示器的缓冲区。
前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous) 的。
信号是进程之间事件异步通知的一种方式,属于软中断,软中断是仿照硬件中断实现的软件逻辑。
新建以下文件结构:
test_signal
├── hello_world.cpp
├── makefile
└── test_signal.cpp
hello_world.cpp 写入打印 Hello World 的代码。 makefile 编写编译规则。
作用:向指定进程发送指定的信号。 kill 的参数:
test_signal.cpp 写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "用法:kill -signo pid" << std::endl;
exit(1);
}
std::string signo_s = argv[1];
std::string pid_s = argv[2];
int signo = std::stoi(std::string(signo_s, 1));
int pid = stoi(pid_s);
if (signo < 0 || pid > 64) {
std::cout << "非法的信号编号" << std::endl;
exit(2);
}
if (kill(pid, signo)) {
perror("kill failed");
exit(3);
}
std::cout << "已发送信号" << std::endl;
return 0;
}
启动一个无限打印 Hello World 的进程,尝试杀死。
作用:发送一个信号给调用者。 sig 是信号的编号。
test_signal.cpp 写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
int main(int argc, char* argv[]) {
int cnt = 0;
for (;;) {
std::cout << "Hello World!" << std::endl;
sleep(1);
cnt++;
if (cnt == 5) raise(9);
}
return 0;
}
结论:kill(getpid(), signo) 和 raise(signo) 作用等价。
作用:给调用者发送 6 号信号 SIGABRT。
可以手动捕获 abort() 发送的 6 号信号,test_signal.cpp 写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
void myhandler(int signo) {
std::cout << "已执行自定义动作,信号编号为" << signo << "号" << std::endl;
}
int main(int argc, char* argv[]) {
signal(SIGABRT, myhandler);
int cnt = 0;
for (;;) {
std::cout << "Hello World!" << std::endl;
sleep(1);
cnt++;
if (cnt == 5) abort();
}
return 0;
}
发现捕获 SIGABRT 信号后,程序仍然终止了。
abort 函数发送 SIGABRT 信号,编号为 6。
结论:abort() 除了能发送信号,内部实现有终止进程这个功能。
Linux 的每个系统调用函数都有对应的系统调用号,以 x86 为例,这个调用号可以在 /arch/x86/entry/syscalls 目录下找到。
可以看到 32 位的系统调用号存在 syscall_32.tbl,64 位的系统调用号存在 syscall_64.tbl。
需要强调的是,同一个系统调用号在 32 位和 64 位下对应的系统调用函数不一样。
这里截取一部分:
syscall_32.tbl:
0 i386 restart_syscall sys_restart_syscall
1 i386 exit sys_exit - noreturn
2 i386 fork sys_fork
3 i386 read sys_read
4 i386 write sys_write
5 i386 open sys_open compat_sys_open
6 i386 close sys_close
7 i386 waitpid sys_waitpid
8 i386 creat sys_creat
syscall_64.tbl:
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
4 common stat sys_newstat
5 common fstat sys_newfstat
6 common lstat sys_newlstat
7 common poll sys_poll
8 common lseek sys_lseek
9 common mmap sys_mmap
Linux 内核中有大量的系统调用,为了管理这些系统调用,需要将这些系统调用放到对应的数据结构中。
在 arch/x86/entry/syscall_32.c 和 /arch/x86/entry/syscall_64.c 中都定义了 sys_call_table 这个数据结构:
syscall_32.c 的部分内容:
/*
* The sys_call_table[] is no longer used for system calls, but
* kernel/trace/trace_syscalls.c still wants to know the system
* call address.
*/
#ifdef CONFIG_X86_32
#define __SYSCALL(nr, sym) __ia32_##sym, const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_32.h> };
#undef __SYSCALL
#endif
syscall_64.c 的部分内容:
/*
* The sys_call_table[] is no longer used for system calls, but
* kernel/trace/trace_syscalls.c still wants to know the system
* call address.
*/
#define __SYSCALL(nr, sym) __x64_##sym, const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h> };
#undef __SYSCALL

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online