当前位置: 首页 > news >正文

【Linux系统】Linux进程信号(产生,保存信号)

1. 信号快速认识

1-1 基本结论
  • 如何识别信号?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
  • 信号产生之后,是知道怎么处理的,同理,如果信号没有产生,也是知道怎么处理信号的。所以,信号的处理方法,在信号产生之前,已经准备好了。
  • 处理信号,立即处理吗?也可能正在做优先级更高的事情,不会立即处理。什么时候?合适的时候。
  • 产生信号->识别信号->保存信号->处理信号(处理时机以及处理方式)
  • 如何进行信号处理?a.默认 b.忽略 c.自定义,后续都叫做信号捕捉。
  • 异步:在Linux系统中,异步是一种重要的操作系统,它允许进程在执行摸个操作时,不必等待该操作完成就可以执行其他任务,当操作完成后,系统会一某种方式通知进程。

1-2 技术应用角度的信号

1-2-1 一个样例
#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 用户输入命令,在Shell下启动一个前台进程
  • 用户按下Ctrl+C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。
  • 前台进程因为收到信号,进而引起进程退出。

注意:

  • ./test 前台进程
  • ./test & 后台进程 bash进程依旧可以进行命令行解释
    • kill -9 PID
    • nohup ./test & 会在当前目录生成nohup文件,回显 [作业号] PID

在这里插入图片描述

1-2-2 一个系统函数
// NAME
//      signal - ANSI signal handling
// SYNOPSIS
//      #include <signal.h>
//      typedef void (*sighandler_t)(int);
//      sighandler_t signal(int signum, sighandler_t handler);
// 参数说明:
// 9 signum: 信号编号[后面解释,只需要知道是数字即可]
// 10 handler: 函数指针, 表示更改信号的处理动作,当收到对应的信号, 就回调执行handler方法

而其实,Ctrl+C的本质是向前台进程发送SIGINT即2号信号,我们证明一下,这里需要引入一个系统调用函数。

开始测试

#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号信号,则不会调用相关方法

注意

  • 要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
  • Ctrl+C产生的信号只能发给前台进程。一个命令后面加&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  • 这种控制键产生的一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl+C 这样的信号。
  • 前台进程在运行过程中用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

1-3 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

1-3-1 查看信号

在终端执行kill -l ,可得到如下信号列表:
在这里插入图片描述

  • 9号信号无法被捕捉。
  • 发送信号的本质:就是写入信号,OS修改进程PCB中对应信号位图。
  • 无论以什么样的方式发送信号,最终都是转换到OS,让OS写入信号,因为tast_struct的唯一管理者是OS 。
  • 问题:OS如何知道键盘上有数据?
    • 冯诺依曼体系下,输入设备对控制器触发硬件中断 (纯硬件信号电路),至此,硬件与OS可以并行了

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。

例如其中有定义:

#define SIGINT 2
/* ISO C99 signals. */
#define SIGABRT 6 /* Abnormal termination. */
#define SIGILL 4 /* Illegal instruction. */
#define SIGFPE 8 /* Erroneous arithmetic operation. */
#define SIGSEGV 11 /* Invalid access to storage. */
/* Historical signals specified by POSIX. */
#define SIGHUP 1 /* Hangup. */
#define SIGQUIT 3 /* Quit. */
#define SIGTRAP 5 /* Trace/breakpoint trap. */
#define SIGKILL 9 /* Killed. */
#define SIGSYS 12 /* Bad system call. */
#define SIGPIPE 13 /* Broken pipe. */
/* Real-time signals. */
/* Nonstandard signals. */
#define SIGURG 23 /* Urgent condition on socket. */
#define SIGCHLD 17 /* Child stopped or terminated. */
#define SIGCONT 18 /* Continue a stopped process. */
#define SIGSTOP 19 /* Stop (cannot be caught or ignored). */
#define SIGTSTP 20 /* Stop typed at terminal. */
#define SIGTTIN 21 /* Background read attempted from control terminal. */
#define SIGTTOU 22 /* Background write attempted to control terminal. */
#define SIGXCPU 24 /* CPU time limit exceeded (System V). */
#define SIGXFSZ 25 /* File size limit exceeded (System V). */
#define SIGVTALRM 26 /* Virtual timer expired. */
#define SIGPROF 27 /* Profiling timer expired. */
#define SIGWINCH 28 /* Window size change (4.3BSD, Sun). */
#define SIGIO 29 /* I/O now possible (4.2BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGUSR1 10 /* User-defined signal 1. */
#define SIGUSR2 31 /* User-defined signal 2. */

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。

这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal

SignalStandardActionComment
SIGABRTP1990TermAbort signal from abort(3)
SIGBUSP2001CoreBus error (bad memory access)
SIGCHLDP1990IgnA child stopped or terminated
SIGCONT--Continue a stopped or paused process
SIGFPEP1990CoreErroneous arithmetic operation
SIGHUPP1990TermHangup detected on controlling terminal
SIGILLP1990CoreIllegal Instruction
SIGINFOP1990TermA synonym for SIGKILL (2BSD)
SIGINTP1990TermInterrupt from keyboard
SIGIOP1990CoreI/O now possible; see signal(7)
SIGIOTP1990TermIOT trap. A synonym for SIGABRT
SIGKILLP1990TermKill (cannot be caught or ignored)
SIGPIPEP1990TermBroken pipe: write to pipe with no readers; see pipe(7)
SIGPROFP2001TermProfiling timer expired
SIGPWR--Power failure (System V)
SIGQUITP1990CoreQuit from keyboard
SIGSEGVP1990CoreInvalid memory reference
SIGSTOPP1990StopStop process (cannot be caught or ignored)
SIGTSTPP1990StopStop typed at terminal
SIGTERMP1990TermTermination signal
SIGTRAPP2001CoreTrace/breakpoint trap
SIGTTINP1990StopBackground read attempted from control terminal
SIGTTOUP1990StopBackground write attempted to control terminal
SIGURGP1990IgnUrgent condition on socket (4.2BSD)
SIGUSR1P1990TermUser-defined signal 1
SIGUSR2P1990TermUser-defined signal 2
SIGXCPUP2001CoreCPU time limit exceeded (4.2BSD)
SIGXFSZP2001CoreFile size limit exceeded (4.2BSD)
SIGWINCH-IgnWindow resize signal (4.3BSD, Sun)
1-3-2 信号处理

信号处理函数稍后详细介绍,可选的处理动作有以下三种:

  • 忽略此信号
#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*/, SIG_IGN); // 设置忽略信号的宏while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 执行该信号的默认处理动作
#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*/, SIG_DFL); // 设置默认信号的宏while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义(就是开始的样例)
// 就是开始的样例
#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);}
}

注意看源码:

#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
typedef void (*__sighandler_t)(int);
// 其实SIG_DFL和SIG_IGN就是把0、1 强转为函数指针类型

为了保证条理,我们采用如下思路来进行阐述:

在这里插入图片描述

2.产生信号

2-1 通过终端按键产生信号

2-1-1 基本操作
  • Ctrl+C(SIGINT)已经验证过,这里不再重复。
  • Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件,用于事后调试)。
#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(SIGQUIT/*3*/, handler); //第13行代码while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}

编译运行,输出:

我是进程: 213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^我是: 213056, 我获得了一个信号: 3

注释掉第13行代码后:

我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Quit
  • Ctrl+Z(SIGTSTP)可以发送停止信号,将当前前台进程挂起到后台等
#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(SIGTSTP/*20*/, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}

编译运行,输出:

我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z我是: 213552, 我获得了一个信号: 20

注释掉第13行代码后:

我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+  Stopped                 ./sig
whb@bite:~/code/test$ jobs
[1]+  Stopped                 ./sig
2-1-2 理解OS如何得知键盘有数据

键盘按下产生电信号,向CPU发送硬件中断,CPU检测到中断信号后,执行操作系统中处理键盘数据的代码,操作系统从外设读入数据到内存,等待进一步处理。
在这里插入图片描述

2-1-3 初步理解信号起源

注意:

  • 信号其实是从纯软件角度,模拟硬件中断的行为。
  • 只不过硬件中断是发给CPU,而信号是发给进程。
  • 两者有相似性,但是层级不同,这点我们后面的感觉会更加明显。

2-2 调用系统命令向进程发信号

示例代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{while(true){sleep(1);}
}

操作步骤:

  1. $ g++ sig.cc -o sig
  2. $./sig & (在后台执行死循环程序)
  3. $ ps ajx |head -1 && ps ajx | grep sig (查找进程信息)
    输出:
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
211805 213784 213784 211805 pts/0    213792 S      1002   0:00./sig
  1. $ kill -SIGSEGV 213784 (给进程发送SIGSEGV信号)
    多按一次回车后显示:
[1]+  Segmentation fault      ./sig

说明:

  • 213784是sig进程的pid。

  • 之所以要再次回车才显示Segmentation fault,是因为在213784进程终止之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。

  • 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -11 213784 ,11是信号SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。

2-3 使用函数产生信号

2-3-1 kill

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。

  • NAME:kill - send signal to a process
  • SYNOPSIS
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
  • RETURN VALUE:On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.

样例:实现自己的kill命令

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main(int argc, char *argv[])
{if(argc != 3){std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int number = std::stoi(argv[1]+1); // 去掉-pid_t pid = std::stoi(argv[2]);int n = kill(pid, number);return n;
}
2-3-2 raise

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

  • NAME:raise - send a signal to the caller
  • SYNOPSIS
#include <signal.h>int raise(int sig);
  • RETURN VALUE:raise() returns 0 on success, and nonzero for failure.

样例

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了一个信号: " << signumber << std::endl;
}int main()
{signal(2, handler);  // 先对2号信号进行捕捉// 每隔1s,自己给自己发送2号信号while(true){sleep(1);raise(2);}
}

编译运行,输出:

获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
2-3-3 abort

abort函数使当前进程接收到信号而异常终止。

  • NAME:abort - cause abnormal process termination
  • SYNOPSIS
#include <stdlib.h>void abort(void);
  • RETURN VALUE:The abort() function never returns. // 就像exit函数一样,abort函数总是会成功的,所以没有返回值。

样例

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了一个信号: " << signumber << std::endl;
}int main()
{signal(SIGABRT, handler);while(true){sleep(1);abort();}
}

编译运行,输出:

获取了一个信号: 6  // 实验可以得知,abort给自己发送的是固定6号信号,虽然捕捉了,但是还是要退出
Aborted

注释掉第15行代码后:

Aborted

2-4 由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数和SIGALRM信号。

  • NAME:alarm - 设置用于传递信号的闹钟
  • SYNOPSIS
#include <unistd.h>unsigned int alarm(unsigned int seconds);
  • RETURN VALUE:alarm() 返回距离发送任何先前计划的警报剩余的秒数,如果之前没有预先计划的警报,则返回 0。

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。

如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

2-4-1 基本alarm验证 - 体会IO效率问题

IO多的代码

// IO多
#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{int count = 0;alarm(1);while(true){std::cout << "count : " << count << std::endl;count++;}return 0;
}

IO少的代码

// IO少
#include <iostream>
#include <unistd.h>
#include <signal.h>int count = 0;
void handler(int signumber)
{std::cout << "count : " << count << std::endl;exit(0);
}int main()
{signal(SIGALRM, handler);alarm(1);while(true){count++;}return 0;
}

运行结果:

count : 107148
count : 107149
Alarm clock
$ g++ alarm.cc -o alarm
whb@bite:~/code/test$ ./alarm
count : 492333713

结论:

  • 闹钟会响一次,默认终止进程。
  • 有IO时效率低。
2-4-2 设置重复闹钟

代码样例:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>using func_t = std::function<void()>;int gcount = 0;
std::vector<func_t> gfuncs;void handler(int signo)
{for(auto &f : gfuncs){f();}std::cout << "gcount : " << gcount << std::endl;int n = alarm(1); // 重设闹钟,会返回上一次闹钟的剩余时间std::cout << "剩余时间 : " << n << std::endl;
}int main()
{gfuncs.push_back([](){ std::cout << "我是一个内核刷新操作" << std::endl; });gfuncs.push_back([](){ std::cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << std::endl; });gfuncs.push_back([](){ std::cout << "我是一个内存管理操作,定期清理操作系统内部的内存碎片" << std::endl; });alarm(1); // 一次性的闹钟,超时alarm会自动被取消signal(SIGALRM, handler);while(true){pause();std::cout << "我醒来了..." << std::endl;gcount++;}
}
// NAME
pause - wait for signal
// SYNOPSIS
#include <unistd.h>int pause(void);
// DESCRIPTION
pause() 使调用进程(或线程)休眠,直到传递终止进程或导致调用信号捕获函数的信号。RETURN VALUE
pause()仅在捕获到信号并返回信号捕获函数时返回。在这种情况下,pause() 返回 -1,并且 errno 设置为 EINTR。

运行结果(窗口1):

我的进程pid是: 216982
剩余时间 : 13  // 提前唤醒它,剩余
剩余时间 : 0
剩余时间 : 0
剩余时间 : 0
剩余时间 : 0

(窗口2):

$ kill -14 216982

结论:

  • 闹钟设置一次,起效一次。
  • 重复设置的方法。
  • 如果时间允许,可以测试一下alarm(0) 。
2-4-3 如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

2-4-4 如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的机制。现代Linux提供了定时功能,定时器也要被管理:先描述,再组织。内核中的定时器数据结构是:

struct timer_list {struct timer_list_entry entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct vec_base *base;
};

我们在这部分进行深究,为了理解它,我们可以看到:定时器超时时间expires和处理方法function。操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为“堆结构”。

2-5 硬件异常产生信号

硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。

例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。

再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

2-5-1 模拟除0
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{//signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a/=0;a/=0;while(1);return 0;
}
2-5-2 模拟野指针

默认行为

#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{//signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}

运行结果:

$./sig
Segmentation fault (core dumped)

捕捉行为

#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}

运行结果:

$./sig
catch a sig : 11
catch a sig : 11
catch a sig : 11

由此可以确认,在C/C++中当除零、内存越界等异常,在系统层面上,是被当成信号处理的

注意
通过上面的实验,我们可能发现:一直有信号产生被捕获。

  • 实际上OS会检查应用程序的异常情况,CPU中有控制和状态寄存器,用于控制处理器操作,状态寄存器有一些标志位,OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
  • 除零异常后,未清理内存等,CPU中保留上下文数据及寄存器内容,所以异常会一直存在,持续发出异常信号。访问非法内存也是如此。
  • OS系统要不要知道CPU内部出错了?是要知道的。所以必须知道是哪一个进程导致的,并通过信号杀掉进程
2-5-3 子进程退出core dump

在这里插入图片描述
在Linux系统中,进程异常终止时可能会进行Core Dump(核心转储),即把进程当时的内存状态记录下来,保存在一个文件中,通常命名为 core.PID (PID为进程号 ) 。而core dump标志位是与进程退出相关的一个标识,具体如下:

  • 位置与含义:在存储进程退出码的变量内部结构中,次第8位表示退出码,最低7位表示终止信号,终止信号的前一位就是core dump标志位 。该标志位用于表示进程收到信号后的动作是Core(核心转储)还是Term(终止) ,一般0表示进程是Core退出(进行核心转储 ),1表示Term退出(仅终止进程 )。
  • 与信号及Core Dump的关联:并非所有信号引起的进程退出都会产生Core Dump文件,只有带有 core 标志的信号(如 SIGSEGV 段错误信号 )引起的退出,在core dump功能开启时才会产生。当core dump开启,遇到有 core 标志的信号,进程会进行核心转储,且退出码里的core dump标志位会被置为1 ;若core dump关闭,即便遇到有 core 标志的信号,也不会进行核心转储,标志位始终为0 。
  • 查看与设置:可通过 ulimit -a 查看当前系统资源限制,其中 core file size 表示core dump功能相关文件大小限制,若其大小为0,说明系统默认关闭core dump功能 。可使用 ulimit -c 命令设置生成的core文件大小的最大限制来开启该功能,如 ulimit -c 10240 可将core文件大小限制设为10240 blocks 。

core dump标志位是判断进程异常退出时是否进行核心转储的重要标识,对程序调试定位问题根源有重要意义。

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>int main()
{if (fork() == 0){sleep(1);int a = 10;a /= 0;exit(0);}int status = 0;waitpid(-1, &status, 0);printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1);return 0;
}

列出部分信号及其属性:

信号进程标志描述
SIGABRTP1990CoreAbort signal from abort(3)
SIGALRMP1990TermTimer signal from alarm(2)
SIGBUSP2001CoreBus error (bad memory access)
SIGCHLDP1990IgnChild stopped or terminated
SIGCONTP1990ContA synonym for SIGCHLD
SIGFPEP1990CoreFloating - point exception
SIGGPE-TermEmulator trap
SIGHPFP1990CoreHangup detected on controlling terminal
SIGILLP1990CoreIllegal Instruction
SIGINFO--A synonym for SIGPWR
SIGINTP1990TermInterrupt from keyboard
SIGIO-TermI/O now possible (4.2BSD)
SIGKILLP1990CoreKill signal. A synonym for SIGABRT
SIGLOST-TermFile lock lost (unused)
SIGPIPEP1990TermBroken pipe: write to pipe with no reader
SIGPOLLP2001TermPollable event (Sys V)
$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 65536
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real - time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7643
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
2-5-4 Core Dump
  • SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump
  • 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
  • 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
  • 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。
  • 在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,如允许core文件最大为1024K:$ ulimit -c 1024
$ ulimit -c 1024
$ ulimit -a
core file size          (blocks, -c) 1024
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7643
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real - time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7643
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

然后写一个死循环程序:

#include <stdio.h>int main()
{printf("pid is : %d\n", getpid());while(1);return 0;
}

前台运行这个程序,然后在终端键入Ctrl-C(貌似不行)或Ctrl-\(这个可以):

#./test
pid is : 4586
^\Quit (core dumped)
# ll
total 92
-rw-------. 1 root root 159744 Apr 21 18:04 core.4586
-rwxr-xr-x. 1 root root   61 Apr 21 18:00 Makefile
-rw-r--r--. 1 root root 4766 Apr 21 18:03 test.c
-rw-r--r--. 1 root root   92 Apr 21 18:03 test.c
-rw-r--r--. 1 root root  627 Apr 21 17:36 test.cpp
[root@localhost test11]#

ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。使用core文件:

#./test
Segmentation fault (core dumped)
# ls
core.2884 Makefile test test.c
# gdb test core.2884
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /BIT/project/test78/test...done.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /BIT/project/test78/test...done.
(gdb) core-file core.2884
[New Thread 2884]
Missing separate debuginfo for
Try: yum --enablerepo='*debug*' install /usr/lib/debug/build-id/d5/942f21cf3002919e5518a033793d3c25a660f3.debug
Reading symbols from /lib/libc-2.12.so...Reading symbols from /usr/lib/debug/lib/libc-2.12.so.debug...done.
done.
Reading symbols from /lib/ld-2.12.so...Reading symbols from /usr/lib/debug/lib/ld-2.12.so.debug...done.
done.
Loaded symbols for /lib/ld-2.12.so
Core was generated by `./test'.
Program terminated with signal 11, Segmentation fault.
#0  0x080483ad in fun () at test.c:8
8       *p = 100;
(gdb) 

3. 保存信号

3-1 常见概念

  • 实际执行信号的处理动作称为信号送达(Delivery)。
  • 信号从产生到送达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行送达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会送达,而忽略是在送达之后可选的一种处理动作。

3-2 在内核中的表示

在这里插入图片描述

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。
  • 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号送达才清除该标志。 在图中例子,SIGHUP信号未阻塞也未产生,当它送达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能送达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前该信号产生过多次,POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在送达之前产生多次只计一次,而实时信号在送达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

// 内核结构 2.6.18
struct task_struct {.../* signal handlers */struct sighand_struct *sighand;sigset_t blocked;struct sigpending pending;...
};struct sighand_struct {atomic_t        count;struct k_sigaction action[_NSIG]; // #define _NSIG  64spinlock_t      siglock;
};struct __new_sigaction {__sighandler_t  sa_handler;unsigned long   sa_flags;void            (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t  sa_mask;
};struct k_sigaction {struct __new_sigaction sa;void            __user *ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);struct sigpending {struct list_head list;sigset_t signal;
};

3-3 sigset_t

从图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

3-4 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

3-4-1 sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 返回值:若成功则为0,若出错则为-1
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

  • 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。

  • 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号送达。

#include <signal.h>
int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

下面用刚学的几个函数做个实验。程序如下:

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>void PrintPending(sigset_t &pending)
{std::cout << "curr process[" << getpid() << "]pending: ";for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo)){std::cout << 1;}else{std::cout << 0;}}std::cout << "\n";
}void handler(int signo)
{std::cout << signo << " 信号被送达!!!" << std::endl;std::cout << "-------------------------" << std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout << "-------------------------" << std::endl;
}int main()
{// 0. 捕捉2号信号signal(2, handler); // 自定义捕捉//signal(2, SIG_IGN); // 忽略一个信号//signal(2, SIG_DFL); // 信号的默认处理动作// 1. 屏蔽2号信号sigset_t block_set, old_set;sigemptyset(&block_set);sigemptyset(&old_set);sigaddset(&block_set, SIGINT); // 我们有没有修改当前进行的内核block表呢??1 0// 1.1 设置进入进程的Block表中sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽!int cnt = 15;while (true){// 2. 获取当前进程的pending信号集sigset_t pending;sigpending(&pending);// 3. 打印pending信号集PrintPending(pending);cnt--;// 4. 解除对2号信号的屏蔽if (cnt == 0){std::cout << "解除对2号信号的屏蔽!!!" << std::endl;sigprocmask(SIG_SETMASK, &old_set, &block_set);}sleep(1);}
}

运行结果:

$./run
curr process[448336]pending: 00000000000000000000000000000000
curr process[448336]pending: 00000000000000000000000000000000
curr process[448336]pending: 00000000000000000000000000000000
^Ccurr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010
curr process[448336]pending: 00000000000000000000000000000010

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

相关文章:

  • 使用 Spring Boot Actuator 实现应用实时监控
  • 《TCP/IP详解 卷1:协议》之第九章:IP选路
  • 项目管理进阶:详解华为研发项目管理(IPD流程管理)【附全文阅读】
  • 机器视觉开发-打开摄像头
  • Selenium:模拟真实用户的爬虫
  • Python与深度学习:自动驾驶中的物体检测,如何让汽车“看懂”世界
  • 前端函数防抖(Debounce)完整讲解 - 从原理、应用到完整实现
  • Arduino程序函数详解与实际案例
  • Qt二维码demo
  • vscode 的空格和 tab 设置 与 Rime 自建词库
  • C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 18)
  • react学习笔记2——基于React脚手架与ajax
  • 数据可视化入门:画一只会动的星空折线图
  • 基于hr2管理系统的学习
  • 并发设计模式实战系列(11):两阶段终止(Two-Phase Termination)
  • 计算机操作系统知识集合
  • 【c++】【STL】queue详解
  • 小白如何入门Python爬虫
  • Qt connect第五个参数
  • 冒泡排序:从入门到入土(不是)的奇妙旅程
  • 2025财政观察①长三角罚没收入增速放缓,24城仍在上涨
  • 三大上市猪企:前瞻应对饲料原材料价格波动
  • 微软上财季净利增长18%:云业务增速环比提高,业绩指引高于预期
  • 莫名的硝烟|“我们最好记住1931年9月18日这个日子”
  • 美国清洗政治:一幅残酷新世界的蓝图正在展开
  • 牛市早报|今年第二批810亿元超长期特别国债资金下达,支持消费品以旧换新