Linux信号种类与函数
主要介绍:
- Linux中的信号种类
- 信号操作的相关函数
Linux中的信号种类
信号是一种进程间通信的方法,应用于异步事件的处理。信号的实质是一种软中断。
使用kill -l
可以查看Linux系统中的所有信号,如下:
1 | deeplearning@deeplearning:~$ kill -l |
对其中一些信号进行介绍:
- SIGHUP:本信号在用户终端连接结束时(正常或非正常)发出。
- SIGINT:程序终止(或中断,interrupt)信号,通常是
Ctrl+c
或Delete
键(INTR字符)时发出。 - SIGQUIT:与SIGINT类似,但由
Ctrl+\
(QUIT字符)控制,进程收到该信号时会产生core文件,类似于一个程序错误信号。 - SIGLL:执行了非法指令,通常是可执行文件本身错误。
- SIGKILL:用来立即结束程序的运行,该信号不能被阻塞、处理或忽略。
- SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞或处理,shell命令的
kill
默认产生该信号。 - SIGSTOP:停止(stopped)进程的执行,注意和terminate及interrupt的区别,该进程还未结束,只是暂停执行,该信号与SIGKILL一样不能被阻塞、处理或忽略。
- SIGWINCH:窗口大小改变时发出的信号。
信号操作的相关函数
信号的处理
signal函数
要对一个信号进行处理(除了无法捕捉的SIGKILL和SIGSTOP),需要为其注册相应的处理函数,通过调用**signal()**函数可以进行注册。
- 捕捉SIGINT信号,catch_sigint.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
查看效果:1
2gcc -o catch_sigint catch_sigint.c
./catch_sigint1
^CCapture signal number:2 (键入“Ctrl+c”)
可以看到,“Ctrl+c”产生的SIGINT信号(代码为2)被程序捕捉到了。
- 忽略SIGINT信号,ignore_sigint.c:
1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
signal(SIGINT,SIG_IGN);
while(1)
sleep(1);
return 0;
}该程序将“Ctrl+C”产生的SIGINT信号忽略掉了,不能结束进行,不过可以通过”Ctrl+"发送SIGQUIT信号。
编译运行:
1
2$ ./ignore_sigint
^C^\Quit (core dumped) (键入“Ctrl+c”,再键入“Ctrl+\”)可以看到,“Ctrl+c”无效,因为被程序忽略了,自能通过“Ctrl+\”结束程序。
- SIGINT信号的默认处理,default_sigint.c:
1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
signal(SIGINT,SIG_DFL);
while(1)
sleep(1);
return 0;
}运行1:
1
2$ ./default_sigint
^C (键入“Ctrl+c”)运行2:
1
2
3$ ./default_sigint
^\Quit (core dumped) (键入“Ctrl+\”)可以看到两种程序结束方式。
- 定义多个信号处理函数,signals.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}运行:
1
2
3
4
5
6
7$ ./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
命令:1
2
3$ bg
[1]+ ./signals &继续发送SIGUP信号:
1
2
3$ kill -HUP 5793
Capture SIGHUP signal,the signal number is 1最后使用kill结束进程:
1
2kill -9 5793
sigaction函数
Linux还提供另外一种功能更加强大的信号处理机制:sigaction系统调用。sigaction函数的功能是检查或修改与指定信号相关联的处理动作,该函数可完全替代signal函数,并且还提供更加详细的信息,确切了解进程接收到信号时所发生的具体细节。
sigaction函数使用举例,sigaction.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
}执行:
1
2
3
4
5
6
7
8
9
10
11$ ./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”,测试如下:
1
2
3
4
5
6$ ./sigaction
hello! (键入“hello!”)
Get: hello!
quit (键入“quit”,程序正常退出)
Get: quit
信号集
在实际应用中,一个用户进程常常需要对多个信号进行处理,在LInux中引入信号集(signal set)概念,用于表示由多个信号所组成集合的数据类型,其定义为sigset_t类型的变量。
信号的发送
发送信号的函数有:kill,raise,sigqueue,alarm,setitimer,abort。
kill函数
kill函数用于向某一进程或进程组发送信号。
父进程使用kill函数向子进程传递一个SIGABRT信号,使子进程非正常结束,kill.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}运行:
1
2
3
4
5$ ./kill
send signal to child process (2689)
Hi I am child process!
child process receive signal 6从结果可以看出,当父进程将SIGABRT发送给子进程(ID 2689)后,子进程非正常结束,第2句输出语句没有执行。
raise函数
raise函数用于向进程本身发送信号。
使用raise函数向自身进程发送一个SIGABRT信号,使自己非正常结束,raise.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}运行:
1
2
3
4$ ./raise
Hello, I like Linux C Progrms!
Aborted (core dumped)可以看到程序非正常结束。
sigqueue函数
sigqueue是比较新的发送信号系统调用,主要针对实时信号提出的,支持信号带有参数,通常与sigaction函数配合使用。
使用sigqueue函数向进程自身发送信号SIGUSR1信号,并附加一个字符串信息,sigqueue.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}运行:
1
2
3
4$ ./sigqueue
Receive signalunmber:10
Receive Meaasage:I like Linux C programs!可以看出,进程成功接收到了自身发送的信号10(SIGUSR1)以及信号携带的字符串参数。
alarm函数
alarm函数专门为SIGALRM信号而设,使系统在一定时间之后发送信号。
使用alarm函数产生SIGALRM信号,alarm时间参数设置为5分钟,alarm.c:
1 |
|
执行:
1 | $ ./alarm |
在for循环运行了5次,即大约5秒后,产生了SIGALRM信号,此时由signal注册信号的处理函数handler,输出字符串。信号处理完毕后又返回先前程序的中断点,继续执行for循环。
setitimer函数
setitimer函数与alarm函数一样,也可以用于使系统在某一时刻发出信号,但它可以更加精确地控制程序。
使用setitimer函数产生SIGALRM信号,setitimer.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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);
}执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15./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,打印当前系统的时间和日期。
abort函数
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可以定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort后,SIGABORT仍能被进程接收。
信号的阻塞
在Linux的信号控制中,有时不希望进程在接收到信号时立刻中断进行的执行,也不希望该信号被完全忽略,而是延时一段时间再去调用相关的信号处理函数。
sigprocmask函数
sigprocmask函数可以用于检查或更改进程的信号掩码(signalmask)。信号掩码是由被阻塞的发送给当前进程的信号组成的信号集。
将sigaction.c程序进行修改,得到如下程序:
阻塞屏蔽SIGINT信号,block_sigint.c函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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;
}运行:
1
2
3
4
5
6
7
8
9$ ./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函数
sigsuspend函数用于使进程挂起,然后等待开放信号的唤醒。注意,此函数没有成功返回值,如果它返回到调用者,则总是返回-1。
计时器与信号
睡眠函数
Linux系统下有两个睡眠函数:sleep()
和usleep()
,函数原型为:
1 |
|
两个函数分别让进程睡眠seconds秒和usec微秒。
sleep函数的内部是使用信号机制进行处理,用到的函数有:
1 |
|
alarm函数告知自身进程,在seconds秒后自动产生一个SIGALRM信号。而pause函数用于将自身进程挂起,直到有信号发生时才从pause返回。
使用pause函数将进程挂起,模拟随眠3秒钟,pause.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}执行:
1
2
3
4
5$ ./pause
Before pause().
signal;14
After pause().在输出第一句”Before pause().”后,等待约3秒钟,采输出第二句。
时钟处理
Linux系统为每个进程维护3个计时器:
- 真实计时器计算的是程序运行的实际时间
- 虚拟计时器计算的是程序运行在用户态时所消耗的时间(实际时间减去系统调用和程序随眠时间)
- 实用计时器计算的是程序处于用户态和内核态所消耗的时间之和