0x00
信号和中断类似,中断是硬件发出,而信号由软件发出。
信号常用于进程间通信,一个信号常见的处理如下:
1、设置对应信号的信号处理函数。
2、当信号来临时,打断正常执行的程序(本质上是在系统调用前检查是否有信号的到来),去执行信号处理函数。
3、信号处理函数执行完,继续执行原程序。
0x01
我们先来看一下linux中一共支持多少种信号。使用命令kill -l,可以列出linux支持的所有信号。
每个信号都有对应的默认信号处理函数,使用命令man 7 signal,可以查看到。
如果信号没有定义对应的信号处理函数,并且没有忽略信号,那么则采用默认的信号处理函数。
这里要注意,信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。
0x02
我们看一个自定义信号处理函数的例子,来学习如何自定义信号处理,并且学习如何发送信号。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void handler(int sig);
int main(int argc, char *argv[])
{
if (signal(SIGINT, handler) == SIG_ERR)
ERR_EXIT("signal error");
for (;;)
pause();
return 0;
}
void handler(int sig)
{
printf("recv a sig=%d\n", sig);
}
这个程序中我们自定义了信号处理函数,对应的信号是SIGINT,那么什么时候会发送这个这个信号呢?答案是当用户按下Ctrl + C时,我们看控制台输出的结果。
对照信号表,SIGINT是2号信号。在程序中pause,可以用来等待下一次信号的来临。
在这个例子中发送信号是通过键盘按Ctrl + C。
这里顺便说一下三号信号SIGQUIT,发送该信号,按键盘Ctrl + \。
0x03
我们接着讲另一种发送信号的方式,通过kill,列出代码:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void handler(int sig);
int main(int argc, char *argv[])
{
if (signal(SIGUSR1, handler) == SIG_ERR)
ERR_EXIT("signal error");
pid_t pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0) //子进程
{
kill(getppid(), SIGUSR1);
exit(EXIT_SUCCESS);
}
int n = 5; //父进程
do
{
n = sleep(n);
} while (n > 0);
return 0;
}
void handler(int sig)
{
printf("recv a sig=%d\n", sig);
}
这个程序中使用了SIGUSR1这个用户自定义信号。使用kill向对应的父进程发送信号,父进程sleep一旦接收到信号,会立刻退出,所以就不会睡眠5秒钟,那么如何让父进程睡眠5秒钟呢?我们采用了循环的方式,即使被子进程发来的信号打断,也可以继续睡眠剩余的时间。
这个例子中发送信号,我们采用kill的方式。
0x04
下面再看一个通过alarm发送信号的例子:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void handler(int sig);
int main(int argc, char *argv[])
{
if (signal(SIGALRM, handler) == SIG_ERR)
ERR_EXIT("signal error");
alarm(1);
for (;;)
pause();
return 0;
}
void handler(int sig)
{
printf("recv a sig=%d\n", sig);
alarm(1);
}
通过alarm函数向自身进程发送SIGALRM的信号,在信号处理函数中再一次发送ALRM信号,起到了一个定时器的作用。
0x05
接下来的例子,我们讲解下结束进程的几种方式,结合waitpid函数,进一步理解waitpid函数。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0) //子进程
{
sleep(3);
printf("this is child\n");
exit(100);
//return 0;
//abort();
//raise(SIGKILL);
}
int ret;
printf("this is parent\n");
int status;
//ret = wait(&status);
//ret = waitpid(-1, &status, 0);
ret = waitpid(pid, &status, 0); //父进程
printf("ret = %d pid = %d\n", ret, pid);
if (WIFEXITED(status))
printf("child exited normal exit status=%d\n", WEXITSTATUS(status));
//else
// printf("child exited abnormal\n");
else if (WIFSIGNALED(status))
printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
else if (WIFSTOPPED(status))
printf("child stoped signal number=%d\n", WSTOPSIG(status));
return 0;
}
这个例子说明了结束一个进程的几种方式:
1、exit(100) ---> 控制台输出:child exited normal exit status=100
2、return 0 ---> 控制台输出:child exited normal exit status=0
3、abort(),向当前进程发送SIGABRT,SIGABRT信号的默认处理函数为终止进程 ---> 控制台输出:child exited abnormal signal number=6 (SIGABRT是6号信号)
4、raise(SIGKILL),向当前进程发送SIGKILL信号,SIGKILL信号的默认处理函数是终止进程 --> 控制台输出:child exited abnormal signal number=9 (SIGKILL是9号信号)
0x06
总结:
信号既可以被捕获(自定义信号处理函数),也可以忽略,也可以走默认信号处理函数。
发送信号有以下几种方式:
1、键盘操作,Ctrl + C,Ctrl + \
2、kill
3、alarm
4、abort
5、raise