生活角度的信号
a.信号在生活中,随时可以产生(信号的产生和我是异步的)
b.你能认识这个信号
c.我们知道信号产生了,我能识别这个信号,信号该怎么处理
d.我们可能正在做着更重要的事情,把到来的信号暂不处理(1.我记得这个事 2.合适的时候处理)
信号介绍
在bash上执行命令kill -l便可看到系统定义的所有信号
我们只研究前31个信号,后面31个是实时信号这里不做研究
每个信号都有一个编号和一个宏定义名称,这些宏定义都可以在signal.h中找到,在man手册中还可以找到各种信号的详细信息
man 7 signal
这里具体介绍了信号在什么时候产生,处理的动作是什么
信号概念的基本储备
信号:Linux系统提供的一种,向指定进程发送特定事件的方式,做识别和处理。
信号产生是异步的。
信号处理常见方式
1、忽略该信号
2、执行信号的默认处理动作(终止自己、暂停、忽略.....)
3、提供一个信号处理函数,要求内核在处理信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个异常
Core和Term:默认动作都是终止,但Core能产生Core文件
如何理解信号的发送和保存?
进程---task_struct---struct---成员变量---用位图来保存收到的信号
uint32_t signals;
0000 0000 0000 0000 0000 0000 0000 0000
发送信号:修改指定进程pcb中的信号指定位图,0->1,写信号
pcb:内核数据结构对象,只有OS有资格修改内核结构对象中的值
信号产生具体过程
kill命令
通过kill命令向指定进程发送指定信号
kill -数字 进程号
通过终端按键来产生信号
ctrl+c 2)SIGINT 向当前进程发送2号信号
ctrl+\ 3)SIGQUIT向当前进程发送3号信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并Core Dump,我们在Linux环境下来验证一下,先来了解一下什么是Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存在磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有BUG,比如非法访问内存导致段错误,事后可以用调试器检查core文件以查清楚错误原因,这叫做事后调试,一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中),默认是不允许改变这个限制,允许产生core文件。首先用ulimit命令来改变shell进程的Resource Limit,允许core文件最大为1024k
通过信号返回值我们发现有一个core dump位。
云服务器默认关闭这个core文件功能
ulimit -c 1024 //打开这个文件功能
写一个死循环程序
编译并执行程序:
看到的现象是先打印出pid然后一直在死循环,按下组合键ctrl+\后退出并提示core dumped
test程序也会core dump的原因是我们先修改了shell的Resource Limit值,而test进程是由shell产生的所以test进程的PCB也是由shell复制而来,所以test进程和shell就具有相同的Resource Limit值,所以就会产生core dump了。
如图所示就是产生的core文件
最新的Linux版本所生成的core文件没有后缀,同一个文件如果多次执行多次异常终止,那么生成的core文件还是一份。这样就避免了一份文件生成多份core文件占内存。
调用系统函数来向进程发信号
kill函数
参数解释:
第一个参数进程id
第二个参数信号标号
返回值:成功返回0失败返回-1
./mykill 2 1234
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
return 1;
}
pid_t pid = std::stoi(argv[2]);
int signum = std::stoi(argv[1]);
kill(pid, signum);
}
raise函数
发信号给自己 == kill(getpid(), sig)
只能向当前进程发送信号
参数解释:
信号标号
返回值:成功返回0失败返回-1
#include <signal.h>
#include <stdio.h>
int main()
{
printf("raise befor.");
raise(9);//结束自己。相当于_exit
printf("raise after.\n");
return 0;
}
CLC@Embed_Learn:~/linux_io/02/02/seven$ ./a.out
Killed //并未打印出raise befor.,所以相当于_exit
信号处理函数:
abort函数
函数功能:使当前进程接收到信号而异常终止
参数:无参数
返回值:无返回值
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
int cnt = 0;
signal(SIGABRT, handler);
while (true)
{
sleep(1);
std::cout << "hello bit, pid: " << getpid() << std::endl;
abort();
}
}
信号虽被捕捉,但abort正常接收终止进程
signal函数
void (*signal(int signum, void (*handler)(int)))(int);
令A= void (*handler)(int) = 函数指针变量。
void (*signal(int signum, A))(int);
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函数;
这个函数的返回值是一个函数指针。
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(SIGABRT, handler);
return 0;
}
捕捉到SIGABRT信号后,可以执行我们所定义的函数,并将signum传入函数sig
注意:
该函数设置一次,进程不结束可以无限捕捉
并不是所有的信号都可以捕捉,9号信号不允许自定义捕捉
软件条件产生
#include <unistd.h> unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动 作是终止当前进程。
函数功能:
设定一个闹钟,告诉内核在seconds秒后给当前进程发送一个SIGALRM信号,该信号的默认处理动作是终止当前进程
函数参数解释:闹钟的时间是多少秒
函数返回值:这个函数的返回值是0或者闹钟剩下的秒数,
当你一直不修改闹钟,直到闹钟响这时的返回值是0
当在设定的秒数之内修改了闹钟的秒数就会返回上个闹钟剩下的时间,将seconds值设为零表示取消闹钟
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(3); // 设定1S后的闹钟 -- 1S --- SIGALRM
int n = alarm(0): 取消闹钟, 上一个闹钟的剩余时间
std::cout << "n : " << n << std::endl;//3
sleep(10);
}
OS对闹钟如何做管理?先描述再组织
struct alarm
{
time_t expired;//未来的超时时间=seconds+Now();时间戳
pid_t pid;
fun_t f;
.....
}
这里是用大堆或者小堆将结构体链式存储起来的
硬件异常产生信号
程序为什么会崩溃???非法访问和操作,导致OS给进程发信号
非法访问:SIGSEGV信号
非法操作:SIGFPE信号
崩溃了为什么会退出?可以不退出吗,可以。
默认是终止进程,捕捉信号即可不退出,但最好终止退出进程释放进程的上下文数据,包括溢出标志数据或者其他异常数据。
int main()
{
// 程序为什么会崩溃???非法访问、操作(?), 导致OS给进程发送信号啦!! --- 为什么
// signal(SIGSEGV, handler);
// signal(SIGFPE, handler);
// 崩溃了为什么会退出?默认是终止进程
// 可以不退出吗?可以,捕捉了异常, 推荐终止进程(为什么?) --- 为什么?
// int *p = nullptr;
// *p = 100; // SIGSEGV
int a = 10;
a /= 0; // 8) SIGFPE
while (true)
{
std::cout << "hello bit, pid: " << getpid() << std::endl;
sleep(1);
}
}
阻塞信号
信号在内核中的表示
信号在内核中一般有三种状态:
(1)信号递达(Delivery):实际执行信号的处理动作称为信号递达(处理)(终止,忽略,暂停)
(2)信号未决(Pending):信号从产生到递达之间的状态;
(3)信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;
注意:阻塞与忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
一个信号的阻塞和它有没有未决没有关系
下面来看一下操作系统为每个进程提供的一套信号机制:
上图的三张表分别为:阻塞表(Block)、未决表(Pending)、递达表
这三张表分别对应三种不同的状态:信号阻塞、信号未决、信号递达之后的自定义捕捉
前两张表都是通过位图来存储的(决定了当前是否能收到信号),信号被阻塞就将相应位置置1,否则就置0。而在pending表中,当前位是1时表示信号存在,置0时表不存在。(pending表中的数据是判断信号是否存在的重要因素)
pending底层原理:
位图 int 0000 0000 0000 0000 0000 0000 0000 0000
比特位的位置:代表信号编号
比特位的内容:代表信号是否收到(存在)
handler数组
信号的编号就是数组的下标,可以采用信号编号,索引信号处理方法
signal(2,handler)
block底层原理:
一张位图和pengding类型完全一样
0000 0000 0000 0000 0000 0000 0000 0000
比特位的位置:代表信号编号
比特位的内容:代表信号是否阻塞
SIGHUP信号(也就是(1)号信号)未阻塞也未产生过,当它抵达时执行默认处理工作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号从未产生过,一旦产生,SIGQUIT信号将被阻塞,它的处理动作是用户自定义的函数singhandler。
总结:两张位图+一张函数指针数组 == 让进程识别信号
小问题:
倘若在进程解除对某种信号的阻塞之前这种信号产生过多次,将会如何处理?
解析:Linux下的实现方式:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
小知识点:普通信号允许丢失、实时信号是不允许的
sigset_t
本质是一个长整型数组(每个4字节)组成的结构体,用它作位图
struct bits { uint32_t bits[400]; // 400*32 }; 40 40/(sizeof(uint32_t)*8) = 1 -> bits[1] 40%(sizeof(uint32_t)*8) = 8 -> bits[1]:8
sigset_t Linux给用户提供的一个用户级的数据类型, 禁止用户直接修改位图
信号集操作函数
#include <signal.h>
// 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信息
int sigemptyset(sigset_t *set)
// 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信息包含系统支持的所有信号
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)
注意点:
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
返回值:这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask
信号屏蔽字(sigprocmask):通过调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
// 返回值:若成功则为0,若出错则为-1
(1)若oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
(2)若set是非空指针,则更改进程的信号屏蔽字,参数how至少如何更改
(3)若oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字
how参数:
* SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
* SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
* SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达
sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
实验:
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.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;
}
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);
}
}
由上图实现结果我们看到SIGINT信号被阻塞,所以按kill之后SIGINT信号处于未决状态,此时信号集数据由0表1。经过15s后,我们将其捕捉处理,未决状态清0。
补充:
1.解除屏蔽,一般会立即处理当前被解除的信号(如果被pending)
2.pending位图对应的信号也要被清0,在递达之前。
捕捉信号
自定义处理信号可能不会被立即处理,而是在合适的时候处理
进程从内核态返回到用户态的时候进行检测和处理
内核态和用户态在地址空间上的理解
谈谈键盘输入数据的过程
我们学习的信号就是模拟中断实现的
信号:纯软件
中断:软件+硬件
谈谈理解OS如何正常运行
1.如何理解系统调用
我们只要找到特定数组下标(系统调用号)的方法,就能执行系统调用了
pid_t fork()
{
mov 2 eax;//系统调用号放入寄存器中
int 0x80;用户态->内核态
}
2.OS是如何运行的
操作系统本质就是一个死循环+时钟中断 不断调度系统的任务的
OS不相信任何用户,用户无法直接跳转到[3,4]GB内核空间
必须在特定的条件下才能跳转过去(3->0)(硬件cpu配合)
sigaction
#include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体;
将 sa_handler 赋值为常数 SIG_IGN传给sigaction表示忽略信号 , 赋值为常数SIG_DFL 表示执行系统默认动作, 赋值为一个函数指针表示用自定义函数捕捉信号 , 或者说向内核注册了一个信号处理函数 , 该函数返回值为void, 可以带一个 int 参数 , 通过参数可以得知当前信号的编号 , 这样就可以用同一个函数处理多种信号。显然, 这也是一个回调函数 , 不是被 main 函数调用 , 而是被系统所调用。当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }
- sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
- sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
- sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
// 当前如果正在对n号信号进行处理,默认n号信号会被自动屏蔽
// // 对n号信号处理完成的时候,会自动解除对n号信号的屏蔽
// // 为什么?
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(30);
break;
}
// exit(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 << "I am a process, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
for(int i = 0; i <= 31; i++)
sigaction(i, &act, &oact);
9号信号不能被捕捉处理
可重入函数
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
volatile int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo:" << 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;
}
如果没有volatile,变量gflag将被优化,默认取0
但实际情况,我们收到信号改变了gflag的值,后面的代码不会被执行
SIGCHLD信号
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
pid_t rid = waitpid(-1, nullptr, 0);
if (rid > 0)
{
std::cout << "wait child success, rid: " << rid << std::endl;
}
}
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;
}
问题1: 如果一共有10个子进程,且同时退出呢?
答:同时退出只会收到一个信号,然后依次回收
问题2: 如果一共有10个子进程, 5个退出,5个永远不退出呢?
答:5个会回收,然后另外5个子进程会阻塞等待,也可以不阻塞等待
void notice(int signo) { std::cout << "get a signal: " << 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; }
对信号进行忽略即可不用回收
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);
}
}