linux-进程信号的产生
Linux中的进程信号(signal)是一种用于进程间通信或向进程传递异步事件通知的机制。信号是一种软中断,用于通知进程某个事件的发生,如错误、终止请求、计时器到期等。
1. 信号的基本概念
- 信号(Signal):是一种异步通知机制,当内核或其他进程需要通知某个进程发生了某种事件时,会向该进程发送一个信号。进程接收到信号后,可以根据预设的处理方式进行响应。
- 默认处理:每种信号都有默认的处理动作,比如终止、忽略、停止或继续执行。
- 捕捉信号:进程可以通过注册信号处理函数(signal handler)来捕捉信号,从而自定义对信号的响应。
查看信号 kill -l
1~31为普通信号。34~64为实时信号。
实时信号需要立即处理,可以不立即处理的是普通信号。
怎么进行信号处理?a.默认处理 b.忽略处理 c.自定义处理, 都叫做信号捕捉。
#include <iostream>
#include <unistd.h>
int main()
{while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}

个系统调用函数。
signal:用于设置对特定信号的处理方式
signum:要处理的信号编号[只需要知道是数字即可]
handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<std::endl;
}
int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT/*2*/, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
这里重定义了二号信号的处理方式,二号信号默认处理是终止进程
2. 信号的产生
在 Linux 中,信号(Signal)是进程间通信(IPC)和异常处理的重要机制。信号的产生方式主要包括 硬件事件、软件命令 和 内核机制 触发。
2.1 硬件事件触发信号
当发生硬件异常时,操作系统会向相应的进程发送信号,例如:
非法操作:程序执行非法指令,如除零 (SIGFPE
)、访问非法内存 (SIGSEGV
)。
键盘输入:用户在终端输入 Ctrl+C
或 Ctrl+Z
,分别产生 SIGINT
(中断进程)和 SIGTSTP
(暂停进程)。
硬件事件 | 触发的信号 |
---|---|
除零错误 | SIGFPE |
非法内存访问 | SIGSEGV |
非法指令 | SIGILL |
总线错误 | SIGBUS |
用户按 Ctrl+C | SIGINT |
用户按 Ctrl+Z | SIGTSTP |
进程访问非法地址触发 SIGSEGV
访问 NULL
指针或越界访问内存,会触发 SIGSEGV(段错误)
遇到除0错误
触发八号信号SIGFPE
2.2 系统调用触发信号
发送信号的本质是相进程写信号,通过进程的pid和信号编号修改位图(本质是OS修改内核的数据)。
用户或进程可以使用 命令 或 系统调用 产生信号。
2.2.1 使用 kill
命令
kill
可用于向指定进程发送信号。例如:
kill -SIGTERM 1234 # 向进程 1234 发送 SIGTERM(终止进程)
kill -9 1234 # 等同于 kill -SIGKILL 1234,强制终止进程
kill -STOP 1234 # 暂停进程
kill -CONT 1234 # 继续运行被暂停的进程
其中:
SIGTERM(15):请求终止进程,进程可捕获并决定是否退出(默认 kill 发送的信号)。
SIGKILL(9):强制终止进程,进程无法捕获,立即终止。
SIGSTOP(19):暂停进程,类似 Ctrl+Z,进程无法忽略。
SIGCONT(18):恢复暂停的进程。
2.2.2 使用 kill()
系统调用
在 C 语言中,kill()
可以向指定进程发送信号:
#include <signal.h>
#include <unistd.h>int main() {pid_t pid = 1234; // 目标进程的 PIDkill(pid, SIGTERM); // 发送 SIGTERM 终止进程return 0;
}
//mykill.c
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>int main(int argc, char *argv[])
{if(argc != 3){std::cout << "./mykill signum pid" << std::endl;return 1;}pid_t target = std::stoi(argv[2]);int signum = std::stoi(argv[1]);int n = kill(target, signum);if(n == 0){std::cout << "Send " << signum << " to " << target << std::endl;}return 0;
}
//testsig.cc
#include <iostream>
#include <signal.h>void handler(int signum)
{std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{signal(SIGINT, handler);while(true){std::cout << "i am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}
2.2.3 使用 raise()
让当前进程向自己发送信号
#include <iostream>
#include <signal.h>void handler(int signum)
{std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{signal(SIGINT, handler);//捕捉信号for(int i = 1; i < 32; i++)signal(i, handler);for(int i = 1; i < 32; i++){sleep(1);raise(i);}return 0;
}
信号 9 也就是 SIGKILL
信号,它是一个强制终止信号,并且不可被捕获、阻塞或忽略的,也就无法对其进行修改和自定义。
2.2.4 abort
abort用于异常终止进程,并生成核心转储(core dump),以便调试程序崩溃的原因。
#include <stdlib.h>void abort(void);无参数,直接终止当前进程
不会返回,进程立即结束
默认产生 SIGABRT 信号,导致进程终止并生成 core dump(如果系统允许)
abort()
与 exit()
的区别
abort() | exit() | |
---|---|---|
终止方式 | 发送 SIGABRT ,可能生成 core dump | 正常终止 |
释放资源 | 不执行 atexit() 注册的函数 | 执行 atexit() 注册的清理函数 |
可捕获 | 可通过 signal(SIGABRT, handler) 处理 | 不发送信号 |
适用场景 | 程序遇到致命错误时终止 | 正常退出,返回状态码 |
捕获SIGABRT
#include <iostream>
#include <signal.h>void handler(int signum)
{std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{signal(SIGABRT, handler);printf("before pause\n");abort();printf("after pause\n");return 0;
}
2.3 软件命令触发信号
2.3.1 使用 alarm()
触发 SIGALRM
alarm()
是一个用于设置定时器的系统调用,它会在指定的秒数后向进程发送 SIGALRM
信号,从而触发相应的信号处理函数或终止进程。
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);seconds:设置的定时秒数。
返回值:返回上一个 alarm() 调用设置的剩余时间(如果没有,则返回 0)。
alarm(0):取消闹钟
alarm()
只能设置一个定时器,如果在定时器未触发前再次调用 alarm()
,则前一个定时器会被覆盖。
#include <iostream>
#include <signal.h>void handler(int signum)
{std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{signal(SIGINT, handler);alarm(3);int cnt = 0;while(true){std::cout << "i am a process " << cnt++ << " pid: " << getpid() << std::endl;sleep(1);}return 0;
}
2.3.2 pause()
pause 是一个系统调用,它使进程挂起(阻塞),直到接收到信号(且该信号的处理方式不是忽略)。它通常与 signal()
或 sigaction()
结合使用,以等待某个特定信号的到来。
函数原型:
#include <unistd.h> int pause(void);返回值:通常不返回,除非被信号中断,此时返回 -1,并设置 errno 为 EINTR(被信号中断的错误)。
#include <iostream>
#include <signal.h>void handler(int signum)
{std::cout << "i get a signal: " << signum << std::endl;
}
int main()
{signal(SIGALRM, handler);alarm(3);printf("before pause\n");pause();printf("after pause\n");return 0;
}
2.3.3 设置重复闹钟
#include <iostream>
#include <signal.h>
#include <vector>
#include <functional>
#include <unistd.h>using func_t = std::function<void()>;
std::vector<func_t> funcs;void Schel()
{std::cout << "我是进程调度" << std::endl;
}
void MemManger()
{std::cout << "我是周期性的内存管理, 正在检查有没有内存问题" << std::endl;
}
void Fflush()
{std::cout << "我是刷新程序,定期刷新内存数据" << std::endl;
}
void handler(int signum)
{gcount++;std::cout << "###################" << std::endl;for(auto &f : funcs)f();std::cout << "###################" << std::endl;int n = alarm(1);std::cout << gcount << std::endl;
}
int main()
{funcs.push_back(Schel);funcs.push_back(MemManger);funcs.push_back(Fflush);signal(SIGALRM, handler);alarm(1);while(true)pause();return 0;
}
alarm内核数据结构
struct timer_list {struct list_head entry; //将 timer_list 结构体组织成链表unsigned long expires; //表示定时器的超时时间void (*function)(unsigned long); //指向回调函数的指针,当定时器超时后,内核会调用该函数。unsigned long data; //作为 function 回调函数的参数,通常用于传递自定义数据。struct tvec_t_base_s *base;
};
2.4 内核触发信号
Linux 内核在特定情况下会向进程发送信号,例如:
2.4.1 进程终止时,父进程收到 SIGCHLD
子进程终止后,父进程会收到 SIGCHLD
,可用于回收子进程资源:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void child_handler(int signum) {printf("Child process exited.\n");
}int main() {signal(SIGCHLD, child_handler);if (fork() == 0) {printf("Child process running...\n");sleep(2);exit(0);}pause(); // 等待信号return 0;
}
(2)磁盘 I/O 错误触发 SIGBUS
访问未映射的内存或硬件错误会触发 SIGBUS
。
(3)后台进程写入终端触发 SIGHUP
后台进程尝试写入终端时,可能收到 SIGHUP
,表示挂起(通常用于会话管理)。
2.4 信号的分类
Linux 信号可分为 终止信号、忽略信号、暂停信号 和 核心转储信号。
类别 | 常见信号 |
---|---|
终止信号 | SIGTERM 、SIGKILL 、SIGINT 、SIGHUP |
暂停信号 | SIGSTOP 、SIGTSTP 、SIGCONT |
核心转储信号 | SIGSEGV 、SIGILL 、SIGABRT |
忽略信号 | SIGCHLD 、SIGURG |
SIGTERM (15):终止信号,用于请求进程正常终止,允许进程进行清理工作后退出。
SIGKILL (9):强制杀死进程的信号,无法被捕捉或忽略,立即终止进程。
SIGSTOP:暂停进程的执行,无法被捕捉或忽略。
SIGCONT:使处于暂停状态的进程继续运行。
SIGHUP (1):挂起信号,常用于通知进程重新读取配置文件或重启。
SIGALRM:定时器信号,定时器到期时发出,用于处理超时操作。
man 7 siganl
Core(终止),Term(终止),Cont(继续),Stop(暂停),Ign(忽略)
信号 vs 通信IPC:
(1)信号是用户和OS,IPC是用户之间
(2)信号是OS修改内核数据结构,IPC是写到缓冲区中
3.目标进程
前台进程是指 直接与终端交互 的进程,用户可以通过 键盘输入 来控制它。
后台进程指的是 不直接与终端交互,在后台运行的进程,用户可以继续在终端执行其他操作。
假如有一个可执行程序code:
./code -> 前台进程
./code & -> 后台进程
命令行shell进程是前台进程。
- 后台进程无法从标准输入获取内容,前台可以。但是都可以向标准输出打印内容。
- 前台进程只能有一个,后台进程可以有多个。
在上图中,testcode
进程成为了前台进程,因此当我们在终端输入 ls
命令时,并未在屏幕上看到输出。这是因为此时命令行 Shell 进程已切换到后台,而 testcode
进程本身 并未提供执行 ls
等命令的接口,导致输入的命令无法被正确解析和执行。
在上述代码中,testcode
进程虽然在标准输出上打印内容,但它是 后台进程,而 前台进程仍然是 Shell 进程。因此,当用户在终端输入 ls
等命令时,Shell 进程能够正常接收输入并执行相应命令,输出也会正确显示在终端上。
几个命令:
jobs查看所有的后台任务
fg(frontground)任务号,将特定的进程提到前台
ctrl + z:将进程暂停。前台进不能被暂停,如果对前台进程使用ctrl+z,该进程会被自动提到后台。
bg:让后台进程回复运行