一、信号介绍
什么是中断:
- 当进程接收到消息后中止当前正在执行的任务,转而执行其它任务,等待其它任务执行完毕后再返回继续执行。这种执行模式称为中断,分为硬件中断和软件中断两种
什么是信号:
-
信号是UNIX、类UNIX以及其他POSIX兼容的系统中,为了完成不同进程之间通讯的一种方式。是一种软中断,是一种异步处理机制,用于提醒进程某个事件发生了,可能要去处理。
-
当一个信号发送给一个进程,操作系统就会中断该进程的正常控制流程,如果中断前预先设置过如何处理该信号(绑定过该信号的信号处理函数),那么中断后会执行该函数,执行完返回中断点继续执行,否则就按信号的默认处理方式进行
# 执行 kill -l 查看当前系统可以产生的所有信号
常见的信号:
SIGINT(2) Ctrl+c 终止
SIGQUIT(3) Ctrl+\ 终止+core
SIGFPE(8) 除0 终止+core
SIGSEGV(11) 段错误 终止+core
SIGTSTP(20) Ctrl+z 暂停
SIGCONT(18) fg 继续
不可靠信号与可靠信号
不可靠信号:
-
建立在早期信号处理机制上的信号被称为不可靠信号(1-31)
-
非实时信号,不支持排队机制,可能会丢失信号,同一个信号产生多次,进程可能只收到一次
-
当进程收到这类信号,执行了第一次用户设置的信号处理函数后,会还原会默认的处理方式。
可靠信号:
-
位于(34-64)的信号是可靠信号
-
实时信号,支持排队机制,信号只要产生就必定会被处理,因此不会丢失
信号的来源:
-
硬件异常:由硬件设备产生的信号,例如除零、非法访问内存、总线错误、未定义指令
-
软件异常:命令kill\函数产生信号
信号处理的默认动作:
-
忽略
-
终止进程
-
终止并产生core文件
-
捕获并处理
二、捕获信号
#include <signal.h>
// 信号处理函数的格式
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:向内核注册一个信号和它的信号处理函数,相当于捕获该信号(绑定信号)
signum:信号码(可以写整数值也可以用宏名)
-
在某些UNIX系统中,通过signal注册信号处理函数后,只有一次有效,后面会变回默认处理方式,为了在这种系统中得到持久的信号绑定,可以在信号处理函数的末尾再次通过signal重新注册一次
-
可以通过命令 kill 信号码 进程号 给该进程发送信号
练习:测试一下哪些信号不可以被捕获处理
9和19号信号不能被忽略也不能被不能被捕获
-
普通用户只能给自己的进程发送信号,只有root用户可以给任何进程发送信号
-
当信号处理完信号处理函数后是会回到产生信号的代码位置继续执行,如果我们捕获并处理的是段错误\除零这种信号,就会产生死循环,因为这些错误并没有因为捕获了信号就消失,而是一直存在并产生信号,正确做法是在他们的信号处理函数中进行数据保存然后直接结束程序。exit()
三、发送信号的方式
键盘:
-
给当前终端控制下的活跃进程发送信号
- Ctrl+c Ctrl+\ Ctrl+z fg
命令:
kill <-信号码> 进程号
killall <-信号码> 进程名
给所有的同名的进程发送信号
事件:
- 当程序执行了某种非法操作,被操作系统发现了,操作系统会给进程发送对应的信号,例如段错误、除零、总线错误、错误指令等。
函数:
int kill(pid_t pid, int sig);
功能:给指定的进程发送信号
pid:给进程号为pid的进程发送
pid > 0 给pid号进程发送信号sig
pid = 0 给同组的所有进程发送信号sig
pid = -1 给所有进程发送信号sig,前提是有向该进程发送信号的权限
pid < -1 向进程组id等于pid的绝对值的所有进程发送信号
int raise(int sig);
功能:给调用进程自己发送信号
void abort(void);
功能:给调用进程自己发送信号SIGABRT(6)
unsigned int alarm(unsigned int seconds);
功能:让内核在seconds秒后,向调用进程发送SIGALRM(14)信号
返回值:上一次alarm剩余的时间
如果是正常走完时间返回0,如果时间还没走完,又再次调用alarm后,会覆盖之前的时间重新计时,并不会产生多个闹钟信号
四、暂停和休眠
int pause(void);
功能:让调用进程进入暂停态执行,进入睡眠状态,直到有信号终止进程或者有信号被捕获,会唤醒并执行信号处理函数,继续后面的执行流程。类似于不限时的sleep
返回值:要么一直睡眠不返回,睡醒返回-1
unsigned int sleep(unsigned int seconds);
功能:让调用进程睡眠seconds秒,除非有信号终止进程或者有信号被捕获,也会唤醒
返回值:剩余的睡眠秒数
int usleep(useconds_t usec)
功能:让调用进程睡眠usec微秒,除非有信号终止进程或者有信号被捕获,也会唤醒
返回值:剩余的睡眠秒数
五、信号集与信号屏蔽
什么是信号集:
-
是一种专门用于存储多个信号的数据类型 sigset_t
-
该类型占128字节,每个字节代表了一种信号的有或无
操作信号集的相关函数:
int sigemptyset(sigset_t *set);
功能:将信号集set中的所有信号置零 清空信号集
int sigfillset(sigset_t *set);
功能:把信号集set中所有信号置1
int sigaddset(sigset_t *set, int signum);
功能:将信号集set中的信号signum置1
int sigdelset(sigset_t *set, int signum);
功能:将信号集set中的信号signum置0
int sigismember(const sigset_t *set, int signum);
功能:测试信号集中是否存在signum信号
返回值:存在返回1, 不存在返回0 非法信号返回-1
#include <stdio.h>
#include <signal.h>
int main(int argc,const char* argv[])
{
sigset_t set;
sigfillset(&set);
sigemptyset(&set);
sigaddset(&set,2);
sigaddset(&set,7);
for(int i=1; i<=128; i++)
{
printf("信号%d 状态:%d\n",i,sigismember(&set,i));
}
}
信号的递送与未决:
-
当信号产生后,系统内核会在其内部维护的进程表中,给响应信号的进程设置一个对应的标志位,着整个过程称为信号的递送
-
在信号产生到完成递送之间会存在一段时间间隔,处于这个时间间隔的信号状态是"未决"
信号屏蔽:
-
每个进程都有用一个信号掩码(signal mask,就是一个信号集),其中存在的信号是需要被该进程屏蔽的信号
-
让需要屏蔽的信号处于"未决状态",当可以接收信号时,让其退出未决状态,完成递送
-
当执行一些特殊的且不想被干扰中断的操作时,例如:更新数据库敏感操作,此时可以把信号放入信号屏蔽集中,等操作完成后,再从信号屏蔽集中删除,继续处理信号,能保证敏感操作的安全性
// 信号屏蔽集的操作函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:修改当前进程的信号掩码(屏蔽集)
how:修改信号掩码的方式:
SIG_BLOCK 将set中的信号加入到信号掩码中
SIG_UNBLOCK 从信号掩码中把set中的信号删除
SIG_SETMASK 把set中的信号替换掉信号掩码的所有信号
set:信号集 用于设置
oldsel:信号集 用于获取旧信号集 NULL则不获取
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint(int num)
{
printf("按下了Ctrl+C\n");
}
void sigrtmin(int num)
{
printf("我可靠!\n");
}
int main(int argc,const char* argv[])
{
signal(SIGINT,sigint);
signal(34,sigrtmin);
sigset_t set,old_set;
sigemptyset(&set);
// 给信号集添加信号
sigaddset(&set,SIGINT);
sigaddset(&set,34);
// 设置信号屏蔽
sigprocmask(SIG_BLOCK,&set,&old_set);
printf("我是进程%u\n",getpid());
sleep(15);
printf("我醒了,解除屏蔽!\n");
// 还原屏蔽,解除屏蔽
sigprocmask(SIG_SETMASK,&old_set,NULL);
for(;;);
}
对于可靠和不可靠信号屏蔽的区别:
-
对于不可靠信号,通过信号屏蔽该信号后,在信号屏蔽期间,该信号产生多次,都只会被屏蔽第一个,只有第一个处于未决,剩余的都不参与排队直接忽略,当解除屏蔽后,只会有第一个不可靠信号被完成递送
-
相反,对于所有在屏蔽期间产生的可靠信号,都会排队变成未决,当屏蔽接触后,会按照次序全部完成递送
六、带参的信号发送与捕获
- 可以让不同进程之间发送信号时,带上一些简易数据,完成不同进程之间简单通信,这叫带参数的信号发送
#include <signal.h>
int sigaction (int signum, const struct sigaction* act,struct sigaction* oldact);
功能:设置信号的处理方案
signum:信号码
act:信号处理方式
oldact:原信号处理方式,可为NULL
struct sigaction {
void (*sa_handler)(int); // 信号处理函数指针1
void (*sa_sigaction)(int,siginfo_t*,void*); // 信号处理函数指针2 带附加数据
sigset_t sa_mask; // 信号掩码
//在信号函数函数执行过程中,默认屏蔽当前信号,如果想屏蔽其它信号可以向sa_mask中添加
int sa_flags; // 信号处理标志
void (*sa_restorer)(void); // 保留,NULL
};
sa_flags可为以下值的位或:
SA_ONESHOT/SA_RESETHAND 执行完一次信号处理后,即对此信号的处理恢复为默认,这也是老版本signal函数的缺省行为。
SA_NODEFER/SA_NOMASK 在信号处理函数的执行过程中,不屏蔽这个正在被处理的信号。
SA_NOCLDSTOP 若signum参数取SIGCHLD,则当子进程暂停时,不通知父进程。
SA_RESTART 系统调用一旦被signum参数所表示的信号中断,会自行重启。
SA_SIGINFO 使用信号处理函数指针2,通过该函数的第二个参数,提供更多信息。
typedef struct siginfo {
pid_t si_pid; // 发送信号的PID
sigval_t si_value; // 信号附加值(需要配合sigqueue函数)
...
}siginfo_t;
typedef union sigval {
int sival_int; 整数
void* sival_ptr; 指针
}sigval_t;
int sigqueue (pid_t pid, int sig,const union sigval value);
功能:向pid进程发送sig信号,附加value值(整数或指针)。
在默认情况下,在信号处理函数的执行过程中,会自动屏蔽这个正在被处理的信号,而对于其它信号则不会屏蔽,通过sigaction::sa_mask成员可以人为指定。 在信号处理函数的执行过程中,需要加入进程信号掩码中的信号,并在信号处理函数执行完之后,自动解除对这些信号的屏蔽。
// 进程A
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigint1 (int signum)
{
printf ("收到SIGINT信号!睡眠中...\n");
sleep (3);
printf ("睡醒了。\n");
}
void sigint2 (int signum, siginfo_t* si, void* pv)
{
printf ("收到发自%u进程的SIGINT信号!\n", si -> si_pid);
printf("附加的数据是:%d\n",si->si_value.sival_int);
//printf("%s\n",si->si_value.sival_ptr);
}
int main ()
{
printf("我是进程%u\n",getpid());
struct sigaction act = {};
act.sa_handler = sigint1;
act.sa_sigaction = sigint2;
act.sa_flags = SA_SIGINFO;
sigaction (SIGINT, &act, NULL);
for (;;) pause ();
return 0;
}
// 进程B 给进程A发送附带数据的信号
#include <sys/wait.h>
int main (void)
{
pid_t pid;
printf("请输入进程号:");
scanf("%u",&pid);
sigval_t sv;
sv.sival_int = 6666;
// sv.sival_ptr = "hehehe";
if (sigqueue (pid, SIGINT, sv) == -1)
{
perror ("sigqueue");
return -1;
}
return 0;
}
七、定时器
系统为每个进程维护三个定时器:
-
真实计时器:进程运行的实际时间
-
虚拟计时器:进程运行在用户态所消耗的时间
-
实用计时器:进程运行在用户态和内核态消耗的时间之和
实际真正的时间(真实计时器) = 用户态时间 + 内核时间 + 睡眠时间 + 状态切换耗时
通过设置计时器的起始时间和重复间隔时间给进程设置定时事件,例如:定时保存、定时上传操作。
获取\设置定时器:
#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
功能:获取当前进程的某个定时器的定时方案
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
功能:设置当前进程的定时方案
which:指定哪个定时器,取值:
ITIMER_REAL 真实计时器 SIGALRM(14)
ITIMER_VIRTUAL 虚拟计时器 SIGVTALRM(26)
ITIMER_PROF 实用计时器 SIGVTALRM(26)
struct itimerval {
struct timeval it_interval; // 间隔时间,定时器开启后,每间隔这个时间就会再发出一次定时器信号,一直重复
struct timeval it_value; // 开始时间,从该时间开始发送第一个定时器信号
};
// 时间结构体
struct timeval {
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒,不能超过1000000
};
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
void sigalrm(int num)
{
printf("闹钟响了!\n");
}
int main(int argc,const char* argv[])
{
signal(SIGALRM,sigalrm);
// 准备定时方案
struct itimerval timer = {
{1,500000},
{5,500000}
};
// 设置定时器
setitimer(ITIMER_REAL,&timer,NULL);
for(;;);
}