文章目录
- 0.对于信号捕捉的理解
- 1.信号处理的时机
- 1.1 何时处理信号?
- 1.2 内核态和用户态
- 1.3 内核态和用户态的切换
- 2.了解寄存器
- 3.信号捕捉的原理
- 4.信号操作函数
- 4.1sighandler_t signal(int signum, sighandler_t handler);
- 4.2int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 5.信号相关题目
0.对于信号捕捉的理解
信号捕捉,并没有创建新的进程或者线程
1.信号处理的时机
信号产生之后,信号可能无法被立即处理,在合适的时候被处理,合适的时候指的是什么时候?
1.1 何时处理信号?
OS什么时候进行信号处理
管理信号的数据结构都在进程PCB内,而进程PCB属于内核数据 ⇒ 信号的检测和处理必须在内核态(交由操作系统)进行 ⇒ 从内核态返回用户态的时候,进行信号的检测和处理。
为什么要在内核态返回用户态之前进行信号处理呢?
内核态下的任务通常更为重要,优先级更高。要先保证内核态下的代码执行完毕,才能进行信号处理工作。
整个过程
用户执行用户代码时(用户态)需要调用系统接口/遇到缺陷陷阱异常/进程替换/时间片用完/调用汇编指令需要转到内核态,在内核态返回用户态时进行信号处理。
了解中断信号int 80【interrupt 80】
在Linux中,int 0x80 是一个特定的软件中断,它通常用于在32位系统上进行系统调用。当你在用户态的代码中使用 int 0x80 指令时,它会导致CPU切换到内核态,并执行与中断号 0x80 相关的中断处理程序。【可以理解为内置在系统调用函数中,一旦进行系统调用,就触发此信号】
在Linux的32位架构中,int 0x80 允许用户态程序请求内核执行某些服务,例如打开文件、读取文件、创建进程等。这些服务被称为系统调用。当执行 int 0x80 时,CPU会将当前的寄存器状态保存到内核栈中,并跳转到中断向量表中与 0x80 对应的地址,开始执行内核代码。
系统调用的参数通常被放置在特定的寄存器中,而返回结果也会被放置在某个寄存器中。这样,当内核完成系统调用的处理后,它会恢复用户态的寄存器状态并返回到用户态的代码。
然而,在64位Linux系统中,情况有所不同。64位Linux使用不同的机制来进行系统调用,而不是依赖于 int 0x80。在64位Linux中,系统调用通常是通过 syscall 指令或 sysenter 指令来完成的,而不是通过软件中断。
因此,如果你在64位Linux代码中尝试使用 int 0x80 进行系统调用,它很可能不会按预期工作,甚至可能导致程序崩溃或未定义的行为。在64位Linux上进行系统调用时,你应该使用为64位架构设计的适当机制。
总结来说,int 0x80 是Linux 32位架构中用于系统调用的软件中断,但在64位Linux中不再使用。在64位Linux上,应该使用专为64位架构设计的系统调用机制。
1.2 内核态和用户态
内核态和用户态是操作系统中的两种不同的执行模式,用于区分操作系统内核的执行环境和用户程序的执行环境。
**内核态(Kernel Mode)**是操作系统内核运行的特权模式,执行的是内核代码。在内核态下,操作系统具有完全的访问权限,可以执行特权指令、访问系统资源和控制硬件设备。内核态下的代码可以执行任意的操作,包括修改内存、修改寄存器、访问硬件等。内核态下的代码运行在操作系统内核的上下文中,拥有最高的权限和最大的控制权。内核态是一个操作系统执行自己代码的一个状态,具备非常高的优先级。
**用户态(User Mode)**是用户程序运行的非特权模式,执行的是用户代码。在用户态下,用户程序只能执行受限的操作,不能直接访问系统资源和控制硬件设备。用户态下的代码运行在用户进程的上下文中,受到操作系统的保护和限制。用户程序需要通过系统调用接口来请求操作系统提供的服务和资源,例如文件操作、网络通信、内存分配等。用户态是一个受管控的状态。
1.3 内核态和用户态的切换
- 操作系统通过将进程或线程从用户态切换到内核态,以及从内核态切换回用户态,来实现对系统资源和硬件设备的管理和保护。
- 当进程或线程遇到以下情况时,需要将控制权转移到内核态,由操作系统内核来完成相应的操作。调用系统调用函数时;操作系统调度和切换进程时;进程出现异常时…一旦操作完成,操作系统将把控制权返回给用户程序,使其继续在用户态下执行。
- 内核态和用户态的切换是由操作系统的调度器和硬件支持来实现的。切换到内核态需要保存用户程序的上下文信息,并加载内核的上下文信息;切换回用户态需要恢复用户程序的上下文信息。这种切换的开销相对较大,因此操作系统会尽量减少内核态和用户态之间的切换次数,以提高系统的性能和响应速度。
- 从进程地址空间的角度来看,内核态和用户态之间的切换涉及到地址空间的切换和访问权限的改变。
- 地址空间切换:在x86架构中,每个进程都有自己的虚拟地址空间,包括用户空间和内核空间。用户空间用于存放进程的用户代码和数据,而内核空间用于存放操作系统的内核代码和数据。当进程从用户态切换到内核态时,需要切换到内核地址空间,以便访问和执行内核代码和数据。这通常涉及到切换页表,将CR3寄存器(表示当前CPU的执行权限)的值设置为内核页表的物理地址,从而实现地址空间的切换。CPU有两套寄存器,一套可见,一套不可见(自己使用)
- 访问权限改变:用户态和内核态具有不同的访问权限。在用户态下,进程只能访问自己的用户地址空间,而不能直接访问内核地址空间。而在内核态下,操作系统具有对整个地址空间的完全访问权限,包括用户地址空间和内核地址空间。当进程从用户态切换到内核态时,访问权限会发生改变,进程可以访问和执行内核代码和数据。
- 在进行内核态和用户态切换时,通常会通过系统调用或异常来触发切换。当进程执行系统调用或发生异常时,处理器会从用户态切换到内核态,并跳转到相应的内核代码进行处理。在处理完系统调用或异常后,处理器会从内核态切换回用户态,恢复进程的执行。
- 通过切换页表和改变访问权限,进程可以在内核态下访问和执行内核代码和数据,从而实现与操作系统的交互和系统服务的调用。
- 不要觉得内核很神奇,内核也是在所有进程的地址空间上下文中跑得!
可以执行进程切换的代码吗???
凭什么用户有权利执行OS的代码?
处于内核态时就可以【实际上还是OS在执行OS的代码】
执行系统调用接口时都发生了什么?
在Linux系统中,执行系统调用接口时,会涉及到用户态、内核态以及CR3寄存器的切换。以下是执行系统调用接口时发生的主要步骤:
首先,需要明确的是,用户态和内核态是Linux系统中两种不同的运行模式。用户态是程序运行在较低的权限级别,不能直接访问系统底层的资源,如硬件设备和内核态的代码。而内核态则是操作系统运行在较高的权限级别,能够直接访问硬件设备和系统资源,并且可以执行操作系统内核的代码。
在执行系统调用接口时,进程原本处于用户态下执行,通过调用系统调用接口,如使用软中断指令“int 0x80”来触发中断请求,使进程从用户态切换到内核态。这个中断请求会导致CPU中断当前的执行流程,并跳转到内核中相应的中断处理程序。
在切换的过程中,CR3寄存器起到了关键的作用。CR3寄存器是CPU中的一个寄存器,用于记录当前进程的状态。当CR3寄存器的值为0时,表示当前状态是内核态;而当CR3寄存器的值为3时,表示当前状态是用户态。因此,在发生系统调用时,CR3寄存器的值会从3变为0,标志着进程从用户态切换到内核态。
一旦进程进入内核态,它就可以访问内核空间和系统资源,并执行内核代码。内核会根据系统调用的参数执行相应的操作,如打开文件、读取文件等。完成这些操作后,内核会将结果返回给用户态的进程,并再次通过修改CR3寄存器的值,将进程从内核态切换回用户态,以便继续执行用户态的代码。
总的来说,执行系统调用接口时,进程会通过中断机制从用户态切换到内核态,利用内核提供的服务完成特定的任务,然后再返回用户态继续执行。在这个过程中,CR3寄存器的值会在用户态和内核态之间切换,确保进程在不同模式下具有不同的权限和访问资源的方式。
进程切换时都发生了什么?
在Linux系统中,用户态和内核态是两种不同的运行模式,它们的主要区别在于程序所处的权限和访问硬件资源的方式。用户态是指程序运行在较低的权限级别,不能直接访问系统底层的资源,如硬件设备和内核态的代码。而内核态则是操作系统运行在较高的权限级别,能够直接访问硬件设备和系统资源,并且可以执行操作系统内核的代码。
进程切换是操作系统中的一项重要任务,涉及从当前运行的进程切换到另一个进程。在进程切换的过程中,CR3寄存器起着关键的作用。CR3寄存器是控制寄存器之一,用于存储页目录的物理地址,是Linux系统中内存管理的重要部分。
当发生进程切换时,以下是发生的主要事件:
保存当前进程的状态:首先,操作系统需要保存当前运行进程的状态。这包括保存该进程在用户态下的寄存器值、内存状态以及其他相关信息。这些信息被保存在进程控制块(PCB)或任务状态段(TSS)等数据结构中,以便后续恢复该进程时可以重新加载这些状态。
更新CR3寄存器:接下来,操作系统需要更新CR3寄存器,以指向新进程的页目录。这是因为每个进程都有自己的虚拟地址空间,通过修改CR3寄存器,CPU可以切换到新进程的地址空间。
加载新进程的状态:然后,操作系统加载新进程的状态。这包括从PCB或TSS中恢复新进程的寄存器值、内存状态等。这样,新进程就可以从它之前被中断的地方继续执行。
切换到内核态:进程切换本身是由操作系统内核来执行的,因此在这个过程中,系统会短暂地切换到内核态。在内核态下,操作系统具有更高的权限,可以执行必要的操作来完成进程切换。
执行新进程:最后,一旦新进程的状态被加载并CR3寄存器被更新,CPU就可以开始执行新进程了。此时,系统仍然处于内核态,但很快就会切换到用户态来执行新进程的用户代码。
在整个进程切换过程中,用户态和内核态的切换是透明的,用户通常不会感知到这一过程。然而,对于操作系统和硬件来说,这是一个复杂且关键的过程,需要确保正确、高效地执行。
需要注意的是,进程切换的具体实现可能因操作系统和硬件架构的不同而有所差异。但上述描述提供了一个大致的框架,有助于理解进程切换时发生的主要事件。
用户态如何转到内核态?
用户态转到内核态的过程主要通过以下三种方式实现:
系统调用:这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序来完成工作。系统调用的机制实际上利用了操作系统为用户特别开发的一个中断机制,即软中断。当系统调用发生时,CPU会从用户态切换到内核态,并执行相应的内核代码来处理该调用。
异常和中断处理:当CPU执行运行在用户态下的程序时,如果发生了某些事先不可知的情况,如硬件故障、程序错误等,就会产生异常。此时,CPU会中断当前的用户态程序执行,并切换到内核态来处理这些异常。同样,外部设备的中断也会触发从用户态到内核态的切换,以便操作系统能够响应并处理这些中断事件。
定时器和任务调度:操作系统中的定时器和任务调度器也可能触发从用户态到内核态的切换。例如,当定时器到期时,CPU会切换到内核态来执行定时器的处理函数。同样,当任务调度器决定切换当前运行的进程时,也会触发从用户态到内核态的切换,以便进行进程状态的保存和恢复等操作。
在切换过程中,CPU会保存当前用户态的上下文信息(如寄存器状态、内存状态等),并加载内核态的上下文信息。这样,当内核态代码执行完毕后,CPU可以恢复之前的用户态上下文并继续执行用户程序。
需要注意的是,从用户态切换到内核态是一个受操作系统严格控制的过程,以确保系统的安全性和稳定性。操作系统通过一系列机制和策略来管理这种切换,以防止恶意程序或错误操作对系统造成损害。
为什么linux下,对信号的处理发生在从内核态返回用户态的时候?
在Linux下,信号处理发生在从内核态返回用户态的时候,这主要是出于系统安全和执行效率的考虑。
首先,我们需要理解信号处理的基本机制。在Linux中,信号可以由内核产生(如硬件异常),也可以由用户进程产生,并由内核传送给特定的进程或线程。这些信号可能表示某种事件或异常的发生,如用户按下Ctrl+C产生的SIGINT信号,或者CPU检测到的内存非法访问等。
当内核检测到需要向某个进程发送信号时,它会将该信号与该进程的描述符关联起来。然而,此时进程可能正在用户态下执行,不能直接处理这个信号。因此,内核会记录下这个待处理的信号,等待进程下一次进入内核态时再进行处理。
接下来,当进程由于某种原因(如系统调用、异常、中断等)从用户态切换到内核态时,内核会检查该进程是否有待处理的信号。如果有,内核会首先处理这些信号。信号处理函数运行在内核态,但用户的信号处理程序是在用户进程的代码段中,所以信号处理程序本身是在用户态执行的。因此,处理完信号后,内核需要将控制权从内核态切换回用户态,以便执行用户的信号处理程序。
这种设计有几个好处:
安全性:内核态具有更高的权限,可以访问和修改系统的关键数据和结构。如果允许在内核态直接执行用户提供的信号处理程序,那么恶意程序可能会利用这一点来执行非法操作或攻击系统。因此,将信号处理过程放在从内核态返回用户态的时机,可以确保信号处理程序在用户态下执行,从而限制其权限和潜在的风险。
执行效率:信号处理通常涉及一些复杂的操作,如保存和恢复进程状态、调用用户定义的信号处理函数等。如果在内核态直接处理这些操作,可能会导致内核态的长时间占用,影响系统的整体性能。通过将信号处理推迟到从内核态返回用户态的时机,可以确保内核态的短暂占用,提高系统的并发性和响应速度。
综上所述,Linux下将信号处理安排在从内核态返回用户态的时候,既保证了系统的安全性,又提高了执行效率。这是Linux信号处理机制的一个重要特点。
如果某个程序从未转到内核态,那它是不是就无法处理信号了。
当一个进程的时间片用完,就要被OS调度,调度这个动作是内核态。
如果某个程序从未转到内核态,那么它确实无法处理由操作系统内核发送的信号。在Linux中,信号处理机制是依赖于内核态和用户态之间的切换的。当内核需要向进程发送信号时,它会在进程的控制块(如task_struct结构)中标记相应的信号状态。然而,进程本身在用户态运行时是无法直接感知到这些信号的。
只有当进程由于某种原因(如系统调用、异常、硬件中断等)从用户态切换到内核态时,内核才会检查并处理该进程待处理的信号。如果进程从未进入内核态,那么这些信号就会一直被挂起,无法得到处理。
在实际操作系统中,进程几乎总是需要与内核进行交互,无论是通过系统调用来请求服务,还是由于硬件事件(如中断)导致的上下文切换。因此,一个程序完全不进入内核态是非常罕见且不符合实际操作系统运行模式的。
总的来说,如果一个程序从未转到内核态,那么它确实无法处理由内核发送的信号。但在实际的操作系统环境中,这种情况几乎不可能发生,因为进程需要与内核进行频繁的交互来完成其工作。
2.了解寄存器
在计算机科学中,寄存器是一种非常重要的部件,它是中央处理器(CPU)内部的组成部分,主要用于存储指令、数据和位址。寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。以下是关于计算机中寄存器的详细介绍:
一、寄存器的定义和功能
寄存器是一种有限存储容量的高速存储部件,用于暂存指令、数据和位址。它们是计算机内部的高速存储单元,用于快速访问和存储数据。由于寄存器的读写速度非常快,因此它们被广泛应用于各种计算和数据处理任务中。
二、寄存器的分类
根据功能和用途,寄存器可以分为以下几类:
数据寄存器:数据寄存器用于存储操作数和运算结果。在执行算术和逻辑运算时,CPU会将需要操作的数据从内存加载到数据寄存器中,进行相应的运算,并将结果存回寄存器或者内存中。
地址寄存器:地址寄存器用于存储指令或数据在内存中的地址。当CPU需要读取或写入内存中的数据时,会使用地址寄存器来指示要操作的位置。
程序计数器:程序计数器(PC)是一种特殊的寄存器,用于存储下一条要执行的指令的地址。每当CPU完成一条指令的执行后,PC会自动递增以指向下一条指令的地址,实现程序的顺序执行。
三、常用的寄存器
在计算机中,有一些常用的寄存器,它们各自扮演着特定的角色:
通用寄存器:如AX、BX、CX、DX等,它们可以用于存储各种类型的数据,并参与各种运算操作。
变址寄存器:如SI、DI等,主要用于在内存寻址时提供偏移量。
指针寄存器:如SP、BP等,用于存储栈顶地址或基地址。
指令指针寄存器:如IP,存储下一条要执行的指令的地址。
段寄存器:如CS、SS、DS、ES等,用于存储内存段的起始地址。
此外,还有一些32位寄存器,如EAX、ECX、EDX、EBX等,它们是AX、BX、CX、DX的延伸,各自具有特定的功能,如EAX是累加器,用于执行加法和乘法操作;EBX是基地址寄存器,用于在内存寻址时存放基地址;ECX是计数器,用于重复指令和循环指令的计数;EDX则用于存放整数除法产生的余数。
四、寄存器的作用
寄存器在计算机中扮演着至关重要的角色,主要体现在以下几个方面:
存储指令和数据:寄存器可以快速存储和访问指令和数据,提高计算机的运行速度。
传输数据:寄存器可以作为数据传输通道,在寄存器之间快速传输数据,实现高速运算。
辅助运算:寄存器可以协助微处理器进行各种运算,如算术运算、逻辑运算等。
总的来说,寄存器是计算机内部的重要组成部分,它们在提高计算机性能和运算速度方面发挥着关键作用。了解并熟练掌握寄存器的使用,对于深入理解计算机的工作原理和提高编程效率具有重要意义。
3.信号捕捉的原理
- 信号检测的过程包括:检测pending信号集 --> 检测信号屏蔽字 --> 查找handler信号处理方法表
- 如果信号的处理方法是默认或忽略,则直接在内核态完成相应的处理动作(终止进程、暂停进程、清除pending标志),不需要切换到用户态。遇到要发生核心转储的信号,在进程退出前,OS把当前进程的核心数据以用户身份dump到磁盘中并以当前进程pid命名,内核态即可完成。遇到暂停进程的信号,不用回到用户态,执行cpu调度算法即可。
- 如果信号的处理方法是自定义捕捉,则切换到用户态执行信号处理程序,完成后再次陷入内核,清除对应信号的pending标志,最后返回用户态继续执行用户程序。
内核态在处理信号时,遇到要终止进程的信号,如果用户态的代码还有一些必要操作(如释放动态分配的内存、关闭打开的文件、断开网络连接等)没完成,怎么办?
在Linux中,atexit是一个特殊的函数,用于在正常程序退出时调用已注册的函数。这些在程序终止时调用的函数被称为终止处理函数。atexit函数的作用是注册这些终止处理函数,确保它们在main函数执行结束后能够被调用。
atexit函数的原型是int atexit(void (*func)(void)),它接受一个函数指针作为参数,该函数指针指向一个没有参数也没有返回值的函数。一个进程可以登记多达32个这样的函数,这些函数将由exit自动调用。
需要注意的是,exit调用这些注册函数的顺序与它们登记时的顺序相反。如果一个函数被多次登记,那么它也将被多次调用。
atexit函数的主要应用场景之一是在程序终止前执行一些清理工作,如释放动态分配的内存、关闭打开的文件、断开网络连接等。通过注册终止处理函数,可以确保这些清理工作能够在程序正常退出或异常终止时得到执行,从而避免资源泄漏和其他潜在问题。
总的来说,atexit是Linux下一种强大的机制,用于在程序退出时执行必要的清理和收尾工作。
在处理信号时处于内核态,内核态当然可以执行用户态的代码包括用户自定义的信号处理函数,因为内核态有较高的权限,但是内核不去执行,即便从内核态到用户态的互相转换要耗费时间和资源,OS的设计者仍然要让用户态去执行用户的信号处理函数代码,为什么?
因为信号处理函数是用户提供的,如果以内核态执行用户代码的话,由于内核态具有完全的访问权限,用户代码可能会修改系统的重要数据,从而导致系统资源和硬件设备遭到破坏。
Linux系统中的信号处理函数是异步执行(多执行流执行)的,它会打断当前正在执行的代码。因此,在编写信号处理函数时,应避免使用不可重入的函数、全局变量等可能引发数据竞争的操作。
⇒ 操作系统不相信任何人 ---- 不能用内核态执行用户的代码!
题外话
OS允许把把内核态的数据给用户态访问比如内核态读取(系统调用接口read)磁盘数据到内核级缓冲区,然后拷贝给用户级缓冲区给用户看。
sigset_t需要把数据从用户态拷贝到内核态。这OS都是支持的,不支持的是让用户态的代码给内核态执行,因为如果一旦这些代码有非法操作(删除,拷贝重要核心数据) ⇒ 完犊子
信号处理的流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
4.信号操作函数
4.1sighandler_t signal(int signum, sighandler_t handler);
4.2int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
在Linux中,sigaction函数是用于设置和修改信号处理的机制,它提供了比早期的signal函数更精细的控制。sigaction的工作原理主要基于操作系统内核和进程之间的交互,以及信号与信号处理程序的关联。sigaction函数是用于获取和修改信号处理程序的系统调用函数。它可以用来注册一个信号处理函数,指定信号的处理方式以及在信号处理期间对信号的阻塞和解除阻塞。
以下是sigaction的工作原理的简要说明:
signum:是要设置的信号的编号
act:若act指针非空,则根据act修改该信号的处理动作(输入型)
oldact:若oldact指针非空,则通过oldact传出该信号原来的处理动作(输出型)
设置信号处理程序:当调用sigaction函数时,进程会向内核传递一个sigaction结构体,该结构体包含了与特定信号(由signum参数指定)相关的处理信息。这些信息包括一个信号处理函数(sa_handler或sa_sigaction字段),以及一个信号掩码(sa_mask字段),用于指定在信号处理函数执行期间哪些信号应该被阻塞。
内核保存信号处理程序:内核接收到sigaction调用后,会保存进程对特定信号的处理程序。这意味着当该信号被发送给进程时,内核知道应该调用哪个函数来处理它。
信号发送与接收:当进程接收到一个信号时,内核会中断该进程的执行,并检查该进程为该信号设置的处理程序。这个检查过程涉及查找之前通过sigaction设置的处理程序。
执行信号处理程序:一旦内核找到与信号关联的处理程序,它会根据sigaction结构体中的设置来执行相应的操作。这可能包括调用用户定义的信号处理函数,或者执行默认的信号处理动作(如终止进程)。
信号处理程序的执行环境:当信号处理函数被调用时,它运行在特殊的上下文中。根据sigaction的设置,一些信号可能会被阻塞,以防止它们在处理函数执行期间被接收。这有助于确保信号处理函数的正确和安全执行。
恢复进程执行:一旦信号处理函数执行完毕,内核会恢复被中断的进程的执行,继续执行其原来的任务。
需要注意的是,sigaction还允许通过oldact参数获取之前设置的信号处理程序,这使得程序可以保存和恢复信号处理程序的状态。
总的来说,sigaction的工作原理涉及进程与内核之间的交互,以及信号与信号处理程序的关联和执行。通过sigaction,程序员可以更加精细地控制信号处理过程,以适应各种复杂的应用场景。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
- 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
/* Structure describing the action to be taken when a signal arrives. */
struct sigaction
{
/* Signal handler. */
#ifdef __USE_POSIX199309
union
{
__sighandler_t sa_handler; /* Used if SA_SIGINFO is not set. */
void (*sa_sigaction)(int, siginfo_t *, void *); /* Used if SA_SIGINFO is set. */
} __sigaction_handler;
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
#else
__sighandler_t sa_handler;
#endif
__sigset_t sa_mask; /* Additional set of signals to be blocked. */
int sa_flags; /* Special flags. */
void (*sa_restorer)(void); /* Restore handler. */
};
sigaction的功能
- 指定信号的处理方式,可以选择默认、忽略或者自定义处理方式。
- 注册信号处理函数,当指定的信号发生时,执行对应的信号处理函数。
- 阻塞指定的信号,以避免在信号处理函数执行时被其他信号中断。
- 解除对指定信号的阻塞。
sa_mask的作用
用户态收到终止信号,转向内核态处理这种中断,假设识别到该信号的处理方式为自定义信号处理函数,则转向用户态去调用该函数,如果在调用该函数过程中,进程又收到了该终止信号或其他信号,那么进程是不是要停下手中的工作去转向内核态处理这种中断而进入递归/循环呢?显然OS为这种情况设置了处理方式。
-
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞,直到当前处理程序结束为止。
-
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段记录这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
测试:进程收到2 去调用处理函数 处理函数未执行完 又收到2 由于OS对这种情况的设置 我们应该能看到2此时被block 即pending对应位为1 当处理函数执行完 会再次调用处理函数处理新收到的2
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(sigset_t *pending)
{
for (int sig = 31; sig >= 1; sig--)
{
if (sigismember(pending, sig))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void handler(int signum)
{
cout << "获取了一个信号: " << signum << endl;
sigset_t pending;
int c = 20;
while (c--)
{
sigpending(&pending);
showPending(&pending);
sleep(1);
}
}
/*
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
*/
int main()
{
// signal(2, SIG_IGN); 测试:设置2号信号的处理函数为忽略 则oldact.sa_handler即为1
cout << "getpid: " << getpid() << endl;
// sigaction是内核数据类型 act, oldact是在用户栈定义的
struct sigaction act, oldact;
// 初始化
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 设置进当前调用进程的pcb中
sigaction(2, &act, &oldact); // 设置2号信号的处理函数为act.sa_handler
cout << "default action : " << (int)(oldact.sa_handler) << endl;
while (true)
sleep(1);
return 0;
}
常用信号
ctrl+c - SIGINT:2
ctrl+\ - SIGQUIT:3
ctrl+z - SIGTSTP
sa_mask字段的使用:屏蔽了某些信号
对于2号信号,当进程收到2,调用handler,在调用handler期间二次收到2,pending对应位变1,意味着在处理2时不会受到另一个2的影响,当第一个2处理完毕,pending对应位恢复成0,再次调用handler。对于被设为掩码的信号(假设为x),对于他的处理是这样的:当进程收到2,调用handler,在调用handler期间收到了x,由于x被屏蔽,所以x不会被处理,pending的x对应位变1,当2的handler调用结束,x才会被处理。总结:sa_mask的意义在于:你为某一个信号a设置了自定义处理函数,在调用这个自定义函数去处理信号a的期间,屏蔽了你所设置为sa_mask的信号,即你不想让handler在处理a的期间受到其他信号(被设置为sa_mask的信号)的影响。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(sigset_t *pending)
{
for (int sig = 31; sig >= 1; sig--)
{
if (sigismember(pending, sig))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void handler(int signum)
{
cout << "获取了一个信号: " << signum << endl;
sigset_t pending;
int c = 5;
while (c--)
{
sigpending(&pending);
showPending(&pending);
sleep(1);
}
}
/*
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
*/
int main()
{
// signal(2, SIG_IGN); 测试:设置2号信号的处理函数为忽略 则oldact.sa_handler即为1
cout << "getpid: " << getpid() << endl;
// sigaction是内核数据类型 act, oldact是在用户栈定义的
struct sigaction act, oldact;
// 初始化
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//int sigaddset(sigset_t *__set, int __signo)
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
sigaddset(&act.sa_mask, 6);
sigaddset(&act.sa_mask, 7);
// 设置进当前调用进程的pcb中
sigaction(2, &act, &oldact); // 设置2号信号的处理函数为act.sa_handler
cout << "default action : " << (int)(oldact.sa_handler) << endl;
while (true)
sleep(1);
return 0;
}
3号信号没有被设置为sa_mask
3号信号被设置为sa_mask
题外话:
- Ctrl+Z会使当前终端的前台进程进入停止态而不是退出
- 非可靠信号在进行注册时,会查看是否已经有相同信号添加到未决集合中,如果有则什么都不做,因此非可靠信号只会添加一次,因此处理完毕后会直接移除(准确来说是先移除,后处理)。
- 可靠信号会重复添加信号信息到sigqueue链表中,相当于可靠信号可以重复添加,处理完毕后,因为有可能还有相同的信号信息待处理,因此并不会直接移除,而是检测没有相同信号信息后才会从pending集合中移除
一个进程无法被kill杀死的可能有哪些?
- 这个进程阻塞了信号:信号被阻塞,则暂时不被处理(SIGKILL/SIGSTOP除外,因为无法被阻塞,这里说的是可能性,因此不做太多纠结)
- 用户有可能自定义了信号的处理方式:自定义处理之后,信号的处理方式有可能不再是进程退出
- 这个进程有可能是僵尸进程:僵尸进程因为已经退出,因此不做任何处理
- 这个进程当前状态是停止状态:进程停止运行,则将不再处理信号
僵尸进程无法被杀死吗?
僵尸进程是一个已经完成执行但在进程表中仍留有一个位置(即进程控制块PCB仍保留)的进程。这是因为它的父进程还没有对其进行回收,释放其PCB所占用的空间。僵尸进程不占用任何系统资源,除了进程表中的一个位置。
然而,僵尸进程本身并不执行任何代码,因此它不会像正常进程那样响应kill命令。kill命令是用来向一个进程发送信号以请求其终止的,但僵尸进程已经不再执行,所以kill命令对它没有直接作用。
要处理僵尸进程,通常的做法是找到其父进程,并让父进程正确地回收它。这通常涉及父进程调用wait()或waitpid()系统调用,以获取子进程的结束状态并释放其在进程表中的位置。
在某些情况下,如果父进程已经终止,而子进程变成了僵尸进程,init进程(PID为1的进程)会接管这些子进程,并负责回收它们。
因此,虽然kill命令不能直接杀死僵尸进程,但可以通过处理其父进程或等待系统接管来间接地解决这个问题。如果你确定某个进程是僵尸进程,并且需要处理它,你应该查找其父进程并确保父进程能够正确地回收它。如果父进程存在但无法正常工作,你可能需要终止父进程或重启系统来清除僵尸进程。
5.信号相关题目
若当前进程处于阻塞状态,则此时到来的信号暂时无法处理。
错误。信号会打断进程当前的阻塞状态去处理信号
程序运行从用户态切换到内核态的操作:中断/异常/系统调用
sin()库函数并不会引起运行态的切换
可重入函数
函数是否可重入的关键在于函数内部是否对全局数据进行了不受保护的非原子操作,其中原子操作指的是一次完成,中间不会被打断的操作,表示操作过程是安全的
因此如果一个函数中如果对全局数据进行了原子操作,但是因为原子操作本身是不可被打断的,因此他是可重入的
函数不可重入指的是函数中可以在不同的执行流中调用函数会出现数据二义问题
函数可重入指的是函数中可以在不同的执行流中调用函数而不会出现数据二义问题
两个线程并发执行以下代码,假设a是全局变量,初始为1,那么以下输出______是可能的?[多选]
void foo(){
a=a+1;
printf("%d ",a);
}
A.3 2
B.2 3
C.3 3
D.2 2
当函数内的操作非原子时因为竞态条件造成数据二义性
a=a+1 和 printf(“%d”, a) 之间有可能会被打断
2 3
:a初始值为1 当A线程执行完a=a+1后a是2,这时候打印2, 线程B执行时+1打印33 3
:当A线程执行完a=a+1后a是2,这时时间片轮转到B线程,进行+1打印3, 然后时间片轮转回来也打印33 2
:printf函数实际上并不是直接输出,而是把数据放入缓冲区中,因此有可能A线程将打印的2放入缓冲区中,还没来得及输出,这时候B线程打印了3,时间片轮转回来再打印22 2
:a=a+1本身就不是原子操作因此有可能同时进行操作,都向寄存器中加载1进去,然后进行+1后,将2放回内存,因此有可能会打印2和2