13.信号机制
概念:
信号机制是Unix、类Unix以及其他POSIX兼容的操作系统中的一种进程间通讯方式,它允许进程在发生特定事件时接收通知。
信号机制是操作系统中的一个重要概念,它提供了一种异步的通知机制,用于在进程之间传递消息。信号可以被看作是一种软中断,它们可以在任何时间被发送给一个进程,以通知该进程发生了某个特定的事件。信号的本质是在软件层次上模拟硬件中断的行为,但它完全由软件控制,因此被称为“软中断”。
信号的处理过程通常涉及以下几个步骤:
- 信号的产生:当某个事件发生时,如用户按下Ctrl+C,或者程序访问了非法内存地址,操作系统会产生一个信号。
- 信号的传递:产生的信号会被操作系统传递给目标进程。这个过程是异步的,意味着信号可以在任何时间点到达。
- 信号的接收:进程通过注册信号处理函数来接收和处理信号。当信号到达时,如果进程已经为该信号注册了处理函数,操作系统会调用该函数。
- 信号的处理:在信号处理函数中,进程可以决定如何处理信号。常见的处理方式包括忽略信号、采取默认行为(如终止进程)或执行自定义的操作。
- 信号的屏蔽:进程可以选择暂时屏蔽某些信号,这样即使在信号产生时也不会被立即处理。这通常用于避免在某些关键操作中被信号中断。
在Linux系统中,信号机制是通过内核实现的。内核负责管理信号的发送和接收,并通过软中断的方式通知进程。进程可以通过系统调用来设置信号处理函数,从而定义对不同信号的响应方式。
常用信号
信号名 | 代号 | 含义 | 默认操作 |
---|---|---|---|
SIGHUP | 1 | 该信号在用户终端关闭时产生,通常是发给和该 终端关联的会话内的所有进程 | 终止 |
SIGINT | 2 | 该信号在用户键入INTR字符(Ctrl-C)时产生,内核发送此信号送到当前终端的所有前台进程 | 终止 |
SIGQUIT | 3 | 该信号和SIGINT类似,但由QUIT字符(通常是 Ctrl-)来产生 | 终止 |
SIGILL | 4 | 该信号在一个进程企图执行一条非法指令时产生 | 终止 |
SIGSEV | 5 | 该信号在非法访问内存时产生,如野指针、缓 冲区溢出 | 终止 |
SIGPIPE | 13 | 当进程往一个没有读端的管道中写入时产生,代 表“管道断裂” | 终止 |
SIGKILL | 9 | 该信号用来结束进程,并且不能被捕捉和忽略 | 终止 |
SIGSTOP | 19 | 该信号用于暂停进程,并且不能被捕捉和忽略 | 暂停进程 |
SIGTSTP | 20 | 该信号用于暂停进程,用户可键入SUSP字符( 通常是Ctrl-Z)发出这个信号 | 暂停进程 |
SIGCONT | 18 | 该信号让进程进入运行态 | 继续运行 |
SIGALRM | 14 | 该信号用于通知进程定时器时间已到 | 终止 |
SIGUSR1/2 | 10/12 | 该信号保留给用户程序使用 | 终止 |
SIGCHLD | 17 | 是子进程状态改变发给父进程的。 | 忽略 |
kill -l
命令查看所有信号
用到的命令
Kill 命令
- 格式:
kill [参数] [进程号]
- 功能:Linux中的kill命令的功能是向指定进程发送信号以终止该进程的运行
- 参数:
- -[信号名或代号],例
kill -9 pid
结束进程pid-l
(小写L):列出所有可用的信号名称。如果不加信号编号,使用此参数会显示全部信号。-s
:指定要发送的信号的名称或编号。这允许用户选择发送不同的信号到进程。-a
:当处理当前进程时,不限制命令名和进程号的对应关系。这在批量脚本中尤其有用。-p
:指定kill命令只打印相关进程的进程号,而不发送任何信号。-u
:指定用户。这个参数用来限定只向特定用户的进程发送信号
killall 命令
- 格式:killall [参数] [进程名]
- 功能:结束所有与给定名称匹配的运行中的进程。这可以简化操作,因为用户不需要先查找进程ID(PID),再使用kill命令来终止进程。
- 参数:
-e | --exact
:要求进程名与指定名称完全匹配。-I | --ignore-case
:在匹配进程名时忽略大小写。-g | --process-group
:结束整个进程组而不仅仅是单个进程。-i | --interactive
:在结束进程前询问用户确认,实现交互式操作。-l | --list
:列出所有已知的信号名称。-q | --quiet
:在进程没有结束时不输出任何信息。-s | --signal
:发送指定的信号到进程,默认为SIGTERM。-v | --verbose
:报告信号是否成功发送到每个匹配的进程。-w | --wait
:等待每个被发送信号的进程终止
发送信号的函数
kill 函数
原型:
#include <signal.h> int kill(pid_t pid, int sig)
功能:指定的进程或进程组发送信号
参数:
pid
:指定要接收信号的进程或进程组
pid > 0
:信号将被发送到进程ID为pid
的进程。pid = 0
:信号将被发送到与调用kill()
的进程属于同一个进程组的所有进程。pid = -1
:信号将被广播发送到系统中所有调用进程有权发送信号的进程,除了进程1(init)。pid < -1
:信号将被发送到以-pid
为进程组标识的所有进程。int sig
:指定要发送的信号的编号。不同的信号编号代表不同的信号,例如SIGTERM代表终止信号,SIGKILL代表强制终止信号。返回值:
- 成功,返回0
- 失败,返回非零数
alarm 函数
原型:
#include <unistd.h> unsigned int alarm(unsigned int seconds);
功能:设置一个定时器,在定时器到期时向调用进程发送SIGALRM信号
参数:
seconds
:指定定时器的秒数。如果seconds
为0,则取消之前设置的定时器,并返回剩余的时间片。如果seconds
非零,则设置一个新的定时器,定时器到期时,将向调用alarm
函数的进程发送SIGALRM信号。返回值:
- 上一个定时器的剩余时间(以秒为单位)
- 如果没有设置过定时器,则返回值为0
注意:如果在定时器到期前再次调用
alarm
函数设置了新的定时器,那么原来的定时器会被取消,新的定时器将从当前时间开始计时。
pause 函数
原型:
#include <stdlib.h> int pause(void);
功能:暂停程序执行,直到接收到一个信号
参数:无
返回值:
- 如果成功,则返回-1
- 如果发生错误,则返回-1并设置errno为相应的错误代码
ualarm 函数
原型:
#include <unistd.h> int ualarm(unsigned int seconds, int interval);
功能:设置一个定时器,在定时器到期时向进程发送SIGALRM信号
参数:
seconds
:指定定时器的秒数。如果seconds
为0,则取消之前设置的定时器。interval
:指定间隔时间(以jiffies为单位)。如果interval
为0或负数,则表示只执行一次定时器。返回值:
- 成功,返回0
- 失败,返回-1
注意:
ualarm
函数已经被标记为已废弃(deprecated),因为它的行为在不同的系统和平台上可能不一致。在新的代码中,建议使用setitimer
函数来代替ualarm
函数,以实现更可靠和可移植的定时器功能。
setitimer函数
原型:
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置一个定时器,该定时器在到期时会向调用进程发送一个SIGALRM信号
参数:
which
:指定定时器类型,可以是以下三种之一:
ITIMER_REAL
:基于真实的时间间隔(Real timer),即按照日历时间来计算。ITIMER_VIRTUAL
:基于虚拟的时间间隔(Virtual timer),即按照进程在用户态消耗的CPU时间来计算。ITIMER_PROF
:基于进程时间(Profiling timer),即按照进程在用户态和内核态消耗的CPU时间来计算。new_value
:指向struct itimerval
结构的指针,该结构定义了定时器的间隔时间和总时间。如果这个指针为NULL,则定时器被取消。old_value
:指向struct itimerval
结构的指针,用于存储之前的定时器设置。如果这个指针为NULL,则不保存之前的定时器设置。结构体:
struct itimerval { struct timeval it_interval; /* 定时器触发的时间间隔 */ struct timeval it_value; /* 第一次触发定时器的时间 */ };
struct timeval { time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒数 };
返回值:
- 成功,返回0
- 失败,返回-1
注:可以通过调用
gettimeofday(&t_start, NULL)
来获取当前时间,并将其存储在timeval
结构体变量t_start
中。
捕获信号
捕获流程
signal函数
原型:
#include <signal.h> typedef void (*sighandler_t)(int);` sighandler_t signal(int signum, sighandler_t handler);
功能:设置指定信号的处理函数
参数:
signum
:指定要处理的信号的编号。handler
:指向信号处理函数的指针。如果设置为SIG_IGN,则忽略该信号;如果设置为SIG_DFL,则采用系统默认处理方式。返回值:返回先前为指定信号设置的处理函数指针,或者如果之前没有设置处理函数,则返回SIG_DFL
注:
typedef void (*sighandler_t)(int);
(*sighandler_t)
表示定义了一个名为sighandler_t
的函数指针类型。这个函数指针类型指向的函数返回值为void
,即没有返回值,同时接受一个整型参数int
,通常用于传递信号编号示例
捕捉
SIGINT
信号,打印"I cath the SIGINT \n"程序执行后按ctrl +c 打印“I cath the SIGINT \n”后信号功能复原,再按一次ctrl +c,程序退出
#include <signal.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <linux/posix_types.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);//设定信号执行的函数,并将原来的处理信号的函数指针赋值给oldact while(1){ sleep(1); }
sigaction函数
原型:
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
sigaction
函数用于设置指定信号signum
的信号处理函数、信号集和标志。- 它允许更细致的控制信号的行为,比
signal
函数提供了更多的选项。参数:
signum
:要处理的信号编号。act
:指向sigaction
结构体的指针,该结构体定义了新的信号处理行为。oldact
:指向sigaction
结构体的指针,用于存储原来的信号处理行为。如果不需要保存旧的行为,可以设置为NULL
。返回值:
- 成功时返回 0
- 失败时返回 -1
sigaction
结构体:struct sigaction { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 带有额外信息的信号处理函数(POSIX标准) sigset_t sa_mask; // 信号集,指定在处理信号时阻塞哪些信号 int sa_flags; // 影响信号处理行为的标志 };
sa_flags
标志
SA_NOCLDSTOP
:如果子进程被终止,父进程不会成为停止状态。SA_ONSTACK
:使用用户栈中的额外空间执行信号处理函数。SA_RESTART
:在信号处理完成后,重启被中断的系统调用。SA_RESETHAND
:在信号处理结束后,将信号处理函数重置为默认行为。SA_SIGINFO
:使用sa_sigaction
成员而不是sa_handler
作为信号处理函数,以提供额外的信号信息。注意:
- 不同的操作系统可能对信号的支持不同,因此在使用前应检查操作系统的文档。
- 在多线程环境中,信号处理可能会被任何线程接收,因此信号处理函数应设计为异步安全的。
- 当一个信号被捕获时,它的默认行为会被禁止,除非在
sa_handler
或sa_sigaction
中明确指定。
sigemptyset函数
原型:
#include <signal.h> int sigemptyset(sigset_t *set);
功能:初始化或清空信号集
参数:
set
:指向要清空的信号集的指针。返回值:
- 成功时返回0
- 失败时返回-1
示例
使用信号回收子进程
-
信号 17)
SIGCHLD
是子进程状态改变发给父进程的。 -
SIGCHLD
的产生条件-
子进程终止时
-
子进程接收到SIGSTOP信号停止时
-
子进程处在停止态,接受到SIGCONT后唤醒时
-
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
// 信号处理函数
void handle(int sig){
wait(NULL); // 等待子进程结束
printf("Get sig =%d\n",sig); // 输出接收到的信号编号
}
int main()
{
pid_t pid; // 用于存储fork()函数的返回值
struct sigaction myact; // 用于存储信号处理结构体
myact.sa_handler = handle; // 设置信号处理函数为handle
myact.sa_flags = 0; // 设置信号处理标志为0
sigemptyset(&myact.sa_mask); // 清空信号集
pid = fork(); // 创建子进程
// 父进程部分
if(pid>0)
{
//wait(NULL);
sigaction(SIGCHLD,&myact,NULL); // 设置SIGCHLD信号的处理函数为handle
while(1)
{
printf("this is father process\n"); // 输出信息
sleep(1); // 休眠1秒
}
}
// 子进程部分
else if(pid==0)
{
sleep(5); // 休眠5秒
exit(0); // 退出子进程
}
}
信号阻塞和信号集
有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况可以通过阻塞信号实现。
概念:信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的状态:
- 信号递达(Delivery ):实际信号执行的处理过程(3种状态:忽略,执行默认动作,捕获)
- 信号未决(Pending):从产生到递达之间的状态
信号集
信号集是一组用于表示信号的集合,它包含了一组信号的状态,通常用来表示这些信号是否已经发生或被处理
信号集操作函数
sigset_t set;
自定义信号集。 是一个32bit 64bit 128bit的数组。sigemptyset(sigset_t *set);
清空信号集sigfillset(sigset_t *set);
全部置1sigaddset(sigset_t *set, int signum);
将第signum个信号添加到集合中sigdelset(sigset_t *set, int signum);
将第signum个信号从集合中移除sigismember(const sigset_t *set,int signum);
判断一个信号是否在集合中。
信号屏蔽函数
sigprocmask函数
原型:
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:设定对信号集内的信号的处理方式(阻塞或不阻塞)
参数:
how:指定函数的行为,可以是以下值之一:
SIG_BLOCK
:将参数set
中的信号加入到进程的信号屏蔽字中。
SIG_UNBLOCK
:从进程的信号屏蔽字中移除参数set
中的信号。
SIG_SETMASK
:将参数set
设置为进程的信号屏蔽字。
SIG_MASK
:清空信号集,然后加入原来的信号屏蔽字。
SIG_BLOCKSET
:类似于SIG_BLOCK
,但不会改变原来的信号屏蔽字。
SIG_UNBLOCKSET
:类似于SIG_UNBLOCK
,但不会改变原来的信号屏蔽字。
set
:指向sigset_t
类型的指针,包含了要添加到信号屏蔽字中的信号集合。
oldset
:指向sigset_t
类型的指针,用于存放调用前的信号屏蔽字。返回值:
- 成功时返回0,
- 失败时返回-1,并设置
errno
为错误码。
示例
SIGINT
信号的处理和屏蔽
在刚开始运行时的5秒内按下ctrl+c
终端没有反应,五秒结束后信号屏蔽解除,终端打印"I get sig=%d\n"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void handle(int sig) // 定义一个信号处理函数,参数为接收到的信号值
{
printf("I get sig=%d\n", sig); // 打印接收到的信号值
}
int main() // 主函数
{
struct sigaction act; // 定义一个sigaction结构体变量act
act.sa_handler = handle; // 设置信号处理函数为handle
sigemptyset(&act.sa_mask); // 初始化清空信号集
act.sa_flags = 0; // 设置标志位为0
sigaction(SIGINT, &act, NULL); // 将SIGINT信号的处理方式设置为act
sigset_t set; // 定义一个信号集变量set
sigemptyset(&set); // 初始化清空信号集
sigaddset(&set, SIGINT); // 将SIGINT信号添加到信号集中
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞信号集set中的信号(即禁止接收SIGINT信号)
sleep(5); // 暂停执行5秒
sigprocmask(SIG_UNBLOCK, &set, NULL); // 解除对信号集中的信号的阻塞(即允许接收SIGINT信号)
while (1)
{
sleep(1);
}
}
信号阻塞函数
pause函数
原型:
#include <unistd.h> int pause(void);
功能:
pause
函数使当前进程进入睡眠状态,直到收到一个信号为止。在睡眠期间,进程不会占用CPU资源。返回值:
- 如果成功,则返回-1,并将errno设置为EINTR(表示被中断)。
- 如果失败,则返回-1,并将errno设置为错误码。
sigsuspend函数
原型:
#include <signal.h> int sigsuspend(const sigset_t *mask);
功能:
sigsuspend
函数使当前进程进入睡眠状态,直到收到一个信号为止。在睡眠期间,进程不会占用CPU资源。- 与
pause
函数不同,sigsuspend
函数可以指定要阻塞的信号集,即在等待信号时,哪些信号应该被阻塞。参数:
mask
:指向sigset_t
类型的指针,表示要阻塞的信号集。如果为NULL,则不阻塞任何信号返回值:
- 如果成功,则返回-1,并将errno设置为EINTR(表示被中断)
- 如果失败,则返回-1,并将errno设置为错误码
示例
展示两个方法阻塞信号
- 法一:通过sigprocmask和pause实现
- 法二:通过sigsuspend实现
#include <signal.h> // 引入信号处理相关的头文件 #include <stdio.h> // 引入标准输入输出相关的头文件 #include <stdlib.h> // 引入标准库函数相关的头文件 #include <unistd.h> // 引入Unix系统调用相关的头文件 void handle(int sig) // 信号处理函数 { printf("I get sig=%d\n", sig); // 打印接收到的信号值 } void mytask() // 任务函数 { printf("My task start\n"); // 打印任务开始信息 sleep(3); // 暂停3秒 printf("My task end\n"); // 打印任务结束信息 } int main() // 主函数 { struct sigaction act; // 定义一个sigaction结构体变量act act.sa_handler = handle; // 设置信号处理函数为handle act.sa_flags = 0; // 设置标志位为0 sigemptyset(&act.sa_mask); // 初始化清空信号集 sigaction(SIGINT, &act, NULL); // 将SIGINT信号的处理方式设置为act sigaction(SIGHUP, &act, NULL); // 将SIGHUP信号的处理方式设置为act sigset_t set, set2; // 定义两个信号集变量set和set2 sigemptyset(&set2); // 初始化清空set2信号集 sigaddset(&set, SIGHUP); // 将SIGHUP信号添加到信号集set中 sigaddset(&set, SIGINT); // 将SIGINT信号添加到信号集set中 pause(); // 暂停执行,等待信号的到来 while (1) // 无限循环 { sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞信号集中的信号(即禁止接收SIGHUP和SIGINT信号) mytask(); // 执行自定义的任务函数 /*法一*/ //sigprocmask(SIG_UNBLOCK,&set,NULL); // 解除对信号集中的信号的阻塞(即允许接收SIGHUP和SIGINT信号) //pause();//暂停执行,等待信号的到来 /*法二*/ sigsuspend(&set2); // 挂起进程,直到收到信号为止 } printf("After pause\n"); // 打印"After pause"信息 while (1) // 无限循环 { sleep(1); // 每次循环暂停执行1秒 } }
具体分析:
sigprocmask
:这个函数用于修改进程的信号屏蔽字,即它可以阻止某些信号的传递。当设置了信号屏蔽字后,指定的信号将不会传递给进程,直到取消屏蔽。这种方法的缺点是,如果在取消屏蔽和调用pause
之间有信号发生,那么这个信号可能会丢失。pause
:这个函数会使进程进入睡眠状态,直到收到一个信号。如果在使用sigprocmask
解除信号屏蔽之后调用pause
,在这两个调用之间的时间窗口内发生的信号可能会导致pause
永远挂起,因为pause
只有在接收到信号后才会返回。sigsuspend
:这个函数结合了sigprocmask
和pause
的功能,它在解除信号屏蔽的同时使进程进入睡眠状态。这是一个原子操作,意味着它保证了在信号解除屏蔽和等待信号之间不会有时间窗口,从而避免了信号丢失的问题。因此,如果需要等待某个信号,建议使用sigsuspend
而不是单独使用sigprocmask
和pause
。总的来说,
sigprocmask
主要用于改变信号屏蔽字,pause
用于等待任何信号的到来,而sigsuspend
则是在等待特定信号时使用的更为安全的方法,因为它可以保证在等待期间不会错过任何信号