目录
1.1.信号的概念
1.1.1什么是linux信号?
本质是一种通知机制, 用户或者OS,通过发送一定的信号,通知进程某些事情已经发生,你可以在后续进行处理,?信号是进程之间事件异步通知的一种方式,属于软中断
1.1.2信号的结论
a.进程要处理信号,必须具备信号 “识别” 的能力(看到 + 处理)
b.凭什么进程能够 “识别” 信号? 程序员!
c.信号产生式随机的,进程可能在忙别的事情,信号的处理可能不是立即处理
d.信号会临时记录下对应的信号,方便后续处理
e.信号什么时候处理?合适的时候
g.信号的产生相对于进程式异步的(一般而言)
1.2为什么要有信号?
可以根据收到的信号~~>执行相应的操作
1.3信号的使用
用kill -l命令可以察看系统定义的信号列表
普通信号 ,?实时信号(编号34以上)
9,18,19信号是管理员信号,它们无法被进程阻塞或忽略
产生 ~~> 发送 ~~> 处理
--kill接口
头文件: ?<signal.h>
函数:?int kill(pid_t pid, int signo);
功能: 向指定进程发送指定信号
--raise接口: 向自己发送指定的信号? ? ?(头文件:signal.h)
头文件: ?<signal.h>
函数:?int raise(int signo);
功能: 给当前进程发送指定的信号(自己给自己发信号)
--abort接口: 自己终止自己(发送确认)? ?(头文件:stdlib.h)
头文件:<stdlib.h>
函数:void abort(void);
功能:abort函数使当前进程接收到信号而异常终止
通过系统调用接口,向进程发送信号
--管道:SIGPIPE是一种由软件条件产生的信号, 如:管道的读端不再读且关闭了,写端再写没有意义,OS会自动终止对应的写端进程(通过发送信号的方式,SIGPIPE)
--alarm:seconds秒之后给当前进程被SIGALRM信号终止。
头文件:<unistd.h>
函数: unsigned int alarm(unsigned int seconds);?
功能:告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
--除0异常(进行计算的是cpu这个硬件)
CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程
--野指针或者越界问题(使用指针需要找到目标未知,将虚拟地址转换为物理地址,通过MMU)
MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
--异常的检测:cpu内部是有记存器的, 状态寄存器, 有对应的状态标记位, 有溢出标记为, OS会自动进行执行完毕的检测!
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号
由OS识别, 解释, 发送
功能:通过回调的方式,修改捕捉信号对应的方法
头文件:#include<signal.h>
函数:sighandler_t signal(int signum, sighandler_t handler); //回调函数
typedef void (*sighandler_t)(int); //函数指针
所有的信号, 有它的来源, 但最总全部都是被OS识别, 解释, 并发送的
1.上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
因为OS是进程的管理者
2.信号的处理是否是立即处理的?
在合适的时候
3.信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
PCB对应的信号位图当中
4.一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
能,程序员已经写好
5.如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
OS去修改位图: 根据编号~~>修改比特位~~>比特位由0置1就完成了信号的发送
--系统调用
用户调用系统接口->执行OS对应的系统调用代码->OS提取参数或设置特定数值->
OS向目标进程写信号->修改对应进程信号标记为->进行后续会处理信号->执行对应处理动作
--如何理解信号发送的本质?
信号位图是在task_struct ->tasj_struct 内核数据结构 ~> OS
信号发送的本质: OS向目标进程写信号 ,OS直接修改pcb中指定的位图结构,完成 “发送” 信号的过程
--如何理解信号被进程保存?
a.什么信号 b.怎么产生 ~~>进程必须具有保存信号的相关数据结构(位图)
PCB内部保存了信号位图字段
--信号处理的常见方式
a.默认 (程序自带,程序员写好的)b.忽略 c.自定义动作(捕捉信号)
--如何理解组合键变成信号?
键盘的工作方式是通过中断方式进行的,当然也能够识别组合键
OS解释组合键 ~>查找进程列表 ~> 前台运行的进程 ~> OS写入对应的信号到进程内部的位图结构中
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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);
//这四个函数都是成功返回0,出错返回-1。
int sigismember(const sigset_t *set, int signo);
//sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号
//若包含则返回1,不包含则返回0,出错返回-1
sigprocmask
功能:调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
头文件:#include <signal.h>
函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
how参数的可选值:
sigpending
功能:读取当前进程的未决信号集,通过set参数传出
头文件:#include <signal.h>
函数:int sigpending(sigset_t *set);
返回值:调用成功则返回0,出错则返回-1
信号产生以后可能无法立即处理,在合适的时候处理(从内核态返回用户态的时候)
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,
举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler.
用户态与内核态
CPU寄存器有2套,一套可见,一套不可见,CR3->表示当前CPU的执行权限: 1内核态 3用户态
我们凭什么有权利执行OS的代码? 根据我们是用户态还是内核态
内核也是在所有进程地址空间上下文中跑得
int80用来切换内核态和用户态(调相关系统接口)
功能:可以读取和修改与指定信号相关联的处理动作
头文件:#include <signal.h>
函数int sigaction(int signo,const struct sigaction *act,struct sigaction *oact);
返回值:用成功则返回0,出错则返回-1
--处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS如何处理?(本质:为什么要有block)
某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项
一个函数如果被多个执行流重复进入了, 不会出问题, 就叫可重入函数 , 反之就叫不可重入函数
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void catchsig(int signum)
{
cout << "捕捉到一个信号: " << signum << "Pid: " << getpid() << endl;
}
int main()
{
// signal(2,fun);
signal(SIGINT, catchsig); // 特定信号的处理动作一般只有一个
// signal函数,仅仅是修改进程对待特定信号的后续处理动作,不是直接调用函数
while (true)
{
cout << "Runing , Pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
效果:
设置一个闹钟,1s后会向进程发送SIGALRM信号
捕捉这个信号,改为执行部分功能,并重新设置一个闹钟(catchsig)
使用vector存放函数指针,catchsig中,执行里面的函数
代码:
1.如果我们对所有的信号都进行了自定义捕捉--我们是不是就写了一个不会被异常或者用户杀掉的进程?
并不是, 操作系统的设计者也考虑了~~>9号信号--管理员信号-->无法设定自定义捕捉动作
2.如果我们将2号信号block,并且不断的获取并打印当前进程的pending信号集, 并突然发送一个2号信号,我们是不是可以看到pending信号集中, 有一个比特位0~>1
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
void handler(int signum)
{
std::cout << "捕捉了一个信号: " << signum << std::endl;
}
int main()
{
signal(2, handler);
// 1.定义信号集对象
sigset_t bset, obset;
sigset_t pending;
// 2.初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3.添加要屏蔽的信号
sigaddset(&bset, 2);
// 4.设置set到对应进程内部(默认情况,进程不会对任何信号进行block)
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(n == 0);
(void)n;
std::cout << "block 2号信号成功, pid: " << getpid() << std::endl;
// 5.重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 获取当前进程的pending信号集
sigpending(&pending);
// 显示pending信号集中,没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 15)
{
// 默认情况下,恢复对于2号信号的block的时候,进行递达,
// 但是2号信号的默认处理动作是终止进程
// 需要对2号信号捕捉
int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
assert(n == 0);
(void)n;
std::cout << "解除2号信号block成功" << std::endl;
}
}
return 0;
}
???????
3.如果我们对所有的信号进行block--我们是不是就写了一个不会被异常或者用户杀掉的进程?
SIGKILL (9): 这是用来强制终止进程的信号,它可以无视进程的阻塞状态直接终止进程。
SIGSTOP (19): 发送这个信号会暂停目标进程的执行,无法被阻塞。
SIGCONT (18): 发送这个信号会恢复已经被暂停的进程
这些信号的特殊性在于它们无法被进程阻塞或忽略,甚至在进程安装了信号处理函数(signal handler)进行处理时也不例外。这确保了在需要强制终止或暂停进程时能够始终生效。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
cout << "获取了一个信号:" << signum << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
// 设置进当前进程的PCB中
sigaction(2, &act, &oact);
cout << "default action:" << (int)oact.sa_handler << endl;
while (1) sleep(1);
return 0;
}
SIGCHLD 是一个由操作系统发送给父进程的信号,用于通知父进程一个子进程已经终止或暂停。当一个子进程变为僵尸进程(zombie process)时,操作系统将会发送 SIGCHLD 信号给父进程。父进程可以通过捕获 SIGCHLD 信号并调用 wait() 或 waitpid() 等函数来处理子进程的终止状态,避免僵尸进程的积累。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signum)
{
cout << "获取到了一个信号: " << signum << endl;
}
int main()
{
signal(SIGCHLD, handler);
if (fork() == 0)
{
sleep(1);
exit(1);
}
while (true)
sleep(1);
return 0;
}
子进程退出后,父进程收到信号(父进程对其捕捉)
--不想等待子进程 + 子进程退出之后, 自动释放僵尸子进程:SIG_IGN
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
//不想等待子进程 + 子进程退出之后, 自动释放僵尸子进程
signal(SIGCHLD,SIG_IGN);
if (fork() == 0)
{
cout << "child: " << getpid() << endl;
sleep(5);
exit(0);
}
while (1)
{
cout << "parent:" << getpid() << "执行自己的任务"<< endl;
sleep(1);
}
}
OS默认的忽略 ~~> 不回收子进程(忽略程度较低, OS拿不准是否真的忽略)
用户使用忽略~~>告诉OS需要忽略~~>回收子进程
--core dump标志: 代表是否发生了核心转储
--核心转储:当接受到信号退出的时候(进程出现某种异常), 将数据dump到磁盘上-->主要是为了调试
一般而言,云服务器(生产环境)的核心转储是被关闭的,(可用ulimit -a 查看当前环境的资源配置)
原因:若写的服务出现异常,且机器自带重启,没过多久,磁盘上就会充满dump来的数据
作用: 避免编译器优化,让内存中的flag不可见
编译器有时候会自动给我们的代码进行优化:在main函数中没有任何语句是修改flag的,程序启动的时候,直接将flag的值放入CPU中的edx中,while循环做检测的时候,直接检测edx中的(不再检测内存中的flag), 下面这段代码修改的是内存中的flag, 但是检测的是edx中的
加上volatile,可避免编译器的优化