OS56.【Linux】理解信号: 信号的产生(1) 键盘输入和kill命令

OS56.【Linux】理解信号: 信号的产生(1) 键盘输入和kill命令

目录

1.信号的定义

生活中的例子

产生信号后,可能不会立即处理

结论

2.从Ctrl+C来看信号的产生

为什么 Ctrl+C 可以杀死进程?

Bash内部对Ctrl+C做了特殊处理

3.信号的种类

细节1: 信号的编号

普通信号

实时信号

细节2: 信号的名称

4.形象理解信号的处理方式

5.测试信号的捕捉

signal系统调用

反思: 能否捕获所有信号?

反思: 为什么进程无法捕获9和19号信号?

Linux内核源码验证: 为什么进程无法捕获9和19号信号


之后的文章将按这个顺序来: 

        信号的产生→信号的保存→信号的捕捉

1首先说明: 信号和信号量没有任何关系! 

        注: 信号量在OS56.【Linux】理解信号量文章讲过

生活中的例子

从生活中的例子理解信号: 信号弹、上下课铃声、闹钟等等都可以认为是信号

那为什么能认为"信号弹、上下课铃声、闹钟"是信号呢?

答: 有人提前教过我们,我们我记住了这些就是信号,可以推出: 即使是现在没有信号产生,我们也知道信号产生之后应该干什么,例如红灯停,绿灯行 → 识别信号,知道信号的处理方法

产生信号后,可能不会立即处理

信号产生了,由于某些原因(例如当前处理的事情非常重要,无法暂停),我们可能并会不立即处理这个信号,在合适的时候才会处理

所以,信号产生后到信号处理这个时间窗口内,进程必须记住信号的到来

结论

1.进程必须识别且能够处理信号: 即使信号没有产生,也要具备处理信号的能力,信号的处理能力属于进程内置功能的一部分

2.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,合适的时候才会去执行对应的动作,那么进程在这个时间窗口需要具有临时保存哪些信号已经发生了的能力

2.从Ctrl+C来看信号的产生

新建以下文件:

test_signal/ ├── makefile ├── send_signal.sh └── test_signal.cpp

makefile写入:

test_signal.out:test_signal.cpp g++ -o $@ $^ -g -std=c++11 .PHONY:clean clean: rm -f test_signal.out

test_signal.cpp写入无限向终端打印hello world的程序:

#include <unistd.h> #include <iostream> using namespace std; int main() { for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }

启动:

/test_signal.out 

运行结果: 按下Ctrl+C,进程停止执行

为什么 Ctrl+C 可以杀死进程?

上方运行的结果可以看到: 输入任何bash命令都不会起作用,但是Ctrl+C杀死了这个进程

Linux中,一次登陆分配一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程

那么前台进程和后台进程的区别是: 只有前台进程能获取键盘输入

那么无限向终端打印hello world的进程为前台进程,bash为后台进程,这样bash无法获取键盘输入

如果将无限向终端打印hello world的进程为启动为后台进程:

/test_signal.out &

运行结果: Ctrl+C无法结束无限向终端打印hello world的进程为启动的后台进程

Bash内部对Ctrl+C做了特殊处理

那Ctrl+C为什么没有杀死bash? 因为Bash内部对Ctrl+C做了特殊处理,Ctrl+C是向前台进程发送了2号信号来终止前台进程的(这个后面的文章再解释)

在bash-5.3的根目录下有一个sig.c文件,注释说明了原因:

#define NULL_HANDLER (SigHandler *)SIG_DFL /* The list of signals that would terminate the shell if not caught. We catch them, but just so that we can write the history file, and so forth. */ static struct termsig terminating_signals[] = { //...... #ifdef SIGINT { SIGINT, NULL_HANDLER, 0 }, #endif //...... };

注释里面说的很清楚: 这些信号如果不捕获(捕获这个概念在本文 4.形象理解信号的处理方式 提到了)的话,会终止shell

附: bash-5.3的国内下载链接https://mirrors.nju.edu.cn/gnu/bash/bash-5.3.tar.gz

结论: Ctrl+C 可以杀死前台进程,Ctrl+C向前台进程发送2号信号SIGINT

3.信号的种类

kill -l命令查看系统的各个信号:

细节1: 信号的编号

仔细看的话,从1到64,没有32号和33号信号(历史原因,这里不讲),一共62个信号

普通信号

1号~31号

实时信号

34号~64号

实时信号的特点: 实时信号产生了必须立即处理,这里不学

细节2: 信号的名称

信号的名称的字母都是大写的,其实它们在Linux内核中被定义为宏,信号的名称的宏对应的数字就是信号的编号

x86平台下,Linux内核定义在/arch/x86/include/uapi/asm/signal.h中

*注: uapi这个目录下存储用户空间api头文件

#define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGTRAP 5 #define SIGABRT 6 #define SIGIOT 6 #define SIGBUS 7 #define SIGFPE 8 #define SIGKILL 9 #define SIGUSR1 10 #define SIGSEGV 11 #define SIGUSR2 12 #define SIGPIPE 13 #define SIGALRM 14 #define SIGTERM 15 #define SIGSTKFLT 16 #define SIGCHLD 17 #define SIGCONT 18 #define SIGSTOP 19 #define SIGTSTP 20 #define SIGTTIN 21 #define SIGTTOU 22 #define SIGURG 23 #define SIGXCPU 24 #define SIGXFSZ 25 #define SIGVTALRM 26 #define SIGPROF 27 #define SIGWINCH 28 #define SIGIO 29 #define SIGPOLL SIGIO /* #define SIGLOST 29 */ #define SIGPWR 30 #define SIGSYS 31 #define SIGUNUSED 31 /* These should not be considered constants from userland. */ #define SIGRTMIN 32 #define SIGRTMAX _NSIG

4.形象理解信号的处理方式

操作系统为进程发送信号,进程需要处理信号,只有3种方式,而且只能3选1

默认动作: 手机响了,接听电话(这是大多数人默认的动作)

        从上面的实验的运行结果,可以得出: 进程收到2号信号的默认动作,就是终止自己

忽略:
手机响了,选择静音,不接听,忽略这个电话

自定义动作: 手机响了,但不想接,于是挂断电话并回复短信(自定义动作)

自定义动作被称为信号的捕获(或捕捉),对于bash而言,不能执行2号信号的默认动作,需要捕获该信号做进一步处理,否则影响用户体验

下面测试信号的捕捉

5.测试信号的捕捉

验证Ctrl+C向进程发送了2号信号,那么就要使用自定义方法捕获2号信号,这里使用signal系统调用

signal系统调用

signal是修改特定进程对于信号的处理动作的

signum: 信号的编号

handler: 函数指针,手册给出:

typedef void (*sighandler_t)(int);

sighandler_t为函数指针类型,这个函数执行捕获信号后的自定义动作,其返回值为void,只接受一个int类型的信号编号

为什么myhandler需要传参?

答: 区分不同的信号,做不同的事情,例如:

void myhandler(int signo) { if (signo==1) { //...... } else if (signo==2) { //...... } else if (...) //...... } int main() { signal(1,my_hander); signal(2,my_hander); //...... }

test_signal.cpp:无限循环正常执行,只不过用户键入Ctrl+C后需要执行自定义动作

#include <unistd.h> #include <iostream> #include <signal.h> void myhandler(int signo) { std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl; } int main() { signal(SIGINT,myhandler); for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }

注: 1.signal(SIGINT,my_hander);只需要设置一次,后面都有效

      2.只有收到2号信号时(例如Ctrl+C或kill -2 pid),myhandler才会调用,调用signal不会触发myhandler的执行

运行结果:test_signal.out进程遇到2号信号不会执行默认动作,而是执行打印任务

反思: 能否捕获所有信号?

如果将系统中所有信号全部捕捉,执行自定义动作,例如自定义动作为无限循环,那么是不是test_signal.out进程无法被杀死?

这里测试1~31号普通信号:

#include <unistd.h> #include <iostream> #include <signal.h> #include <sys/types.h> void myhandler(int signo) { std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl; } int main() { for (int i=1;i<=31;i++) signal(i,myhandler); std::cout<<"本进程的pid为: "<<getpid()<<std::endl; for (;;) { std::cout<<"Hello World!"<<std::endl; sleep(1); } return 0; }

启动一个脚本,批量向该进程发送信号:

send_signal.sh写入:

#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=1; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done

运行结果: 9号信号SIGKILL无法捕获

从10号信号开始:

#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=10; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done

运行结果: 19号信号SIGSTOP无法捕获        

从20号信号开始:

#!/usr/bin/bash if [ "$1" = "" ] || [ "$1" = " " ] then echo "需要提供进程的pid,用法: ./send_signal.sh pid" exit -1 fi pid=$1 for ((i=20; i<=31; i++)) do kill -${i} ${pid} sleep 0.2 done

运行结果: 20~31号信号可全部被捕获

结论: 1~31号普通信号,只有9号信号SIGKILL、19号信号SIGSTOP无法捕获   

反思: 为什么进程无法捕获9和19号信号?

如果进程捕获了所有信号但是不退出,这样的进程十分危险,它一直占用操作系统的资源,而且可能执行一些危险的动作

为了避免这样的事情发生,Linux中设置: 9和19号信号无法捕获(可以理解为这两个信号是操作系统留的"后门"),分别用于强制终止进程和强制暂停进程,否则系统将彻底失去对进程的最终控制权

结论: 内核必须在任何情况下都能无条件终止或停止进程,以保证系统的可控性和稳定性

Linux内核源码验证: 为什么进程无法捕获9和19号信号

linux 6.18.6在/kernel/signal.c中定义:

static bool sig_task_ignored(struct task_struct *t, int sig, bool force) { void __user *handler; handler = sig_handler(t, sig); /* SIGKILL and SIGSTOP may not be sent to the global init */ if (unlikely(is_global_init(t) && sig_kernel_only(sig))) return true; if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) && handler == SIG_DFL && !(force && sig_kernel_only(sig))) return true; /* Only allow kernel generated signals to this kthread */ if (unlikely((t->flags & PF_KTHREAD) && (handler == SIG_KTHREAD_KERNEL) && !force)) return true; return sig_handler_ignored(handler, sig); } 

注释明确说明:

/* SIGKILL and SIGSTOP may not be sent to the global init */

sig_kernel_only在include/linux/signal.h中定义:

#define sigmask(sig) (1UL << ((sig) - 1)) #if SIGRTMIN > BITS_PER_LONG #define rt_sigmask(sig) (1ULL << ((sig)-1)) #else #define rt_sigmask(sig) sigmask(sig) #endif #define SIG_KERNEL_ONLY_MASK (\ rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP)) #define siginmask(sig, mask) \ ((sig) > 0 && (sig) < SIGRTMIN && (rt_sigmask(sig) & (mask))) #define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)

x86下,SIGRTMIN在/arch/x86/include/uapi/asm/signal.h中定义:

#define SIGRTMIN 32

BITS_PER_LONG在/include/asm-generic/bitsperlong.h中定义,和具体架构有关:

#ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */

Intel 32位下: 

 sig_kernel_only(sig) == siginmask(sig, SIG_KERNEL_ONLY_MASK) == siginmask(sig, (rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))) == siginmask(sig, ((1ULL << ((SIGKILL)-1)) | (1ULL << ((SIGSTOP)-1)))) 

Read more

解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程

解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程

文章目录 * 解锁Dify与MySQL的深度融合:MCP魔法开启数据新旅程 * 引言:技术融合的奇妙开篇 * 认识主角:Dify、MCP 与 MySQL * (一)Dify:大语言模型应用开发利器 * (二)MCP:连接的桥梁 * (三)MySQL:经典数据库 * 准备工作:搭建融合舞台 * (一)环境搭建 * (二)安装与配置 Dify * (三)安装与配置 MySQL * 关键步骤:Dify 与 MySQL 的牵手过程 * (一)安装必要插件 * (二)配置 MCP SSE * (三)创建 Dify 工作流 * (四)配置 Agent 策略 * (五)搭建MCP

By Ne0inhk
如何在Cursor中使用MCP服务

如何在Cursor中使用MCP服务

前言 随着AI编程助手的普及,越来越多开发者选择在Cursor等智能IDE中进行高效开发。Cursor不仅支持代码补全、智能搜索,还能通过MCP(Multi-Cloud Platform)服务,轻松调用如高德地图API、数据库等多种外部服务,实现数据采集、处理和自动化办公。 本文以“北京一日游自动化攻略”为例,详细讲解如何在 Cursor 中使用 MCP 服务,完成数据采集、数据库操作、文件生成和前端页面展示的全流程。 学习视频:cursor中使用MCP服务 一、什么是MCP服务? MCP(Multi-Cloud Platform)是Cursor内置的多云服务接口,支持调用地图、数据库、文件系统等多种API。通过MCP,开发者无需手动写HTTP请求或繁琐配置,只需在对话中描述需求,AI助手即可自动调用相关服务,极大提升开发效率。 二、环境准备 2.1 cursor Cursor重置机器码-解决Too many free trials. 2.

By Ne0inhk
MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

本系列主要通过调用天气的mcp server查询天气这个例子来学习什么是mcp,以及怎么设计mcp。话不多说,我们开始吧。主要参考的是B站的老哥做的一个教程,我把链接放到这里,大家如果有什么不懂的也可以去看一下。 https://www.bilibili.com/video/BV1NLXCYTEbj?spm_id_from=333.788.videopod.episodes&vd_source=32148098d54c83926572ec0bab6a3b1d https://blog.ZEEKLOG.net/fufan_LLM/article/details/146377471 最终的效果:让deepseek-v3使用天气查询的工具来查询指定地方的天气情况 技术介绍 MCP,即Model Context Protocol(模型上下文协议),是由Claude的母公司Anthropic在2024年底推出的一项创新技术协议。在它刚问世时,并未引起太多关注,反响较为平淡。然而,随着今年智能体Agent领域的迅猛发展,MCP逐渐进入大众视野并受到广泛关注。今年2月,

By Ne0inhk
可以在命令行通过大模型使用上下文协议(MCP)与外部工具交互的软件:小巧的MCPHost

可以在命令行通过大模型使用上下文协议(MCP)与外部工具交互的软件:小巧的MCPHost

小巧的MCPHost MCPHost 可以在命令行下使用,使大型语言模型(LLM)能够通过模型上下文协议(MCP)与外部工具进行交互。目前支持Claude 3.5 Sonnet和Ollama等。本次实践使用自己架设的Deepseek v3模型,跑通了Time MCP服务。  官网:GitHub - mark3labs/mcphost: A CLI host application that enables Large Language Models (LLMs) to interact with external tools through the Model Context Protocol (MCP). 下载安装 使用非常方便,直接下载解压即可使用。官网提供Windows、Linux和MacOS三个系统的压缩包: https://github.com/

By Ne0inhk