【Linux系统编程】(三十五)揭秘 Linux 信号产生:从终端到内核全解析

【Linux系统编程】(三十五)揭秘 Linux 信号产生:从终端到内核全解析

前言

        在 Linux 系统中,信号是进程间异步通信的 “信使”,而 “信号产生” 则是这个通信过程的起点。无论是我们熟悉的Ctrl+C终止进程,还是程序运行中出现的段错误、定时器超时,本质上都是信号被触发产生的过程。很多开发者只知道 “信号能终止进程”,却不清楚信号到底是怎么来的 —— 是用户操作触发的?还是系统自动产生的?不同场景下信号的产生机制有何不同?

        本文将基于 Linux 内核原理,结合 5 种核心信号产生场景(终端按键、系统命令、函数调用、软件条件、硬件异常),用通俗的语言,带你全方位揭秘信号产生的底层逻辑,让你不仅 “知其然”,更 “知其所以然”。下面就让我们正式开始吧!

一、信号产生的核心本质:谁在 “发送” 信号?

        在深入具体场景之前,我们先明确一个核心问题:信号是由谁产生并发送的?答案是操作系统(OS)

        无论信号的触发源头是用户按键、函数调用还是硬件异常,最终都必须经过 OS 的 “中转”——OS 会将这些触发事件解释为对应的信号,再发送给目标进程。这是因为 OS 是进程的 “管理者”,只有 OS 拥有操作进程 PCB(进程控制块)的权限,能够修改进程的未决信号集,完成信号的 “投递”。

        举个通俗的例子:信号就像快递,触发信号的源头(用户、函数、硬件)是 “寄件人”,OS 是 “快递员”,目标进程是 “收件人”。寄件人不会直接把快递交给收件人,而是交给快递员,由快递员负责投递到收件人手中,信号的产生与发送也是如此。

        信号产生的完整链路可以总结为:

触发事件(用户/函数/硬件等)→ OS识别事件 → OS将事件映射为对应信号 → OS修改目标进程PCB的未决信号集 → 信号产生并等待递达 

        这一链路是所有信号产生场景的共同底层逻辑,接下来我们将针对不同的 “触发事件”,逐一拆解具体场景。

二、场景 1:终端按键触发 —— 最直观的信号产生方式

        终端(Terminal)是用户与 Linux 系统交互的主要界面,我们日常使用的Ctrl+CCtrl+\Ctrl+Z等组合键,本质上都是通过终端触发信号产生的。这种方式最直观,也是我们接触最多的信号产生场景。

2.1 核心原理:终端按键如何触发信号?

        当我们在终端中按下组合键时,会发生以下一系列动作:

键盘按键产生硬件中断,终端驱动程序捕获该中断;终端驱动程序将按键事件转换为对应的信号(如Ctrl+C对应SIGINT信号);终端将信号发送给 OS,告知 OS “需要向当前前台进程发送某个信号”;OS 接收请求后,找到当前前台进程,修改其 PCB 中的未决信号集,完成信号产生。

        这里有一个关键规则:终端组合键产生的信号,只能发送给当前前台进程。后台进程(通过&启动的进程)无法接收终端组合键产生的信号,这是为了避免后台进程被用户误操作中断。

2.2 三大常用终端信号:实战验证

        Linux 终端中最常用的三个组合键对应的信号分别是:Ctrl+C(SIGINT)Ctrl+\(SIGQUIT)Ctrl+Z(SIGTSTP),我们通过实战代码逐一验证它们的产生与作用。

2.2.1 Ctrl+C:SIGINT 信号(2 号)—— 终止进程

    SIGINT信号的默认处理动作是 “终止进程”,这是我们最常用的 “强制终止进程” 的方式。

代码验证 1:默认动作 —— 终止进程
// sig_int_default.cpp #include <iostream> #include <unistd.h> using namespace std; int main() { cout << "进程PID:" << getpid() << ",正在运行...(按下Ctrl+C终止)" << endl; while (true) { sleep(1); cout << "进程正常运行中..." << endl; } return 0; } 
编译运行
g++ sig_int_default.cpp -o sig_int_default ./sig_int_default 

        运行后,终端会持续打印 “进程正常运行中...”,此时按下Ctrl+C,进程会立即终止,终端输出如下:

进程PID:12345,正在运行...(按下Ctrl+C终止) 进程正常运行中... 进程正常运行中... ^C # 按下Ctrl+C 
代码验证 2:自定义信号处理 —— 让Ctrl+C不终止进程

        我们可以通过signal函数自定义SIGINT信号的处理动作,让按下Ctrl+C后进程不终止,而是执行我们定义的逻辑。

// sig_int_catch.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; // 自定义信号处理函数 void sigint_handler(int signum) { cout << "\n捕获到信号:" << signum << "(SIGINT),Ctrl+C无效!" << endl; cout << "进程继续运行..." << endl; } int main() { cout << "进程PID:" << getpid() << ",正在运行...(按下Ctrl+C测试)" << endl; // 注册SIGINT信号的处理函数 signal(SIGINT, sigint_handler); while (true) { sleep(1); cout << "进程正常运行中..." << endl; } return 0; } 
编译运行
g++ sig_int_catch.cpp -o sig_int_catch ./sig_int_catch 

        运行后按下Ctrl+C,进程不会终止,而是打印自定义信息:

进程PID:12346,正在运行...(按下Ctrl+C测试) 进程正常运行中... 进程正常运行中... ^C 捕获到信号:2(SIGINT),Ctrl+C无效! 进程继续运行... 进程正常运行中... 

2.2.2 Ctrl+\:SIGQUIT 信号(3 号)—— 终止进程并生成 Core Dump

    SIGQUIT信号的默认处理动作是 “终止进程并生成 Core Dump 文件”。Core Dump 文件是进程异常终止时的内存镜像文件,包含进程终止时的内存数据、寄存器状态等信息,用于事后调试(Post-mortem Debug)。

核心知识点:Core Dump 文件
默认情况下,Linux 系统会禁用 Core Dump 功能(避免泄露敏感信息),可以通过ulimit -c 1024命令临时开启(允许生成最大 1024KB 的 Core 文件);Core 文件的默认名称为core.进程PID,存储在进程运行目录下;可以通过gdb 程序名 core文件名命令调试 Core 文件,定位进程崩溃原因。
代码验证:SIGQUIT 信号的默认动作
// sig_quit_core.cpp #include <iostream> #include <unistd.h> using namespace std; int main() { cout << "进程PID:" << getpid() << ",正在运行...(按下Ctrl+\\生成Core文件)" << endl; while (true) { sleep(1); cout << "进程正常运行中..." << endl; } return 0; } 
编译运行与调试
# 开启Core Dump功能(临时生效) ulimit -c 1024 # 编译 g++ sig_quit_core.cpp -o sig_quit_core # 运行 ./sig_quit_core 

        运行后按下Ctrl+\,进程终止并生成 Core 文件:

进程PID:12347,正在运行...(按下Ctrl+\生成Core文件) 进程正常运行中... 进程正常运行中... ^\Quit (core dumped) # 按下Ctrl+\ # 查看生成的Core文件 ls -l core* 

        终端会显示类似core.12347的文件,使用 gdb 调试:

gdb sig_quit_core core.12347 

        调试输出会显示进程终止的原因(收到 SIGQUIT 信号),验证了信号的产生与作用。

2.2.3 Ctrl+Z:SIGTSTP 信号(20 号)—— 暂停前台进程

    SIGTSTP信号的默认处理动作是 “暂停前台进程”,将进程从 “运行态” 切换为 “停止态(Stopped)”,并将其转入后台。暂停的进程可以通过fg命令恢复到前台,或通过bg命令让其在后台继续运行。

代码验证:SIGTSTP 信号的默认动作
// sig_tstp_stop.cpp #include <iostream> #include <unistd.h> using namespace std; int main() { cout << "进程PID:" << getpid() << ",正在运行...(按下Ctrl+Z暂停)" << endl; while (true) { sleep(1); cout << "进程正常运行中..." << endl; } return 0; } 
编译运行与操作
g++ sig_tstp_stop.cpp -o sig_tstp_stop ./sig_tstp_stop 

        运行后按下Ctrl+Z,进程被暂停并转入后台:

进程PID:12348,正在运行...(按下Ctrl+Z暂停) 进程正常运行中... 进程正常运行中... ^Z[1]+ Stopped ./sig_tstp_stop # 进程被暂停 

        后续操作命令:

# 查看后台暂停的进程 jobs # 将暂停的进程恢复到前台运行 fg %1 # 将暂停的进程在后台继续运行 bg %1 # 终止后台进程 kill -9 12348 

2.3 终端信号的核心总结

组合键对应信号信号编号默认动作核心用途
Ctrl+CSIGINT2终止进程快速终止前台进程
Ctrl+\SIGQUIT3终止进程 + Core Dump调试时生成内存镜像
Ctrl+ZSIGTSTP20暂停进程临时暂停前台进程

        关键规则:终端信号仅发送给前台进程,后台进程(&启动)不受终端组合键影响。

三、场景 2:系统命令触发 —— 通过 Shell 命令发送信号

        除了终端组合键,我们还可以通过 Linux 系统提供的命令主动向进程发送信号,最常用的命令是killpkill。这种方式的核心是:通过命令告知 OS “向指定进程发送某个信号”,由 OS 完成信号的产生与投递。

3.1 核心命令:kill 命令的用法

    kill命令的本质是调用系统调用kill()函数,向指定进程发送信号。其基本语法如下:

# 格式1:通过信号名称发送 kill -信号名 进程PID # 格式2:通过信号编号发送 kill -信号编号 进程PID # 格式3:列出所有信号 kill -l 

        常用信号与 kill 命令组合:

kill -SIGINT 进程PID:等价于Ctrl+C,终止进程;kill -SIGQUIT 进程PID:终止进程并生成 Core Dump;kill -SIGKILL 进程PID:强制终止进程(9 号信号,不可捕捉、不可忽略);kill -SIGSTOP 进程PID:暂停进程(19 号信号,不可捕捉、不可忽略);kill -SIGCONT 进程PID:恢复暂停的进程。

3.2 实战验证:用 kill 命令发送信号

步骤 1:编写一个后台运行的死循环程序

// sig_backend.cpp #include <iostream> #include <unistd.h> using namespace std; int main() { cout << "后台进程PID:" << getpid() << ",正在运行..." << endl; while (true) { sleep(1); // 后台运行,不输出过多信息 } return 0; } 

步骤 2:编译运行并查看进程 PID

# 编译 g++ sig_backend.cpp -o sig_backend # 后台运行 ./sig_backend & # 查看进程PID(确认进程正在运行) ps aux | grep sig_backend 

        终端输出类似如下(PID 为 12349):

user 12349 0.0 0.0 4384 820 pts/0 S 10:00 0:00 ./sig_backend 

步骤 3:用 kill 命令发送不同信号

验证 1:发送 SIGINT 信号(2 号)
kill -SIGINT 12349 # 或 kill -2 12349 

        由于后台进程默认不会处理 SIGINT 信号(除非自定义),进程会继续运行,我们可以通过jobs命令查看:

jobs 

        输出显示进程仍在运行:

[1]+ Running ./sig_backend & 
验证 2:发送 SIGKILL 信号(9 号)—— 强制终止进程
kill -SIGKILL 12349 # 或 kill -9 12349 

        再次查看进程,发现进程已被终止:

ps aux | grep sig_backend # 无相关进程输出(或显示<defunct>,表示僵尸进程,后续会被系统清理) 
验证 3:发送 SIGSEGV 信号(11 号)—— 触发段错误

    SIGSEGV 信号的默认动作是 “终止进程并生成 Core Dump”,通常由非法内存访问触发,但我们也可以通过 kill 命令主动发送:

# 先开启Core Dump功能 ulimit -c 1024 # 重新启动后台进程 ./sig_backend & # 发送SIGSEGV信号 kill -SIGSEGV 12350 # 或 kill -11 12350 

        终端输出如下,进程被终止并生成 Core 文件:

[1]+ Segmentation fault (core dumped) ./sig_backend 

3.3 扩展命令:pkill 命令 —— 按进程名发送信号

    pkill命令可以根据进程名发送信号,无需手动查询 PID,更方便快捷:

# 终止所有名为sig_backend的进程 pkill -f sig_backend # 向所有名为sig_backend的进程发送SIGINT信号 pkill -SIGINT -f sig_backend 

3.4 系统命令触发信号的核心总结

系统命令(kill/pkill)是用户主动发送信号的手段,本质是通过命令调用kill()系统调用;信号的产生仍由 OS 完成,命令仅负责传递 “发送信号” 的请求;9 号信号(SIGKILL)和 19 号信号(SIGSTOP)不可捕捉、不可忽略,是 OS 强制控制进程的终极手段。

四、场景 3:函数调用触发 —— 在代码中主动产生信号

        除了通过终端和命令,我们还可以在 C/C++ 代码中调用特定函数,主动产生信号并发送给进程。Linux 系统提供了三个核心函数:kill()raise()abort(),分别用于 “向指定进程发送信号”、“向当前进程发送信号”、“强制当前进程异常终止”。

4.1 kill () 函数:向指定进程发送信号

    kill()函数是kill命令的底层实现,允许进程向另一个进程发送信号,其函数原型如下:

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

参数说明

  • sig:要发送的信号编号或宏定义(如 SIGINT、SIGKILL);
  • 返回值:成功返回 0,失败返回 - 1,并设置errno

pid:目标进程的 PID,有三种取值:

pid > 0:发送信号给 PID 为pid的进程;pid = 0:发送信号给当前进程所在进程组的所有进程;pid = -1:发送信号给当前用户有权限发送的所有进程(除了 init 进程);

实战:实现自己的 “kill 命令”

        我们可以用kill()函数实现一个简单的自定义 kill 命令,支持通过 “- 信号编号 进程 PID” 的格式发送信号:

// mykill.cpp #include <iostream> #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <cstdlib> using namespace std; int main(int argc, char *argv[]) { // 检查参数个数:./mykill -signumber pid if (argc != 3) { cerr << "用法错误!正确格式:" << argv[0] << " -signumber pid" << endl; cerr << "示例:" << argv[0] << " -2 12345(发送SIGINT信号给PID为12345的进程)" << endl; return 1; } // 解析信号编号(去掉argv[1]的 '-' 前缀) int sig = stoi(argv[1] + 1); // 解析目标进程PID pid_t pid = stoi(argv[2]); // 调用kill()函数发送信号 int ret = kill(pid, sig); if (ret == 0) { cout << "成功向进程PID=" << pid << "发送信号:" << sig << endl; } else { cerr << "发送信号失败!可能原因:进程不存在、无权限发送信号" << endl; return 1; } return 0; } 

编译运行与测试

# 编译 g++ mykill.cpp -o mykill # 先启动一个后台进程(如之前的sig_backend) ./sig_backend & # 用自定义mykill发送SIGINT信号(2号) ./mykill -2 12351 # 用自定义mykill发送SIGKILL信号(9号),强制终止进程 ./mykill -9 12351 

        终端输出如下,验证了kill()函数的功能:

成功向进程PID=12351发送信号:2 成功向进程PID=12351发送信号:9 

4.2 raise () 函数:向当前进程发送信号

    raise()函数用于向当前进程发送信号,等价于kill(getpid(), sig),函数原型如下:

#include <signal.h> int raise(int sig); 

参数说明

sig:要发送的信号编号或宏定义;返回值:成功返回 0,失败返回非 0。

实战:每隔 1 秒向自己发送 SIGINT 信号

// sig_raise.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; // 自定义信号处理函数 void sig_handler(int signum) { cout << "当前进程PID:" << getpid() << ",捕获到信号:" << signum << endl; } int main() { cout << "进程PID:" << getpid() << ",开始运行..." << endl; // 注册SIGINT信号的处理函数 signal(SIGINT, sig_handler); // 每隔1秒,向当前进程发送SIGINT信号 while (true) { sleep(1); // 调用raise()函数发送信号 raise(SIGINT); } return 0; } 

编译运行

g++ sig_raise.cpp -o sig_raise ./sig_raise 

        终端输出如下,进程每隔 1 秒捕获到一次 SIGINT 信号:

进程PID:12352,开始运行... 当前进程PID:12352,捕获到信号:2 当前进程PID:12352,捕获到信号:2 当前进程PID:12352,捕获到信号:2 ... 

4.3 abort () 函数:强制当前进程异常终止

    abort()函数用于强制当前进程异常终止,其本质是向当前进程发送SIGABRT信号(6 号),函数原型如下:

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

核心特点

abort()函数永远不会返回,调用后进程必然终止;即使进程自定义了SIGABRT信号的处理函数,abort()函数仍会强制终止进程(处理函数会执行,但执行完毕后进程仍会退出);默认动作是 “终止进程并生成 Core Dump 文件”。

实战:验证 abort () 函数的作用

// sig_abort.cpp #include <iostream> #include <unistd.h> #include <stdlib.h> #include <signal.h> using namespace std; // 自定义SIGABRT信号处理函数 void sigabrt_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGABRT),abort()函数被调用!" << endl; cout << "处理函数执行完毕,进程即将终止..." << endl; } int main() { cout << "进程PID:" << getpid() << ",开始运行..." << endl; // 注册SIGABRT信号的处理函数 signal(SIGABRT, sigabrt_handler); cout << "3秒后调用abort()函数..." << endl; sleep(3); // 调用abort()函数,强制终止进程 abort(); // 以下代码永远不会执行 cout << "进程继续运行..." << endl; return 0; } 

编译运行

g++ sig_abort.cpp -o sig_abort ./sig_abort 

        终端输出如下,验证了abort()函数的强制终止特性:

进程PID:12353,开始运行... 3秒后调用abort()函数... 捕获到信号:6(SIGABRT),abort()函数被调用! 处理函数执行完毕,进程即将终止... Aborted (core dumped) 

4.4 函数调用触发信号的核心总结

函数功能核心特点适用场景
kill()向指定进程发送信号支持跨进程发送,需要目标 PID进程间信号通信
raise()向当前进程发送信号仅能向自身发送,等价于 kill (getpid (), sig)进程自我触发信号
abort()强制当前进程异常终止发送 SIGABRT 信号,不可避免终止程序异常时主动退出

五、场景 4:软件条件触发 —— 由程序运行状态产生信号

        软件条件触发是指:信号的产生源于程序的运行状态或软件逻辑,而非用户操作或硬件异常。

        最典型的例子是alarm()函数设置的定时器超时(触发SIGALRM信号),以及向已关闭的管道写数据(触发SIGPIPE信号)。

5.1 核心案例 1:alarm () 函数 —— 定时器超时触发 SIGALRM 信号

    alarm()函数用于设置一个定时器,当定时器超时后,OS 会向当前进程发送SIGALRM信号(14 号),其默认处理动作是 “终止进程”。函数原型如下:

#include <unistd.h> unsigned int alarm(unsigned int seconds); 

参数与返回值

seconds:定时器超时时间(秒),若为 0 则取消之前设置的定时器;返回值:若之前已设置定时器,返回剩余超时时间;若之前无定时器,返回 0。

通俗理解 alarm () 函数

    alarm()函数就像一个 “闹钟”:你设定一个时间(seconds),时间到后闹钟响起(OS 发送 SIGALRM 信号)。如果在闹钟响之前你重新设定了一个新时间,那么旧的闹钟会被取消,返回值是旧闹钟剩余的时间。

实战 1:基本用法 ——1 秒后终止进程

// sig_alarm_basic.cpp #include <iostream> #include <unistd.h> using namespace std; int main() { cout << "进程PID:" << getpid() << ",设置1秒后触发闹钟..." << endl; // 设置定时器:1秒后发送SIGALRM信号 alarm(1); // 死循环,等待信号触发 int count = 0; while (true) { count++; // 不输出过多信息,避免IO影响计数 } return 0; } 

编译运行

g++ sig_alarm_basic.cpp -o sig_alarm_basic ./sig_alarm_basic 

        1 秒后,进程被 SIGALRM 信号终止,终端输出:

进程PID:12354,设置1秒后触发闹钟... Alarm clock # SIGALRM信号的默认终止信息 

实战 2:捕捉 SIGALRM 信号 —— 统计 1 秒内的循环次数

        我们可以自定义 SIGALRM 信号的处理函数,让定时器超时后不终止进程,而是执行统计逻辑:

// sig_alarm_catch.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int count = 0; // 自定义SIGALRM信号处理函数 void sigalrm_handler(int signum) { cout << "1秒时间到!捕获到信号:" << signum << "(SIGALRM)" << endl; cout << "1秒内循环执行次数:" << count << endl; // 退出进程 exit(0); } int main() { cout << "进程PID:" << getpid() << ",设置1秒后触发闹钟..." << endl; // 注册SIGALRM信号的处理函数 signal(SIGALRM, sigalrm_handler); // 设置定时器:1秒后发送SIGALRM信号 alarm(1); // 死循环计数 while (true) { count++; } return 0; } 

编译运行

g++ sig_alarm_catch.cpp -o sig_alarm_catch ./sig_alarm_catch 

        终端输出如下,1 秒后进程打印统计结果并退出:

进程PID:12355,设置1秒后触发闹钟... 1秒时间到!捕获到信号:14(SIGALRM) 1秒内循环执行次数:492333713 # 数值因CPU性能而异 

实战 3:重复闹钟 —— 每隔 1 秒触发一次 SIGALRM 信号

    alarm()函数是 “一次性闹钟”,触发后会自动取消。如果想要实现重复触发,可以在信号处理函数中重新调用alarm()

// sig_alarm_repeat.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int g_count = 0; // 自定义SIGALRM信号处理函数 void sigalrm_handler(int signum) { g_count++; cout << "第" << g_count << "次触发闹钟,信号编号:" << signum << endl; // 重新设置闹钟:1秒后再次触发 alarm(1); } int main() { cout << "进程PID:" << getpid() << ",设置重复闹钟(每隔1秒触发)..." << endl; // 注册SIGALRM信号的处理函数 signal(SIGALRM, sigalrm_handler); // 第一次设置闹钟:1秒后触发 alarm(1); // 暂停进程,等待信号触发(避免死循环占用CPU) while (true) { pause(); // pause()函数会让进程睡眠,直到收到一个信号 } return 0; } 

编译运行

g++ sig_alarm_repeat.cpp -o sig_alarm_repeat ./sig_alarm_repeat 

        终端输出如下,每隔 1 秒触发一次 SIGALRM 信号:

进程PID:12356,设置重复闹钟(每隔1秒触发)... 第1次触发闹钟,信号编号:14 第2次触发闹钟,信号编号:14 第3次触发闹钟,信号编号:14 ... 

5.2 核心案例 2:SIGPIPE 信号 —— 向已关闭的管道写数据

    SIGPIPE信号(13 号)的产生条件是:当进程向一个 “读端已关闭” 的管道(pipe)写入数据时,OS 会向该进程发送 SIGPIPE 信号,默认处理动作是 “终止进程”。

管道的核心特性

管道是半双工的,分为读端(r)和写端(w);当所有读端关闭后,写端进程向管道写入数据时,OS 会发送 SIGPIPE 信号终止写端进程;这是为了避免写端进程无意义地写入数据(没有进程读取,数据会丢失)。

实战:触发 SIGPIPE 信号

// sig_pipe.cpp #include <iostream> #include <unistd.h> #include <signal.h> #include <cstring> using namespace std; // 自定义SIGPIPE信号处理函数 void sigpipe_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGPIPE),向已关闭的管道写数据!" << endl; exit(1); } int main() { int pipefd[2]; // pipefd[0]:读端,pipefd[1]:写端 // 创建管道 if (pipe(pipefd) == -1) { perror("pipe创建失败"); return 1; } cout << "进程PID:" << getpid() << ",管道创建成功(读端:" << pipefd[0] << ",写端:" << pipefd[1] << ")" << endl; // 注册SIGPIPE信号的处理函数 signal(SIGPIPE, sigpipe_handler); // 关闭读端(模拟读端已关闭的场景) close(pipefd[0]); cout << "已关闭管道读端,尝试向写端写入数据..." << endl; // 向管道写端写入数据(此时读端已关闭,会触发SIGPIPE信号) const char *msg = "Hello, Pipe!"; while (true) { ssize_t ret = write(pipefd[1], msg, strlen(msg)); if (ret == -1) { perror("write失败"); sleep(1); } else { cout << "成功写入" << ret << "字节数据:" << msg << endl; } sleep(1); } // 关闭写端(不会执行到这里) close(pipefd[1]); return 0; } 

编译运行

g++ sig_pipe.cpp -o sig_pipe ./sig_pipe 

        终端输出如下,触发了 SIGPIPE 信号:

进程PID:12357,管道创建成功(读端:3,写端:4) 已关闭管道读端,尝试向写端写入数据... 捕获到信号:13(SIGPIPE),向已关闭的管道写数据! 

5.3 软件条件触发信号的核心总结

软件条件信号的产生源于程序的运行状态(如定时器超时、管道读写异常);这类信号是 OS 对程序运行逻辑的 “反馈”,用于告知程序 “某个软件事件已发生”;常见的软件条件信号包括 SIGALRM(定时器超时)、SIGPIPE(管道写失败)、SIGCHLD(子进程终止)等。

六、场景 5:硬件异常触发 —— 由硬件错误产生信号

        硬件异常触发是指:信号的产生源于 CPU 或其他硬件设备的错误,如除零操作、非法内存访问、总线错误等。硬件检测到错误后,会通知 OS,OS 将其映射为对应的信号,发送给当前进程。

        这类信号的本质是:硬件错误通过 OS 转换为软件层面的信号,让进程有机会处理错误(如打印日志、保存数据),若不处理则执行默认动作(通常是终止进程并生成 Core Dump)。

6.1 核心案例 1:除零操作 —— 触发 SIGFPE 信号(8 号)

        当进程执行 “除以零” 的算术运算时,CPU 的运算单元会检测到该错误,通知 OS,OS 将其映射为SIGFPE信号(Floating-point exception,浮点异常),默认处理动作是 “终止进程并生成 Core Dump”。

实战:模拟除零操作触发 SIGFPE 信号

// sig_fpe_divzero.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; // 自定义SIGFPE信号处理函数 void sigfpe_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGFPE),发生除零错误!" << endl; // 退出进程(避免无限循环触发信号) exit(1); } int main() { cout << "进程PID:" << getpid() << ",尝试执行除零操作..." << endl; // 注册SIGFPE信号的处理函数 signal(SIGFPE, sigfpe_handler); sleep(1); // 延迟1秒,便于观察 // 执行除零操作 int a = 10; int b = 0; int c = a / b; // 除零错误,触发SIGFPE信号 // 以下代码不会执行 cout << "计算结果:" << c << endl; return 0; } 

编译运行

g++ sig_fpe_divzero.cpp -o sig_fpe_divzero ./sig_fpe_divzero 

        终端输出如下,触发了 SIGFPE 信号:

进程PID:12358,尝试执行除零操作... 捕获到信号:8(SIGFPE),发生除零错误! 

关键注意:为什么会无限触发信号?

        如果我们不在处理函数中退出进程,会发现 SIGFPE 信号会被无限触发。原因是:除零错误发生后,CPU 的状态寄存器会记录该错误状态,若不清理该状态,OS 会持续检测到错误,不断发送 SIGFPE 信号。

        因此,在处理 SIGFPE 信号时,通常需要在处理函数中调用exit()_exit()终止进程,避免无限循环。

6.2 核心案例 2:非法内存访问 —— 触发 SIGSEGV 信号(11 号)

        当进程访问非法内存地址(如空指针、数组越界)时,MMU(内存管理单元)会检测到该错误,通知 OS,OS 将其映射为SIGSEGV信号(Segmentation fault,段错误),默认处理动作是 “终止进程并生成 Core Dump”。

实战 1:空指针访问触发 SIGSEGV 信号

// sig_segv_nullptr.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; // 自定义SIGSEGV信号处理函数 void sigsegv_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGSEGV),非法内存访问!" << endl; exit(1); } int main() { cout << "进程PID:" << getpid() << ",尝试访问空指针..." << endl; // 注册SIGSEGV信号的处理函数 signal(SIGSEGV, sigsegv_handler); sleep(1); // 访问空指针(非法内存访问) int *p = nullptr; *p = 100; // 触发SIGSEGV信号 // 以下代码不会执行 cout << "赋值成功:" << *p << endl; return 0; } 

编译运行

g++ sig_segv_nullptr.cpp -o sig_segv_nullptr ./sig_segv_nullptr 

        终端输出如下,触发了 SIGSEGV 信号:

进程PID:12359,尝试访问空指针... 捕获到信号:11(SIGSEGV),非法内存访问! 

实战 2:数组越界访问触发 SIGSEGV 信号

// sig_segv_array.cpp #include <iostream> #include <unistd.h> #include <signal.h> using namespace std; // 自定义SIGSEGV信号处理函数 void sigsegv_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGSEGV),数组越界访问!" << endl; exit(1); } int main() { cout << "进程PID:" << getpid() << ",尝试数组越界访问..." << endl; // 注册SIGSEGV信号的处理函数 signal(SIGSEGV, sigsegv_handler); sleep(1); // 数组越界访问(非法内存访问) int arr[5] = {1, 2, 3, 4, 5}; cout << "arr[10] = " << arr[10] << endl; // 触发SIGSEGV信号 return 0; } 

编译运行

g++ sig_segv_array.cpp -o sig_segv_array ./sig_segv_array 

        终端输出如下,触发了 SIGSEGV 信号:

进程PID:12360,尝试数组越界访问... 捕获到信号:11(SIGSEGV),数组越界访问! 

6.3 核心案例 3:总线错误 —— 触发 SIGBUS 信号(10 号)

    SIGBUS信号(Bus error)的产生条件是:进程访问的内存地址是有效的,但访问方式不正确(如对齐错误、内存映射失败)。与 SIGSEGV 信号(非法地址)的区别在于:SIGBUS 是 “地址有效但访问方式错误”,SIGSEGV 是 “地址本身无效”。

实战:内存对齐错误触发 SIGBUS 信号

        在某些 CPU 架构(如 ARM)中,访问未对齐的内存地址会触发 SIGBUS 信号。以下代码在 x86 架构中可能不会触发,但在 ARM 架构中会触发:

// sig_bus_align.cpp #include <iostream> #include <unistd.h> #include <signal.h> #include <cstring> using namespace std; // 自定义SIGBUS信号处理函数 void sigbus_handler(int signum) { cout << "捕获到信号:" << signum << "(SIGBUS),总线错误(内存对齐错误)!" << endl; exit(1); } int main() { cout << "进程PID:" << getpid() << ",尝试访问未对齐的内存地址..." << endl; // 注册SIGBUS信号的处理函数 signal(SIGBUS, sigbus_handler); sleep(1); // 内存对齐错误:char数组的地址是1字节对齐,强制转换为int*(4字节对齐) char buf[10]; int *p = (int *)(buf + 1); // buf+1的地址不是4的倍数,未对齐 *p = 0x12345678; // 触发SIGBUS信号 // 以下代码不会执行 cout << "赋值成功:" << *p << endl; return 0; } 

编译运行(ARM 架构)

# 在ARM架构的Linux系统中编译运行 g++ sig_bus_align.cpp -o sig_bus_align ./sig_bus_align 

        终端输出如下,触发了 SIGBUS 信号:

进程PID:12361,尝试访问未对齐的内存地址... 捕获到信号:10(SIGBUS),总线错误(内存对齐错误)! 

6.4 硬件异常触发信号的核心总结

硬件异常对应信号信号编号触发原因默认动作
除零操作SIGFPE8算术运算错误(除以零、浮点溢出)终止 + Core Dump
非法内存访问SIGSEGV11访问无效内存地址(空指针、数组越界)终止 + Core Dump
总线错误SIGBUS10访问方式错误(内存对齐错误、映射失败)终止 + Core Dump

        关键区别:

SIGSEGV:地址无效(“地址不存在”);SIGBUS:地址有效,但访问方式错误(“地址存在但进不去”)。

总结

        信号产生是 Linux 信号机制的基础,理解了不同场景下信号的产生逻辑,才能更好地掌握信号的处理与应用。本文的所有代码都经过实战验证,建议大家亲手编译运行,感受信号产生的过程。如果在学习过程中遇到问题,欢迎在评论区留言讨论!

Read more

苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

苹果最贵手机要来了!折叠屏iPhone将于9月亮相;部分高校严禁校内使用OpenClaw;黄仁勋预言:传统软件和APP或将消失 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * 多所高校要求警惕 OpenClaw 安全风险,部分严禁校内使用 * 荣耀 CEO 李健:荣耀机器人全栈自研,将聚焦消费市场 * 马化腾凌晨 2 点发声:还有一批龙虾系产品陆续赶来 * 前快手语言大模型中心负责人张富峥,已加入智源人工智能研究院,负责 LLM 方向 * 最新全球 AI 应用百强榜发布,豆包/DeepSeek/千问上榜 * 苹果折叠 iPhone 将于九月亮相,融合 iPhone 与 iPad 体验

By Ne0inhk
不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

编译 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) “如果你周日去旧金山的咖啡馆,会发现几乎每个人都在工作。” 这是 AI 创业公司 Mythril 联合创始人 Sanju Lokuhitige 最近最直观的感受。去年 11 月,他特地搬到旧金山,只为了更接近 AI 创业浪潮的中心。但很快,他也被卷入了这股浪潮带来的另一面——一种越来越极端的工作文化。 Lokuhitige 坦言,他现在几乎每天工作 12 小时,每周 7 天。除了每周少数几场刻意安排的社交活动(主要是为了和创业者们建立联系),其余时间几乎都在写代码、做产品。 “有时候我整整一天都在编程,”他说,“我基本没有什么工作与生活的平衡。”而这样的生活,在如今的 AI 创业圈里并不算罕见。 旧金山 AI 创业圈的真实日常 一位在旧金山一家 AI

By Ne0inhk
黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

AI 究竟是什么?在 NVIDIA CEO 黄仁勋看来,它早已不只是聊天机器人或某个大模型,而是一种正在迅速成形的“新型基础设施”。 近日,黄仁勋在英伟达官网发布了一篇长文,提出一个颇具形象的比喻——AI 就像一块“五层蛋糕”。从最底层的能源,到芯片、基础设施、模型,再到最上层的应用,人工智能正在形成一整套完整的产业技术栈,并像电力和互联网一样,逐渐成为现代社会的底层能力。 这也是黄仁勋自 2016 年以来公开发表的第七篇长文。在这篇文章中,他从计算机发展史与第一性原理出发,试图解释 AI 技术栈为何会演化成如今的形态,以及为什么全球正在掀起一场规模空前的 AI 基础设施建设。 在他看来,过去几十年的软件大多是预先编写好的程序:人类设计好算法,计算机按指令执行,数据被结构化存储在数据库中,通过精确查询调用。而 AI 的出现打破了这一模式——计算机开始能够理解图像、文本和声音,并根据上下文实时生成答案、推理结果甚至新的内容。 正因为智能不再是预先写好的代码,而是实时生成的能力,支撑它运行的整个计算体系也必须被重新设计。

By Ne0inhk
猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 过去几年里,科技公司几乎都在同一件事上加速:让 AI 参与写代码。 从自动补全、自动生成函数,到直接修改系统配置,生成式 AI 已经逐渐走进真实生产环境。但最近发生在亚马逊的一连串事故,却给整个行业泼了一盆冷水——当 AI 开始真正参与生产环境开发时,事情可能远比想象复杂。 最近,多家媒体披露,本周二亚马逊内部紧急召开了一场工程“深度复盘(deep dive)”会议,专门讨论最近频繁出现的系统故障——其中,一个被反复提及的关键词是:AI 辅助代码。 一周 4 次严重事故,亚马逊内部紧急复盘 事情的起点,是最近一段时间亚马逊系统稳定性明显下降。 负责亚马逊网站技术架构的高级副总裁 Dave Treadwell 在一封内部邮件中坦言:“各位,正如大家可能已经知道的,最近网站及相关基础设施的可用性确实不太理想。” 为此,公司决定把原本每周例行举行的技术会议

By Ne0inhk