目录
一、信号概念
二、信号的产生
三、信号的保存
四、信号集函数
五、捕捉信号
六、可重入函数和volatile 关键字
七、SIGCHLD信号
信号是进程之间事件异步通知的一种方式,属于软件中断。
1、Ctrl+c结束前台进程
当我们在shell中启动一个前台进程时,使用Ctrl+c可以结束这个前台进程。用户按下Ctrl-C时键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
注意:
1)ctrl+c产生的信号只能发送给前台进程。一个命令后+&可以放到后台运行,后台运行的进程不能使用Ctrl+c终止同时后台进程shell不进行等待,可以接收其他命令。
2)shell可以同时运行一个前台进程一个或多个后台进程。
3)使用Ctrl+c时操作系统会向前台进程发送一个SIGINT信号(2号信号)。
2、kill -l命令查看系统中定义的信号
注意:
1)每个信号都有一个编号和一个宏名称,使用时两者都可以。
2)1~31号信号是普通信号,34~64是实时信号(不进行讨论)。
3)没有32、33号信号,因此一共有62种信号。
3、信号处理的三种方式
1)忽略信号
2)使用默认处理动作处理
3)自定义处理信号。自定义一个处理函数,当收到该信号时使用该函数对信号进行处理,也成为捕捉一个信号。
4、信号的其他概念
1)实际信号执行的处理动作称为信号的递达。
2)信号从产生到递达之间的过程称为信号未决。
3)进程可以选择阻塞某个信号,被阻塞的信号当产生时将保持在未决状态,直到进程接触对该信号的阻塞才进行递达。
注意:信号的阻塞和忽略不是相同的概念,阻塞的信号处于未决状态当解除阻塞后还会在执行;忽略是递达的一种方式。
1、终端按键产生
使用Ctrl+c向前台进程发送一个SIGINT信号的方式实际上就是通过终端案件产生信号。
SIGINT信号的默认处理动作是终止进程,SIGQUIT信号的默认处理动作是终止进程并且core dump.
1)Core Dump ---核心转储
core Dump:当一个进程异常终止时,可以选择把用户空间内存数据全部保存到磁盘上,文件名为core,这就称为Core Dump。
生成core文件:一个进程终止通常是程序中存在错误,例如数组越界等,当进程终止后我们可以查看core文件信息查清错误原因(事后调试)。但是在默认情况下时不允许产生core文件的(存在安全问题等),如果需要产生可以使用ulimit命令改变shell进程的Resource Limit。例如:ulimit -c 1024表示允许产生大小为1024k的core文件。
使用ulimit -c命令更改core文件大小
当运行一个存在除0错误的程序时,当程序运行起来后操作系统检测到除0错误会发送一个SIGFPE信号,此时在当前目录下还会产生一个core文件。在gdb调试时可以使用core-file查看具体错误位置。
2、通过系统调用产生
1)kill命令
使用:kill -信号 进程 ---给指定进程发送指定信号
例如:kill -2 2148 ---给2148进程发送二号信号 kill -SIGINT 2148也表示给2148进程发送二号信号
注意:使用kill命令给进程发送信号,实际上操作系统是通过调用kill函数发送的信号。
kill函数:int kill(pid_t pid, int sig); //给指定pid的进程发送指定信号sig
参数:pid接收信号的进程id,sig表示信号
返回值:成功返回0,失败返回-1
2)raise函数
int raise(int sig);//给当前进程发送指定信号
参数:发送的信号
返回值:成功返回0,失败返回非0.
3)abort函数
void abort();//使当前函数接收到异常信号而终止
注意:abort函数就想exit一样,总是会成功的,因此没有返回值。
3、软件条件产生
使用pipe创建管道是,发生异常会产生一个SIGPIPE信号,SIGPIPE的产生属于软件条件产生。调用alarm函数也可以产生信号SIGALRM,这也属于软件条件产生。
unsigned int alarm(unsigned int seconds); //在seconds秒之后向该进程发送一个SIGALRM信号,该信号的默认处理动作时终止进程。
参数:等待的时间,单位为秒。
返回值:返回剩余的秒数或0
#include
#include
int main()
{
unsigned int sec = alarm(5);
while(1)
{
printf("i am runing\n");
sleep(1);
}
return 0;
}
4、硬件产生
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
5、简单理解信号的捕捉
使用signal函数可以通过自定义的方式去处理一个信号。
typedef void (*sighandler_t)(int);//函数指针,参数为int,返回值为void的函数。
sighandler_t signal(int signum, sighandler_t handler);//自定义处理信号
参数:signum表示信号,handler表示自定义处理函数的地址。
返回值:
#include
#include
void sigcb(int sig)
{
//当从键盘输入ctrl+c时会自动打印这句话
printf("catch a sig: %d\n",sig);
}
int main()
{
//将2号信号的处理方式改为自定义处理方式
signal(SIGINT,sigcb);
while(1);
return 0;
}
1、从系统的层面理解信号的保存
信号是操作系统发送给进程的,因此进程应该要知道发送的是什么信号以及是否接收到该信号。从操作系统的层面我们可以理解在进程的task_struct结构体中,有一个保存信号的sigbitmap,其中sigbitmap的每一个比特位的位置表示的是什么信号,比特位的内容表示是否收到该信号(为0表示没有收到,为1表示收到)。当进程收到操作系统发来的信号时,进程会将sigbitmap中的相应的比特位置为1,表示收到了该信号。
因此,操作系统给进程发送信号实际上是给进程“写信号”。
2、从内核的层面理解信号的保存
在内核中是通过pending、block、handler表来保存信号的信息的。其中,block表示信号的阻塞,pending表表示信号的未决,它们的表示方式也是位图方式;而handler表中存储的是一个函数指针,表示的是信号的处理动作。
一个信号产生时,内核在进程控制块中设置该信号的未决状态(pending表)为1,直到信号递达才清除该标志。如果同时还给该信号设置了了阻塞和忽略,即在该信号的block表中设置了1且handler中设置了SIG_IGN,当接收到该信号时在对该信号解除阻塞之前也不能忽略该信号。
当一个信号被设置为阻塞时,该信号未产生过,一旦产生该信号将被阻塞,且将它的处理动作设置为用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,但是只能记一次(这里只考虑常规信号)。因为在内核中一个进程只有一张表用来保存信号的未决状态。
1、sigset_t
对于普通信号(非实时性信号)来说,每个信号只有一个比特位来表示未决状态,并不记录该信号产生了多少次,阻塞标志也是如此。因此,在内核中阻塞和未决使用相同的数据类型sig_set来存储,sig_set称为信号集。这个类型可以表示信号的有效和无效状态,在阻塞信号中有效和无效表示的是是否被阻塞,在未决信号中有效和无效表示的是是否处于未决状态。阻塞信号集也称为当前进程的屏蔽字。
2、操作sigset_t变量的函数
1)int sigismember(const sigset_t *set, int signo); //sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
2)int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
#include
#include
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判断该信号集中的有效信号是否包含i号信号
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信号集
sigemptyset(&st);
//打印信号集
printSig(&st);
return 0;
}
3)int sigfillset((sigset_t *set);//初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
#include
#include
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判断该信号集中的有效信号是否包含i号信号
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信号集
sigfillset(&st);
//打印信号集
printSig(&st);
return 0;
}
4)int sigaddset(sigset_t *set, int signo);//在该信号集中添加有效信号
5)int sigdelset(sigset_t *set, int signo);//在该信号集中删除有效信号
#include
#include
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判断该信号集中的有效信号是否包含i号信号
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信号集
sigemptyset(&st);
//打印信号集
printSig(&st);
//向该信号集中添加2号信号
sigaddset(&st,2);
printSig(&st);
//删除该信号集中的2号信号
sigdelset(&st,2);
printSig(&st);
return 0;
}
注意:除了sigismember函数外,其余四个函数的返回值相同,成功返回0失败返回-1;在使用sigset_t类型之前,一定要对信号集进行sigemptyset或者sigfillset做初始化,使信号集处于确定状态。
3、sigprocmask函数
int sigprocmask(int how,sigset_t* set,sigset_t* oset);//读取或更改进程的信号屏蔽字。
返回值:成功返回0失败返回-1
函数说明:如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前进程的信号屏蔽字为mask,下表说明了how参数的可选值。
注意:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
#include
#include
int main()
{
sigset_t set;
sigset_t oset;
//初始化set为empty
sigemptyset(&set);
//向set中添加2号信号
sigaddset(&set,2);
//使用sigprocmask阻塞二号信号
sigprocmask(SIG_BLOCK,&set,&oset);
while(1);
return 0;
}
4、sigpending函数
int sigpending(sigset_t *set);//读取当前进程的未决信号集,通过set参数传出
返回值:成功返回0,失败返回-1
#include
#include
#include
void printset(sigset_t* set)
{
int i = 1;
for(;i < 32;i++)
{
if(sigismember(set,i))
printf("1");
else
printf("0");
}
printf("\n");
}
int main()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set,2);
//阻塞二号信号
sigprocmask(SIG_BLOCK,&set,NULL);
int i = 0;
sigemptyset(&set);
for(;i < 10;i++)
{
sigpending(&set);
printset(&set);
sleep(1);
}
return 0;
}
SIG_INT信号被阻塞,当使用ctrl+c时未决信号二号位为1
操作系统是进程的管理者,因此无论通过那种方式产生的信号要想发送给进程都需要经过操作系统。也就是说,一个信号从产生到递达一定要经过从用户态->内核态的切换。而信号的递达就是发生在从内核态切换到用户态时进行的。
1、内核如何实现信号的捕捉
1)用户态和内核态的切换
程序在运行时经常需要在用户态和内核态之间来回切换,因为用户态没有权限执行内核态的程序,只有切换到内核态才能执行。如何切换?
在32位系统下,程序地址空间为4G,其中1G是内核空间,其余3G是用户空间;内核空间和用户空间有各自独立的页表,当需要执行内核的代码时CPU会通过内核页表找到相应的地址。(CPU的寄存器会记录当前是用户态还是内核态)
2)内核对信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
2、sigaction函数
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); //读取和修改与指定信号相关联的处理动作。
参数:signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。
返回值:成功返回0,失败返回-1
1)struct sigaction结构体
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
1、可重入函数
1)重入函数
当一个函数的主执行流正在向链表中插入一个节点时,向该进程发送一个信号,并且该信号的处理方式为自定义。如下:
像这样的,insert函数被不同的执行流调用,有可能第一个执行流还没有执行完就去执行第二个执行流,insert函数就称为重入函数。
2)可重入函数
但是上面程序存在一个问题,当程序运行结束析构函数释放链表空间时,导致由自定义处理函数插入的链表节点没有办法释放造成内存泄露问题。因此,该函数称为不可重入函数。
如果一个函数只访问自己的局部变量或参数,称为可重入函数(当重入时不会对程序造成破坏)。
3)常见的不可重入函数
#include
#include
#include
int a = 10;
void handler(int sig)
{
printf("catch a sig :%d\n",sig);
a = 0;
}
int main()
{
signal(2,handler);
while(a);
return 0;
}
上面代码在执行时,当向进程发送一个2号信号时去执行sighandler函数,将a修改成0,循环应该结束,但是并没有结束。这是因为,CPU将a保存在寄存器中,而修改的a在内存中,因此程序并没有立即结束。
当我们不需要CPU进行这种优化时,就可以使用volatile关键字修饰该变量,使得每次看到的值都是内存中最新的值。
在进程中,当子进程退出父进程没有进行进程等待时子进程会成为僵尸进程。如果使用waitpid函数进行非阻塞等待则父进程可以边执行自己的程序边观察子进程是否退出,但是这样程序实现复杂,但是如果阻塞等待则父进程不能执行自己的代码直到子进程退出后对子进程处理完才可以。
实际上子进程在退出时会给父进程发送一个SIGCHILD信号,我们可以使用自定义处理该信号,在该信号的处理函数中调用waitpid函数对子进程进行处理即可。这样,父进程便不再等待子进程还可以“专心”的干自己的事情。
#include
#include
#include
#include
#include
#include
void sighandler(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,sighandler);
pid_t id = fork();
if(id == 0)
{
printf("i am child,id : %d\n",getpid());
exit(1);
}
else
{
while(1);
}
return 0;
}