赞
踩
上次介绍了:(Linux:进程信号(一.认识信号、信号的产生及深层理解、Term与Core))[https://blog.csdn.net/qq_74415153/article/details/140624810]
信号未决(Pending):当信号产生时,会首先进入未决状态,即信号还没有被进程处理。此时,信号被标记为未决状态,等待进程处理。
信号递达(Delivery):当进程解除对信号的阻塞时,信号才会被递达,即信号被传递给进程的信号处理函数进行处理。
三种信号处理方式:
默认处理(Default Handling):每个信号都有一个默认的处理方式,当信号递达时,操作系统会执行默认的信号处理动作,传入
SIG_DFL
自定义处理(Custom Handling):进程可以通过设置信号处理函数(一般是handler)来自定义对信号的处理方式。当信号递达时,操作系统会调用进程设置的信号处理函数来处理信号
忽略处理(Ignore Handling):进程还可以选择忽略某个信号,即在信号递达时不做任何处理。通过将信号处理函数设置为
SIG_IGN
,进程可以忽略某个信号
阻塞信号:进程可以选择阻塞某个或多个信号,使其在未决状态下等待。被阻塞的信号不会递达,保持在未决状态,直到进程解除对此信号的阻塞
在操作系统中,进程信号相关的"Pending位图"和"Block位图"是两种数据结构,用于跟踪进程当前挂起/未决(pending)的信号和已经阻塞(blocked)的信号
Pending位图:
Block位图:
其中信号的阻塞与否,跟是否收到信号毫无关系
对应信号在进程的信号未决位图中的比特位会在信号递达前被设置为1,表示信号需要处理,而在信号被处理完后会被清零,即改为0
是先清0,再进行递达
而进程能识别信号,也是因为早在未收到信号之前,我们就已经知道是否堵塞,怎么处理了(利用上述三个表)
- 信号处理表:在进程创建时,内核会为其分配一个信号处理表,用于记录每个信号对应的信号处理函数(Signal Handler)。当进程收到一个信号时,内核会根据信号处理表中对应信号的处理函数来执行相应的操作。
- 信号未决位图:在进程接收到一个信号时,内核会更新进程的信号未决位图,用于记录当前未被屏蔽的信号。这个位图帮助进程确定是否有信号需要处理。
- 信号挂起位图:当一个信号被进程接收但尚未处理时,内核会将这个信号标记为挂起,即更新进程的信号挂起位图。这个位图帮助进程确定哪些信号需要等待处理。
这三个表是操作系统内核为了管理进程信号处理而设计的数据结构,它们在进程创建时被初始化并与进程关联,帮助进程识别和处理信号
每个信号只有一个
bit
的未决标志,非0即1,不记录该信号产生了多少次(只表示收到否信号,对于信号的数量没办法也没必要),阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态
- 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞
- 在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
对于我们使用者来说,应该将sigset_t
类型看作一个抽象的信号集合,而不需要关心其内部的具体实现细节。sigset_t类型的具体表示方式可能会因系统而异,可能是一个位图、一个数组或其他数据结构,但这些细节对于使用者来说并不重要。
我们使用者应该通过系统提供的函数来操作sigset_t变量,比如sigemptyset、sigfillset、sigaddset、sigdelset等函数来对信号集进行操作。这些函数会根据系统的具体实现来正确处理信号集的操作,确保其正确性和可移植性。
因此,直接打印sigset_t变量是没有意义的,因为sigset_t类型的内部表示对于使用者来说是不透明的
#include <signal.h>
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset (sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);
int sigismember(const sigset_t* set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
sigprocmask
是一个系统调用,用于检查或修改当前进程的信号屏蔽集(signal mask)。信号屏蔽集是一个用来指定哪些信号在进程处理信号时应该被阻塞的集合。通过操作信号屏蔽集,进程可以控制哪些信号可以被接收和处理,哪些信号应该被暂时屏蔽。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how
:表示对信号屏蔽集的操作方式,有三种取值:
SIG_BLOCK
:将set
中指定的信号添加到当前信号屏蔽集中。SIG_UNBLOCK
:从当前信号屏蔽集中移除set
中指定的信号。SIG_SETMASK
:将当前信号屏蔽集设置为set
中指定的信号集。set
:指向一个sigset_t
类型的指针,用于指定要操作的信号集。oldset
:指向一个sigset_t
类型的指针,用于存储之前的信号屏蔽集。返回值:
errno
变量来指示错误类型。功能:
sigprocmask
函数允许进程检查或修改当前进程的信号屏蔽集。how
参数指定的操作,可以添加、移除或替换信号屏蔽集中的信号。oldset
参数不为NULL,则会将之前的信号屏蔽集存储到oldset
中。sigpending
是一个系统调用,用于获取当前进程挂起/未决(pending)的信号集。挂起的信号是指已经发送给进程但尚未被处理的信号。通过sigpending
函数,进程可以查询当前有哪些信号处于挂起状态,以便进一步处理这些信号。
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
set
:指向一个sigset_t
类型的指针,用于存储当前进程挂起的信号集。返回值:
errno
变量来指示错误类型。功能:
sigpending
函数允许进程获取当前进程挂起的信号集。set
参数返回当前进程挂起的信号集,可以进一步对这些信号进行处理。在信号处理中,一般情况下有三种处理方式,分别是:
我们之前只是泛泛的讲:进程会在合适时候进行对信号的处理,那什么是合适的时候?——进程从内核态切换会用户态的时候,信号会被检测并处理
每次进程从内核态切换到用户态时,操作系统会依次检查进程是否有未处理的信号。如果有未处理的信号,操作系统会根据信号的处理方式(比如忽略、捕获、默认处理等)来进行相应的处理。如果信号没有被阻塞,操作系统会执行信号处理程序来处理该信号,然后继续执行用户态程序。
在第三步我们讨论的是自定义处理,如果是默认和忽略呢?
- 默认:更改PCB的状态即可观在是内核身份,直接杀掉进程
- 忽略:处理这个信号什么都不做,直接把pending表对应比特位置为0
为什么在第四步里,特地回到用户态执行自定义处理函数:操作系统不相信任何人,不会轻易执行用户的代码,因为用户代码可能包含恶意代码或错误代码,可能会导致系统崩溃、数据泄露等安全问题
用户态和内核态是操作系统中的两种运行模式,用于区分程序的权限和访问级别。下面是它们的主要特点和区别:
用户态(User Mode):
内核态(Kernel Mode):
我们不同的状态主要是不同的权限:通过改变CPU内的执行权限,设置了寄存器内的特定标志位,来改变状态
进程无论如何切换,总能找到OS:我们访问OS,本质就是通过进程的地址空间的[3,4]GB的内核空间来访问的
调用系统调用也是在地址空间内进行的
在操作系统内核中,通常会有一个系统调用表(System Call Table)用于存储系统调用号与对应系统调用处理程序的映射关系。当用户进程发起系统调用时,会将系统调用号放入特定寄存器中,CPU根据系统调用号找到对应的系统调用处理程序在系统调用表中的位置,然后跳转到该函数的地址进行调用。
在这个过程中,操作系统内核会确保系统调用表的起始虚拟地址是已知的,并且系统调用号与处理程序的映射关系是正确的。通过这种方式,CPU能够根据系统调用号正确地找到对应的系统调用处理程序,并执行相应的操作。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字。如果我们处理完对应的信号,该信号默认也会从信号屏蔽字中进行移除——不想让信号,嵌套式进行捕捉处理(正在处理时你又来了,那就又去调用处理函数)
sigaction()
函数是用于设置和修改信号处理程序的系统调用函数。通过sigaction()
函数,进程可以指定在接收到特定信号时应该执行的处理程序。这个处理程序可以是系统默认的处理方式,也可以是用户自定义的处理函数。
sigaction()
函数的原型如下:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中,signum
参数指定了要设置的信号的编号,act
参数指定了新的信号处理方式,oldact
参数用于保存之前的信号处理方式。
二者都是struct sigaction类型的,对于struct sigaction
void (*sa_handler)(int)
:这是一个函数指针,用于指定信号处理函数的地址。当接收到信号时,系统会调用这个函数来处理信号。函数接受一个整型参数,表示接收到的信号编号。如果将sa_handler
设置为SIG_IGN
,表示忽略该信号;将其设置为SIG_DFL
,表示使用系统默认的信号处理方式。
void (*sa_sigaction)(int, siginfo_t *, void *)
:这也是一个函数指针,用于指定扩展的信号处理函数的地址。与sa_handler
不同的是,sa_sigaction
函数接受三个参数:第一个参数是信号编号,第二个参数是一个指向siginfo_t
结构体的指针,其中包含了关于信号的更多信息,第三个参数是一个指向void
类型的指针。(一般用于实时信号,我们不管这个)
sigset_t sa_mask
:这是一个信号集合,用于指定在信号处理函数执行期间需要屏蔽的信号。如果有信号在sa_mask
指定的信号集合中,则这些信号会被阻塞,直到信号处理函数执行完毕。
int sa_flags
:用于指定信号处理的行为。可以是以下几个标志的组合:
SA_RESTART
:表示系统调用在接收到信号后会自动重启。SA_NOCLDSTOP
:子进程暂停和继续时不会产生SIGCHLD
信号。SA_NODEFER
:不会在执行信号处理函数期间阻止同一信号的传递。SA_SIGINFO
:表示使用sa_sigaction
字段指定的信号处理函数。(我们一般设置为0就行了)
void (*sa_restorer)(void)
:这是一个保留字段,已经废弃,不再使用。
返回值为0表示函数调用成功,返回-1表示函数调用失败。在函数调用失败的情况下,可以通过errno
全局变量获取具体的错误信息。
通过sigaction()
函数,进程可以设置信号的处理方式为以下几种之一:
SIG_IGN
)SIG_DFL
)#include <iostream> #include <signal.h> #include <unistd.h> using namespace std; void handler(int signum) { cout << "收到了信号:" << signum << endl; } int main() { struct sigaction act, oldact; act.sa_handler = handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(2, &act, &oldact); // 进行信号捕捉 while (true) sleep(2); return 0; }
被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,函数有可能因为重入而造成错乱,像这样的函数称为不可重入函数
反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数
可重入函数(Reentrant Function)也称为可重入代码(Reentrant Code)或重入函数(Reentrant Routine),是指在并发执行环境中,能够被多个线程同时调用的函数。这种函数能够在任何时候被中断,并在之后从中断点恢复执行,而不会导致数据错误或系统崩溃。
为了实现可重入性,可重入函数必须满足以下条件:
volatile
关键字在 C 和 C++ 语言中是一个类型限定符,它告诉编译器不要对访问该关键字声明的变量的代码进行优化,即每次都需要从内存中读取变量的值,而不是使用存储在寄存器中的副本。这是为了确保多线程环境或者硬件中断等场景下,对该变量的访问总是最新的、未被其他线程或硬件修改过的值。有时因为编译器优化的原因,会导致我们代码出错
#include <stdio.h> #include <unistd.h> #include <signal.h> int g_flag = 0; void changeflag(int signo) { (void)signo; printf("将g_flag,从%d->%d\n", g_flag, 1); g_flag = 1; } int main() { signal(2, changeflag); while(!g_flag); // 故意写成这个样子, 编译器默认会对我们的代码进行优化 //因为,g_flag一直都没使用过 printf("process quit normal\n"); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
这里,如果编译器进行优化,会把内存里的g_flag拷贝一份到寄存器里,那下一次判断直接从寄存器里拿。不用再去内存里拿,收到信号2后我们更改的是内存里的g_flag,但是我们while判断的是寄存器里的g_flag——寄存器屏蔽了内存
SIGCHLD信号是在Linux系统中用于进程间通信的一种机制。具体来说,当子进程终止或停止时,子进程会向其父进程发送SIGCHLD信号。这个信号是子进程状态改变时发送给父进程的信号,用于通知父进程其子进程的状态已经发生了变化。
父进程可以捕获这个信号,并通过调用如wait()或waitpid()等函数来获取子进程的退出状态、终止原因等信息。SIGCHLD信号常用于以下几种情况:
在处理SIGCHLD信号时,通常会在信号处理函数中循环调用waitpid()函数来非阻塞等待子进程状态改变,以避免僵尸进程的产生。
有可能:有100个子进程,有50个退出了,50个还没有。那么在循环到51次时,waitpid会一直堵塞住,父进程就一直卡在那里,所以不能堵塞等待
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> void CleanupChild(int signum) { while (true) { pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 : 回收任意一个子进程;这里非堵塞 if (rid > 0)//等待成功 { std::cout << "wait child success: " << rid << std::endl; } else if (rid <= 0) break; } } int main() { signal(SIGCHLD, CleanupChild); for (int i = 0; i < 100; i++) { pid_t id = fork(); if (id == 0) { // child int cnt = 5; while (cnt--) { std::cout << "I am child process: " << getpid() << std::endl; sleep(1); } std::cout << "child process died" << std::endl; exit(0); } } // father while (true) sleep(1); return 0; }
事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用
signal(SIGCHLD, SIG_IGN);//直接这样就行
好了今天就到这里啦
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。