目录
1 什么是信号
2 为什么要有信号
3 对于信号的反应
3.1 默认行为
3.2 signal()函数 -- 自定义行为对信号做出反应
3.3 对信号进行忽略
4 信号的产生的类型
4.1 kill命令
4.2 键盘输入产生信号
4.3 系统调用接口
4.3.1 kill()
4.3.2 raise() 函数
4.4 软件条件
4.5 异常
5. 信号产生的原因
5.1 键盘输入转化成信号的过程
5.2 代码除零发出异常信号的过程
5.3 野指针发出异常信号的过程
6 关于core dump标志位
7 信号的保存
7.2 三张表的匹配操作和系统调用
7.2.1 sigset_t
7.2.2 sigprocmask
7.2.3 sigpending
7.2.4 signal
8 信号的处理
9 用户态和内核态(进程地址空间第三讲)
9.2 内核态和用户态的标志
10 操作系统是怎么样运行起来的
11 信号捕捉的又一个系统调用sigaction
12 可重入函数
13 volatile关键字
14 SIGCHLD信号
15 特例
1 什么是信号
Linux提供的让用户(进程)给其他进程发送异步信息的一种方式。
- 1.Linux保证了信号还没有发出的时候,我们就知道怎么去处理。
- 2.信号能够被认识,是因为信号早就被在我们的大脑里设置好了。
前两点保证我们能够识别信号,并且知道怎么去处理信号
- 信号到来的时候,我们还在处理其他更重要的事情,我们暂时还不能处理信号,这时就需要我们能够去保存信号。
- 信号到了,可以不立即处理,可以在合适的时候处理。
- 信号的产生是随机产生的,我们无法准确预料到,所以信号是异步发送的(信号是由别人(用户/进程)发出的,此时,我在忙我自己的事情) 。
2 为什么要有信号
系统要求进程有随时相应外界的能力,然后做出反应。
3 对于信号的反应
对于信号的反应即,OS对于信号的处理凡是:
- 默认行为
- 自定义行为---捕捉
- 忽略信号
注:忽略信号也算处理了信号,处理方式就是忽略。
3.1 默认行为
下面就是利用 kill命令向进程当中发生2号信号,OS执行默认行为,将进程停下。
3.2 signal()函数 -- 自定义行为对信号做出反应
参数
signum
:指定要捕获的信号编号。例如,SIGINT
表示中断信号(通常由 Ctrl+C 产生),SIGSEGV
表示段错误信号。handler
:指定信号处理函数,它是一个接受单个整数(信号编号)作为参数的函数。如果传递SIG_IGN
,则忽略该信号;如果传递SIG_DFL
,则使用默认的信号处理行为。
返回值
- 成功时,返回之前的信号处理函数指针。
- 失败时,返回
SIG_ERR
,并设置errno
以指示错误原因。
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;void handler(int sig)
{cout << "I get sidnal, the signalnum: " << sig<< endl;
}
int main()
{signal(2,handler);//这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为while (1){cout << "my pid: " << getpid() << endl;sleep(1);}return 0;
}
你看上面的的进程,在对信号进行捕获了之后,就不在执行默认行为,反而执行自定义行为
3.3 对信号进行忽略
同样也是signal()系统调用,只不过参数不同
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;void handler(int sig)
{cout << "I get sidnal, the signalnum: " << sig<< endl;
}
int main()
{signal(2, SIG_IGN);//ignore//这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为while (1){cout << "my pid: " << getpid() << endl;sleep(1);}return 0;
}
上面的进程就对2号信号,毫无反应
4 信号的产生的类型
4.1 kill命令
下面就是利用 kill命令向进程当中发生2号信号,OS执行默认行为,将进程停下。
4.2 键盘输入产生信号
键盘输入:也算是信号,如:ctrl + c 和 ctrl + \
其实上ctrl + c 就等于输入 2 号信号
我们都知道,ctrl + c可以终止进程,但是为什么说他被解释为信号呢?
#include <iostream>
#include <unistd.h>
#include<signal.h>
using namespace std;void handler(int sig)
{cout << "I get sidnal, the signalnum: " << sig<< endl;
}
int main()
{signal(2, handler);//ignore//这个自定义行为,只要被设置过就改变了,不需要写入循环当中。被设置的时候,不会触发行为,只有当进程收到信号是,才会产生行为while (1){cout << "my pid: " << getpid() << endl;sleep(1);}return 0;
}
这里输入了ctrl + c 触发了对于2号信号的自定义行为。
4.3 系统调用接口
4.3.1 kill()
参数
pid
:要发送信号的进程的进程 ID(PID)。可以是以下几种特殊值之一:0
:向属于调用进程所在进程组的所有进程发送信号。-1
:向除了进程组 1 和调用进程本身之外的所有进程发送信号(需要超级用户权限)。< -1
:向进程组 ID 为-pid
的所有进程发送信号。
sig
:要发送的信号。例如,SIGKILL
用于强制终止进程,SIGTERM
用于请求进程正常终止。
返回值
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
以指示错误类型。
这里如果是简单的使用一下这个系统调用,就未免有点太简单了,我们利用系统调用接口封装一个自己的my_kill
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include<error.h>
#include<cstring>
using namespace std;// mykill -9 pid
int main(int argc, char *argv[])
{if (argc != 3){cout << " usage error: " << argv[0] << " -signumber pid" << endl;}pid_t pid = stoi(argv[2]);int sig = stoi(argv[1]+1);int n = kill(pid, sig);if (n < 0){cerr << "kill error, " << strerror(errno) << endl;}return 0;
}
上面是先形成可执行文件,在将路径加到环境变量PATH 中,这样就不用我们输入路径了
4.3.2 raise() 函数
这个也没什么好讲的就是一个,底层封装了kill()系统调用的函数。
功能:对自己发送任意信号
4.4 软件条件
我们以alarm()为例,以这个闹钟函数模拟软件条件
参数:
seconds:闹钟定时的时间,单位是秒
返回值:
alarm()
函数的返回值是上一次 alarm()
设置的定时器剩余的时间(以秒为单位)。具体行为如下:
-
首次调用或之前没有设置定时器:如果这是第一次调用
alarm()
函数,或者之前没有设置过定时器(即之前调用alarm(0)
取消了定时器),则alarm()
函数返回 0。 -
定时器正在运行:如果之前已经设置了一个定时器,并且该定时器还没有到期,那么再次调用
alarm()
函数时,它会返回上一次设置的定时器剩余的时间。例如,如果之前设置了 10 秒的定时器,然后在 5 秒后再次调用alarm()
,那么它将返回 5(表示还有 5 秒定时器才会到期)。 -
定时器已到期:如果之前设置的定时器已经到期(即已经发送了
SIGALRM
信号),那么再次调用alarm()
函数时,它将返回 0,因为此时没有正在运行的定时器。 -
取消定时器:如果调用
alarm(0)
,这将取消当前正在运行的定时器(如果有的话),并且alarm()
函数将返回被取消的定时器剩余的时间。如果没有正在运行的定时器,则返回 0。
需要注意的是,在某些系统实现中,如果 alarm()
函数调用失败(例如,由于系统资源不足),它可能会返回一个非零的负值(如 -1),但这种情况比较少见。在大多数情况下,alarm()
函数都会成功执行并返回上述描述的值。
此外,alarm()
函数设置的定时器是进程级别的,即每个进程只有一个 alarm()
定时器。如果在同一个进程中多次调用 alarm()
,则之前的定时器设置将被新的设置替换。
void handler(int sig)
{std::cout << "get a sig: " << sig << " g_cnt: " << g_cnt << std::endl;unsigned int n = alarm(5);// 定时器正在运行:如果之前已经设置了一个定时器,并且该定时器还没有到期,那么再次调用 alarm() ,函数时,它会返回上一次设置的定时器剩余的时间。// 例如,如果之前设置了 10 秒的定时器,然后在 5 秒后再次调用 alarm(),那么它将返回 5(表示还有 5 秒定时器才会到期)。cout << "还剩多少时间: " << n << endl;// exit(0);
}int main()
{//设定一个闹钟signal(13, handler);//捕获13号信号,正面alarm()发送的是13号信号。alarm(5); // 响一次while (true){g_cnt++; // 纯内存级}// int cnt = 0;// while (true)// {// sleep(1);// cout << "cnt : " << cnt++ << ", pid is : " << getpid() << endl; // IO其实很慢// if (cnt == 2)// {// int n = alarm(0); // alarm(0): 取消闹钟// cout << " alarm(0) ret : " << n << endl;// }// }
}
谈及到闹钟的概念,其实上OS系统内部也有很多闹钟,其实上就是OS系统自己设定的。
也不用多说:先描述,后组织
然后,利用结构体指针,在最小堆中组织起来。
4.5 异常
这里就简单讲两个异常:8)SIGFPE
代码除零了:11)SIGSEGV
5. 信号产生的原因
5.1 键盘输入转化成信号的过程
首先,我们要明白的是内核怎么知道,键盘要输入的,如果是内核不断地去询问的话,按照我们现在这种我们几乎感觉不到的延迟的话,对于内核的资源要求很高,而且,也不只一个硬件等待内核的询问,还有网卡,硬盘...,所以肯定是键盘通知内核来读,内核才来读
首先就是键盘一直都是处于通电状态的,这个毫无疑问,那么怎么表示有输入了呢?那就是输入一个高电频,表示有数据输入。
还有就是,键盘与CPU上的一个针脚(每一个针脚都有编号)相连,就通过这个针脚向CPU传递高电频。
这个过程就是:
键盘有内容输入,向CPU传递高电频,CPU检测到是从2号针脚传递上来的,将2号记录在寄存器内,传递给OS,OS在中断向量表里的arr[2]中读取数据,发现是一个函数指针,然后调用该函数(去键盘里读数据)。
读取到数据之后:
判断是文本内容(abcd)还是信号(ctrl + c)
如果是文本内容,就直接输入标准输入流当中
如果是信号,就写入进程当中的pending位图当中,等待进程响应。
5.2 代码除零发出异常信号的过程
5.3 野指针发出异常信号的过程
6 关于core dump标志位
man 7 signal
我们发现60%--70%的信号都是终止进程,但是终止进程却又Core和Term两种行为。那么这两种行为又有什么区别。
Core的终止进程的行为,会在当前路径下形成一个Core文件,里面就存储着进程的退出信息,在调试的时候可以直接帮助我们定位到错误位置。(这个就叫核心转储功能)
如果你默认是云服务器,那么云服务器的Core是默认关闭的。即Core dump标志位默认是0.
ulimit -a 查看核心转储
默认情况下,核心转储功能是没有打开的。即这个core文件大小是0
ulimit -c 10240
ulimit -c :查看资源
7 信号的保存
实际执行信号的处理动作称为信号递达(Delivery):处理动作就是之前提到的,三个动作
信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
我们现在就看到三张表
block表:其实就是block位图
- 比特位的位置表示:信号的编号
- 比特内容表示:该信号是否被阻塞
pending表:其实就是pending位图
- 比特位的位置表示:信号的编号
- 比特内容表示:该信号是否收到该信号
handler表:
里面就存储着函数指针,指向信号的默认处理方法。
如果,程序员对信号进行了捕获,那么handler表里的函数指针就指向你写的函数方法
阻塞一个信号,和是否收到了指定信号,有关系吗?--- 没有关系
7.2 三张表的匹配操作和系统调用
7.2.1 sigset_t
sigset_t
是一个在Unix和类Unix系统(如Linux)中使用的数据类型,它用于表示信号集。信号集本质上是一个信号的集合,可以用来指定多个信号。
一、定义与用途
- 定义:
sigset_t
是一个数据类型,一般来讲是位图,用于存储一组信号的集合。 - 用途:信号集主要用于与信号阻塞、信号等待等操作相关。通过使用sigset_t,程序员可以方便地管理一组信号,而无需单独处理每个信号。
二、相关函数
与sigset_t
相关的函数主要包括信号集的初始化、添加/删除信号、检查信号集是否包含某个信号等。以下是一些常用的函数:
sigemptyset(sigset_t *set):初始化信号集,将其置为空集。
返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。
sigfillset(sigset_t *set):初始化信号集,将其置为包含系统支持的所有信号的集合。
返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。
sigaddset(sigset_t *set, int signum):向信号集中添加指定的信号。
返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。
sigismember(const sigset_t *set, int signum):检查信号集是否包含指定的信号。
- 返回值:如果signum是set中的一个成员,则返回非零值(通常为 1);如果signum不是set的成员,则返回 0;出现错误时返回 -1 并设置 errno以指示错误的原因。
三、注意事项
- 在使用
sigset_t
类型的变量之前,需要先将其初始化为一个空集或包含所有信号的集合,这可以通过sigemptyset
和sigfillset
函数来完成。 - 在向信号集中添加或删除信号时,应使用
sigaddset
和sigdelset
函数。 - 在检查信号集是否包含某个信号时,应使用
sigismember
函数。 - 信号集通常与信号阻塞和信号等待等操作一起使用,以实现对信号的有效管理。
综上所述,sigset_t
是一个在Unix和类Unix系统中用于表示信号集的数据类型,通过相关的函数可以方便地管理一组信号。
7.2.2 sigprocmask
sigprocmask
是一个系统调用函数,用于修改当前进程的信号屏蔽字集合。信号屏蔽字是一个位掩码,每个位对应一个特定的信号。当某个信号对应的位被置为1时,表示该信号被阻塞,不会被传递给进程进行处理。
一、参数说明
how:指定如何修改当前进程的信号屏蔽字。它可以是以下三个值之一:
SIG_BLOCK | 将set 中指定的信号添加到当前进程的信号屏蔽字中。 |
| 从当前进程的信号屏蔽字中移除set 中指定的信号。 |
| 将当前进程的信号屏蔽字设置为set 中指定的信号集合 |
set:指向一个信号集合,用于指定需要修改的信号集合。如果how
为SIG_BLOCK
或SIG_UNBLOCK
,则set
指向的信号集合表示待添加或移除的信号;如果how
为SIG_SETMASK
,则set
指向的信号集合将替代当前进程的信号屏蔽字。
oldset:是一个可选的输出参数,用于获取调用sigprocmask
函数前的旧的信号屏蔽字。如果不需要保存旧的信号屏蔽字,可以将其设置为NULL
。
二、返回值
- 成功时,
sigprocmask
返回0。 - 失败时,返回-1,并设置
errno
以指示错误原因。
7.2.3 sigpending
sigpending
函数,用于查询当前进程的未决信号集合。未决信号集合是指那些已经发送给进程但尚未被处理的信号,这些信号可能因为被屏蔽而无法立即传递给进程。
一、函数原型
二、参数说明
- set:指向一个
sigset_t
类型的变量,用于存储当前进程的未决信号集合。调用sigpending
函数后,该函数会将当前进程的未决信号集合复制到set
指针所指向的信号集合中。
三、返回值
- 成功时,
sigpending
返回0。 - 失败时,返回-1,并设置
errno
以指示错误原因。
7.2.4 signal
signal
函数是C语言标准库中的一个函数,用于设置特定信号的处理方式。它允许程序在接收到特定信号时执行自定义的处理函数,或者采用默认的处理方式,也可以选择忽略该信号。以下是对signal
函数的详细解释:
一、函数原型
二、参数说明
- signum:指定要处理的信号编号。常见的信号有SIGINT(通常由Ctrl+C产生)、SIGTERM(通常用于请求程序终止)等。不同的操作系统可能支持不同的信号集。
- handler:指定信号的处理方式。它可以是一个指向自定义信号处理函数的指针,也可以是两个特殊的常量:SIG_DFL(表示使用默认的信号处理方式)或SIG_IGN(表示忽略该信号)。
三、返回值
- 成功时,
signal
函数返回之前的信号处理函数的指针。 - 失败时,返回SIG_ERR,并设置
errno
以指示错误原因。
四、信号处理函数
- 信号处理函数应该尽量简单快速,避免执行复杂的操作或长时间的阻塞操作。因为信号可能在任何时候中断程序的执行,如果信号处理函数执行时间过长,可能会影响程序的响应性。
- 信号处理可能会被其他信号中断,所以在信号处理函数中要考虑到这种情况。例如,如果在处理一个信号时,又接收到了另一个信号,可能需要采取适当的措施来确保正确的处理顺序。
五、注意事项
- 在使用
signal
函数之前,需要包含<signal.h>
头文件。 - 不同的操作系统对信号的处理可能会有所不同,所以在跨平台开发时需要注意兼容性问题。
- 一旦设置了信号处理函数,它将在程序的整个生命周期内有效,除非再次调用
signal
函数来改变信号的处理方式。 - 有些信号(如SIGKILL和SIGSTOP)是不能被捕获或忽略的。
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;void print(sigset_t pending)
{for (int num = 31; num > 0; num--){if (sigismember(&pending, num)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int sig)
{sigset_t set;sigisemptyset(&set);sigfillset(&set);int n = sigpending(&set); // 我正在处理2号信号哦!!// 将二号信号再次设置进入pending标准,模拟一直受到二号信号assert(n == 0);// 3. 打印pending位图中的收到的信号std::cout << "递达中...: ";print(set); // 递达之前,pending 2号已经被清0 先清零,再递达std::cout << sig << " 号信号被递达处理..." << std::endl;
}int main()
{// 屏蔽2号信号signal(2, handler);// 初始化sigset_t// 定义一个sigset_t,并将其初始化,因为sigset_t是一个封装的结构体,我们也需要用封装的方法对其进行初始化// 其实他并没有被设定进入blick表中sigset_t ss, old_ss;sigisemptyset(&ss);sigfillset(&ss);sigaddset(&ss, 2);//添加2号信号// 此时才是调用系统调用接口,将信号集写入内核//将屏蔽二号信号的block表写入内核int n = sigprocmask(SIG_BLOCK, &ss, &old_ss);assert(!n);cout << "signal 2 getin block" << endl;cout << "pid : " << getpid() << endl;int cnt = 0;while (true){//获取当前的pending表sigset_t pend;sigisemptyset(&pend);sigpending(&pend);// 3. 打印pending位图中的收到的信号print(pend);cnt++;// 4. 解除对2号信号的屏蔽if (cnt == 20){std::cout << "解除对2号信号的屏蔽" << std::endl;n = sigprocmask(SIG_UNBLOCK, &ss, &old_ss); // 2号信号会被立即递达, 默认处理是终止进程assert(n == 0);}// 我还想看到pending 2号信号 1->0 : 递达二号信号!sleep(1);}
}
8 信号的处理
信号的处理必须在一个合适的时候处理 ---- 进程从内核态切换回用户态的时候,信号会被检测并且处理。
可以简单的认为,内核态就是系统在执行系统调用时,要对内核数据进行处理时的状态(OS所处的状态,权限较高)
用户态就是用户在简单的执行C语言代码时,没有对内核数据进行处理时,所处的状态就是用户态(用户所处的状态,就是我)。
但是,是不是没有使用系统调用就不会处于内核态,一直处于用户态呢?--- 不是的,当你的进程被调度,出让CPU和进入CPU的时候,都是处于内核态的。
- 就是当程序在运行的时候,如果收到某条指令的时候,就会进入内核态处理异常 1->2
- 当进程进入内核态的时候,处理完了刚刚收到的信号,不会马上退出,而会检测该进程是否有被递达的信号,(遍历pending位图,如果,为1,并且对应的block上为0,处于未阻塞的状态)2->3
- 此时有可以被递达的信号,如果默认动作时SIG_DFL,就直接把进程删除;如果默认动作时SIG_IGN,就直接忽略,回到1(3->1)返回用户态执行程序;如果默认动作时STOP,就直接把进程的状态设置为S;如果是自定义动作,就要到用户态去执行自定义函数(3->4)
- 当进入用户态,去执行代码内自定义的函数时,执行完了不能直接回到刚刚中断的代码,而要重新进入内核态(4->5)
- 回到了内核态之后,在回到用户态,刚刚代码中断的地方。
将进程切换简单的抽象为一个横置的数字8
问题1:进程切换的步骤4是否有存在的必要?既然在权限小的用户态都能够执行代码,那么权限更大的内核态不也能够执行代码吗?
有必要存在,用户写的代码就应该让用户执行,让用户更小的权限去约束代码的行为。
如果让内核态去执行,用户写的代码,如果代码有一些非法的操作,在内核态极大的权限下,就可能被执行,导致操作系统出错。
问题2:就是既然60%的信号都是杀掉进程,为什么还要让进程去识别一下自己是否要被直接杀掉。
因为,进程可能还在处理一些比较重要的事情,如果不通知进程直接让他退出可能会有一些未知的错误,正确做法应该是通知进程,让进程判断自己要不要先把完成当下这个动作在退出。
9 用户态和内核态(进程地址空间第三讲)
我们使用系统调用的时候,进行跳转,本质上还是在自己的进程地址空间内进行跳转
不同的进程之间使用的都是同一个页表,因为操作系统只有一份加载进入内存,所以每一份进程的内核空间内的虚拟地址都是完全一致的。(这意味着进程无论怎么切换,进程都能找到OS)
所以,我们访问OS,本质上就是通过我的进程的地址空间的内核空间[3,4]来访问即可
9.2 内核态和用户态的标志
10 操作系统是怎么样运行起来的
信号技术本来就是通过软件的方式,来拟的硬性中断
谁让OS运行起来呢?
非常高频率的,每个非常短的时间,就给CPU发送中断,CPU不断地进行处理中断
OS的周期时钟中断(利用硬件周期性的发生终断)
操作系统是一个死循坏,不断在接受的外部的其他硬件中断
对于中断的反应行为集成在中断向量表
在调用系统调用的流程
11 信号捕捉的又一个系统调用sigaction
二、参数说明
- signum:要设置或获取处理程序的信号编号。该参数可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号。因为这两个信号定义了自己的处理函数,所以为它们指定自己的处理函数将导致信号安装错误。
- act:指向sigaction结构体的指针,在结构体实例中指定了对特定信号的处理方式。该参数可以为空,此时进程会以缺省方式对信号处理。
- oldact:指向sigaction结构体的指针,用于保存原来对相应信号的处理方式。该参数可以指定为NULL。
三 特性
当某个信号的处理函数被调用时,OS自动将当前信号加入到进程的信号屏蔽字中,直到信号处理函数返回时,解除对当前信号的屏蔽,这样防止了信号被嵌套式的捕捉处理,如果此信号再次被产生时,它会被阻塞到当前处理结束为止。 如果调用信号处理函数时,除了屏蔽当前信号之外,还希望自动屏蔽其他信号,就可以用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
在调用信号的时候,出了当前信号被自动屏蔽之外,还希望屏蔽其他的信号,则可以用sa_mask字段说明
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;void PrintSig(sigset_t pending)
{cout << "pending bitmap: ";for (int i = 31; i > 0; i--){if (sigismember(&pending, i))cout << "1";elsecout << "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;// for(int signo = 1; signo <= 31; signo++) // 9, 19号信号无法被屏蔽, 18号信号会被做特殊处理// sigaddset(&block, signo); // SIGINT --- 根本就没有设置进当前进程的PCB block位图中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;
}
12 可重入函数
该函数被执行流重复进入导致了产生问题,这样的函数就叫做不可重入函数,否则就叫做可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
- 我们使用的大部分函数都是不可重入函数
13 volatile关键字
-
编译器(gcc、g++)提供了多个级别的优化,-O0不启用优化、-O1启用基本的优化、-O2启用更高级别的优化,-O. . . ,数字越大,优化级别越高。
-
当编译器对代码进行优化时,它会减少访问内存的次数,以提高程序的运行速度。
// volatile int g_flag = 0;
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); // 故意写成这个样子, 编译器默认会对我们的代码进行自动优化!// {// printf("hello world\n");// sleep(1);// }printf("process quit normal\n");return 0;
}
volatile关键字的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许作优化,对该变量的任何操作,都必须在真实的内存中进行操作。
volatile int g_flag = 0;
// 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); // 故意写成这个样子, 编译器默认会对我们的代码进行自动优化!// {// printf("hello world\n");// sleep(1);// }printf("process quit normal\n");return 0;
}
正常来讲:编译器不做任何优化的情况下,CPU无论是进行数据运算,还是逻辑运算都要从内存中读取数据再进行运算。
但是呢,编译器为了提高效率,一般会自作主张的进行一下优化,就是每次运算不从CPU中拿数据了,就直接使用CPU寄存器上的数据,这样就导致了第一中情况,明明改变了g_val(改变的是内存上的g_val,因为优化,CPU不再从内存上读取,转而一直使用寄存器上的)所以导致了循环一直执行。(这种情况叫,寄存器屏蔽内存)
而volatile就是阻止优化产生(保存内存的可见性)。
14 SIGCHLD信号
子进程退出,父进程不wait(),子进程就会变成僵尸进程。
子进程退出的时候,不是默默退出的,是会向父进程发送信号的,-17 SIGCHLD
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int sig)
{cout << "I am father process get message: " << sig << endl;}
int main()
{signal(17, handler);pid_t id = fork();if (0 == id){int cnt = 5;while (cnt){cout << "I am child process pid: " << getpid() << endl;cnt--;sleep(1);}exit(0);}while (1);cout << "quit normal" << getpid() << endl;return 0;
}
既然,子进程退出,是发送信号的,那我们能不能利用这个信号,让父进程不用主动的去等待,反而是等到子进程发送了信号,再去回收子进程
#include <assert.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include<sys/wait.h>
using namespace std;void handler(int sig)
{cout << "I am father process get message: " << sig << endl;while (1)//第一个问题的解决方法,就是直接使用循环,来给回收子进程也套上循环{pid_t rid = waitpid(-1,nullptr,WNOHANG);//第二个问题的解决方法:让回收子进程变成轮询模式,如果没有等到子进程,就直接退出循环,去执行自己的代码if(rid>0){cout << "wait success " << endl;}else{break;}}}
int main()
{signal(17, handler);for (int i = 0; i < 100; i++)//第一个问题:创建100个线程,同时退出,进程又只能记录下一个信号,根本来不及处理 父进程该怎么回收呢?{ //第二个问题:如果只退出其中的50个进程,而另外的50个直接不退出了,此时,父进程一直阻塞在回收子进程的循环中怎么办?pid_t id = fork();if (0 == id){int cnt = 5;while (cnt){cout << "I am child process pid: " << getpid() << endl;cnt--;sleep(1);}exit(0);}}while (1);cout << "quit normal" << getpid() << endl;return 0;
}
15 特例
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
什么意识呢?就是把17号信号的默认行为改为忽略,这样就可以让OS帮你自动回收变成僵尸的子进程。
但是,也要注意,这样你的父进程也就得不到任何子进程退出的信息了。
我们发现,就是系统层面的SIGCHLD默认动作也是IGN,那么为什么我们设置一下的IGN就会有这个特性呢?
其实上,在Linux底层,这两个IGN也是有区别的,两个IGN一个系统的在底层是0 强转为函数指针
我们设置的是1强转为函数指针。目前只在Linux下有这个差异。其实上也就在Linux这个特殊情况下有差异。