文章目录
- Linux信号概念
- 信号种类
- Linux信号产生
- 异步
- LInux信号阻塞
- 递达、未决、阻塞、忽略
- 信号集操作函数
- 阻塞信号集操作函数
- 未决信号集操作函数
- Linux信号捕捉
- signal函数
- sigaction函数
- 总结
Linux信号概念
信号在我们的生活中无处不在,常见的如电话铃声,闹钟等,这些的信号都是给我们传达某些信息,在接收到信息之后来选择采取什么样的措施,因此我们可以将信号理解为传达信息的事物。在Linux中也有这么一批信号,信号是操作系统提供的一种机制,用于通知进程发生了某种事件或异常情况。当特定的事件发生时(例如按下 Ctrl+C 来中断程序),操作系统会生成相应的信号,并将其发送给目标进程。总之,信号是操作系统提供的一种重要的进程间通信机制,操作系统捕获和分发信号,允许进程对信号进行响应和处理。
信号种类
Linux中有多种不同的信号,每个信号都有一个编号和一个宏定义名称,例如1表示SIGHUP,2表示SIGINT等等,这些宏定义可以在signal.h中找到。
在linux中也可以用kill -l
命令来查看信号,以下信号可以分为两个主要类别:实时信号和标准信号。实时信号是为了满足实时应用程序的需求而引入的,具有较高的优先级和可预测的传递和处理时间。实时信号的信号编号通常在34到64之间,例如SIGRTMIN、SIGRTMIN+1等。它们具有实时信号队列,按照顺序接收和处理,以确保信号的顺序性。实时信号有两个优先级:实时信号和实时时钟信号。
标准信号是常规的信号类型,用于各种进程通信、控制和处理异常情况。标准信号的信号编号通常在1到31之间,包括SIGINT、SIGHUP、SIGTERM等。它们没有明确定义的优先级,信号处理通常较为基本。这两种信号类型都用于与进程通信、控制进程行为和处理各种事件。实时信号主要用于实时应用,而标准信号则更常用于一般用途的进程间通信和控制。
Linux信号产生
Linux信号可以通过多种方式产生,通常由内核、其他进程或硬件事件触发。例如进程终止或错误时也会产生信号进程执行发生错误时,如除以零,产生SIGFPE信号,进程访问未分配给它的内存或无效内存时,产生SIGSEGV信号。当用户在终端按下Ctrl+C会产生SIGINT信号。用户在终端按下Ctrl+\,产生SIGQUIT信号。用户按下Ctrl+Z,将进程置于后台并产生SIGTSTP信号。
在某些情况下,系统调用失败也会产生特定的信号,如SIGPIPE。这些信号产生方式可以由操作系统、应用程序或用户发起。处理这些信号可以通过注册信号处理函数来定义接收到信号时要执行的特定操作。例如可以使用kill命令向另一个进程发送信号。
此外还可以使用kill()函数向另一个进程发送信号,kill 函数通常用于终止一个进程。它是Unix和类Unix操作系统中的系统调用之一。
它的参数 pid代表的是要终止的进程的进程ID(PID)。sig表示要发送给目标进程的信号。通常,SIGTERM(15号信号)用于请求优雅地终止进程,而SIGKILL(9号信号)用于强制终止进程。
如果成功发送信号,则返回0。如果失败,则返回-1,并设置errno来指示错误的原因。如下代码:
void Usage(string str)
{
cout << "Usage : \n\t";
cout << str << "int sig pid_t pid" << endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signo = atoi(argv[1]);
int target_id = atoi(argv[2]);
int ret = kill(target_id, signo); //给指定进程发送指定信号
if(ret == -1)
{
cout << errno << strerror(errno) << endl;
}
return 0;
}
这是一个简单的C++程序,用于给指定的进程发送信号。Usage 函数用于打印程序的用法说明,它接受一个字符串参数 str,用 cout 打印用法信息。main 函数首先检查命令行参数的数量,如果不等于3个(期望的参数数量),则调用 Usage 函数打印用法信息,并使用 exit(1) 终止程序。如果命令行参数数量正确,它将解析这两个参数为整数 signo 和 target_id,分别表示要发送的信号和目标进程的PID。然后,调用 kill 函数来向指定的进程发送指定的信号,并将返回值存储在 ret 变量中。最后,程序检查 kill 函数的返回值,如果返回值为-1,表示发送信号失败。它将使用 errno 和 strerror 函数来打印错误信息。结果如下:
异步
异步是指在程序执行过程中,某个操作可以独立于主程序流程,不阻塞主程序的执行,而在完成后通知主程序或回调执行相应的处理。简而言之,异步操作允许程序在等待某些事件完成的同时,可以继续执行其他任务。举例来说,假设你在下载一个大文件,如果是同步操作,程序会一直等待下载完成才能进行其他操作。而异步下载则会允许你在下载的同时执行其他任务,当下载完成时会通过回调或其他机制通知你。异步通常用于处理需要等待的操作,比如网络请求、文件读写、图形界面事件等。它可以提高程序的响应性和效率,因为在等待某些操作完成的同时,可以执行其他任务,充分利用了计算资源。
而信号相对于进程的控制流程来说就是异步的,因为对于进程来说,信号来源不确定,信号可以来自多种来源,包括其他进程、操作系统或硬件事件。进程无法预测何时会收到信号,因此无法像在同步控制流程中那样精确地安排信号的处理。并且信号可以随时中断进程,当进程正在执行某个任务时,随时可能收到信号。这个信号会中断进程当前的执行,转而执行与信号相关联的信号处理函数。这种中断式的行为使得信号处理是异步的,因为进程无法控制何时会被中断。信号处理被时间也是不确定的,信号处理函数的执行时间是不确定的,取决于信号的类型和处理函数的复杂性。进程无法预测信号处理将花费多长时间,因此无法以确定的方式管理信号的异步性质。
综上所述,可以说信号相对于进程的控制流程来说是异步,因为它们可以在进程不预期的时间中断进程的执行,且信号的来源和处理时间都是不确定的,无法由进程精确地控制。这使得处理信号的机制更加灵活,但也需要谨慎处理,以确保进程的稳定性和可靠性。
LInux信号阻塞
Linux中的信号阻塞是一种机制,它可以防止特定信号在某段代码执行期间被中断。这样可以确保关键部分的代码不会被信号中断,从而保证程序的一致性和可靠性。
递达、未决、阻塞、忽略
递达、未决、阻塞、忽略这些术语通常用于描述与信号处理相关的状态和操作:
- 信号递达(Delivered):表示信号已经成功传递给目标进程,但目标进程尚未开始处理信号。信号已经到达进程的信号队列中,等待被处理。
- 信号未决(Pending):未决状态表示信号已经递达,但目标进程还没有处理完该信号。在某些情况下,一个进程可以同时拥有多个未决信号,这些信号排队等待处理。
- 信号阻塞(Blocked):阻塞表示进程已经将某些信号阻塞,即在信号掩码中将这些信号的位设置为1。阻塞信号的目的是防止它们在关键代码段执行期间中断进程。
- 信号忽略(Ignored):忽略表示进程对特定信号的处理方式是忽略。当进程忽略信号时,该信号不会触发默认的操作或用户定义的信号处理函数,而会被静默忽略。
这些概念一起用于描述进程如何处理信号。例如,一个进程可以选择忽略某些信号,将其他信号阻塞以在关键部分执行时不被中断,然后处理已递达但未决的信号。递达和未决信号的状态告诉我们哪些信号已经到达,哪些正在等待被处理。信号处理是进程中管理这些状态的关键部分。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号集操作函数
在操作系统中,通常会有三张表与信号处理相关:阻塞表(Block Table)、未决表(Pending Table) 和 处理程序表(Handler Table)。这些表用于管理和描述进程的信号处理状态和方式。
block表维护了有关哪些信号被当前进程阻塞的信息,对于每个进程,内核会维护一个信号掩码,该掩码是一个位掩码,用于表示哪些信号被阻塞,哪些可以传递给进程。控制信号掩码可以通过系统调用如 sigprocmask 进行,使进程可以选择性地阻塞或解除阻塞信号。
pending表记录了哪些信号已经被递送给进程,但尚未被处理。每个进程都有一个未决信号集合,这些信号在被递送后保持未决状态,直到被处理。未决信号的状态可以在内核中进行管理,以确保每个信号都得到适当的处理。
handler表是一个数据结构,用于将信号与它们的处理方式(处理函数)相关联。对于每个信号,内核会维护一个处理程序表,记录了该信号的默认操作或用户自定义处理函数。当进程接收到一个信号时,内核会查找相应信号的处理程序表,并执行与之相关联的处理函数。
这三张表协同工作,允许进程管理信号的接收和处理。阻塞表控制哪些信号会被阻塞,未决表跟踪哪些信号等待处理,而处理程序表定义了每个信号的行为。这些机制使进程能够在异步情况下处理信号,以应对不同类型的事件和通信。
Linux中常规信号在递达之前产生多次的话只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里统计多次。
从这里就可以知道,每个信号只有一个 bit 的未决和阻塞标志,不是0就是1。因此在Linux中未决和阻塞标志可以用相同的数据类型sigset_t来存储。sigset_t 是一个数据类型,通常用于表示信号集合,用于管理和操作一组信号的状态。它是 POSIX 标准中定义的一种数据类型,用于在信号处理中指定和操作一组信号。sigset_t 通常是一个用于存储信号掩码的数据结构。信号掩码是一个位掩码,其中每一位对应一个特定的信号。如果某个信号的位被设置为 1,表示该信号被阻塞;如果为 0,表示该信号可以传递给进程。通过设置和修改 sigset_t 对象,可以实现对信号的阻塞和解除阻塞。以下是一些与 sigset_t 相关的常用函数和操作:
- sigemptyset(sigset_t *set):用于初始化一个空的信号集合,即将所有信号位都设置为 0。
- sigfillset(sigset_t *set):用于将一个信号集合设置为包含所有可能的信号,即将所有信号位都设置为 1。
- sigaddset(sigset_t *set, int signum):用于将指定信号添加到信号集合中,将对应信号位设置为 1。
- sigdelset(sigset_t *set, int signum):用于从信号集合中移除指定信号,将对应信号位设置为 0。
- sigismember(const sigset_t *set, int signum):用于检查指定信号是否包含在信号集合中。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。其他四个函数都是成功返回0,出错返回-1。sigset_t 对象通常在信号处理函数中使用,以防止在关键代码段执行时被特定信号中断。通过操作 sigset_t 对象,可以实现对信号的精细控制和管理。
阻塞信号集操作函数
对于进程的阻塞信号集可以调用函数sigprocmask来读取或更改,sigprocmask 是一个 POSIX 标准定义的系统调用,该系统调用的函数原型如下:
其中 how 参数指定了如何修改信号屏蔽字,有以下3种选择:
- SIG_BLOCK:将 set 中的信号添加到当前的信号屏蔽字中。
- SIG_UNBLOCK:从当前的信号屏蔽字中移除 set 中的信号。
- SIG_SETMASK:将当前的信号屏蔽字设置为 set 中的值。
set 参数和 oldset 参数都是一个指向 sigset_t 类型的指针,set 参数包含了要设置的新的信号屏蔽字。oldset 参数用于存储调用 sigprocmask 前的信号屏蔽字。如果不关心原始的信号屏蔽字,可以将 oldset 设为 NULL。
未决信号集操作函数
对于进程的未决信号集可以用 sigpending 函数来查看,sigpending 是一个 POSIX 标准定义的函数,用于检索当前进程中的未决信号,函数原型如下:
set 参数是一个指向 sigset_t 类型的指针,用于存储检测到的未决信号。这个函数会将当前进程中的未决信号的信号集合存储到 set 中。sigpending 函数通常在信号处理程序中使用,以确定哪些信号在当前上下文中已经到达但尚未被处理。未决信号的检查可以帮助进程采取相应的行动来处理这些信号,比如选择性地解除信号阻塞、执行相应的处理程序等。
未决信号是在 sigpending 函数被调用时确定的,因此在函数返回后,仍然可能会有新的信号到达并成为未决信号。
Linux信号捕捉
Linux的信号产生之后,就需要有东西能接收到这个信号,然后在决定接收到这个信号后采取什么样的措施。如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
signal函数
在Linux中,可以通过注册信号处理函数来捕捉信号,以定义在接收到特定信号时要执行的操作。其中就可以使用signal()
函数来对信号进行捕捉处理,signal()函数是用于在Linux和UNIX系统中注册信号处理函数的函数之一,它的原型如下:
signal()函数的主要作用是告诉操作系统,在接收到特定信号(由signum指定)时,去调用特定的信号处理函数(由handler指定)。这个函数返回一个函数指针,该指针指向以前注册的信号处理函数(通常是默认处理方式)。一般你可以忽略这个返回值,但如果需要以后恢复默认的信号处理行为的话就可以使用它。如下代码:
int main()
{
while(1)
{
cout << "run......." << endl;
sleep(1);
}
return 0;
}
代码是一个死循环,当我们按下Ctrl + C的时候进程会收到一个2号信号SIGINT从而结束进程。
但是当使用signal函数对2号信号进行自定义捕捉时,这时候再去按下Ctrl + C就不再是执行默认动作结束进程,而是去执行我们的自定义动作,如下代码:
void handler(int signo)
{
cout << "get signo : " << signo << endl;
}
int main()
{
signal(2, handler);
while(1)
{
cout << "run......." << endl;
sleep(1);
}
return 0;
}
在这段代码中,当按下Ctrl + C发送2号信号时,程序会去执行handler方法并打印出捕捉到的信号。
sigaction函数
sigaction 是一个 POSIX 系统调用,用于检查或修改进程的信号处理行为。与 signal 函数相比,sigaction 提供了更完整和灵活的控制机制来管理信号处理。以下是 sigaction 的函数原型:
其中参数 signum表示指定要操作的信号。act 是一个指向 struct sigaction 的指针,该结构描述了新的信号处理行为。参数 oldact 是一个指向 struct sigaction 的指针,用于保存之前的信号处理行为。如果不关心之前的行为,可以设置为 NULL。
struct sigaction 结构包含以下主要字段:
- sa_handler:指向信号处理函数的指针。
- sa_sigaction:用于更高级的信号处理,并提供关于信号的更多信息。
- sa_mask:在处理该信号时要阻塞的其他信号集合。
- sa_flags:用于指定各种信号处理选项。
- sa_restorer:此字段已不再使用,但在一些旧的系统中仍然存在。
sa_flags 可能包括以下选项之一或多个:
- SA_NOCLDSTOP:如果信号是 SIGCHLD,则在子进程停止或继续时不会产生该信号。
- SA_NOCLDWAIT:使父进程在其子进程终止时不创建僵尸进程。
- SA_NODEFER:不将处理的信号自动添加到信号掩码中。
- SA_ONSTACK:使用为信号专门定义的备用堆栈,如果有的话。
- SA_RESETHAND:在信号处理程序被调用后,将信号的处理程序重置为默认值。
- SA_RESTART:使某些被信号中断的系统调用重新启动。
- SA_SIGINFO:指示信号处理程序应该使用 sa_sigaction 字段而不是 sa_handler。
总的来说,sigaction 提供了一个强大的机制来查看和更改信号处理行为。由于它提供了对信号处理的详细控制,所以它通常被推荐为设置信号处理程序的首选方法。如下代码:
void sighandler(int signo)
{
cout << "get signo : " << signo << endl;
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = sighandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 2);
sigaddset(&act.sa_mask, 3);
sigaction(2, &act, &oldact);
sigaction(3, &act, &oldact);
while(1)
{
cout << "PID : " << getpid() << endl;
sleep(1);
}
return 0;
}
这段代码设置了信号处理函数 sighandler,然后通过 sigaction 函数为信号2和信号3分别设置了新的信号处理方式,同时设置了信号屏蔽集,以确保在处理这些信号时不会被其他信号中断。然后,程序进入无限循环,输出PID,并持续运行,等待信号的触发。当信号2或3被触发时,将会调用 sighandler 函数来处理它们。
总结
文章中介绍了Linux中信号的概念、信号的种类以及信号的产生方式进行分析,并对Linux信号的阻塞和捕捉函数进行分析,对函数的参数逐一分析,并提供示例代码供参考。总之,Linux信号是一种重要的进程通信机制,允许进程之间以及操作系统与进程之间进行异步通信,以响应各种事件和异常情况。了解如何正确使用和处理信号对于编写可靠的Linux应用程序至关重要。
码文不易,客官如果觉得文章对你有帮助的话就来一个三连吧👍。