信号更多的是通知事件的发生,信号产生之后第一时间也不是直接处理,而是先存储下来,处理信号
时会打断当前进程正在执行的工作,然后去准备处理事件,事件处理完毕之后进程会回到原先运行的位置继续运行。
Linux下有62种信号,可以使用 kill -l 查看
分类
- 1-31不可靠信号(非实时信号)
- 34-64可靠信号(实时信号)
注:在书写程序时,建议书写信号宏名,便于跨平台
信号的生命周期: 信号的产生->信号注册(修改信号集)->(信号阻塞)->信号注销->信号处理
信号的产生
1、 通过硬件中断产生
例如: CTRL + C
2、程序异常
SIGFPE(数据运算错误)
SIGSEGV(内存错误)
3、软件函数
kill函数
int kill(int pid,int sig); //向指定进程发送指定信号
pid //进程id
sig //信号
//命令
kill pid // 默认向一个进程发送SIGTERM(15)--终止信号
kill -15 pid //向进程发送15信号
raise函数
int raise(int sig); //给当前进程发送指定的信号(向自身发送信号)
abort函数
void abort(void); //向自身发送SIGABRT信号
alarm函数 ——定时器
unsigned int alarm(unsigned int seconds); //设置一个定时器,在n秒后给当前进程发送SIGALRM信号(该信号的默认处理动作是终止当前进程)。同时取消上一次的定时器,并且返回上一个定时器的剩余时间
seconds //n秒
alarm(0) //取消上一次定时器,并且返回上一个定时器剩余时间
sigqueue函数
int sigqueue(pid_t pid, int sig, union sigval)
pid //进程ID
sig //信号编号
sigvl //参数
//这个函数不仅可以发送信号,还可以顺便携带一个参数
信号注册
信号注册:给一个进程发送信号,就是修改这个进程pcb中关于信号的pending位图,将其相应的信号位置1。
信号阻塞
暂时不处理信号(阻止信号的递达),并不是不接收信号,即阻止信号的递达。
信号的递达: 信号的处理
要阻塞一个信号,就是将pcb中关于信号的block位图,将相应的信号位置1。
注: 阻塞信号集(block)与未决信号集(pending)分别具有一个集合,这两个集合都是位图,当某个信号阻塞 / 未决 时,将位图相应的位置置1。
信号未决: 这是一种状态,指的是信号从注册成功到信号递达的之间的一种状态。
对信号集操作的一些函数
int sigprocmask(int how, const sigest_t *set, sigset_t *old-set); //阻塞信号/解除阻塞
how
SIG_BLOCK //阻塞
SIG_UNBLOCK //解除阻塞
SIG_SETMASK //设置阻塞集合当中的信号
set //阻塞或解除阻塞的集合 从block中加入/剔除
oldset //保存原先阻塞集合中的信号
int sigpending(sigset_t *set); //获取未决(未被处理)信号
int sigemptyset(sigset_t *set); //清空一个信号集合
int sigfillset(sigset_t *set); //将所有的信号都添加到set中(置1)
int sigaddset(sigset_t *set, int signum); //添加指定的单个信号到set集合中
int sigdelset(sigset_t *set, int signum); //从集合移除一个指定的信号
int sigismember(const sigset_t *set, int signum); //判断一个信号是否在set集合中
信号处理
从pending集合中将即将要处理的信号相应位置0(从pcb的pending集合中移除)。
- 非可靠信号注册(1-31)就是将相应pending位图置1,然后添加一个sigqueue结构到链表,之后如果有相同信号到来时,位图已经置1,就不做操作了,意味着后来的信号在前一个信号处理之前不会重复注册,代表丢了。
- 可靠信号注册(34-64)不管有没有注册都要置1,并且添加结点到链表中,所以不会丢信号。
- 非可靠注销就是删除链表结点,相应位图置1。
- 可靠信号注销就是删除结点,判断链表中是否有相同信号结点,如果没有位图置0,如果有就不置0。
信号的处理
处理方式
1、默认操作 按照操作系统中对信号事件的既定处理方式
2、忽略操作 直接将信号丢掉(不注册)
3、自定义处理 用户自己定义事件处理的方式
自定义型号处理方式接口函数
01、signal函数
typedef void (*sighandler_t)(int); //int signo
sighandler_t signal(int signum, sighandler_t handler); //指定改变某一个信号的处理方式
signum //信号编号
handler //函数指针,处理方式
//SIG_IGN //忽略信号(宏)
//SIG_DFL //默认
02、sigaction
int sigaction(int signum,const struct sigaction *act,struct sigaction *odlact)
signum //信号编号
act //新的处理方式
odlact //保存原有的处理方式
struct sigaction
{
//回调函数指针
void (*sa_handler)(int); //函数指针 处理函数
void (*sa_sigactionr)(int, siginfo_t *, void*); //函数指针 处理函数
sigset_t sa_mask; //位图 信号集合 在处理信号时可以通过这个mask暂时阻塞一些信号,处理完毕后会还原回去
int sa_flags; //决定了我们使用那个回调接口,并且还有一些其他选项信息
//SA_SIGINFO //使用sa_sigaction
//否则使用sa_handler
void (*sa_restorer)(void);
};
信号的捕捉流程——主要是针对信号的自定义处理方式
信号并不是立即处理的,而是选择一个合适的时机,合适的时机就是当前进程从内核态切换到用户态的时候。
信号的捕捉是当我们发起系统调用/程序异常/程序中断时,当前程序从用户态运行切换到内核态,去处理这些事情,处理完毕之后,要从内核态返回用户态,但是在返回之前会看一下是否有信号需要处理,如果有,就处理信号(切换用户态执行信号的自定义处理方式),处理完毕之后,再次返回内核态,如果没有信号要处理就调用sys_sugreturn返回用户态(我们程序之前运行的位置)。
可重入函数/不可重入函数
可重入函数: 不管怎么调用,都不会对函数内部功能/程序逻辑造成影响,称为可重入函数。
不可重入函数: 如果函数在不同的地方/时序进行调用,会对函数的功能逻辑造成影响,称为不可重入函数(两个进程在不同时间对同一个函数中的变量修改,可能造成程序紊乱)。
不可重入函数的一些要点(什么函数是不可重入函数)
- 内部包含对全局性变量的修改操作
- 函数传参的参数和其余位置共同使用同一变量
因为这些对全局变量的操作不是原子性的,因此这些修改操作有可能同时在不同地方进行修改(被打断)。
一个函数是否可以重入
- 是否对全局性数据进行修改操作
- 操作是否是原子性
SIGCHLD信号
僵尸进程的避免
操作系统如何通知父进程子进程退出——SIGCHLD信号
以前为了避免产生僵尸进程,只能让父进程一直等待子进程退出(不知道子进程什么时候退出),浪费了父进程的资源,现在,自定义信号SIGCHLD的处理方式,相当于提前告诉进程,当接收到这个信号的时候使用waitpid,这样就不用一直等。