生活中的信号
例如:红绿灯,闹钟,转向灯,狼烟
什么是Linux信号?
本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。
结合进程,得出信号结论:
a. 进程要处理信号,必须具备信号"识别"能力(看到+处理动作)。
b. 为什么进程能识别信号?--- 程序员
c. 信号产生是随机的,进程这时候可能正在执行任务,所以进程的后续处理,可能不是立即处理。
d. 信号会临时的记录下对应的信号,方便后续(合适的时候)进行处理。
e. 一般而言,信号的产生相对于进程而言是异步的。
信号处理的常见方式
a. 默认(进程自带的,程序员写好的逻辑)
b. 忽略
c. 自定义捕捉
常见的信号
【1 - 31】: 普通信号 【34 - 64】:实时信号
Term:终止 Core:核心转储终止 Ign:忽略 Cont:继续 Stop:暂停
如何理解组合键ctrl+c变成信号呢?
答:键盘的工作方式是通过中断方式进行的,os找到与组合键ctrl+c对应的信号即可。
如何理解信号被进程保存?
答:什么信号(位图中哪一个比特位),是否产生(1,产生 0,没有)---进程中的位图结构,os只要将信号写入到进程pcb内部的位图结构中。
如何理解信号发送的本质?
答:os向目标进程写信号,os直接修改进程pcb中指定的位图结构,完成发送信号过程。
产生信号
a. 终端键盘产生信号
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
自定义捕捉系统调用方法
sighandler_t signal(int signum, sighandler_t handler);
返回值:返回老的捕捉方法的函数地址
参数1:信号的值/宏
参数2:通过回调函数的方式,修改对应的信号捕捉方法
代码实现:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
//typedef void(*sighandler_t)(int);
void handler(int sig)
{
cout<<"接受到一个信号....pid: "<<getpid()<<endl;
}
int main()
{
signal(SIGINT,handler);
while(1)
{
sleep(1);
}
return 0;
}
可使用ctrl+c:2号信号捕捉,也可以使用kill -2 进程pid 捕捉,原理是一样的
核心转储
关闭时
打开时
当进程出现某种异常的时候,由os将当前进程在内存中的相关核心数据,转储到磁盘中。
核心转储作用和功能:当进程出现某种异常的时候,是否由os将当前进程在内存中的相关核心数据,转储到磁盘中,主要是为了调试。
验证进程等待中的信号信息和core dump标记位
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
//typedef void(*sighandler_t)(int);
int main()
{
pid_t id=fork();
if(id==0)
{
int a=100;
a/=0;
cout<<"run here...."<<endl;
}
int status;
wait(&status);
cout<<"信号信息:"<<(status&0x7F)<<" core dump:"<<(status>>7&1)<<endl;
return 0;
}
//结果:信号信息:8 是核心转储:1
b. 系统调用接口产生信号
本质:用户调用系统接口 -> 执行os对应的系统调用代码 -> os提取参数,或者设置特定的数值 -> os向目标进程写消息 -> 修改对应进程的信号标记位-> 进程后续会处理信号-> 执行对应的处理动作
a. 第一种
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
返回值:succeed:0 fail:-1并且errno被设置
参数:pid:进程pid sig:信号
b. 第二种
#include <signal.h>
int raise(int sig);
返回值:succeed:0 fail:!0
参数:sig:信号
c. 第三种
#include <stdlib.h>
//发送6号信号,终止进程
void abort(void);
c. 软件条件产生信号
1.软件中管道通信
代码验证:
1.创建匿名管道 2.让父进程读取,子进程写入 3.父子进程通信一段时间时间 4.让父进程 关闭读端&&waitpid(),子进程一直写入 5.子进程退出,父进程拿到子进程退出码status 6.提取退出信号
#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>
int main()
{
int pipefd[2]={0};
int m=pipe(pipefd);
assert(m!=-1);
(void)m;
pid_t id=fork();
//子进程
if(id==0)
{
//关闭读端
close(pipefd[0]);
//写数据
int count=0;
char buffer[1024]="我是子进程,我正在给你发消息: ";
while(1)
{
char buffer1[1024]={0};
count++;
snprintf(buffer1,sizeof(buffer1),"%s count:%d",buffer,count);
ssize_t s =write(pipefd[1],buffer1,strlen(buffer1));
sleep(1);
}
exit(1);
}
//父进程
char buffer2[1024];
close(pipefd[1]);
int n=0;
while(1)
{
//sleep(1);
ssize_t s=read(pipefd[0],buffer2,sizeof(buffer2)-1);
if(s>0)
{
buffer2[s]='\0';
}
std::cout<<buffer2<<std::endl;
n++;
if(n==10)
{
close(pipefd[0]);
break;
}
}
//close(pipefd[0]);
int status=0;
wait(&status);
std::cout<<(status&0x7F)<<std::endl;
return 0;
}
2. 闹钟问题
先看一份代码:
#include<iostream>
#include<unistd.h>
int count=0;
int main()
{
alarm(1);
while(1)
std::cout<<cout++<<std::endl;
return 0;
}
最终结果:95927Alarm clock
为什么cpu计算这么慢:IO+网络的原因
改进,没有IO影响
#include<iostream>
#include<unistd.h>
void showCount(int signum)
{
cout<<"final count: "<<count<<endl;
}
int count=0;
int main()
{
signal(SIGALRM,showCount);
alarm(1);
while(1) cout++;
return 0;
}
结果:final count: 582527911
勘探定时器功能原理
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<functional>
#include<vector>
using namespace std;
typedef function<void ()> func;
uint64_t count=0;
vector<func> callbacks;
void showCount()
{
cout<<"final count: "<<count<<endl;
}
void logUser()
{
if(fork()==0)
{
execl("/usr/bin/who","who");
exit(1);
}
wait(nullptr);
}
void showLog()
{
cout<<"这个时日志功能"<<endl;
}
void catchSig(int signum)
{
for(auto& f:callbacks)
{
f();
}
alarm(1);
}
int main()
{
alarm(1);
signal(SIGALRM,catchSig);
callbacks.push_back(showCount);
callbacks.push_back(logUser);
callbacks.push_back(showLog);
while(1) sleep(1);
return 0;
}
问:如何理解软件条件给进程发送消息?
答:a. os先识别到某种软件条件触发不满足 b. os构建信号,发送在指定的进程
d. 硬件异常产生信号
除0代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
std::cout<<"除0错误"<<std::endl;
sleep(1);
}
int main()
{
signal(SIGFPE,handler);
int m=100;
m/=0;
while(1) sleep(1);
return 0;
}
输出结果:死循环打印:除0错误
问:如何理解除0问题?
答:1. 进行计算的是软件cpu 2. cpu内部是有寄存器的,状态寄存器(位图),有对应的状态标记位(溢出标记位),os会自动进行计算完毕之后的检测,如果溢出标记位是1. os识别到有溢出问题,立即找到当前运行进程,提取pid,os完成信号发送的过程,进程在适当的时候进行处理 3. 一旦出现硬件异常,进程一定会退出嘛,不一定,一般默认处理方式是退出 4. 为什么会死循环?--寄存器中的异常一直没有被解决
指针越界访问代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
void catcher(int signo)
{
std::cout<<"越界访问"<<std::endl;
sleep(1);
}
int main()
{
signal(SIGSEGV,catcher);
int* arr=nullptr;
*arr=6;
return 0;
}
问:如何理解野指针或者越界问题?
答:1. 都必须通过地址,找到目标位置 2. 我们语言上面的地址,全都是虚拟地址 3. 将虚拟地址转化为物理地址 4. 页表+(页表将数据导入)MMU中找到虚拟地址对应的物理内存(Memory Manager Unit,硬件,内部也有寄存器)5.野指针,越界- > 非法地址 - > MMU转化的时候,一定会报错。
注意:所有的信号,有他的来源,但最终全部都是被os识别,解释,并且发送的。
阻塞信号
信号其他相关概念
a. 实际执行信号的处理动作叫做信号递达
b. 信号从产生到递达之间的状态称为信号未决
c. 进程可以选择阻塞某个信号
d. 被阻塞的信号产生时保持在未决状态,直到进程对信号解除阻塞,才进行递达动作
e. 注意阻塞和忽略是不同的,前者处于阻塞,没有被递达,后者完成递达,但是不做处理。
信号在内核中表示的示意图
每个信号都有俩个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
sigset_t:每个信号只有一个bit的未决标志,非0即,不记录该信号产生了多少次,阻塞标志也是这样表示。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(signal Mask),这里"屏蔽"应该理解为阻塞而表示忽略。
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如使用printf直接打印sigset_t变量是没有意义的。
对信号集本身操作:
#include <signal.h>
//初始化set所指信号集,将所有信号bit置0,表示信号全部无效
int sigemptyset(sigset_t *set);
//初始化set所指信号集,将所有信号bit置1,表示信号全部有效
int sigfillset(sigset_t *set);
//将信号集中signo信号改成有效信号
int sigaddset (sigset_t *set, int signo);
//将信号集中signo信号改成无效信号
int sigdelset(sigset_t *set, int signo);
//以上返回值全部相同,succeed:0 fail:-1
//检测一个信号在set所指信号集是否存在,存在返回0,表示真,不存在返回-1,表示假
int sigismember(const sigset_t *set, int signo);
对进程task_struct中block和pending进行操作:
#include <signal.h>
读取当前进程的信号集,通过set参数传出,返回0表示成功,-1表示失败
int sigpending(sigset_t *set);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:how SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
SIG_BLOCK:表示了我们要将set信号传入到进程block信号屏蔽字,相当于mask=mask|set
SIG_UNBLOCK:表示我们要使用set对应的信号集取消进程对应的block信号屏蔽字,
相当于mask=mask&(~set)
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值
参数oldset:表示传入set信号集之前,进程的block信号屏蔽字
返回值:0表示成功,-1表示失败
问:当我们对所有的信号都进行了自定义捕捉,这个进程是不是就意味着不能被信号杀死?
答:不是,9号信号不能被自定义捕捉,9号信号可以强制杀死一切进程。
问:当我们将2号信号block,并且不断获取并打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就可不可以看到pending信号集中,有一个比特位0->1?
代码验证如下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signo)
{
std::cout<<"捕捉到一个信号: "<<signo<<std::endl;
}
void showSignal(sigset_t* pending)
{
for (int i = 1; i <= 31; i++)
{
int m = sigismember(pending, i);
if (m == 1)
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
signal(SIGINT,handler);
//1.创建俩个sigset_t变量
sigset_t bset, obset,pending;
//2.初始化信号集
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
//3.向bset信号集中加入2号信号
sigaddset(&bset, SIGINT);
//4.将2号信号进行阻塞
sigprocmask(SIG_BLOCK, &bset, &obset);
int count=0;
while(1)
{
//5.获取pending信号集
sigpending(&pending);
//6.打印全部的pending信号集中所有的信号信息
showSignal(&pending);
sleep(1);
if(count++==20) break;
}
return 0;
}
问:当我们对所有的信号都进行block,我们是不是就写了一个不会被异常或者用户杀掉的进程?
答:不是,9号信号和19号信号不会被block,可以杀掉进程,前者是杀掉进程,后者是暂停进程,20号信号默认行为是忽略,本意是在终端停止输入,也不会被block。
问:信号产生之后,可能无法被立即处理,会在合适的时候处理,这个合适的时候是什么时候?
答:由于信号存在于进程的pcb的信号位图中,所以要处理信号,必须在内核态中,从内核态返回用户态的时候,操作系统会进行信号检测和处理。
问:进程为什么会进入内核态中?
答:进行系统调用,缺陷陷阱异常等,汇编指令int 80是一种中断指令,会内置在系统调用函数中,当调用系统接口时,在cpu中进行身份认证,cpu中存在俩套寄存器,一套给自己用,其中有一个寄存器CR3,表示当前执行权限,相当于位图,1表示内核态,3表示用户态。
注意:页表有俩种,内核级页表,用户级页表,所以操作系统就可以实现在进程地址空间上,俩种状态的转换,0-3G用户空间,3-4G表示内核空间,这个内核级页表是被所有进程地址空间共享,用户态是一个受管控的状态,内核状态是操作系统执行自己代码的一个状态,具有非常高的优先级
问:进程凭什么有权执行os的代码?
答:凭的是处于内核态还是用户态?
捕捉信号
1. 信号在pcb中是如何被os检测和处理流程?(图解)
问:os能做到帮进程执行对应的handler方法嘛?为什么os不在内核态执行用户代码呢?
答:可以,但是os不想,因为用户的代码可能会存在安全隐患。
2. 内核如何实现信号的捕捉(文字解释)
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
3. sigaction(读取和修改与指定信号相关的处理动作)
#include <signal.h>
int sigaction(int signo, const struct sigaction *act,
struct sigaction *oldact);
//sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。
//act和oact指向sigaction结构体:
struct sigaction
{
void (*sa_handler)(int);//处理方法的函数指针,普通信号
void (*sa_sigcation)(int,siginfo_t*,void*);//实时信号
sigset_t sa_mask;//传入需要屏蔽的信号集
int sa_flags;//默认为0,不考虑
void (*sa_restorer)(void);//不考虑
}
注意:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
问:处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,os如何处理?
代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
//7.打印pending信号集
void showSignal(sigset_t* pending)
{
for(int i=1;i<=31;i++)
{
if(sigismember(pending,i)==1) cout<<"1";
else cout<<"0";
}
cout<<endl;
}
void handler(int signo)
{
cout<<"获取一个信号: "<<signo<<endl;
sigset_t pending;
sigemptyset(&pending);
int count=10;
while(count--)
{
//6.验证捕捉方法期间,如果进程收到多个同样的信号,会将block中对应的信号设置为1,
//等捕捉方法完成,自动将block中对应的信号设置为0
sigpending(&pending);
showSignal(&pending);
sleep(1);
}
}
int main()
{
//1.设置结构体
struct sigaction act,oact;
//2.自定义捕捉方法
signal(SIGINT,handler);
//3.初始化结构体内的变量
act.sa_flags=0;
act.sa_handler=handler;
//4.设置要屏蔽的信号
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaddset(&act.sa_mask,6);
//5.调用捕捉方法
sigaction(SIGINT,&act,&oact);
while(1) true;
return 0;
}
可重入函数
如果一个函数符合以下条件之一,则是不可重入的:
a. 调用了malloc或free,因为malloc也是全局链表来管理堆的
b.调用了标准I/O库函数,标准I/O库的很多实现以不可重入的方式使用全局数据结构
volatile
代码如下:
#include<iostream>
#include<signal.h>
using namespace std;
int flag=0;
void changeFlag(int signum)
{
(void)signum;
cout<<"change flag: "<<flag;
flag=1;
cout<< "->" <<flag<<endl;
}
int main()
{
signal(2,changeFlag);
while(!flag);
cout<<"进程正常退出后:"<<flag<<endl;
return 0;
}
//编译时优化:+-O3,一直运行不退出
//不优化:打印结果退出
问:为什么进程一直运行不退出?
答:cpu将flag的值保存到寄存器中,不从内存中读取flag的内容,解决方案,volatile int flag=0;volatile作用:保持内存的可见性。
SIGCHLD信号(子进程退出向父进程发送的信号)
验证父进程是否会收到子进程退出的信号码17:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
void handler(int signo)
{
cout<<"收到信号:"<<signo<<endl;
pid_t id;
while(id=wait(nullptr))
{
cout<<"wait pid: "<<id<<endl;
break;
}
}
int main()
{
signal(SIGCHLD,handler);
pid_t id=fork();
if(id==0)
{
cout<<"child pid: "<<getpid()<<endl;
sleep(3);
exit(1);
}
while(1)
{
cout<<"father process pid: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
如果我们不想得到子进程的退出信息(退出码,退出信号)代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
signal(SIGCHLD,SIG_IGN);
if(fork()==0)
{
cout<<"child: "<<getpid()<<endl;
sleep(5);
exit(0);
}
while(true)
{
cout<<"parent: "<<getpid()<<" 执行我自己的任务 "<<endl;
sleep(1);
}
return 0;
}
结果如下:可见父进程创建的子进程会被os自动回收,不会出现僵尸进程的状态。