主要介绍:
信号是一种进程间通信的方法,应用于异步事件的处理。信号的实质是一种软中断。
使用kill -l
可以查看Linux系统中的所有信号,如下:
deeplearning@deeplearning:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
对其中一些信号进行介绍:
Ctrl+c
或Delete
键(INTR字符)时发出。Ctrl+\
(QUIT字符)控制,进程收到该信号时会产生core文件,类似于一个程序错误信号。kill
默认产生该信号。要对一个信号进行处理(除了无法捕捉的SIGKILL和SIGSTOP),需要为其注册相应的处理函数,通过调用**signal()**函数可以进行注册。
#include
#include
#include
#include
void SignHandler(int iSignNum)
{
printf("Capture signal number:%d\n",iSignNum);
exit(1);
}
int main(void)
{
signal(SIGINT,SignHandler);
while(1)
sleep(1);
return 0;
}
使用gcc编译,并执行,通过Ctrl+c
查看效果:
$ gcc -o catch_sigint catch_sigint.c
$ ./catch_sigint
^CCapture signal number:2 (键入“Ctrl+c”)
可以看到,“Ctrl+c”产生的SIGINT信号(代码为2)被程序捕捉到了。
#include
#include
#include
int main(void)
{
signal(SIGINT,SIG_IGN);
while(1)
sleep(1);
return 0;
}
该程序将“Ctrl+C"产生的SIGINT信号忽略掉了,不能结束进行,不过可以通过”Ctrl+"发送SIGQUIT信号。
编译运行:
$ ./ignore_sigint
^C^\Quit (core dumped) (键入“Ctrl+c”,再键入“Ctrl+\”)
可以看到,“Ctrl+c”无效,因为被程序忽略了,自能通过“Ctrl+\”结束程序。
#include
#include
#include
int main(void)
{
signal(SIGINT,SIG_DFL);
while(1)
sleep(1);
return 0;
}
运行1:
$ ./default_sigint
^C (键入“Ctrl+c”)
运行2:
$ ./default_sigint
^\Quit (core dumped) (键入“Ctrl+\”)
可以看到两种程序结束方式。
#include
#include
#include
#include
void sigroutine(int dunno)
{
switch(dunno)
{
case 1:printf("Capture SIGHUP signal,the signal number is %d\n",dunno);break;
case 2:printf("Capture SIGINT signal,the signal number is %d\n",dunno);break;
case 3:printf("Capture SIGQUIT signal,the signal number is %d\n",dunno);break;
}
return;
}
int main(void)
{
printf("process ID is %d\n", getpid());
if(signal(SIGHUP,sigroutine)==SIG_ERR)
printf("Couldn't register signal handler for SIGHUP!\n");
if(signal(SIGINT,sigroutine)==SIG_ERR)
printf("Couldn't register signal handler for SIGINT!\n");
if(signal(SIGQUIT,sigroutine)==SIG_ERR)
printf("Couldn't register signal handler for SIGQUIT!\n");
while(1)
sleep(1);
return 0;
}
运行:
$ ./signals
process ID is 5793
^CCapture SIGINT signal,the signal number is 2 (键入“Ctrl+c”)
^\Capture SIGQUIT signal,the signal number is 3 (键入“Ctrl+\”)
^Z (键入“Ctrl+z”)
[1]+ Stopped ./signals
键入“Ctrl+z”后,进程置于后台,继续使用bg
命令:
$ bg
[1]+ ./signals &
继续发送SIGUP信号:
$ kill -HUP 5793
Capture SIGHUP signal,the signal number is 1
最后使用kill结束进程:
kill -9 5793
Linux还提供另外一种功能更加强大的信号处理机制:sigaction系统调用。sigaction函数的功能是检查或修改与指定信号相关联的处理动作,该函数可完全替代signal函数,并且还提供更加详细的信息,确切了解进程接收到信号时所发生的具体细节。
sigaction函数使用举例,sigaction.c:
#include
#include
#include
#include
int g_iSeq=0;
void SignHandlerNew(int iSignNo, siginfo_t *pInfo, void *pReserved)
{
int iSeq = g_iSeq++;
printf("%d Enter SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
sleep(3);
printf("%d Leave SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
}
int main(void)
{
char szBuf[20];
int iRet;
struct sigaction act;
act.sa_sigaction = SignHandlerNew;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
sigaction(SIGQUIT, &act, NULL);
do{
iRet=read(STDIN_FILENO, szBuf, sizeof(szBuf)-1);
if(iRet<0)
{
perror("read fail.");
break;
}
szBuf[iRet]=0;
printf("Get: %s", szBuf);
}while(strcmp(szBuf, "quit\n")!=0);
return 0;
}
执行:
$ ./sigaction
hello! (键入“hello!”)
Get: hello!
linux! (键入“linux!”)
Get: linux!
^C0 Enter SignHandlerNew, signo:2. (键入“Ctrl+c”,产生SIGINT信号)
^\1 Enter SignHandlerNew, signo:3. (3秒内再次,键入“Ctrl+\”,产生SIGQUIT信号)
1 Leave SignHandlerNew, signo:3. (SIGQUIT信号处理完毕)
0 Leave SignHandlerNew, signo:2. (SIGINT信号处理完毕)
read fail.: Interrupted system call (读出错,进程中断,程序非正常退出)
可以看出,当终端还未产生SIGINT或SIGQUIT信号时,可以正确的进行输入,并打印出输入的数据,而当信号产生时,进程被中断了。
再次运行程序,使用退出字符“quit”,测试如下:
$ ./sigaction
hello! (键入“hello!”)
Get: hello!
quit (键入“quit”,程序正常退出)
Get: quit
在实际应用中,一个用户进程常常需要对多个信号进行处理,在LInux中引入信号集(signal set)概念,用于表示由多个信号所组成集合的数据类型,其定义为sigset_t类型的变量。
发送信号的函数有:kill,raise,sigqueue,alarm,setitimer,abort。
kill函数用于向某一进程或进程组发送信号。
父进程使用kill函数向子进程传递一个SIGABRT信号,使子进程非正常结束,kill.c:
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int status;
if(!(pid=fork()))
{
printf("Hi I am child process!\n");
sleep(10);
printf("Hi I am child process, again!\n");
return 1;
}
else
{
printf("send signal to child process (%d)\n", pid);
sleep(1);
if(kill(pid, SIGABRT)==1)
printf("kill failed!\n");
wait(&status);
if(WIFSIGNALED(status))
printf("child process receive signal %d\n", WTERMSIG(status));
}
return 0;
}
运行:
$ ./kill
send signal to child process (2689)
Hi I am child process!
child process receive signal 6
从结果可以看出,当父进程将SIGABRT发送给子进程(ID 2689)后,子进程非正常结束,第2句输出语句没有执行。
raise函数用于向进程本身发送信号。
使用raise函数向自身进程发送一个SIGABRT信号,使自己非正常结束,raise.c:
#include
#include
#include
#include
int main(void)
{
printf("Hello, I like Linux C Progrms!\n");
if(raise(SIGABRT)==-1)
{
printf("raise failed!");
exit(1);
}
printf("Hello, I like Linux C Progrms, again!\n");
return 0;
}
运行:
$ ./raise
Hello, I like Linux C Progrms!
Aborted (core dumped)
可以看到程序非正常结束。
sigqueue是比较新的发送信号系统调用,主要针对实时信号提出的,支持信号带有参数,通常与sigaction函数配合使用。
使用sigqueue函数向进程自身发送信号SIGUSR1信号,并附加一个字符串信息,sigqueue.c:
#include
#include
#include
#include
void SigHandler(int signo, siginfo_t *info, void *context)
{
char *pMsg=(char*)info->si_value.sival_ptr;
printf("Receive signalunmber:%d\n",signo);
printf("Receive Meaasage:%s\n",pMsg);
}
int main(void)
{
struct sigaction sigAct;
sigAct.sa_flags=SA_SIGINFO;
sigAct.sa_sigaction=SigHandler;
if(sigaction(SIGUSR1, &sigAct, NULL) == -1)
{
printf("sigaction filed!\n");
exit(1);
}
sigval_t val;
char pMsg[] = "I like Linux C programs!";
val.sival_ptr = pMsg;
if(sigqueue(getpid(), SIGUSR1, val) == -1)
{
printf("sigqueue failed!\n");
exit(1);
}
sleep(3);
return 0;
}
运行:
$ ./sigqueue
Receive signalunmber:10
Receive Meaasage:I like Linux C programs!
可以看出,进程成功接收到了自身发送的信号10(SIGUSR1)以及信号携带的字符串参数。
alarm函数专门为SIGALRM信号而设,使系统在一定时间之后发送信号。
使用alarm函数产生SIGALRM信号,alarm时间参数设置为5分钟,alarm.c:
#include
#include
#include
void handler()
{
printf("Hello, I like linux C programs!\n");
}
int main(void)
{
int i;
signal(SIGALRM, handler);
alarm(5);
for(i=1;i<7;i++)
{
printf("sleep %d ...\n", i);
sleep(1);
}
return 0;
}
执行:
$ ./alarm
sleep 1 ...
sleep 2 ...
sleep 3 ...
sleep 4 ...
sleep 5 ...
Hello, I like linux C programs!
sleep 6 ...
在for循环运行了5次,即大约5秒后,产生了SIGALRM信号,此时由signal注册信号的处理函数handler,输出字符串。信号处理完毕后又返回先前程序的中断点,继续执行for循环。
setitimer函数与alarm函数一样,也可以用于使系统在某一时刻发出信号,但它可以更加精确地控制程序。
使用setitimer函数产生SIGALRM信号,setitimer.c:
#include
#include
#include
#include
#include
#include
static void ElsfTimer(int signo)
{
struct timeval tp;
struct tm *tm;
gettimeofday(&tp, NULL);
tm = localtime(&tp.tv_sec);
printf("sec = %ld\t", tp.tv_sec);
printf("usec = %ld\n", tp.tv_usec);
printf("%d-%d-%d%d:%d:%d\n", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
}
static void InitTime(int tv_sec, int tv_usec)
{
struct itimerval value;
signal(SIGALRM, ElsfTimer);
value.it_value.tv_sec = tv_sec;
value.it_value.tv_usec = tv_usec;
value.it_interval.tv_sec = tv_sec;
value.it_interval.tv_usec = tv_usec;
setitimer(ITIMER_REAL, &value, NULL);
}
int main(void)
{
InitTime(5, 0);
while(1){}
exit(0);
}
执行:
./setitimer
sec = 1574475716 usec = 295047
2019-11-2310:21:56
sec = 1574475721 usec = 295042
2019-11-2310:22:1
sec = 1574475726 usec = 295041
2019-11-2310:22:6
sec = 1574475731 usec = 295041
2019-11-2310:22:11
sec = 1574475736 usec = 295041
2019-11-2310:22:16
sec = 1574475741 usec = 295041
2019-11-2310:22:21
^\Quit (core dumped) (键入“Ctrl+\”退出)
可以看出,程序每隔5秒便会调用信号处理函数ElsfTimer,打印当前系统的时间和日期。
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可以定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort后,SIGABORT仍能被进程接收。
在Linux的信号控制中,有时不希望进程在接收到信号时立刻中断进行的执行,也不希望该信号被完全忽略,而是延时一段时间再去调用相关的信号处理函数。
sigprocmask函数可以用于检查或更改进程的信号掩码(signalmask)。信号掩码是由被阻塞的发送给当前进程的信号组成的信号集。
将sigaction.c程序进行修改,得到如下程序:
阻塞屏蔽SIGINT信号,block_sigint.c函数:
#include
#include
#include
#include
int g_iSeq = 0;
void SignHandlerNew(int iSignNo, siginfo_t *pInfo, void *pReserved)
{
int iSeq = g_iSeq++;
printf("%d Enter SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
sleep(3);
printf("%d Leave SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
}
int main(void)
{
char szBuf[20];
int iRet;
struct sigaction act;
act.sa_sigaction = SignHandlerNew;
act.sa_flags = SA_SIGINFO;
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet, SIGINT);
sigprocmask(SIG_BLOCK, &sigSet, NULL);
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
sigaction(SIGQUIT, &act, NULL);
do{
iRet = read(STDIN_FILENO, szBuf, sizeof(szBuf)-1);
if(iRet<0)
{
perror("read fail.");
break;
}
szBuf[iRet]=0;
printf("Get:%s",szBuf);
}while(strcmp(szBuf, "quit\n")!=0);
return 0;
}
运行:
$ ./block_sigint
hello! (键入“hello!”)
Get:hello!
linux! (键入“linux!”)
Get:linux!
^\0 Enter SignHandlerNew, signo:3. (键入“Ctrl+\”,产生SIGQUIT信号)
0 Leave SignHandlerNew, signo:3. (SIGQUIT信号处理完毕)
read fail.: Interrupted system call (读出错,进程中断,程序非正常退出)
与上面 的sigaction.c程序相比,此程序键入“Ctrl+c"不再有反应,屏蔽了SIGINT信号。
sigsuspend函数用于使进程挂起,然后等待开放信号的唤醒。注意,此函数没有成功返回值,如果它返回到调用者,则总是返回-1。
Linux系统下有两个睡眠函数:sleep()
和usleep()
,函数原型为:
#include
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);
两个函数分别让进程睡眠seconds秒和usec微秒。
sleep函数的内部是使用信号机制进行处理,用到的函数有:
#include
unsigned int alarm(unsigned int seconds);
int pause(void);
alarm函数告知自身进程,在seconds秒后自动产生一个SIGALRM信号。而pause函数用于将自身进程挂起,直到有信号发生时才从pause返回。
使用pause函数将进程挂起,模拟随眠3秒钟,pause.c:
#include
#include
#include
void SignHandler(int iSignNo)
{
printf("signal;%d\n", iSignNo);
}
int main(void)
{
signal(SIGALRM, SignHandler);
alarm(3);
printf("Before pause().\n");
pause();
printf("After pause().\n");
return 0;
}
执行:
$ ./pause
Before pause().
signal;14
After pause().
在输出第一句"Before pause()."后,等待约3秒钟,采输出第二句。
Linux系统为每个进程维护3个计时器:
参考:《精通Linux C编程》- 程国钢