SIGINT(中止进程)
按下Ctrl+C可以中止进程,这是因为终端向该进程发送了一个SIGINT信号。默认情况下,当进程接收到SIGINT信号时,它会执行终止操作。但是,我们可以注册我们自己的处理函数,以便进程在接收到SIGINT信号时能退出得更优雅一些:
/* 捕获Ctrl+C信号 */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
// 信号处理函数
void signal_handler(int signum) {
printf("\n捕获到信号:%d, 程序将退出。\n", signum);
exit(0);
}
int main() {
// 注册信号处理函数
signal(SIGINT, signal_handler);
while(1) {
printf("程序运行中, 按Ctrl+C退出...\n");
sleep(1); // 睡眠1秒
}
}
上述代码的运行结果如下:
Linux总共定义了64种信号,SIGINT的编号是2。输入命令kill -l,可以查看Linux的信号表:
输入命令man 7 signal,可以查看64种信号各自的含义:
Action这一列是默认处理方式,共有五种:CoreDump(终止,并且保存现场信息),Terminate(终止),Ignore(忽略),Stop(挂起)和Continue(恢复)。可以看到,SIGINT信号的Action(默认处理方式)是Term(终止),产生于键盘的中断(Interrupt from keyboard)。
下面我们来认识一下Linux里面更多的信号。
SIGALRM(定时器)
Linux提供了alarm()函数,使得进程可以设置自己的定时器:
/* 设置定时器 */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
// 信号处理函数
void signal_handler(int signum) {
printf("捕获到信号:%d。\n", signum);
}
int main() {
// 注册信号处理函数
signal(SIGALRM, signal_handler);
// 设置一个3秒的定时器
alarm(3);
printf("尝试睡眠10秒。\n");
int ret = sleep(10);
printf("剩余未休眠的秒数:%d。\n", ret);
return 0;
}
上述代码的运行结果如下:
可以看到,进程只睡了3秒钟,然后就被定时器信号给唤醒了 。
SIGSEGV(段错误)
当程序试图访问它没有权限访问的内存区域时就会产生段错误,比如,往地址0写入1:
/* 段错误 */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
// 信号处理函数
void signal_handler(int signum) {
printf("捕获到信号:%d。\n", signum);
}
int main() {
// 注册信号处理函数
signal(SIGSEGV, signal_handler);
int* p = NULL;
*p = 1;
return 0;
}
上述代码的运行结果如下:
额,因为信号处理函数啥也没干,跳转回来之后又会再次产生段错误,所以陷入死循环了。
SIGCHLD(子进程状态变化)
当子进程结束、停止或恢复时,操作系统会通过SIGCHLD信号通知父进程。SIGCHLD信号的示例代码如下:
/* 子进程状态改变(停止、继续或终止) */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
// 信号处理函数
void signal_handler(int signum) {
printf("进程%d捕获到信号:%d。\n", getpid(), signum);
}
int main() {
// 注册信号处理函数
signal(SIGCHLD, signal_handler);
int pid = fork();
if (pid == 0) { // 子进程
printf("我是子进程, PID=%d。\n", getpid());
sleep(2);
printf("子进程结束。\n");
} else { // 父进程
printf("我是父进程, PID=%d, 子进程PID=%d。\n", getpid(), pid);
sleep(3);
printf("父进程结束。\n");
}
return 0;
}
上述代码运行结果如下:
SIGUSR1(自定义信号)
虽然信号通常由操作系统产生,但用户进程同样可以发送信号给其他进程。Linux提供了kill(int pid, int sig)这个系统调用,并且Linux特别预留了SIGUSR1(10)和SIGUSR2(12)这两个信号,供用户进程自定义使用,以实现进程间的特定通信需求。
/* 使用kill()发送信号 */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
// 信号处理函数
void signal_handler(int signum) {
printf("进程%d捕获到信号:%d。\n", getpid(), signum);
}
int main() {
// 注册信号处理函数
signal(SIGUSR1, signal_handler);
int ppid = getpid();
int pid = fork();
if (pid == 0) { // 子进程
printf("我是子进程, PID=%d。\n", getpid());
sleep(1);
kill(ppid, SIGUSR1); // 向父进程发送信号
sleep(1);
printf("子进程结束。\n");
} else { // 父进程
printf("我是父进程, PID=%d。\n", getpid());
sleep(1);
kill(pid, SIGUSR1); // 向子进程发送信号
sleep(1);
printf("父进程结束。\n");
}
return 0;
}
上述代码的运行结果如下:
上面的情况是子进程先收到父进程的信号。如果父进程先收到子进程的信号,那么结果就会类似这样:
总结
信号机制与中断机制相似。中断机制允许CPU在执行当前任务时暂停并响应突发特殊事件。同样地,信号机制使进程在执行其既定任务时能够灵活地响应来自内核或其他进程的特殊事件。
信号机制最初是为了让操作系统能够通知进程(特别是用于终止或暂停进程)。Linux进一步扩展了信号机制,允许用户进程之间通过相互发送信号来实现异步通信。