Linux系列文章目录
第一章 Linux信号
目录
前言
服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用”Ctrl+c“中止与杀程序是相同的效果。
如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。
一、如何让服务程序运行在后台
1. 加“&”符号
如果想要程序在后台运行,执行的时候,命令后面加“&”符号。
代码:
#include <stdio.h>
#include <unistd.h>
void exectask() {
printf("执行了一次任务!\n");
}
int main() {
while (1) {
exectask();
sleep(5);
}
return 0;
}
king@ubuntu:~/share/Student/Linux/Signal$ ./signal1 &
可见程序signal1在shell的一个子程序下运行,如果该终端退出,后台程序将由系统托管。
关闭后台程序:
king@ubuntu:~$ ps -ef | grep signal1
2. fork()
采用fork,让主程序执行fork,生成一个子进程,然后父进程退出,留下子进程继续运行,子进程将由系统托管。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exectask() {
printf("执行了一次任务!\n");
}
int main() {
if (fork() > 0) exit(0); // 父进程退出子进程继续运行
while (1) {
exectask();
sleep(5);
}
return 0;
}
king@ubuntu:~/share/Student/Linux/Signal$ gcc -o signal1 signal1.c
king@ubuntu:~/share/Student/Linux/Signal$ ./signal1
可见当父进程退出后,子进程将由系统托管。
二、信号的妙用
signal信号是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称为软中断。
程序在运行的过程中,用Ctrl+c、kill、killall中止其本质是向程序发送信号,程序对这两个信号的缺省行为是程序中止运行。在程序中,可以捕获信号,编写信号处理函数,即收到信号后执行的代码。
1. 从代码认识信号
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void exectask() {
printf("执行了一次任务!\n");
}
void func(int sig) {
printf("收到了信号:%d\n", sig);
}
int main() {
signal(SIGINT, func); // 键盘中断“Ctrl + c” SIGINT = 2
signal(SIGTERM, func); // 采用“kill 进程编号”或“killall 程序名”通知程序 SIGTERM = 15
//if (fork() > 0) exit(0); // 父进程退出子进程继续运行
while (1) {
exectask();
sleep(5);
}
return 0;
}
2. 信号基本概念
软中断信号(signal,又简称为信号)用来通知进程发生了事件。进程之间可以通过调用kill库函数发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。
注意,信号只是用来通知某进程发生了什么事件,无法给进程传递任何数据,进程对信号的处理方法有三种:
-
忽略某个信号,对该信号不做任何处理,就像未发生过一样。
-
设置中断的处理函数,收到信号后,由该函数来处理。
-
对该信号的处理采用系统默认操作,大部分信号的默认操作是终止进程。
3. 信号的类型
信号名 | 信号值 | 默认处理动作 | 发出信号的原因 |
---|---|---|---|
SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
SIGINT | 2 | A | 键盘中断“Ctrl + c” |
SIGQUIT | 3 | C | 键盘的退出键被按下 |
SIGILL | 4 | C | 非法指令 |
SIGABRT | 6 | C | 由abort(3)发出的退出指令 |
SIGFPE | 8 | C | 浮点异常 |
SIGKILL | 9 | AEF | 采用“kill -9 进程编号”强制杀死程序 |
SIGSEGV | 11 | C | 无效的内存引用 |
SIGPIPE | 13 | A | 管道破裂: 写一个没有读端口的管道 |
SIGALRM | 14 | A | 由alarm(2)发出的信号 |
SIGTERM | 15 | A | 采用“kill 进程编号”或“killall 程序名”通知程序 |
SIGUSR1 | 30,10,16 | A | 用户自定义信号1 |
SIGUSR2 | 31,12,17 | A | 用户自定义信号2 |
SIGCHLD | 20,17,18 | B | 子进程结束信号 |
SIGCONT | 19,18,25 | 进程继续(曾被停止的进程) | |
SIGSTOP | 17,19,23 | DEF | 终止进程 |
SIGTSTP | 18,20,24 | D | 控制终端(tty)上按下停止键 |
SIGTTIN | 21,21,26 | D | 后台进程企图从控制终端读 |
SIGTTOU | 22,22,27 | D | 后台进程企图从控制终端 |
处理动作一项中的字母含义如下:
A 缺省的动作是终止进程
B 缺省的动作是忽略此信号
C 缺省的动作是终止进程并进行内核映像转储(dump core)
D 缺省的动作是停止进程
E 信号不能被捕获
F 信号不能被忽略
4. signal库函数
sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum:处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler:信号处理的动作,有以下三种值:
-
SIG_IGN:忽略参数signum所指的信号
-
SIG_DFL:恢复参数signal所指信号的处理方法为默认值
-
一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数
一般不关心signal的返回值
5. 信号有什么用
服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用”Ctrl+c“中止与杀程序是相同的效果。
如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。
信号还可以用于网络服务程序抓包等......
6. 信号应用示例
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void EXIT(int sig) {
printf("收到信号%d,程序退出。\n", sig);
// 在这里添加释放资源的代码
exit(0); // 程序退出
}
int main() {
for (int i = 0; i < 100; ++ i) signal(i, SIG_IGN); // 屏蔽全部的信号
signal(SIGINT, EXIT); // 键盘中断“Ctrl + c” SIGINT = 2
signal(SIGTERM, EXIT); // 采用“kill 进程编号”或“killall 程序名”通知程序 SIGTERM = 15
while (1) {
sleep(1);
}
return 0;
}
运行结果:
7. 发送信号
Linux操作系统提供了kill命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其他进程或者线程发送信号。
int kill(pid_t pid, int sig);
kill()函数将参数sig指定的信号发送给参数pid指定的进程。
参数pid有以下三种情况:
-
pid > 0 信号发送给进程号为pid的进程
-
pid = 0 将进程发送给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号。(注意,发送者进程也会受到自己发出的信号)
-
pid = -1 将信号广播发送给系统内所有进程(例如系统关机时,会向所有的登录窗口广播关机信息)
参数sig:准备发送信号的类型,假如其值为零则没有任何信号发送,但是系统会执行错误检查通常会利用sig值为 零来检验某个进程是否仍在运行。
返回值说明:成功执行,返回0;失败,返回-1;errno被设为以下某值。
EINVAL:指定的信号无效(参数sig不合法)
EPERM:权限不够无法发送信号给指定进程
ESRCH:参数pid所指定的进程或进程组不存在
三、可靠信号与不可靠信号
信号分为不可靠信号(1~32)和可靠信号(34~64)
不可靠信号主要有以下问题:
-
每次处理完信号之后,就会恢复成默认处理(早期的signal函数,linux2.6.3 5.6内核经验证已经不在恢复默认动作)
-
存在信号丢失的问题(进程收到信号不作排队处理,相同的信号多次到来会合并为一个)
现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失
信号丢失示例:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 子进程退出时调用的函数
void hdfunc(int sig) {
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
}
int main() {
signal(15, hdfunc);
signal(34, hdfunc);
for (int i = 1; i < 100; ++ i) {
printf("i=%d\n", i);
sleep(2);
}
return 0;
}
运行结果(不可靠信号):
运行结果(可靠信号):
四、信号处理函数被中断
当一个信号到达后,调用处理函数,如果这时候有其他的信号发生,会中断之前的处理函数,等新的信号处理函数执行完成后再继续执行之前的处理函数。
但是,同一个信号会排队阻塞。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 子进程退出时调用的函数
void hdfunc(int sig) {
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
}
int main() {
signal( 2, hdfunc);
signal(15, hdfunc);
signal(34, hdfunc);
signal(35, hdfunc);
for (int i = 1; i < 100; ++ i) {
printf("i=%d\n", i);
sleep(2);
}
return 0;
}
示例1(不可靠信号)
示例2(可靠信号)
五、信号的阻塞
如果不希望在接到信号时中断当前的处理函数,也不希望忽略该信号,而是延时一段时间再处理这个信号,这种情况可以通过阻塞信号实现。
信号的阻塞和忽略信号是不同的,被阻塞的信号也不会影响进程的行为,信号只是暂时被阻止传递。
进程忽略一个信号时,信号会被传递出去但进程会将信号丢弃。
执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 子进程退出时调用的函数
void hdfunc1(int sig) {
sigset_t set; // 定义一个信号集
sigemptyset(&set); // 清空信号集
sigaddset(&set, 15); // 将15加入信号集
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞信号集(set)里的所有信号
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
sigprocmask(SIG_UNBLOCK, &set, NULL); // 停止阻塞信号集(set)里的所有信号
}
// 子进程退出时调用的函数
void hdfunc2(int sig) {
sigset_t set; // 定义一个信号集
sigemptyset(&set); // 清空信号集
sigaddset(&set, 2); // 将2加入信号集
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞信号集(set)里的所有信号
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
sigprocmask(SIG_UNBLOCK, &set, NULL); // 停止阻塞信号集(set)里的所有信号
}
int main() {
signal( 2, hdfunc1);
signal(15, hdfunc2);
for (int i = 1; i < 100; ++ i) {
printf("i=%d\n", i);
sleep(2);
}
return 0;
}
运行结果:
六、功能更强大的sigaction
代码(阻塞信号):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
// 子进程退出时调用的函数
void hdfunc(int sig) {
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
}
int main() {
struct sigaction stact;
memset(&stact, 0, sizeof(stact)); // 初始化
stact.sa_handler = hdfunc; // 指定信号处理函数
sigaddset(&stact.sa_mask, 15); // 阻塞信号15
sigaddset(&stact.sa_mask, 2); // 阻塞信号2
sigaction( 2, &stact, NULL); // 设置信号2的处理行为
sigaction(15, &stact, NULL); // 设置信号15的处理行为
for (int i = 1; i < 100; ++ i) {
printf("i=%d\n", i);
sleep(2);
}
return 0;
}
运行结果(阻塞信号):
代码(重启系统调用):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
// 子进程退出时调用的函数
void hdfunc(int sig) {
printf("sig=%d\n", sig);
for (int i = 1; i <= 5; ++ i) {
printf("i(%d)=%d\n", sig, i);
sleep(1);
}
}
int main() {
struct sigaction stact;
memset(&stact, 0, sizeof(stact)); // 初始化
stact.sa_handler = hdfunc; // 指定信号处理函数
sigaddset(&stact.sa_mask, 15); // 阻塞信号15
sigaddset(&stact.sa_mask, 2); // 阻塞信号2
//stact.sa_flags = SA_RESTART; // 如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
sigaction( 2, &stact, NULL); // 设置信号2的处理行为
sigaction(15, &stact, NULL); // 设置信号15的处理行为
char str[50]; memset(str, 0, sizeof(str));
scanf("%s", str);
printf("str=%s\n", str);
return 0;
}
运行结果(重启系统调用)有注释的:
运行结果(重启系统调用)无注释的:
总结
在指令后加“&”符号或者用fork()可以让程序在后台运行,我们可以发送信号可以让处于后台运行的程序产生相应的关联(执行函数、退出程序、给其他进程发送信号...)。后来的信号可以将信号处理函数中断,可以阻塞信号防止被中断。相同的信号短时间多次发送可能会丢失。使用更能更强大的sigaction可以让我们更有效的使用信号。