目录
一、信号的概念
概念:信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式 。所有信号的产生及处理全部都是由内核完成的。
信号处理方式:
1 缺省方式
2 忽略信号
3 捕捉信号
在信号处理中,通常有三种处理方式:
缺省方式(Default):这是操作系统针对每种信号定义的默认行为。对于大多数信号,缺省行为是终止进程。例如,
SIGINT
的缺省行为是终止进程。忽略信号(Ignore):这种方式下,进程对收到的信号不做任何响应。这意味着当进程收到指定信号时,不会采取任何动作。这通常用于某些不需要处理的信号,或者是在某些特定情况下临时禁用信号处理器。
捕捉信号(Catch):这种方式下,进程定义一个信号处理函数,用于处理特定信号。当进程收到指定信号时,会调用这个信号处理函数。这允许程序员自定义对信号的处理方式,例如,收到
SIGINT
时执行特定的操作而不是终止进程。
在如下代码中使用了第三种方式,即捕捉信号。通过调用 signal(SIGINT, handle)
,定义了一个信号处理函数 handle
,用于处理 SIGINT
信号。当程序收到 SIGINT
信号时,会调用 handle
函数来处理它,而不是按照默认的方式终止进程。
注意typedef void (*sighandler_t)(int);不能少
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
typedef void (*sighandler_t)(int);
sighandler_t oldact;
void handle(int sig)
{
printf("I cath the SIGINT\n");
signal(SIGINT,oldact);
}
int main()
{
oldact = signal(SIGINT,handle);
while(1)
{
sleep(1);
}
return 0;
}
代码注释:
使用 signal 函数来设置信号处理函数,并捕获 SIGINT 信号(通常由键盘上的Ctrl+C组合键发送)。这段代码的功能是在收到 SIGINT 信号时打印一条消息,并重新设置 SIGINT 信号的处理函数为先前的处理函数。
下面是代码的简要解释:
signal(SIGINT, handle):这行代码设置了 SIGINT 信号的处理函数为 handle。
在收到 SIGINT 信号时,系统将调用 handle 函数来处理该信号。
handle 函数:这是 SIGINT 信号的处理函数。当程序收到 SIGINT 信号时,会调用这个函数,并打印一条消息 "I catch the SIGINT"。
然后,它将信号的处理函数重新设置为 oldact,这样可以恢复先前的信号处理函数。
oldact:这是一个全局变量,用于保存先前 SIGINT 信号的处理函数。
while(1) 循环:这是一个无限循环,程序在这里持续运行,每次循环睡眠1秒钟。
为什么“它将信号的处理函数重新设置为 oldact,这样可以恢复先前的信号处理函数”?
先前的处理函数,在这里就是系统的默认状态,即CTRL+C可以终止信号。
在这段代码中,oldact 保存了先前 SIGINT 信号的处理函数。
当程序收到 SIGINT 信号时,handle 函数会被调用。
在 handle 函数中,执行了 signal(SIGINT, oldact),这样做的目的是将 SIGINT 信号的处理函数重新设置为先前保存的处理函数 oldact。
这样做的原因是为了确保在 handle 函数执行完毕后,再次收到 SIGINT 信号时,会调用先前的处理函数,而不是再次调用 handle 函数。
这样可以恢复程序在接收 SIGINT 信号时的默认行为,或者是由用户自定义的其他行为。
通过在调用 signal 函数时将 oldact 作为第二个参数传递进去,可以获取先前的信号处理函数。然后,你可以在需要的时候使用这个指针来重新设置信号处理函数,从而恢复到先前的处理方式。
在 signal 函数中,oldact 是一个指向先前信号处理函数的指针。signal 函数的原型如下:
void (*signal(int signum, void (*handler)(int)))(int);
它返回了一个指向先前信号处理函数的指针。
这个指针的类型是 sighandler_t,它是一个函数指针类型,它指向一个接受 int 参数并返回 void 的函数。
当程序启动时,通常会有一些默认的信号处理方式。例如,对于
SIGINT
信号(通常由用户按下 Ctrl+C 发送),默认情况下会终止程序。当你调用
signal(SIGINT, handle)
时,你指定了一个自定义的信号处理函数handle
,用于处理SIGINT
信号。在这之前,如果有其他函数被注册为SIGINT
信号的处理函数,那么signal
函数会返回这个先前的处理函数,并将其保存在oldact
变量中。之后,如果你想要恢复先前的处理方式,你可以调用
signal(SIGINT, oldact)
,这样SIGINT
信号会再次被先前的处理函数处理,而不是你自定义的handle
函数。
常用信号:
信号名 |
含义 |
默认操作 |
SIGKILL |
该信号用来结束进程,并且不能被捕捉和忽略 |
终止 |
SIGSTOP |
该信号用于暂停进程,并且不能被捕捉和忽略 |
暂停进程 |
SIGTSTP |
该信号用于暂停进程,用户可键入SUSP字符( 通常是Ctrl-Z)发出这个信号 |
暂停进程 |
SIGCONT |
该信号让进程进入运行态 |
继续运行 |
SIGALRM |
该信号用于通知进程定时器时间已到 |
终止 |
SIGUSR1/2 |
该信号保留给用户程序使用 |
终止 |
SIGCHLD |
是子进程状态改变发给父进程的。 |
忽略 |
信号名 |
含义 |
默认操作 |
SIGHUP |
该信号在用户终端关闭时产生,通常是发给和该 终端关联的会话内的所有进程 |
终止 |
SIGINT |
该信号在用户键入INTR字符(Ctrl-C)时产生,内 核发送此信号送到当前终端的所有前台进程 |
终止 |
SIGQUIT |
该信号和SIGINT类似,但由QUIT字符(通常是 Ctrl-\)来产生 |
终止 |
SIGILL |
该信号在一个进程企图执行一条非法指令时产生 |
终止 |
SIGSEV |
该信号在非法访问内存时产生,如野指针、缓 冲区溢出 |
终止 |
SIGPIPE |
当进程往一个没有读端的管道中写入时产生,代 表“管道断裂” |
终止 |
二、定时器
1. alarm函数
alarm
函数的原型如下:
unsigned int alarm(unsigned int seconds);
它接受一个无符号整数参数 seconds
,表示定时器的超时时间,单位是秒。调用 alarm
函数会设置一个定时器,在指定的秒数之后,会产生 SIGALRM
信号。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void alarm_handler(int signum)
{
printf("Alarm signal received!\n");
}
int main()
{
signal(SIGALRM, alarm_handler);
alarm(5);
printf("Waiting for alarm...\n");
while (1)
{
sleep(1);
}
return 0;
}
alarm(5)
函数调用设置了一个5秒的定时器。但是,在程序调用 alarm(5)
设置定时器之后,程序会立即继续执行下一条语句,而不会等待5秒钟。这意味着在调用 alarm(5)
之后,立即执行了 printf("Waiting for alarm...\n");
这行代码,所以会立即打印出 "Waiting for alarm..."。然后,程序会进入 while
循环,在那里它会等待5秒钟,直到定时器超时并产生 SIGALRM
信号。
因此,程序执行的步骤是这样的:
- 执行
alarm(5);
设置一代码个5秒的定时器。 - 立即执行
printf("Waiting for alarm...\n");
,打印 "Waiting for alarm..."。 - 进入
while
循环,程序会等待5秒钟。 - 定时器超时,产生
SIGALRM
信号,调用alarm_handler
函数。 - 打印 "Alarm signal received!\n"。
- 程序继续执行
while
循环中的其他代码。
2. setitimer函数
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:定时的发送alarm信号
参数:
which: ITIMER_REAL:以逝去时间递减。发送SIGALRM信号、
ITIMER_VIRTUAL: 计算进程(用户模式)执行的时间。 发送SIGVTALRM信号
new_value: 负责设定 timout 时间
old_value: 存放旧的timeout值,一般指定为NULL
struct itimerval
{
struct timeval it_interval; // 闹钟触发周期
struct timeval it_value; // 闹钟触发时间
};
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
#include<sys/time.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
void timer_handler(int signum)
{
printf("Timer expired!\n");
}
int main()
{
struct itimerval timer;
struct sigaction act;
act.sa_handler = timer_handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
timer.it_value.tv_sec = 5; // 5秒后定时器启动
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 1; // 间隔1秒
timer.it_interval.tv_usec = 0;
sigaction(SIGALRM, &act,NULL);
if (setitimer(ITIMER_REAL, &timer, NULL) == -1)
{
perror("setitimer");
exit(EXIT_FAILURE);
}
while (1)
{
sleep(1);
}
return 0;
}
定义了一个信号处理函数 timer_handler,用于在定时器到期时打印 "Timer expired!"。
创建了一个 struct itimerval 结构体变量 timer,并设置了定时器的参数:
it_value 成员设置为 5 秒,表示定时器将在 5 秒后启动。
it_interval 成员设置为 1 秒,表示定时器在启动后每隔 1 秒触发一次。
创建了一个 struct sigaction 结构体变量 act,并设置了其中的成员:
sa_handler 成员设置为 timer_handler 函数,表示 SIGALRM 信号的处理函数为 timer_handler。
sa_flags 设置为 0,表示不设置特殊标志。
sa_mask 使用 sigemptyset 函数清空,表示在执行 timer_handler 函数期间不阻塞任何信号。
使用 sigaction 函数将 SIGALRM 信号的处理函数设置为 timer_handler 函数。
使用 setitimer 函数设置实时定时器,并传入 timer 结构体,启动定时器。
进入一个无限循环,程序将持续运行,每隔 1 秒调用 sleep(1) 函数,等待定时器的触发。
总的来说,这段代码实现了一个定时器,在程序启动 5 秒后启动,并且每隔 1 秒触发一次定时器,在定时器触发时打印一条消息。
3.signal和sigaction函数
signal函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:捕捉信号执行自定义函数
返回值:成功时返回原先的信号处理函数,失败时返回SIG_ERR
参数:
signo 要设置的信号类型
handler 指定的信号处理函数: SIG_DFL代表缺省方式; SIG_IGN 代表忽略信号;
系统建议使用sigaction函数,因为signal在不同类unix系统的行为不完全一样。
sigaction函数:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
参数:
signum:处理的信号
act,oldact: 处理信号的新行为和旧的行为,是一个sigaction结构体。
sigaction结构体成员定义如下:
sa_handler: 是一个函数指针,其含义与 signal 函数中的信号处理函数类似
sa_sigaction: 另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
sa_flags参考值如下:
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
SA_RESTART:使被信号打断的系统调用自动重新发起。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
re_restorer:是一个已经废弃的数据域
三、使用SIGCHLD信号实现回收子进程
SIGCHLD的产生条件
1子进程终止时
2子进程接收到SIGSTOP信号停止时
3子进程处在停止态,接受到SIGCONT后唤醒时
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<string.h>
#include<errno.h>
void handle(int sig)
{
wait(NULL);
printf("GET sig = %d\n",sig);
}
int main()
{
pid_t pid;
struct sigaction act;
act.sa_handler = handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
pid = fork();
if(pid > 0)
{
//wait(NULL);
sigaction(SIGCHLD,&act,NULL);
while(1)
{
printf("this is father process\n");
sleep(1);
}
}
else if(pid == 0)
{
sleep(5);
exit(0);
}
else
{
perror("fork");
return 0;
}
}
这段代码创建了一个父子进程,并使用 wait 函数等待子进程结束。
在父进程中,设置了 SIGCHLD 信号的处理函数为 handle 函数,用于处理子进程终止的信号。
具体来说,父进程会周期性地打印一条消息,而子进程在启动后会等待 5 秒后自行退出。
下面是这段代码的执行逻辑:
程序开始执行,父进程调用 fork 创建了一个子进程。
父进程中,如果 fork 成功(返回大于 0 的值),则进入了一个无限循环,不断地打印 "this is father process",并在其中使用 sigaction 函数设置了 SIGCHLD 信号的处理函数为 handle 函数。
子进程中,如果 fork 成功(返回 0),则进入了 if (pid == 0) 的分支,子进程会休眠 5 秒后自行退出。
如果 fork 出错(返回小于 0 的值),则父进程中输出错误信息并退出。
当子进程结束时,父进程会收到 SIGCHLD 信号,进而调用 handle 函数来处理。在 handle 函数中,调用 wait 函数等待子进程结束,并打印 "GET sig = xx" 的信息,其中 xx 是接收到的信号值。
总的来说,这段代码通过信号处理机制实现了在父进程中对子进程的终止进行处理。