信号
认识
什么是信号?
信号:软件(进程)中断,通知事件的发生。
信号有不同的种类,每个信号对应不同事件。
-
信号的生命周期:从产生,到处理。
-
Linux下信号的种类:(62种)
1~31
号继承于Unix
系统,每个信号都有各自对应的事件。(非可靠信号 / 非实时信号)。
34~64
后续添加的信号,没有特定事件。(可靠信号 / 实时信号)。
非可靠 / 可靠:信号有可能会
丢失
/ 有多少信号就要处理多少次
实时 / 非实时:信号立刻 / 选择合适时机处理
。
指令kill -l
:查看所有信号信息
生命周期
- 产生
- 注册(到进程中)
- 注销
- 处理
- (阻塞)暂时不处理信号。
信号的产生
硬件产生
组合键(通过硬件产生软件信号)
Ctrl+c
(2号SIGINT
中断信号)Ctrl+|
(3号SIGQUIT
退出信号)Ctrl+z
(19号SIGTSTOP
停止信号)
软件产生
kill -<signum> [pid]
命令
- 主动:函数
kill()
(man 2)
int kill(pid_t pid,int sig);
给指定进程发送指定信号,使用宏定义数字,如果要使用英文缩写要包含头文件<signal.h>
。
宏定义数字查看:文件处于/usr/include/bits/signum.h
中
kill
实际上是15
号信号SIGTERM
。
kill -9
实际上是9
号信号SIGKILL
。
raise()
int raise(int sig);
发送给调用进程/线程(自己)发送指定信号。(使用函数需要包含头文件<signal.h>
)
abort()
void abort(void);
相当于调用了raise
接口,给调用进程发送SIGABRT
信号退出(使用函数需要包含头文件<stdlib.h>
)
alarm()
unsigned int alarm(unsigned int seconds);
定时器,seconds
秒之后给调用进程发送SIGALRM
信号,返回上一个定时器剩余的时间或0
(如果没有定时器)。
alarm(0)
:参数设0
表示取消定时器,并且返回剩余时间。
- 被动:程序异常
core dump
:程序异常退出时保存程序运行信息,用于事后调试,默认关闭(因为占用磁盘资源,安全性考虑(例如对段错误的数据进行分析获得用户密码))。
ulimit -a
:查看core dump
是否开启。
ulimit -c + [num]
设置核心转储文件大小,开启核心转储(num
设置为unlimited
表示不限制大小)
核心转储文件core.
之后的数字意为pid
。
gdb
加载程序进行调试流程:
gdb + [.c程序名]
core-file [corepid]
bt
查看调用栈
执行结束即可查看到在那个接口发生的错误。
kill()
只负责发送信号,进程在处理这个信号时,kill
还没有返回,函数调用栈就到此为止了。错误并不是函数产生的,而是在函数期间收到异常信号而退出的。
信号的注册
这里讲的注册是信号在进程中的注册。
sigset_t
结构体:
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct {
unsigned long int __val[_SIGSET_NWORDS];
}__sigset_t;
其中是一个数组,对于一个进程来说,判断是否收到了某个信号,就代表进程有没有在数组中做标记。
实际上是在进程的PCB
中注册标记,需要待处理。这些标记在这个结构体中记录。通过位图实现。
信号的种类有62
类,按数字来标记的话是0~64
,在信号的集合__sigset_t
中添加一个信号,实际上是将这个信号的编号所定义的位进行置1
操作。
这个数组的长度为1024 * (8 * sizeof(unsigned long int))
,再加上这个数据的类型unsigned long int
,应该是8*1024/8*8
位,最终计算下来有1024
个比特位。
-
非可靠信号的注册:
判断pending
(信号集合位图)相应位是否为1
,若为0
,为信号组织sigqueue
节点添加到链表中,并且pending
位图置1
。若为1
,说明信号已经被注册过,还没有被处理,第二个相同的信号等于被丢弃。 -
可靠信号的注册:
不管位图是否为1
,组织节点,添加到链表中,并且位图置1
,信号没有被丢弃。
信号的注销
非可靠信号(1~31
号):因为非可靠信号的信号结点只有一个,因此删除结点,位图直接置0
。
可靠信号(34~64
号):因为可靠信号的结点有可能会有多个,若还有相同信号结点,则位图依然置1
,否则置0
。
信号的处理
signal(int signum, sighandler_t handler); //回调函数,修改信号的处理方式
- 默认处理:
SIG_DFL
(default) - 忽略处理:
SIG_IGN
(ignore)
如果形如signal(2,SIG_IGN);
,运行后不能通过2
号信号停止,但可以通过3
号信号进行停止。 - 自定义处理
信号的捕捉
signal接口
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
:信号编号handler
:函数指针
使用handler
函数替换signum
信号的处理方式。
信号自定义处理方式的捕捉流程:
- 在用户态因为系统调用,或中断,或异常,进入内核运行。
- 当完成内核功能,准备返回用户态主控流程前,先检测是否有信号待处理。
- 如果现在有一个信号未处理,并且是自定义处理方式的信号,此时调用
do_signal()
,返回用户态,找到自定制处理函数(此时为sigcb
函数),去处理这个信号。
(默认或者忽略的处理方式在内核态直接完成) - 事件处理完毕,通过特殊的系统调用
sigreturn()
返回内核态,此时信号处理完毕,准备从内核态返回用户态,判断没有未处理信号了,通过sys_sigreturn()
从主控制流程中上次被打断的位置继续向下运行。
怎么从用户态切换到内核态?
系统调用
、中断
、异常
。
sigaction接口
使用act
动作替换signum
原有的处理动作,并且将原有处理动作拷贝到oldact
中。
int sigaction(
int signum,
const struct sigaction *act,
struct sigaction *oldact
);
signum
:编号。act
:动作,用户自己定义一个动作。oldact
:旧动作。
结构体:
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t *,void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
sigemptyset接口
int sigemptyset(sigset_t *set);
清空一个信号集合。
信号的阻塞
阻塞:暂时阻止信号被递达,信号依然可以注册,但是暂时不处理,解除阻塞之后才会处理。
- 信号的递达:(动作)信号的处理。
- 信号的未决:(状态)信号的产生到处理之前,所处的状态。
pending
未决信号集合,每一个信号到来之后,都会在pending
位图中修改本信号的位,添加本信号结点。blocked
阻塞信号集合。handler
动作函数数组。
信号的阻塞过程实际就是在PCB
的blocked
信号阻塞集合中标记哪些信号到来之后暂时不处理,将blocked
位图集合中对应的位置1
,进行阻塞这个信号。
【注意】:
9号信号SIGKILL
与19号信号SIGSTOP
无法被忽略,无法被阻塞,用户也无法对它们进行自定义处理方式。
sigprocmask接口
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 阻塞
set
集合中的信号,将原来阻塞的信号保存到oldset
中,不关心则置NULL
。 - 返回值:成功返回
0
,失败返回-1
。 how
的取值:
SIG_BLOCK
:阻塞 set 中的信号。
计算方式:mask = mask | set
SIG_UNBLOCK
:对 set 中的信号解除阻塞。
计算方式:mask = mask & (~set)
SIG_SETMASK
:将 set 中的信号设置为阻塞信号。
计算方式:mask = set
sigfillset接口
int sigfillset(sigset_t *set);
将所有信号添加到set
集合中。
sigaddset接口
int sigaddset(sigset_t *set,int signum);
将指定信号signum
添加到set
集合中。
总结:
sigpromask
:阻塞 / 解除阻塞信号
sigemptyset
: 清空信号集合
sigfillset
: 向集合中添加所有信号
sigaddset
: 向集合中添加指定信号
sigismember
:判断信号信号是否在集合中sigdelset
:从集合中移除执行信号sigpending
:获取当前进程的未决信号
竞态条件
因为运行时序而造成数据竞争,导致数据二义性。
函数中所完成的操作并非原子性操作,并且操作的数据是一个全局数据。
- 不可重入函数:
如果一个函数操作了全局性数据,并且这个操作不是原子性操作,且这个操作不受保护,则这个函数是一个不可重入函数。
- 不可重入函数:不可在多个时序的运行中重复调用,重复调用有可能会造成数据二义性,数据混乱。
- 可重入函数:在多个时序运行中重复调用,不会造成异常影响,多数为数据二义性问题。
【注】不可重入函数:malloc
/ free
SIGCHLD信号
子进程退出,系统通过SIGCHLD
信号通知父进程,但是此信号的默认处理方式是忽略处理,导致子进程成为僵尸进程。
因此用户选择自定义SIGCHLD
信号处理方式:在回调函数中执行waitpid
等待子进程退出。为了将同一时间退出的子进程全部处理,防止事件丢失。