1. 什么是信号
1.1 信号的特点
- 简单
- 不能携带大量信息
- 满足某个特设条件才会给进程发送信号
- 信号是通过软件方法实现的(时钟中断是通过硬件实现的),其实现手段导致信号有很强的延时性,但对用户来说很难感知(软中断)
- 所有进程收到的信号,都是由内核负责发送的,也由内核处理。
- 进程A给进程B发信号其实本质是进程A驱使内核给进程B发信号。
补充:软中断不如硬中断靠谱与及时,硬中断能精确控制中断的时间,但是软中断可能会有延时。因为软件运行时间有随机性
1.2 与信号相关的事件和状态
- 产生信号的事件
- 按键产生:如ctrl+c,ctrl+z
- 系统调用产生:如kill、raise、abort
- 软件产生:如定时器alarm
- 硬件异常产生:如非法访问内存、除0、内存对其出错
- 命令产生:kill
- 递达状态:
- 信号已经被进程接收时的状态。
- 未决状态:
- 信号从产生到递达之间的状态
- 阻塞状态:
- 如果没有阻塞状态,未决状态几乎立马就会变成递达状态
- 因为有了阻塞状态,会阻止未决状态变为递达状态。
- 信号处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用用户处理函数)
- 未决信号集(pending)
- 是BItSet类型(位图)
- 信号存在则将相应位置为1,不存在则置为0
- 阻塞信号集(block)
- 是BitSet类型(位图)
- 信号被阻塞就将相应位置为1,否则就置为0.
1.3 信号4要素
- 编号(1~64)
- 名称
- 事件
- 默认处理动作
- Term:终止进程
- Ign:忽略信号
- Core:终止进程,生成Core文件(可以用于检查死亡原因,用于gdb调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
1.4 信号相关补充
- 查看linux中的信号
man -7 signal
kill -l
- 9号信号(SIGKILL)和19号信号(SIGSTOP)不允许捕捉和忽略,只能执行默认动作。
2. 信号的产生
2.1 终端按键产生信号
- ctrl+c:产生2号信号(SIGINT) ,中断进程
- ctrl+z:产生成20号信号(SINTSTP),暂停与终端交互进程的运行(后台挂起)
- ctrl+\: 产生3号信号(SINQUIT),退出进程
2.2 硬件异常产生的信号
- 除0错误:产生8号信号(SIGFPE),浮点错误
- 非法访问内存:产生11号信号(SIGEGV),段错误
- 总线错误:产生7号信号(SIGBUS)
2.3 kill函数/命令产生信号
- 命令模式
# kill 信号 进程pid
kill -19 19665
- kill函数
int kill(pid_t pid, int sig);
- 参数pid
- pid>0:表示进程的id,发送信号给指定进程
- pid=0:发送信号给与调用kill进程属于同一个进程组的所有进程
- pid<0:取|pid|,发给对应的进程组
- pid=-1:发送给当前进程有权限发送的系统中的所有进程
- 补充
- 进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组的id与进程组长的id相同。
- 权限保护:super用户可以向任意用户发送信号,而不普通用户不能向系统用户发送信号。普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。
2.4 raise函数和abort函数产生信号
- raise函数:自己给自己发信号
int raise(int sig);
- abort函数:给自己发异常终止信号(6,SIGABRT)
void abort(void);
2.5 软件条件产生信号
- alarm函数:指定时间后内核给当前进程发14号信号(SIGALRM,默认动作终止进程)
unsigned int alarm(unsigned int seconds);
- 每一个进程有且仅有一个定时器
- 定时器的计时与进程所处状态无关。
- 返回值:上一次定时没有定时完的剩余时间
- setitimer函数:也是定时器,更精确,可以周期性定时
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 返回值:是否成功
- which:
- ITIMER_REAL:自然定时,计算自然时间,会发送14号信号(SIGLARM)
- ITIMER_VIRTUAL:虚拟空间计时(用户空间),只计算进程占用CPU时间,会发送26号信号(SIGVTALRM)
- ITIMER_PROF:运行时计时(用户+内核),计算占用cpu以及执行系统调用的时间,会发送27号信号(SIGPROF)
- new_value:传入参数,结构体
- old_value:传出参数,结构体
3. 信号相关的操作
3.1 信号集相关操作
- 将某个信号集全部清0
int sigemptyset(sigset_t *set);
返回值:0代表成功,-1代表失败
set:本质是位图,实际是无符号长整型
- 将某个信号集全部置为1
int sigfillset(sigset_t *set);
- 将某个信号加入信号集
int sigaddset(sigset_t *set, int signum);
- 将某个信号清出信号集
int sigdelset(sigset_t *set, int signum);
- 判断某个信号是否在信号集中
int sigismember(const sigset_t *set, int signum);
- 返回值
- 0:不在
- 1:在
- -1:出错
3.2 sigprocmask函数
- 用来屏蔽信号、接触屏蔽。
- 屏蔽信号是将信号处理延后执行;而忽略表示对信号的处理为丢弃
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 返回值:是否成功
- how:有三个取值(假设当前的信号屏蔽字为mask)
- SIG_BLOCK:表示set参数代表需要屏蔽的信号。相当于mask=mask|set
- SIG_UNBLOCK:表示set参数代表需要解除屏蔽的信号
- SIG_SETMASK:表示用set参数替换原先的mask
- set:传入参数,是个位图。set中哪一位为1就表示当前进程要屏蔽哪个信号
- oldset:传出参数,保存旧的信号屏蔽集。
3.3 sigpending函数
- 读取当前进程的未决信号集
int sigpending(sigset_t *set);
- 返回值:是否成功
- set:传出参数
3.4 signal函数:给信号注册捕捉函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 定义一个函数指针,返回值类型为void,参数类型为int
- 内核会调用自定义动作。
- 该函数不推荐使用
3.5 sigaction函数:linux中给信号注册一个捕捉函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 返回值:是否成功
- act:传入参数
- oldack:传出参数
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler:指定信号捕捉后的处理函数名
- sa_mask:CPU在执行信号处理函数期间,所要屏蔽的信号集合
- sa_flags:通常设置为0,表使用默认属性。
- 0:表示 表使用默认属性
- SA_SIGINFO:选用sa_sigaction来指定捕捉函数
- SA_INTERRURT:慢系统调用被信号中断后,不重启。
- SA_RESTART:慢系统调用被信号中断后,重启慢系统调用
- SA_DEFER:不自动屏蔽本信号。
- sa_sigaction:指定带参数的信号捕捉函数。
3.6 总结:内核实现信号捕捉过程
- 进程A在执行控制流程的某条指令时,收到信号,要进行中断、异常处理或系统调用进入内核
- 内核处理完异常,准备回用户模式之前,先处理当前进程中可以递送的信号。
- 如果信号的处理动作是自定义信号处理函数,则回到用户态执行信号处理函数(但不是回到主控制流程)
- 信号处理函数返回时会执行特殊的系统调用再次进入内核,
- 内核再返回到用户模式,回到从主控制流程中上次被中断的地方继续执行。
4. 竞态条件
4.1pause函数:进程主动挂起自己
- 调用该函数可以造成进程主动挂起自己,等待信号唤醒。
- 调用该系统调用后,进程将处于阻塞状态,直到有信号递达将其唤醒才会有返回值。
int pause(void);
- 返回值: -1,并设置errno为EINTR
- 如果信号的默认处理动作是终止进程,则进程终止,pause函数有机会返回。
- 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
- 如果信号的处理动作是捕捉,则调用完信号处理函数后,pause返回-1。并将errno设置为EINTR,表示“被信号中断”
- pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒
4.2 时序问题小案例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/*所有信号处理函数的原型,都类此,
*无返回值(void),只有一个参数,表示信号编号*/
void sig_alrm(int signo)
{
/*用来占位,可以不做任何事,但这个函数存在
*SIGALRM信号
*就不执行默认动作终止进程,而做其它事情*/
}
unsigned int mysleep(unsigned int sec)
{
struct sigaction act, old;
unsigned int unslept; //保存未休眠够的时间
act.sa_handler = sig_alrm;
sigemptyset(&act.sa_mask); //清空
act.sa_flags = 0;
sigaction(SIGALRM, &act, &old); //注册信号处理函数sig_alrm
//同时要保存旧的处理方式
alarm(sec); //设置sec秒闹钟
/*如果在此时,进程失去cpu控制权,然后5秒钟也过去了,就会给进程发一个信号,
*等之后再获得cpu也会先执行信号处理。则就会产生严重的bug,被pause的进程
*永远不会被唤醒。
*/
pause(); //进程阻塞,收到一个信号后,pause返回-1,解除阻塞
unslept = alarm(0); //取消旧的定时器,将剩余时间保存
sigaction(SIGALRM, &old, NULL); //恢复SIGALRM信号原来的处理方式
return unslept;
}
int main(void)
{
while(1){
mysleep(5);
printf("Five seconds passed\n");
}
return 0;
}
- 如何解决时序问题带来的bug
4.3 sigsuspend函数
- 对于时序要求严格的场合一般用sigsuspend函数代替pause
int sigsuspend(const sigset_t *mask);
- 功能:也是挂起进程,等待信号。
- 参数mask:在执行这个语句的期间,进程的屏蔽字集合由mast决定。执行完这个语句之后,进程的屏蔽字再换回进程原本的屏蔽字集合。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
/* nothing to do */
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
//1.为SIGALRM设置捕捉函数,一个空函数
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//2.设置阻塞信号集,阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask); //信号屏蔽字 mask
//3.定时n秒,到时后可以产生SIGALRM信号
alarm(nsecs);
/*4.构造一个调用sigsuspend临时有效的阻塞信号集,
* 在临时阻塞信号集里解除SIGALRM的阻塞*/
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
/*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
* 这个信号集中不包含SIGALRM信号,同时挂起等待,
* 当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
sigsuspend(&suspmask);
unslept = alarm(0);
//6.恢复SIGALRM原有的处理动作,呼应前面注释1
sigaction(SIGALRM, &oldact, NULL);
//7.解除对SIGALRM的阻塞,呼应前面注释2
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}
int main(void)
{
while(1){
mysleep(2);
printf("Two seconds passed\n");
}
return 0;
}