定义:
在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,由内核产生,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
信号的优先级比较高,进程收到信号之后,暂停正在处理的工作,优先处理信号,处理完成之后再继续暂停的工作。
信号的特点及状态
特点:
简单
携带的信息量少
使用在某个特定的场景中
状态:
产生
未决状态 - 没有被进程处理
递达 - 信号被处理了
产生方式包括
键盘:ctrl+c
命令:kill
系统函数:kill
软条件:定时器
硬件:段错误、除0错误
信号列表:
进程收到信号的三种处理方式
1.默认处理:
忽略信号:内核将信号丢弃,信号没有对进程产生任何影响
终止进程:进程异常终止
产生核心转储文件,同时终止文件
停止进程:暂停进程的执行
恢复之前暂停的进程继续执行
2.忽略处理:
信号来了不做任何处理
注意:不能忽略SIGKILL和SIGSTOP,保护操作系统的方式,必须能够有强制性的手段杀死进程
3.捕获并处理:
信号来了捕获信号,并执行程序员自己写的程序
注意:不能捕获SIGKILL和SIGSTOP,原因同上
信号相关函数
kill
函数原型:
#include <sgnal.h>
int kill(pid_t pid, int sig);
参数:
pid:可能选择有以下四种
pid的取值 | 所代表的意义 |
pid>0 | 发给ID为pid的进程 |
pid==0 | 发给进程组所有的进程 |
pid<-1 | 发给指定进程组的进程 |
pd==-1 | 发给所有的进程 |
sig:表示要发送的信号的编号,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为0来检验某个进程是否仍在执行。
返回值:
成功返回0,失败返回-1
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
int main(int argc, const char* argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
if(pid > 0)
{
while(1)
{
printf("parent process pid = %d\n", getpid());
sleep(1);
}
}
else if(pid == 0)
{
sleep(2);
// 杀死父亲
kill(getppid(), SIGKILL);
}
return 0;
}
raise
作用:
自己给自己发信号
函数原型:
int raise(int sig);
参数:
sig:表示要发送的信号的编号,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为0来检验某个进程是否仍在执行。
返回值:
成功返回0,失败返回-1
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
int main(int argc, const char* argv[])
{
pid_t pid = fork();
if(pid > 0)
{
//父进程回收子进程资源
int s;
wait(&s);
if(WIFSIGNALED(s))
{
printf("term by signal: %d\n", WTERMSIG(s));
}
}
else if(pid == 0)
{
//自己给自己发信号
raise(SIGQUIT);
}
return 0;
}
abort
函数原型:
void abort(void);
说明:
给自己发送异常终止信号
没有参数没有返回值,永远不会调用失败
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
int main(int argc, const char* argv[])
{
pid_t pid = fork();
if(pid > 0)
{
//父进程回收子进程资源
int s;
wait(&s);
if(WIFSIGNALED(s))
{
printf("term by signal: %d\n", WTERMSIG(s));
}
}
else if(pid == 0)
{
//自己给自己发信号
while(1)
{
abort();
}
}
return 0;
}
alarm
函数原型:
unsigned int alarm(unsigned int seconds);
参数:
定时秒数
函数说明:
设置定时器(每个进程只有一个定时器)
使用的自然定时法,不受进程状态的影响,也就是说时间一直在走
当时间到达之后, 函数发出一个信号:SIGALRM
返回值:
如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代;当参数seconds为0时,之前设置的定时器闹钟将被取消,并将剩下的时间返回
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, const char* argv[])
{
int ret = alarm(5);
printf("ret = %d\n", ret);
sleep(2);
//重新设置定时器
ret = alarm(2);
printf("ret = %d\n", ret);
while(1)
{
printf("hello\n");
sleep(1);
}
return 0;
}

setitimer
函数原型:
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};
struct timeval {
long tv_sec;
long tv_usec;
};
说明:
定时器,实现周期性定时
参数:
which为定时器类型,setitimer支持3种类型的定时器:
ITIMER_REAL: 以系统真实的时间来计算,它送出SIGALRM信号。
ITIMER_VIRTUAL: -以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。
ITIMER_PROF: 以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号。
it_interval指定间隔时间,it_value指定初始定时时间。如果只指定it_value,就是实现一次定时;如果同时指定 it_interval,则超时后,系统会重新初始化it_value为it_interval,实现重复定时;两者都清零,则会清除定时器。
tv_sec提供秒级精度,tv_usec提供微秒级精度,以值大的为先,注意1s = 1000000us。
ovalue用来保存先前的值,常设为NULL。
返回值:
和alarm函数类似
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/time.h>
#include <signal.h>
void myfunc(int sig)
{
printf("起床\n");
}
int main(int argc, const char* argv[])
{
//设置定时器
struct itimerval new_value;
//周期性定时
new_value.it_interval.tv_sec = 3;
new_value.it_interval.tv_usec = 0;
//第一次触发时间
new_value.it_value.tv_sec = 2;
new_value.it_value.tv_usec = 0;
signal(SIGALRM, myfunc);
//倒计时2s
setitimer(ITIMER_REAL, &new_value, NULL);
while(1)
{
}
return 0;
}
信号集
概念
未决信号集:
没有被当前进程处理的信号
阻塞信号集:
将某个信号放到阻塞信号集,这个信号就不会被进程处理
阻塞解除之后,信号被处理
未决信号集与阻塞信号集的关系
信号产生,信号处于未决状态,进程收到信号之后,信号被放入未决信号集
放入未决信号集中的信号等待处理,在处理之前需要做一些事情:判断阻塞信号集中该信号对应的标志位是否为1,如果为1,不处理,如果是0则处理该信号
当阻塞信号集中该信号对应的标志位为时,该信号被处理
自定义信号集相关函数
int sigemptyset(sigset_t *set); 将set集合置空
int sigfillset(sigset_t *set); 将所有信号加入set集合
int sigaddset(sigset_t *set,int signo); 将signo信号加入到set集合
int sigdelset(sigset_t *set,int signo); 从set集合中移除signo信号
int sigismember(const sigset_t *set,int signo); 判断信号是否存在
sigpending函数
作用:
读取当前进程的未决信号集
函数原型:
int sigpending(sigset_t *set)
参数:
内核将未决信号集写入set
返回值:
函数调用成功返回0,否则返回-1;
sigprocmask函数
作用:
将自定义信号集设置给阻塞信号集
函数原型:
int sigprocmask(int how, t_t *set,sigset_t *oldset);
参数:
how:用于指定信号修改的方式,可能选择有三种
SIG_BLOCK//将set所指向的信号集中包含的信号加到当前的信号掩码中。即信号掩码和set信号集进行或操作。
SIG_UNBLOCK//将set所指向的信号集中包含的信号从当前的信号掩码中删除。即信号掩码和set进行与操作。
SIG_SETMASK //将set的值设定为新的进程信号掩码。即set对信号掩码进行了赋值操作。
set:为指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL。
oldset:也是指向信号集的指针,在此存放原来的信号集。可用来检测信号掩码中存在什么信号。
返回值:
成功执行时,返回0。失败返回-1,errno被设为EINVAL。
代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/time.h>
#include <signal.h>
int main(int argc,const char* argv[])
{
//手动屏蔽信号
//自定义信号集集合
sigset_t myset;
//清空集合
sigemptyset(&myset);
//添加要阻塞的信号
sigaddset(&myset,SIGINT); //ctrl+c
sigaddset(&myset,SIGQUIT); //ctrl+反斜杠
sigaddset(&myset,SIGKILL);//反例,不能设置为阻塞的信号
//自定义集合数据设置给内核的阻塞信号集
sigprocmask(SIG_BLOCK,&myset,NULL);
//每隔1s读一次内存的未决信号集,并把未决信号集的值输出到屏幕
while(1)
{
sigset_t pendset;
//注意:读取的是未决信号集,不是阻塞信号集
sigpending(&pendset);
//1-31
for(int i=1;i<32;++i)
{
//对每一个信号一次判断
if(sigismember(&pendset,i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
sleep(1);
}
return 0;
}
说明,开始时未决信号集中的各信号都被置位0,即默认不阻塞。但按下ctrl+c时,进程会从阻塞信号集中判断该信号对应的标志位是否为1,因为设置了,所以未决信号集中的该信号置为1。
信号捕捉函数
siganl
函数原型:
ypedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:
第一个参数是要捕捉的信号
第二个参数表示我们要对信号进行的处理方式
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
void myfunc(int sig)
{
printf("cathc you signal: %d\n", sig);
}
int main(int argc, const char* argv[])
{
//ctrl+c
// 注册信号捕捉函数
signal(SIGINT, myfunc);
while(1)
{
printf("hello\n");
sleep(2);
}
return 0;
}
sigaction
函数原型:
int sigaction(int signum,
const struct sigaction *act,
struct sigaction *oldact
);
参数:
signum:要操作的信号。
act:要设置的对信号的新处理方式,指向sigaction结构的指针。
oldact:原来对信号的处理方式。一般为NULL
struct sigaction {
void (*sa_handler)(int);//和下面的sa_sigaction二者选一
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;//在信号处理函数执行过程中,临时屏蔽指定的信号
int sa_flags;//只需记住,若是sa_handler,则sa_flags必为0
};
返回值:
0 表示成功,-1 表示有错误发生。
代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
void myfunc(int sig)
{
printf("hello signal: %d\n", sig);
sleep(5);
printf("wake up .....\n");
}
int main(int argc, const char* argv[])
{
// 注册信号捕捉函数
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myfunc;
// 设置临时屏蔽的信号
sigemptyset(&act.sa_mask); // 0
// ctrl + 反斜杠
sigaddset(&act.sa_mask, SIGQUIT);
sigaction(SIGINT, &act, NULL);
while(1);
return 0;
}
说明:sigemptyset(&act.sa_mask)是设置临时屏蔽的信号,比如按了ctrl+c,此时进入信号处理函数myfunc,然后接着按下ctrl+\,本应该立马结束程序,但是ctrl+\被设置为临时屏蔽的信号,所以等到myfunc函数执行完,才结束了进程。
常见的信号
SIGINT 终止进程,通常我们的Ctrl+C就发送的这个消息。
SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl- / )来控制,进程收到该消息退出时会产生core文件。
SIGKILL 消息编号为9,我们经常用kill -9来杀死进程发送的就是这个消息,程序收到这个消息立即终止,这个消息不能被捕获,封锁或这忽略,所以是杀死进程的终极武器。
SIGSTOP 停止进程的执行,同SIGKILL一样不可以被应用程序所处理,注意它和SIGINT的区别:该进程还未结束, 只是暂停执行。
SIGCHILD 这个同样是高性能服务器需要关注的信号,如果服务器采用fork产生的子进程推出后要调用wait进行资源回收,防止僵尸进程的产生,但是如果程序对子进程退出后的状态不感兴趣的话可以调用signal(SIGCHLD,SIG_IGN); 交给系统init去回收。子进程也不会产生僵尸进程了。
SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间。alarm函数使用该信号。
两个不能被忽略或捕获的信号:SIGKILL和SIGSTOP
网络编程相关信号
SIGHUP
SIGHUP 信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联. 系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
SIGHUP会在以下3种情况下被发送给相应的进程:
1.终端连接中断时,SIGHUP 会被发送到控制进程,后者会在将这个信号转发给会话中所有的进程组之后自行了断;
2.session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程;
3.若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。
SIGPIPE
默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接受到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引发SIGPIPE信号的写操作将设置errno为EPIPE。
SIGURG
在Linux环境下,内核通知应用程序带外数据到达主要有两种方法:一种是I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件;另一种方法就是使用SIGURG信号。
可靠信号和不可靠信号(转)
1.可靠信号与不可靠信号有哪些?
SIGHUP(1号) 至 SIGSYS(31号)之间的信号都是继承自UNIX系统,是不可靠信号,也称为非实时信号;
SIGRTMIN(33号) 与 SIGRTMAX(64号)之间的信号,它们都是可靠信号,也称为实时信号;
2.什么是可靠信号?
可靠性是指信号是否会丢失,即该信号是否支持排队;
如果支持排队就是可靠的,不支持排队就是不可靠的。
3.那么问题来了,在哪里排队?
在未决信号队列排队。当导致产生信号的事件发生时,内核就产生一个信号。信号产生后,内核通常会在进程表中设置某 种形 式的标志,当内核设置了这个标志,我们就说内核向一个进程递送了一个信号。
而信号产生(generate)和递送(delivery)之间的时间间隔称为信号未决(pending)。
4.还有,什么情况下会产生排队呢?
举个栗子,进程可以调用sigpending将某个信号设置为阻塞,即进程产生了一个阻塞的信号,而对该信号的动作是捕捉该信号 (即不忽略信号),则内核将为该进程的此信号保持为未决状态,直到该进程对此信号解除阻塞或将对此信号更改为忽略。
如果在进程解除对这个信号的阻塞之前,这种信号发生了多次,在这种情况下,将发生未决信号排 队。
5.那个支持排队和不支持排队怎么理解呢?不支持排队就是来了直接扔了吗?
如果被阻塞的信号来了多个(信号在未决信号队列里排队)
(1)如果来多少个,就递送多少个。则称为可靠信号,即支持排队。
(2)如果来了多个,但只被递送一次。则称为不可靠信号,即不支持排队。
可重入函数
信号的发生导致程序的指令执行顺序被打乱。
但是在信号处理函数中,无法知道原进程的执行情况。
如果原进程这个在分配内存或者释放内存,或者调用了修改static变量的函数,并在信号处理函数中再次调用该函数,会发生不可预期的结果。
在信号处理函数中可以安全调用的函数称为可重入函数,也叫做异步信号安全的函数。除了保证可重入,这些函数还会阻塞可能导致结果不一致的信号。
如果函数满足下面的一种或者几种条件,则说明是不可重入的函数:
- 使用static数据结构
- 调用malloc或free
- 标准IO库中的函数,因为大部分的标准IO函数都使用了全局数据结构
信号处理函数处理信号时是否会被其他信号中断?
void myfunc(int sig)
{
sleep(10);
printf("cathc you signal: %d\n", sig);
}
void myfunc1(int sig)
{
//sleep(10);
printf("cathc you signal: %d\n", sig);
}
int main(int argc, const char* argv[])
{
//ctrl+c
// 注册信号捕捉函数
signal(SIGINT, myfunc);
signal(SIGQUIT, myfunc1);
while(1)
{
printf("hello\n");
sleep(2);
}
return 0;
}
从实验结果可以看出,一个信号的信号处理函数过程是可以被其他信号中断的,但如果是同一个信号再次来临,则是发生阻塞,并将多个相同信号合并为一个,待处理完前面那个信号之后,再处理后面的这个相同的信号。