进程在启动时,就已经设置好了识别特定信号的方式,且在信号产生之前,进程就知道如何处理这些信号,因为进程在启动时,OS会为其分配一个信号处理表(handle表,即:函数指针表),用来记录每个信号对应信号的处理函数,且信号的识别方式也是通过它。
这种暂时保存是通过进程控制块(task_struct)中的pending位图来记录的,pending位图用来记录哪些信号已经被发送但尚未处理,位图中的每一位对应一个信号,如果该位为1,则表示该信号已经被发生但尚未处理,一旦这个信号被处理完毕,OS会将此信号从位图中清除(由1置为0)。
信号产生时,如果进程正在内核态执行,一般不会立即处理,而是等从内核态切回到用户态之前进行检查是否有未处理的信号,如果有未处理的信号且满足信号处理条件,就会在此时处理信号。
信号的产生具有不确定性和临时性,我们无法准确预料何时会有信号产生,因此信号是异步发送的。
信号的产生是由外部事件触发的(如:用户输入、硬件异常等),不是进程主动请求的,这体现了异步发送的不可预测性。
信号的产生与接收信号的进程的执行流是不同步的。当信号产生时,进程可能正在执行其他任务或处于某种状态,而信号的到达不会中断或阻塞当前进程的执行流,而是在合适的时候处理,这体现了异步发送的独立性。
由于信号是异步发送的,进程可以根据自身需求选择在何时以及如何处理信号(如:设置信号函数或忽略某些信号等),这体现了异步发送的灵活性。
kill -信号编号 进程的PID
#include<iostream>
#include<cstdio>
#include<csignal>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
void handle(int signo)
{
cout << "handing signao " << signo << endl;
}
int main()
{
for(int signo = 1; signo <= 32; signo++)
{
signal(signo, handle);
}
while(true)
{
printf("I am a process, pid: %d\n", getpid());
sleep(3);
}
return 0;
}
term:进程在接收到信号后,有机会执行清理操作并优雅地退出,通常是通过SIGTERM(信号编号15)来实现的。
问1:为什么要存在核心转储?
核心存储文件包含了进程异常终止时的内存状态、寄存器值、调用栈等调试信息,有助于协助程序调试,从而快速定位到错误原因(如:进程为什么退出、进程执行到哪行代码退出)。
事后调试:可以通过核心存储文件对已经异常终止的进程,使用调试器进行调试,以便定位到错误原因,因为进程异常终止通常是有Bug(如:非法访问内存导致的段错误等)。
云服务器为了节省磁盘空间、避免资源浪费或出于安全考虑,对以core方式终止的进程进行了特定的设定,默认关闭core文件的生成。
ulimit -a
ulimit -c
sudo bash -c “echo core.%p > /proc/sys/kernel/core_pattern”
当进程异常终止时,OS会根据core_pattern文件中的设置生成核心转储文件。
core.%p表示核心转储文件名为core.pid,为了防止未知的core dump一直运行,导致服务器磁盘被打满,因为程序每次运行都是全新的进程,pid均不同,因此通常将其设置为core,表示核心转储文件名为core,其大小是固定的;
一、OS如何检测和处理键盘输入
效率问题:通过硬件中断机制,OS无需定期检测键盘是否被按下,大大提高了系统的效率和响应速度。这是因为硬件中断是异步发生的,当键盘被按键按下时,会立即触发中断,CPU会立即响应并处理该中断。
中断向量表:OS在启动时,会初始化一张中断向量表,这张表实际上是一个函数指针数组,每个下标对应一个中断编号,每个元素对应对应一个具体的中断处理函数。
硬件中断机制:当键盘上某个键被按下时,键盘控制器会向CPU发送一个硬件中断信号,这个信号通常是通过主板上一个固定的针脚发送的,该针脚与CPU某个特定中断输入引脚相连。
OS响应中断:CPU收到中断信号后,会将中断编号保存在寄存器中,并且会要求OS根据中断编号查找中断向量表中的中断处理函数,并执行该函数。
对于键盘的输入,OS提供了读取键盘数据的方法,通过这个函数,OS就可获取键盘中输入的数据。
二、发送信号的本质
给进程发送信号的本质是将信号写入进程的PCB中,而PCB是内核数据结构,只有OS才有资格写入,用户只能通过调用OS提供的系统调用来写入信号。
无论信号的产生有多少种,最终都是OS负责将信号写入到目标进程的PCB中,并触发相应的信号处理机制。
int kill(pid_t pid, int sig) ;
Tisp:kill命令底层封装了系统调用kill函数。
#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<signal.h>
#include<errno.h>
#include<cstring>
using namespace std;
void Usage(char* argv[])
{
cout << argv[0] << " -signumber PID" << endl;
}
int main(int argc, char* argv[]) //模拟实现kill命令
{
if(argc != 3) Usage(argv); //用法错误
int pid = stoi(argv[2]), signo = stoi(argv[1] + 1);
int n = kill(pid, signo); //底层封装了系统调用kill
if(n < 0)
cerr << "kill error: " << strerror(errno) << endl;
return 0;
}
int raise(int sig);
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handle(int signo)
{
cout << "get a signal, number is " << signo << endl;
}
int main()
{
signal(2, handle); //设定信号捕捉的方法
int cnt = 4;
while(cnt--)
{
cout << "I am a process, pid: " << getpid() << endl;
if(cnt == 2) raise(2); //给当前进程发送任意信号 —— 给自己发送2号信号
sleep(1);
}
return 0;
}
void abort(void);
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handle(int signo)
{
cout << "get a signal, number is " << signo << endl;
}
int main()
{
signal(6, handle); //设定信号捕捉的方法
int cnt = 4;
while(cnt--)
{
cout << "I am a process, pid: " << getpid() << endl;
if(cnt == 2) abort(); //给当前进程发送指定信号(6号信号)
sleep(1);
}
return 0;
}
abort后续行为:即使捕捉了SIGABRT并返回了处理函数,abort函数仍然会尝试执行其标准的终止流程,这包括调用raise(SIGABRT),然后执行一些清理操作,并最终调用_exit(1)来终止程序,,因此它不关心循环中是否还有未执行的代码。
SIGPIPE信号(13号信号)是由操作系统内核检测到的管道写端已关闭这一软件条件触发的信号。
产生条件:当一个进程向已经关闭写端的管道中写入数据时,OS会向该进程发送SIGPIPE信号,其默认行为是终止进程。
为什么向已关闭写端的管道写入数据,被视为软件条件?
这一过程涉及操作系统内核中软件代码来管理管道的状态、检查写入条件以及产生和处理信号。这与硬件条件(如:物理设备的状态变化)不同,是由物理设备的物理特性决定的。
管道是通过OS中特定的数据结构来实现的,但这些数据结构及其操作逻辑都是由OS软件来管理的;当一个进程进行写入时,OS会检查管道的状态,包括写端是否关闭,OS会检测到写端已关闭这一错误条件,会进行响应产生SIGPIPE信号,以通知进程发生了错误,这一过程是由操作系统内核中的软件代码来实现的,因此它属于软件条件范畴。
一、SIGALRM信号
SIGALRM信号(14号信号)是由软件条件触发的信号。
产生条件:通过调用alarm函数来设置一个定时器,在second秒后定时器到期,OS会给当前进程发送SIGALRM信号,其默认行为是终止进程。
二、系统调用接口alarm
unsigned int alarm(unsigned int seconds);
功能:设置一个定时器,定时器在seconds秒后到期,OS会向当前进程发送SIGALRM信号,这个信号可以被进程捕捉处理(signal函数),或执行默认处理动作(终止进程)。
返回值:调用失败:返回UINT_MAX,并设置错误码以指示错误的原因。
调用成功:a. 如果调用alarm函数前,已经设置了一个全新的定时器且在运行,则alarm函数会取消之前的定时器,用新的定时器代替,此时,alarm函数返回值。b.如果调用alarm函数前,没有设置全新的定时器,则alarm返回值为0。
在调用alarm函数前,设置了alarm(0),表示取消之前设置的定时器,返回值为离之前设置的定时器剩余的时间。
#include<unistd.h>
#include<signal.h>
using namespace std;
void handle(int signo)
{
cout << "get a signal, number is " << signo << endl;
}
int main()
{
signal(14, handle); //对SIGALAM信号进行捕捉
alarm(50);
int cnt = 5;
while(cnt--)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
if(cnt == 3)
{
size_t n = alarm(0); //取消之前设定的定时器
cout << "alarm(0) retval " << n << endl; //返回值为离之前设置的定时器剩余的时间
}
}
return 0;
}
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
using namespace std;
int cnt;
/*如果在信号处理函数中设置了alarm(2),并且接着是一个死循环,
现象:在2秒过后OS尝试发送SIGALRM信号,由于进入了死循环,
意味着处理函数不会返回,导致新的信号无法被处理,因为无法进入该信号处理函数*/
void handle(int signo)
{
cout << "catching..." << endl;
int n = alarm(2); //在上一个定时器调用前,设置一个全新的定时器
cout << "alarm(2) retval" << n << endl; //离之前设置的定时器剩余的时间
}
int main()
{
signal(SIGALRM, handle); //设置SIGALRM信号的捕捉方法
alarm(10); //设置一个定时器
while(true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
创建描述定时器的结构体,该结构体通常包含以下信息:设置定时器进程的PID、定时器的超时时间、触发时要发送给进程的信号等。
组织定时器结构体,使用最小堆,堆顶始终表示最近的一个超时的闹钟。
eg1:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
int main()
{
int a = 0, b = 3;
int c = b / a; //除数为0,会导致除零错误,接收到SIGFPE信号(8号信号)
return 0;
}
当CPU执行除法运算,如果除数为0,会导致除零错误,会触发一个除0异常;
CPU进行计算时会出现溢出的情况,这会导致CPU会更新EFLAGS寄存器中相应的标志位(如:OF、ZF等);
OS会识别到这些标志位的变化,则OS的异常处理机制会捕获这个异常,并发送SIGFPE信号(8号信号)给进程;
如果进程设置了该信号的自定义捕捉handle方法,那么该方法会被调用。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handle(int signo) //未终止进程,导致异常一直存在
{
cout << "get signumber: " << signo << endl;
sleep(1);
}
int main()
{
signal(SIGFPE, handle);
int a = 0, b = 3;
int c = b / a; //除数为0,会导致除零错误
while(true)
sleep(1);
return 0;
}
问:发生除零错误,触发除0异常,收到SIGFPE信号,为什么会一直执行自定义捕捉handler方法?
OS会识别到这些标志位的变化,并发送SIGSEGV信号(11号信号)给进程。
如果进程设置了该信号的自定义捕捉handle方法,那么该方法会被调用,若handle方法中未设置终止进程,那么每次上下文恢复时,OS识别到错误仍然存在,就会再次发送信号。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handle(int signo) //未终止进程,导致异常一直存在
{
cout << "get signumber: " << signo << endl;
sleep(1);
}
int main()
{
signal(SIGSEGV, handle);
int* p = NULL; //非法访问野指针,会导致野指针异常
*p = 10;
while(true)
sleep(1);
return 0;
}
当信号产生时,如果进程正在处理更重要的事情(如:处于临界区或执行不可中断操作),而暂时不能处理到来的信号,为了确保信号不会丢失,OS会将这个信号暂时保存起来,那我们就来看看内核中如何保存信号。
信号递达:实际执行信号的处理动作。
信号未决:信号产生到递达之间的状态。
进程可以选择阻塞某个信号。
被阻塞的信号产生时,将一直保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
Tips:注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是抵达之后可选的一种处理动作,形象来说忽略是视而不见,阻塞是看不到。
struct task_struct {
// ...
struct signal_struct *signal; // 指向信号结构体
// ...
};
struct signal_struct {
// ...
sigset_t blocked; // 被阻塞的信号集合
sigset_t pending; // 待处理的信号集合
struct sigaction *actions; // 信号处理函数指针数组
// ...
};
当一个信号被阻塞时,即使该信号被发送给进程,进程也不会立即处理它,而是将其暂时保存起来,直到进程解除对此信号的阻塞。
在block位图中,比特位的位置表示信号编号,比特位的内容表示信号是否被阻塞,如果某一个位为1,则表示该信号被阻塞。
当一个信号被发送时,它会被添加到这个集合中,对应比特位的内容由0变为1,直到信号递达,OS才会将此信号从pending位图中清除,对应比特位由1置为0(细节:pending位图先被清0,在递达)。
在pending位图:比特位的位置表示信号编号,比特位的内容表示是否收到信号,如果某一位为1,则表示该信号已被发送但尚未处理。
当一个信号被发送,且未被阻塞,在合适的时候,此信号需要被处理,OS会根据信号编号来在这个数组中找到对应的处理函数,并调用该函数来处理信号。
数组下标表示信号编号,数组的内容表示信号递达时的处理动作,包括默认SIG_DEF、忽略SIG_IGN、自定义捕捉函数(handle)。
Tips:POSIX.1允许信号在递达之前产生多次,在Linux中是这样实现的,普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列中。
一、sigset_t
sigset_t用于表示当前进程的阻塞信号集时,它被称为阻塞信号集或信号屏蔽字。
sigset_t用于表示当前进程已发送但尚未处理的信号集合时,它被称为未决信号集。
每个信号通常只有一个未决标志和阻塞标志,且这两个标志都只有两种状态(“有效"或"无效”,非0即1),不记录该信号产生了多少次,因此未决标志和阻塞标志可以使用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,在未决信号集中"有效"和"无效"的含义是信号是否处于未决状态。
sigset_t类型对于每个信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit依赖于OS的实现,且不同的OS或OS实现采用内部的存储方式不尽相同,从使用者的角度不关心底层的具体的存储方式,不直接访问或解释sigset_t内部的数据(如:printf直接打印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);
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signum)、int sigdelset(sigset_t* set, int signum);
int sigismember(const sigset_t* set,int signum);
Tips:注意,在使用sigset_t类型的变量之前,一定要先调用sigemptyset、sigfillset做初始化,使信号集处于确定的状态,之后就可以调用sigaddset、sigdelset在该信号集中添加或删除某个有效信号。
问:调用信号集操作函数后,有没有将数据(信号的设置)写入到内核中,从而修改PCB中有关信号的字段呢?
int sigprocmask(int how , const sigset_t* set , sigset_t* oldset);
功能:读取或更改当前进程的信号屏蔽字。
返回值:成功返回0,失败返回-1,并设置errno以指示错误类型。
参数:set参数:指向信号集的指针,它指定了新的信号集; oldset参数:指向信号集的指针,用于存储修改前的信号屏蔽字。how参数指定如何修改当前的信号屏蔽字。
a. 如果只想读取当前的信号屏蔽字,将set设置为NULL,oset为非空指针,则读取到的信号屏蔽字通过oset传出。
b. 如果set为非空指针,修改当前的信号屏蔽字,how指示如何修改,如果不需要保存旧的信号屏蔽字,则oset可以被设置为NULL。
c. 如果oset和set都为非空指针,先会把原来的信号屏蔽字保存在oset中,再根据set和how来修改信号屏蔽字。
int sigpending(sigset_t* set);
功能:获取当前进程的未决信号集。
返回值:成功返回0,失败返回-1,并设置errno以指示错误类型。
set参数:指向信号集的指针,用于存储当前进程的未决信号集,它会将当前进程的未决信号集复制到set指向的信号集中。
应用场景1实现:屏蔽2号信号 -> 获取当前进程的pending位图,并打印 -> 给进程发送2号信号 -> 获取当前进程的pending位图,并打印 -> 解除2号信号的屏蔽。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>
using namespace std;
void PrintSig(sigset_t pending)
{
cout << "pending bitmap: ";
for(int i = 31; i > 0; i--)
{
if(sigismember(&pending, i))
cout << "1";
else cout << "0";
}
cout << endl;
sleep(1);
}
void handle(int signo)
{
cout << "2 signo 递达中" << endl;
sigset_t pending;
sigemptyset(&pending);
int n3 = sigpending(&pending);
PrintSig(pending); //细节:先清空pengding位图,在递达
cout << "2 signo 递达完毕" << endl;
}
int main()
{
signal(2, handle); //设置2号信号捕捉的方法
//1.阻塞2号信号
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, 2);
sigemptyset(&oldset);
int n1 = sigprocmask(SIG_SETMASK, &set, &oldset);
assert(n1 == 0);
cout << "block 2 signo success! pid: " << getpid() << endl;
sleep(1);
int cnt = 0;
while(true)
{
//2.获取penging位图
sigset_t pending;
sigemptyset(&pending);
int n2 = sigpending(&pending);
assert(n2 == 0);
//3.打印pending位图
PrintSig(pending);
cnt++;
//4.解除对2号信号的阻塞
if(cnt == 8)
{
cout << "release 2 signo block!" << endl;
n2 = sigprocmask(SIG_UNBLOCK, &set, &oldset); //解除完后,立即递达
assert(n2 == 0);
}
}
return 0;
}
应用场景2:对所有信号进行阻塞,观察收到所有信号时是否都未被递达,进程只能正常退出吗
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>
using namespace std;
void PrintSig(sigset_t pending)
{
cout << "pending bitmap: ";
for(int i = 31; i > 0; i--)
{
if(sigismember(&pending, i))
cout << "1";
else cout << "0";
}
cout << endl;
sleep(1);
}
int main()
{
sigset_t set, oldset;
sigemptyset(&set);
sigemptyset(&oldset);
cout << "begin block all signo! pid: " << getpid() << endl;
for(int i = 31; i > 0; i--) //将1~31号阻塞
{
sigaddset(&set, i);
int n1 = sigprocmask(SIG_SETMASK, &set, &oldset);
assert(n1 == 0);
}
while(true)
{
sigset_t pending;
sigemptyset(&pending);
int n2 = sigpending(&pending); //获取pending位图
assert(n2 == 0);
PrintSig(pending); //打印pending位图
}
return 0;
}
执行默认处理动作。
忽略。
自定义捕捉信号函数handle。
这个处理函数的执行要求内核切换到用户态,这种方式称为捕捉一个信号。
信号捕捉:信号的处理动作是用户的自定义函数,信号递达时就调用这个函数,这称为信号捕捉。
sighandler_t sigal(int signum,sighandler_t handler);
功能:设置信号处理函数。
参数:signum参数:要处理的信号编号、handler参数:信号处理函数指针,handler可以是用户自定义的函数指针,也可以是预定义的常量(SIG_IGN,忽略信号,或SIG_DFL,默认动作)。
int sigaction(innt signum,const struct sigaction* act,struct sigaction* oldact);
功能:读取或修改与指定信号相关联的处理动作。
返回值:成功返回0,失败返回-1,并设置errno以指示错误原因。
参数:signum参数:指定要捕捉或处理的信号编号; oldact参数:输出型参数,如果为非空指针,保存了信号原来的处理动作; act参数:指定了信号新的处理动作。
sa_handler:指向信号处理函数的指针。 如果sa_handler被赋值为常数SIG_IGN,表示忽略信号、如果sa_handler被赋值为常数SIG_DFL,表示执行系统的默认处理动作、如果sa_handler被赋值为一个函数指针,表示自定义函数捕捉信号。
sig_flags:此处设置为0即可。 sa_sigaction:是实时信号的处理函数,此处不做解释。
sa_restorer:此处不管。
struct sigaction {
void (*sa_handler)(int); // 信号处理器函数指针
void (*sa_sigaction)(int, siginfo_t *, void *); // 异步安全信号处理器函数指针
sigset_t sa_mask; // 信号掩码
int sa_flags; // 标志位
void (*sa_restorer)(void); // 用于恢复上下文
};
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>
using namespace std;
void PrintSig(sigset_t pending)
{
cout << "pending bitmap: ";
for(int i = 31; i > 0; i--)
{
if(sigismember(&pending, i))
cout << "1";
else cout << "0";
}
cout << endl;
sleep(1);
}
void handler(int signo)
{
cout << "get signo: " << signo << endl;
sigset_t pending;
sigemptyset(&pending);
while(true)
{
int n3 = sigpending(&pending);
assert(n3 == 0);
PrintSig(pending);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
int n = sigaction(2, &act, &oact);
assert(n == 0);
cout << "I am a process! pid: " << getpid() << endl;
while(true)
sleep(1);
return 0;
}
问:信号什么时候被处理的?
用户态下的代码通常是应用程序或库函数代码,它们通过系统调用等方式向OS请求服务,以完成各种功能。
在内核态下进程具有最高的权限,可以直接访问内核所有资源(如:硬件设备、内核数据结构、内核代码和数据等),并且可以执行一些特权指令。
内核状态下的代码通常是操作系统内核代码。
内核态:CPU处于最高特权级别,0级。
用户态:CPU处于最低特权级别,3级。
当进程执行系统调用函数时,OS会自动将进程的身份由用户态变为内核态,让内核执行系统调用对应的任务,完成任务后,再将进程的身份由内核态转变为用户态,以便进程在用户态执行后续的代码。
内核级页表在整个OS中只有一份,使得所有进程共享相同的内核级页表,指向相同的OS代码和数据。
![](https://cdn.nlark.com/yuque/0/2024/png/42574816/1728637030076-60509ee1-4d6d-458d-8920-c2ceb2bf3d34.png)
在执行主控制流程(main函数)的某条指令时,发生中断、异常或系统调用,使得进程需要从用户态切换到内核态。
当内核处理完毕后准备返回用户态的main函数时,内核会检查penging位图中是否有可以递达的信号。
如果可以递达的信号处理动作是默认或忽略,在执行完信号的处理动作后,就在pending位图中清除信号对应的标志位,如果没有其他信号需要递达,就直接返回到用户态,从主控制流程中上次被中断的地方继续向下执行。
如果可以递达的信号处理动作是执行自定义捕捉函数,因为自定义捕捉函数的代码是在用户空间的,就得要切换到用户态执行对应的捕捉函数。
这意味着函数可以被不同的控制执行流调用,函数在执行过程中可以被中断,然后再另一个执行流中再次被调用,而不会破坏函数的正确性和数据的完整性。
重入:像上例,insert函数被不同的控制流调用,在第一次调用还未返回时就再次进入该函数,这称为重入。
insert函数访问一个全局链表,有可能因为重入而造成错乱(node2节点丢失),这样的函数就成为不可重入函数。
如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
独立的栈空间:每个控制流程都有自己独立的运行环境,其中包括独立的栈空间。意味着每个控制流程在调用函数时,都会在自己的栈上创建该函数局部变量和参数的副本。
操作的独立性:每个控制流程都在操作自己的局部变量副本,它们的操作不会影响其他控制流程的局部变量副本。
函数执行的独立性:每个控制流程对函数的执行也是独立的,只在自己的上下文中进行,不会影响其他控制流程。
编译器(gcc、g++)提供了多个级别的优化,-O0不启用优化、-O1启用基本的优化、-O2启用更高级别的优化,-O. . . ,数字越大,优化级别越高。
当编译器对代码进行优化时,它会减少访问内存的次数,以提高程序的运行速度。
eg:在循环中频繁访问某个变量g_val,编译器可能会将存储在内存中的g_val的值,拷贝到CPU的寄存器中,因为访问寄存器的速度比访问内存的速度快的多,如果修改了g_val值,则是对内存中的g_val进行了修改,这个修改不会自动反映到存储在寄存器中的g_val值。
int g_val;
void handler(int signo)
{
(void)signo; //骗过编译器,不要警告,此变量不光光是定义,在后面还使用了它
g_val = 1; //对存放在内存中的g_val值进行修改,不会自动更新寄存器的值
cout << "g_val由0变为1" << endl;
}
int main()
{
signal(2, handler);
/*编译器会作优化,将存放在内存中的g_val值拷贝到CPU寄存器中,
之后直接从寄存器中读取此变量的值*/
while(!g_val) ;
printf("process normal quit!\n");
return 0;
}
volatile int g_val;
void handler(int signo)
{
(void)signo; //骗过编译器,不要警告,此变量不光光是定义,在后面还使用了它
g_val = 1; //对存放在内存中的g_val值进行修改,不会自动更新寄存器的值
cout << "g_val由0变为1" << endl;
}
int main()
{
signal(2, handler);
/*编译器会作优化,将存放在内存中的g_val值拷贝到CPU寄存器中,
之后直接从寄存器中读取此变量的值*/
while(!g_val) ;
printf("process normal quit!\n");
return 0;
}
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
void clean(int signo)
{
if(signo == SIGCHLD)
{
/*在同一时刻多个子进程退出,会同时向父进程发送SIGCHLD信号,
但pending位图只会记录一次,即只清理了一个子进程的资源,
所以使用while循环*/
while(true)
{
/*若为阻塞等待,只有部分子进程退出,由于while循环,
会再次调用waitpid,就会一直在这阻塞,所以使用非阻塞轮询等待*/
pid_t rid = waitpid(-1, nullptr, WNOHANG);
if(rid > 0)
cout << "child wait success!" << endl;
else break;
}
}
}
int main()
{
signal(SIGCHLD, clean);
for(int i = 0; i < 100; i++) //父进程创建100个子进程
{
pid_t id = fork();
if(id == 0)
{
cout << "I am child process, pid: " << getpid() << endl;
exit(0); //100个子进程全部退出
}
sleep(1);
}
return 0;
}
与系统默认处理动作中的忽略无区别,但这是个特例,这只适用于父进程不需要知道子进程的执行情况,反之,还得在自定义捕捉函数中设置waitpid获取子进程的退出码、退出信号。
int main()
{
//子进程在终止时自动清理掉,不会产生僵尸,也不会通知父进程
signal(SIGCHLD, SIG_IGN);
for(int i = 0; i < 100; i++) //父进程创建100个子进程
{
pid_t id = fork();
if(id == 0)
{
cout << "I am child process, pid: " << getpid() << endl;
exit(0); //100个子进程全部退出
}
sleep(1);
}
return 0;
}