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