认识阻塞、未决、递达几个概念
- 实际执行信号的处理动作称为信号递达(Delivery) --- 默认、忽略、自定义捕捉
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 注意,阻塞和忽略是不同的,只要信号阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
信号的保存
两张位图 + 一张函数指针数组
信号的保存其实就是两张位图 + 一张函数指针数组 == 让进程识别信号!
pending:未决信号集,位图int,比特位的位置:代表信号编号,比特位的内容:代表信号是否收到。
handler:信号的编号就是数组的下标,可以采用信号编号,索引信号处理方法!
signal捕捉就是传入函数地址替换到handler表中对应索引的信号。
block:一张位图,与pending类型完全一样,比特位的位置:代表信号编号,比特位的内容:代表信号是否阻塞。
再看signal就能理解sighandler_t是什么意思?
其实就是handler表,函数指针数组 --- sighandler_t handler[]。
那么阻塞一个信号,就是把block对应比特位置1,那么对应的信号一旦产生,永不递达,一直未决,直到主动接触阻塞。所以一个信号如果阻塞和他有没有未决无关!!!
sigset_t
sigset_t是Linux给用户提供的一个用户级的数据类型, 禁止用户直接修改位图
sigset_t本质是一个位图结构!
为了更好的操作信号集,OS为我们提供了几个对信号集操作的函数
sigemptyset:初始化sigset位图结构,进行清空。
sigfillset:初始化系统中所有的信号到指定位图中。
sigaddset:修改对应信号索引的位图结构中的bit位值为1。
sigdelset:取消一个信号,本质是把位图结构对应信号索引置为0。
sigismember:查看该信号是否在该位图中。
上面函数返回值都是成功返回0,出错返回-1。
sigprocmask
参数
how:
SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字(阻塞)的信号,相当于mask=mask|set
SIG_UNBLOCK:set包含了我们希望添加到当前信号屏蔽字(阻塞)中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set
oldset:
输出型参数,保存老的信号屏蔽字返回给用户。
sigpending
获取当前进程的pending位图保存到set中。
测试代码
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "curr process[" << getpid() << "]pending: ";
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << "\n";
}
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-------------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-------------------------------" << std::endl;
}
int main()
{
// 0. 捕捉2号信号
signal(2, handler); // 自定义捕捉
signal(2, SIG_IGN); // 忽略一个信号
signal(2, SIG_DFL); // 信号的默认处理动作
// 1. 屏蔽2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT); // 我们有没有修改当前进行的内核block表呢???1 0
// 1.1 设置进入进程的Block表中
sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽!
int cnt = 15;
while (true)
{
// 2. 获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
// 3. 打印pending信号集
PrintPending(pending);
cnt--;
// 4. 解除对2号信号的屏蔽
if (cnt == 0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
sleep(1);
}
}
1.解除屏蔽,一般会立即处理当前被解除的信号(如果被pending)
2.pending位图对应的信号也要被清0,在递达之前!
信号的处理
信号的处理又叫信号捕捉,也叫递达信号,可以是signal(2, handler)自定义捕捉
signal(2, SIG_IGN)忽略一个信号, signal(2, SIG_DFL)信号的默认处理动作。
内核态和用户态
这是在信号传递的过程中用户态和内核态相互转换的流程图!!
信号可能不会被立即处理,而是在合适的时候处理,合适的时候?进程从内核态返回到用户态的时候,进行处理!
OS能不能直接转过去执行用户提供的handler方法呢?不能!!必须用用户身份执行handler
再谈地址空间
下面我们以32位机器为例,我们把磁盘中的数据加载到内存,那么我们知道OS是在我们开机时最先被加载到我们的内存中的,我们知道我们的磁盘中的数据和代码是被映射到地址空间中的!!那OS的数据呢?其实在地址空间中还有一块属于内核级的地盘,相应就要有内核级页表进行映射关系的对应。所以OS本身就在我们进程的地址空间中啊!!!
假如上面是A进程,那么还有B进程...那么B进程需不需要再创建一份内核级页表呢???
不需要!所以用户级页表可以有很多份,那么内核级页表只需要维护一份就好了!!!
所以,无论进程如何切换,我们总能找到OS,我们访问OS,其实还是在我们的地址空间中进行的,和我们访问库函数没有区别!!!
OS不相信任何人,用户访问[3, 4]地址空间的时候,要受到一定的约束,只能通过系统调用!!
谈谈键盘的输入数据的过程
根据冯诺依曼体系结构键盘这种外设是不能直接跟CPU打交道的,那CPU如何读取键盘输入的数据?其实在键盘和CPU直接有一种芯片一类的,键盘发送硬件中断通过芯片发送到CPU表面的棱角上,CPU就会接收到键盘的数据,然后在内存中会有一张函数指针数组,里面存放着各种读取硬件数据的函数指针方法,其实这个就是OS,那么假如键盘方法就在这个数组的下标为3的位置,那么键盘发送的中断号就是这个数组下标索引到内存中!!!然后读取OS内设好的读取键盘数据的方法!!!
所以以后OS再也无需检测键盘是否有数据!!!在硬件上我们也叫函数指针数组为中断向量表!
所以我们有没有感到熟悉?是信号吗?
其实我们学习的信号,就是模拟中断实现的!!
信号是纯软件层,中断是软件+硬件!
谈谈如何理解OS如何正常的运行
1.OS是如何运行的
操作系统的本质是一个死循环!!!时钟中断,不断调度OS的任务的!!!
2.如何理解系统调用
其实系统调用在OS内部有一张系统调用表,一张函数指针数组,我们只要找到特定数组的下标
的方法就能执行系统调用了!数组下标就是系统调用号!!!
外部中断的目的,不就是让CPU内部寄存器形成一个中断号的数字吗!!
其实每个系统调用内部都会mov一个系统调用号到寄存器中!
CPU可不可以直接形成一个数字去执行OS,这里我们假如是0x80这个数字,那么由CPU直接找到我们的系统调用,通过保存的系统调用号去查系统调用的函数指针数组就能去执行我们的系统调用方法了!!!
我们一般把CPU从外部形成的中断叫外部中断,由内部直接形成的中断我们叫缺陷、陷阱!!!
总结 --- 内核态与用户态互转?
OS不是不相信任何用户吗?用户无法直接跳转到3,4GB地址空间范围,那如何做到?
必须在特定条件下,才能跳转过去,需要硬件(CPU)配合!!!
其实在CPU内部有一个状态寄存器cs(code semgment)存放代码区的范围,在其中有两个比特位可以用来状态表示,0表示当前CPU处于内核态,3表示当前CPU处于用户态,
如何跳转?不就是在我们的系统调用方法内把寄存器的两个比特位,由3置0不就完成了由用户态跳转到内核态!!!
sigaction
sigaction的参数,第一个传入指定信号,第二个参数是输入型参数,第三个参数是保存旧的信号处理方式,以便后面恢复以前的信号处理方式。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void Print(sigset_t &pending)
{
for(int sig = 31; sig > 0; sig--)
{
if(sigismember(&pending, sig))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
//当前如果正在对n号信号进行处理,默认n号信号会被自动屏蔽
//对n号信号处理完成的时候,会自动解除对n号信号的屏蔽
//为什么?
void handler(int sig)
{
// alarm(1);
std::cout << " get a sig:" << sig << std::endl;
//打印pending(未决)位图
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask); //如果你想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽!!
sigaddset(&act.sa_mask, 3);
act.sa_flags = 0;
sigaction(2, &act, &oact);
while (true)
{
std::cout << "my pid is:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
测试结果:
由测试结果可知,sigaction可以同时屏蔽多个信号,发送信号时,信号就成为了未决状态,在pending表中就出现了1,处于未决状态!
那如果我们把所有信号都屏蔽了呢,那进程就无法被终止了???我们想到了,OS也想到了,所以OS对有些信号规定了,不允许被屏蔽,9号信号禁止被屏蔽!!!
可重入函数
我们先要理解在我们从用户态转到内核态不只是系统调用才能进入,而是在任何时刻只要时间片到达,都有可能进入内核态!!!
下面是一个链表进行头插的步骤,如果我们在insert时执行第一条代码之后进行信号的捕捉处理,信号捕捉又再次头插,最终造成4号的结果,造成node2丢失,内存泄漏!!!
insert函数被重复进入了 --- 被重入了,造成内存泄漏,所以这个函数应该是不可重入函数
我们用到的大部分函数都是不可重入的!!!因为重入可能就引起内存泄漏等问题!!!
可重入函数描述的是函数的特点,所以不能说是优缺点!
volatile
volatile是c语言为我们提供的关键字,以前我们可能没听说过,比较冷门。但现在我们可以说一下了!
#include <iostream>
#include <unistd.h>
#include <signal.h>
int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo" << ", change gflag 0->1" << std::endl;
gflag = 1;
}
int main()//没有任何代码对gflag进行修改!!!
{
signal(2, changedata);
while(!gflag);//while不要其他代码
std::cout << "process quit normal" << std::endl;
return 0;
}
正常退出了,我们画一下原理图!
gflag被读入CPU,在CPU内部进行逻辑运算,while循环判断寄存器中的值,然后执行其他代码!
我们的编译器其实有很多优化,但我们现在默认是没有优化的,如果我们使编译器自动的进行优化使用-O1/-O2/-O3,-O0是没有优化的。
然后再运行试试看!
我们发现进程不会退出了!!!为什么?
这是因为当我们从内存读取数据到CPU中的寄存器后,而while循环并不执行任何其他代码,编译器认为无需每次都要从内存中重新读取数据就优化为,只读取一次内存中的gflag数据,gflag被捕捉后修改的是内存中的数据,寄存器中的数据不会被修改,就一直while循环了!!!寄存器隐藏了内存中的真实值!
如何解决这种问题?就需要使用关键字volatile来保持内存的可见性。
#include <iostream>
#include <unistd.h>
#include <signal.h>
volatile int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo" << ", change gflag 0->1" << std::endl;
gflag = 1;
}
int main()//没有任何代码对gflag进行修改!!!
{
signal(2, changedata);
while(!gflag);//while不要其他代码
std::cout << "process quit normal" << std::endl;
return 0;
}
SIGCHLD
子进程退出时,不是静悄悄的退出的,会给父进程发送信号 --- SIGCHLD!!!
如何证明子进程退出发送信号,代码证明。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void notice(int signo)
{
std::cout << "get a signo:" << signo << " pid:" << getpid() << std::endl;
pid_t rid = waitpid(-1, nullptr, 0);
if(rid > 0)
{
std::cout << "wait child success, rid:" << rid << std::endl;
}
else if(rid < 0)
{
std::cout << "wait child success done" << std::endl;
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
pid_t id = fork();
if(id == 0)
{
std::cout << "I am child process, pid:" << getpid() << std::endl;
sleep(3);
exit(1);
}
//father
while(true)
{
DoOtherThing();
sleep(1);
}
return 0;
}
如果一共有10个进程,且同时退出呢?
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void notice(int signo)
{
std::cout << "get a signo:" << signo << " pid:" << getpid() << std::endl;
while (true)
{
pid_t rid = waitpid(-1, nullptr, 0);//-1表示可以等待任何一个子进程!!!
if (rid > 0)
{
std::cout << "wait child success, rid:" << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success done" << std::endl;
break;
}
else
{
std::cout << "wait child success done" << std::endl;
break;
}
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
std::cout << "I am child process, pid:" << getpid() << std::endl;
sleep(3);
exit(1);
}
}
// father
while (true)
{
DoOtherThing();
sleep(1);
}
return 0;
}
如果一共有10个子进程,5个退出,5个永不退出呢?
这种其实当等第6个时就会被阻塞,等待这个进程的退出!!!
这就需要我们设置非阻塞等待了!!
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void notice(int signo)
{
std::cout << "get a signo:" << signo << " pid:" << getpid() << std::endl;
while (true)
{
pid_t rid = waitpid(-1, nullptr, WNOHANG);//阻塞 -》 非阻塞等待
if (rid > 0)
{
std::cout << "wait child success, rid:" << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success done" << std::endl;
break;
}
else
{
std::cout << "wait child success done" << std::endl;
break;
}
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
std::cout << "I am child process, pid:" << getpid() << std::endl;
sleep(3);
exit(1);
}
}
// father
while (true)
{
DoOtherThing();
sleep(1);
}
return 0;
}
我们如果不需要知道子进程的各种退出信息,该怎么做呢?
我们可以不让父进程等待子进程退出,但是我们有说过,当子进程退出,父进程如果不等待子进程,子进程进入僵尸状态,那如何解决这个问题?
我们可以使用signal捕捉,设置捕捉状态为忽略!
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
signal(SIGCHLD, SIG_IGN); // 收到设置对SIGCHLD进行忽略即可
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
std::cout << "child running" << std::endl;
cnt--;
sleep(1);
}
exit(1);
}
while (true)
{
std::cout << "father running" << std::endl;
sleep(1);
}
}
与系统级ign进行区别,我们这里用的是用户级ign。系统默认的不会执行