信号

A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停执行,去处理信号,处理完毕后在继续执行。
与硬件中断类似(异步模式)。但信号是软件层面上实现的中断,早期被称为软中断.

信号的共性:简单、不能携带大量信息、满足条件才能发送。

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由内核负责发送以及处理的

与信号相关的事件和状态

产生信号的方式如下表:

事件 例子
按键产生 Ctrl+cCtrl+zCtrl+\
系统调用产生 killraiseabort
软件条件产生 定时器alarm
硬件异常产生 非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
命令产生 kill

递达:递送并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态

信号处理的方式:

  1. 执行默认动作(即进程不去处理)
  2. 忽略(丢弃)
  3. 捕捉(调用用户处理函数)

在linux的实现PCB的结构体task_struct中包含了信号相关的信息,主要是指阻塞信号集和未决信号集。

  • 阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽(对应位 置1),当屏蔽某信号后,在收到该信号,该信号的处理将延后(解除屏蔽后)
  • 未决信号集:
    1. 信号产生,未决信号集中描述该信号的位立即翻转为1, 表示信号处于为未决状态。当信号被处理对应位翻转回0。这一时刻往往非常短暂。
    2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称为未决信号集。在屏蔽解除前,信号一直处于未决状态。

不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。
信号值被定义在文件 /usr/include/bits/signum.h 中,其源文件是 /usr/src/linux/kernel/signal.c
在 Linux 下,可以查看 signal(7) 手册页来查阅信号名列表、信号值、默认的行为和它们是否可以被捕获。其命令如下所示:

1
man 7 signal

下表列出Linux中常见的进程信号:

编号 名称 事件 默认操作
1 SIGHUP (Hangup)当你不在控制终端时,或者当你关闭gnome-termnal或断开modem内核会产生该信号。由于后台进程没有控制的终端,因而它们常用SIGUP来发出需要重新读取其配置文件的信号。 (Abort)挂断控制终端信号或进程。
2 SIGINT (Interrupt)来自键盘的中断。通常终端驱动程序会将其与Ctrl+c绑定 (Abort)终止程序
3 SIGQUIT (Interrupt)来自键盘的中断。通常终端驱动程序会将其与Ctrl+\绑定 (Dump)程序被终止并产生dump core文件。
4 SIGILL (Illegal Instruction)程序出错或执行了一个非法的操作命令 (Dump)程序被终止并产生dump core文件。
5 SIGTRAP (Breakpoint/Trace Trap)调试用,跟踪断点
6 SIGABRT (Abort)放弃执行,异常结束。 (Dump)程序被终止并产生dump core文件。
7 SIGBUS (Bus)当进程引起一个总线错误时,BUS 信号将被发送到进程。例如,访问了一部分未定义的内存对象 (Dump)程序被终止并产生dump core文件。
8 SIGFPE (Floating Point Exeception)浮点异常 (Dump)程序被终止并产生dump core文件。
9 SIGKILL (Kill)程序被终止。该信号不能被捕获或被忽略。想立即终止一个进程就发送信号9.注意程序将没有任何机会做清理工作。 (Abort) 程序被终止。
10 SIGUSR1 (User defined Signal 1) 用户定义的信号。 (Abort) 进程被终止。
11 SIGSEGV (Segmentation Violation) 当程序引用无效的内存时会产生此信号。比如:寻址没有映射的内存;寻址未许可的内存。 (Dump) 程序被终止并产生 dump core 文件。
12 SIGUSR2 (User defined Signal 2) 保留给用户程序用于 IPC 或其他目的。 (Abort) 进程被终止。
13 SIGPIPE (Pipe) 当程序向一个套接字或管道写时由于没有读者而产生该信号 (Abort) 进程被终止。
14 SIGALRM (Alarm) 该信号会在用户调用 alarm系统调用所设置的延迟秒数到后产生。该信号常用判别于系统调用超时。 (Abort) 进程被终止。
15 SIGTERM (Terminate) 用于和善地要求一个程序终止。它是 kill的默认信号。与 SIGKILL 不同,该信号能被捕获,这样就能在退出运行前做清理工作。 (Abort) 进程被终止。
16 SIGSTKFLT (Stack fault on coprocessor) 协处理器堆栈错误。 (Abort) 进程被终止。
17 SIGCHLD (Child) 停止或终止子进程。可改变其含义挪作它用。 (Ignore) 子进程停止或结束。
18 SIGCONT (Continue) 该信号致使被 SIGSTOP 停止的进程恢复运行。可以被捕获。 (Continue) 恢复进程 的执行。
19 SIGSTOP (Stop) 停止进程的运行。该信号不可被捕获或忽略 (Stop) 停止进程运行。
20 SIGTSTP (Terminal Stop) 向终端发送停止键序列。该信号可以被捕获或忽略。 (Stop) 停止进程运行。
21 SIGTTIN (Terminal Input on Background) 后台进程试图从一个不再被控制的终端上读取数据,此时该进程将被停止,直到收到 SIGCONT 信号。该信号可以被捕获或忽略。 (Stop) 停止进程运行。
22 SIGTTOU (TTY Output on Background) 后台进程试图向一个不再被控制的终端上输出数据,此时该进程将被停止,直到收到 SIGCONT 信号。该信号可被捕获或忽略。 (Stop) 停止进程运行。

tips: 只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!


当程序运行的过程中出现异常终止或崩溃,系统就会将程序崩溃时的内存、寄存器状态、堆栈指针、内存管理信息记录下来,保存在一个文件中,叫作核心转储(Core Dump)
可以通过以下操作开启核心转储并修改核心转储文件的保存路径

1
2
3
4
5
6
7
8
9
10
11
12
13
ulimit -c    # 默认关闭 0 
ulimit -c unlimited # 临时开启
sudoedit /etc/security/limits.conf # 将core的value 0修改成unlimited,永久开启
#<domain> <type> <item> <value>
#

#* soft core 0
* soft core unlimited

# 修改 core_users_pid, 使核心转储文件名变为core.[pid]
echo 1 > /proc/sys/kernel/core_uses_pid
# 还可以修改 core_pattern, 保存到/tmp目录,文件名为 core-[filename]-[pid]-[time]
echo /tmp/core-%e-%p-%t > /proc/sys/kernel/core_pattern

若有一个核心转储文件,可以使用gdb进行调试

1
2
gdb [filename] [core file]
# gdb a.out /tmp/core-a.out-12345-123123123

系统调用产生信号

kill函数

该函数用来发生一个信号给一个进程

函数原型

1
2
#include <signal.h>
int kill(pid_t pid, int sig);

参数说明

1
2
3
4
5
6
7
8
9
10
@param:
pid:
>0: 发送信号给指定的进程
=0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程
=-1: 发送给进程有权限发送的系统中所有进程
<-1: 取|pid|发给对应进程组

@return:
sucessful: 至少有一个信号被发生,返回0
failure: -1, set errno

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们互相关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同.

权限保护:

  • 超级用户可以发送信号给任意用户
  • 普通用户没有权限向系统用户以及其他普通用户发送信号
  • 普通用户的基本规则是:发送者实际或有效UID==接受者实际或有效UID

封装了kill函数的几个发出信号的函数

raise函数

当进程中只有一个线程的适合它等价于kill(getuid(), sig)
当进程中只有多个线程的适合它等价于pthread_kill(pthread_self(), sig)

1
2
3
4
5
6
7
#include <signal.h>
/**
* @return:
- sucessful: 0
- failure: 非0
*/
int raise(int sig);

abort函数

该函数永远没有返回值,其等价于raise(SIGABRT)

1
2
#include <stdlib.h>
noreturn void abort(void);

软件条件产生信号

alarm函数

每个进程都有且只有唯一一个定时器, 本质就是PCB中的alarm变量。
在指定seconds后(设置current->alarm),在CPU数了seconds包含的总系统滴答后,即当前进程task_struct的成员变量alarm 小于 系统从开机算起的滴答数jiffies (current->alarm && current->alarm < jiffies)时,内核会给当前进程发送SIGALRM信号。系统收到该信号,默认动作终止。

函数原型

1
2
3
4
5
6
7
8
#include <unistd.h>
unsigned int alarm(unsigned int seconds){
int old = current->alarm;
if(old) old = (old - jiffies) / HZ;

current->alarm = (seconds > 0)? (jiffies + HZ * seconds): 0;
return old;
}

参数说明

1
2
3
4
5
6
7
@param:
seconds: 要设置定时的秒数

@return:
1. 在程序中第一次设置报警定时间值,返回0
2. 其他情况返回继上次设置后的剩余秒数
没有出错的情况

用法示例

1
2
alarm(5); // 设置定时时间为5
alarm(0); // 取消定时器,返回闹钟剩余秒数

setitimer函数

设置定时器。可代替alarm函数。精度微秒us,可实现周期定时

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/time.h>
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *restrict new_value,
struct itimerval *restrict old_value);

参数说明

1
2
3
4
5
6
7
8
9
10
11
12
@param:
which: 指定定时方式
- 自然定时:ITIMER_REAL -> SIGLARM
- 只计算进程占用CPU的时间(用户空间):ITIMER_VIRTUAL -> SIGVTALRM
- 计算占用CPU及执行系统调用的时间(用户+内核):ITIMER_PROF -> SIGPROF
new_value: 定时秒数
old_value: 传出参数,上次定时剩余时间。
curr_value: 传出参数,上次定时剩余时间。

@return:
sucessful: 0
failure: -1, set errno

e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

void foo(int signo){
printf("hello world\n");
}

int main(int argc, char* argv[])
{
struct itimerval it, oldit;
// 初次设置定时时间, 若为0,则关闭计时器
it.it_value.tv_sec = 1;
it.it_value.tv_usec = 0;
// 设置周期定时时间,时间到了会自动重载current->alarm
it.it_interval.tv_sec = 1;
it.it_interval.tv_usec = 0;

signal(SIGALRM, foo); // 注册foo函数捕捉信号SIGALRM

if(setitimer(ITIMER_REAL, &it, &oldit) == -1) {
perror("setitimer error");
exit(-1);
}

while(1);

return 0;
}

信号集操作函数

自定义信号集

sigset_t类型本质是位图,但不应该直接使用位操作,而应该使用下面的函数来保证程序的可移植性。

设定自定义信号集函数如下表,它们的头文件都是signal.h

函数原型 功能 返回值
int sigemptyset(sigset_t *set); 将信号集set全清0 return 0 on sucess and -1 on error
int sigfillset(sigset_t *set); 将信号集set全置1 return 0 on sucess and -1 on error
int sigaddset(sigset_t *set, int signum); 将信号signum加入集合set return 0 on sucess and -1 on error
int sigdelset(sigset_t *set, int signum); 将集合set中删除信号signum return 0 on sucess and -1 on error
int sigismember(const sigset_t *set, int signum); 查找信号signum是否在集合set signum在集合set中:1,不在:0,出错:-1

操作信号屏蔽字

可以用sigprocmask函数来操作PCB中的信号屏蔽字来达到屏蔽信号、解除屏蔽信号的效果。

函数原型

1
2
3
#include <signal.h>
int sigprocmask(int how, const sigset_t *set,
sigset_t *oldset);

参数说明

1
2
3
4
5
6
7
8
9
10
11
@param:
how: 做什么操作
- SIG_BLOCK: 将自定义set中的信号添加到mask
- SIG_UNBLOCK: 将自set中的信号从mask中删除
- SIG_SETMASK: 用自定义set替换mask
set: 用户定义的信号集。
oldset: 传出参数,在被设置前的信号屏蔽字

@return:
sucessful: 0
failure: -1, set errno

查看未决信号集

可以用sigpending 函数查看PCB中未决信号的状态

函数原型

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

参数说明

1
2
3
4
5
6
@param:
set: 传出参数,将未决信号集传出到set中

@return:
sucessful: 0
failure: -1, set errno

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>


/** 打印未决信号集 */
void print_set(sigset_t *set);

int main(int argc, char* argv[])
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);

int ret = 0;

ret = sigprocmask(SIG_SETMASK, &set, NULL);
if(ret == -1){
perror("sigprocmask error");
}

while(1){
ret = sigpending(&set);
if(ret == -1){
perror("sigpending error");
}
print_set(&set);
}

return 0;
}

void print_set(sigset_t *set){
for(int i = 1; i <= 32; i++) {
if(sigismember(set, i))
putchar('1');
else
putchar('0');
}

printf("\n");
}

信号捕捉

信号处理程序的调用方式如下图

signal函数

signal函数用于注册一个信号的捕捉函数,它由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。

函数原型

1
2
3
4
#include <signal.h>
typedef void (*sighandler_t)(int);
// 当signum信号递达时,由handler函数处理
sighandler_t signal(int signum, sighandler_t handler);

sigaction函数

修改信号处理动作,用来注册一个信号的捕捉函数

函数原型

1
2
3
4
5
6
7
8
9
10
11
#include <signal.h>

struct sigaction {
void (*sa_handler)(int); // 默认使用的回调函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 当sa_flag被设置成SA_INFO时使用这个类型的回调函数
sigset_t sa_mask; // 在这个集合的信号会在执行回调函数期间被屏蔽, 防止多级中断,传空集会自动屏蔽signum信号
int sa_flags; // 用来决定sigaction函数的行为,一般传0, 设置了SA_NODEFER就不会自动屏蔽signum信号
void (*sa_restorer)(void);
};
int sigaction(int signum, const struct sigaction *restrict act,
struct sigaction *restrict oldact);

参数说明

1
2
3
4
5
6
7
8
@param:
signum: 信号编号
act: 不为NULL时signum信号的处理动作被设置成act
oldact:不为NULL时signum信号的上一个处理动作被传出到oldact中

@return:
sucessful: 0
failure: -1, set errno

e.g. 简单的使用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>


void sig_foo(int signo){ //运行期间屏蔽signo|SIGQUIT
printf("catch you!! %d\n", signo);
sleep(5);
}

int main(int argc, char* argv[])
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGQUIT);
sigprocmask(SIG_SETMASK, &set, NULL); // 屏蔽Ctrl+"\"

struct sigaction act, oldact;
act.sa_handler = sig_foo; // 设置信号递达后的回调函数
sigemptyset(&act.sa_mask); // 使sig_foo被调用时要屏蔽的信号为空集,但此时也会屏蔽被捕捉的信号
act.sa_flags = 0;

int ret = sigaction(SIGINT, &act, &oldact);
if(ret == -1){
perror("sigaction error");
exit(EXIT_FAILURE);
}
ret = sigaction(SIGBUS, &act, &oldact);
if(ret == -1){
perror("sigaction error");
exit(EXIT_FAILURE);
}

while(1);
return 0;
}

信号捕捉特性

  1. 程序正常运行时,PCB中有一个信号屏蔽集,当注册了某个捕捉函数,且捕捉到了该信号后,此时会有一个临时的屏蔽集sa_mask,在函数回调过程中,信号的屏蔽集为临时的屏蔽集和原来的屏蔽集的并集,函数调用结束后,再恢复到原来的屏蔽集。
  2. xxx信号捕捉函数执行期间,xxx信号被自动屏蔽.
  3. 阻塞的常规信号不支持排队,产生多次只记录一次.后32个实时信号支持排队

使用信号回收子进程

SIGCHLD信号的产生条件:

  • 子进程终止时
  • 子进程接受到SIGSTOP信号停止时
  • 子进程处于停止态,接受到SIGCONT后唤醒时

e.g. 借助SIGCHLD信号回收子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

#define PRO_POOL 5 //进程池的数量
int child_end = 0; // 子进程死亡的数量

void catch_child(int signum){
int wpid = 0;
while(( wpid = wait(NULL) ) != -1) //循环回收,防止多个子进程同时白给,只能回收一个的情况
{
++child_end;
printf("[%d] catch the %d child\n", signum, wpid);
}
}

int main(int argc, char* argv[])
{
pid_t pid = 0;
int i = 0;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL); // 提前阻塞SIGCHILD信号,防止子进程在父进程注册回调函数之前死亡

for(;i < PRO_POOL; ++i)
if(!(pid = fork())) break;

if(PRO_POOL == i){
struct sigaction act = {0};
act.sa_handler = catch_child;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);

sigprocmask(SIG_UNBLOCK, &set, NULL); // 开始捕获SIGCHLD信号

printf("I'm parent %d\n", getpid());
while(child_end != PRO_POOL); //回收完所有子进程,父进程结束
child_end ^= child_end;

}else{
printf("\tI'm child %d\n", getpid());
sleep(5);
printf("\tover ....\n");
}

return 0;
}