目录
一、信号量概述
二、信号概述
三、信号产生
1、终端按键产生信号
2、调用系统函数产生信号
3、硬件异常产生信号
4、软件条件
四、信号保存
1、信号阻塞
2、信号捕捉流程
五、信号递达
一、信号量概述
- 信号量:一个计数器,通常用来表示公共资源中,资源数量的多少
- 公共资源:能够被多个进程同时访问的资源
- 访问没有被保护的公共资源:数据不一致
- 被保护的公共资源:临界资源
- 资源(内存、文件、网络......)是要被使用的,一定有该进程对应的代码来访问这部分临界资源(临界区),不访问临界资源的代码是非临界区
- 如何保护公共资源:互斥 && 同步
- 所有进程在访问公共资源之前,都需要先申请sem信号量 ---> 信号量本身就是一个公共资源
- 公共资源的使用:1. 作为一个整体使用;2. 划分为一个个子资源使用
- 进程预定公共资源,信号量sem--(P操作);进程释放公共资源,信号量sem++(V操作);若信号量sem == 0,进程无法预定公共资源
- 信号量的 --、++ 操作是原子性的(要么不执行,要么执行完)
申请信号量
控制信号量
设置信号量(PV操作)
二、信号概述
信号与信号量的关系,就如同老婆和老婆饼的关系,没有任何关系。
[1, 31]是普通信号,[34, 64]是实时信号
- 进程本身是被程序员编写的属性和逻辑的集合 ------ 程序员编码完成
- 当进程收到信号的时候,进程可能正在执行更重要的代码,信号不一定会被立即处理
- 进程本身要有对应信号的保存能力
- 进程对信号的三种处理方式:默认、自定义、忽视 ------ 信号被捕捉
- 进程对信号的保存:task_struct中用位图结构保存
- 发送信号的本质:修改pcb中的信号位图
- pcb是内核维护的数据结构对象,由os管理
- 所有发送信号的方式,本质都是os向目标进程发信号: os向用户提供发送信号的系统调用
三、信号产生
1、终端按键产生信号
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while (1)
{
cout << "this is a process, pid = " << getpid() << endl;
sleep(1);
}
return 0;
}
键盘按键 ctrl C ---> os将ctrl+C解释为2号信号(SIGINT) ---> 进程终止
自定义捕捉方法
signal函数的调用,并不是自定义捕捉方法函数的直接调用
仅仅设置了对目标信号的捕捉方法,该方法并不一定会被调用
这个自定义捕捉方法只有收到了捕捉信号的时候才会被调用
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void Handler(int signo)
{
cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
}
int main()
{
signal(2, Handler);
while (1)
{
cout << "this is a process, pid = " << getpid() << endl;
sleep(1);
}
return 0;
}
当自定义2号信号(SIGINT)的捕捉方法之后,进程收到2号信号并不会终止,而是执行自定义捕捉方法的打印函数
2、调用系统函数产生信号
- kill() 可以向任意进程发送任意信号
- raise() 可以向自身进程发送任意信号 ----- kill(getpid(), signal)
- abort() 给自己发送指定的信号SIGABRT ----- kill(getpid(), SIGABRT)
mykill.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdio>
using namespace std;
static void Usage(const string& proc)
{
cout << "\nUsage: " << proc << "pid signo\n" << endl;
}
void Handler(int signo)
{
cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int n = kill(pid, signo);
if (n != 0)
perror("kill");
return 0;
}
test.cc 一个死循环进程
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
myraise.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
using namespace std;
int main()
{
int cnt = 0;
while (true)
{
printf("cnt: %d\n", cnt++);
sleep(1);
if (cnt >= 5)
{
raise(9);
//abort();
}
}
return 0;
}
5s后进程被自己发送的信号杀死(或放弃)
3、硬件异常产生信号
SIGFPE
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
using namespace std;
int main()
{
while (true)
{
cout << "我在运行中..." << endl;
sleep(1);
int a = 10;
a /= 0;
}
return 0;
}
除0 浮点数错误SIGFPE
获取信号编号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
using namespace std;
void CatchSig(int signo)
{
cout << "获取一个信号,信号编号是:" << signo << endl;
sleep(1);
}
int main()
{
signal(SIGFPE, CatchSig);
while (true)
{
cout << "我在运行中..." << endl;
sleep(1);
int a = 10;
a /= 0; //除0 浮点数报错
}
return 0;
}
- os通过状态寄存器当中的溢出标记位判断cpu是否发生运算异常,若产生异常,os发送信号
- 进程收到信号,不一定会立即执行
- cpu每次切换进程时进行上下文保存和恢复,os都会检测一次异常
4、软件条件
设置闹钟,进程1s之后结束,统计cnt累加次数
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
using namespace std;
int main()
{
alarm(1);
int cnt = 0;
while (1)
{
cout << "cnt: " << cnt++ << endl;
}
return 0;
}
将cnt定义为全局重新,用捕捉信号重新定义
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
int cnt = 0;
void CatchSig(int signo)
{
cout << "cnt: " << cnt << endl;
}
int main(int argc, char* argv[])
{
signal(SIGALRM, CatchSig);
alarm(1);
while (1)
{
cnt++;
}
return 0;
}
由此可见计算机将数据从内存打印到外设时间损耗巨大,IO效率很低
闹钟是一次性闹钟,执行之后不再执行
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
static int cnt = 0;
void CatchSig(int signo)
{
cout << "cnt: " << cnt << endl;
alarm(1);
}
int main(int argc, char* argv[])
{
signal(SIGALRM, CatchSig);
alarm(1);
while (1)
cnt++;
return 0;
}
任何一个进程都可以通过alarm()系统调用设置闹钟,操作系统如何管理闹钟?先描述,再组织
核心转储
部分服务器默认关闭了核心转储
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
//核心转储
while (1)
{
int a[10];
a[100000] = 100;
}
return 0;
}
越界访问,不仅报了段错误,还生成了core.30202文件
以Term退出的没有核心转储
核心转储:当进程出现异常的时候,将进程在对应的时刻,在内存中的数据转储到磁盘中
核心转储意义:支持调试(事后调试)
对所有信号做自定义捕捉测试
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
using namespace std;
void CatchSig(int signo)
{
cout << "收到一个信号,信号编号是:" << signo << endl;
}
int main(int argc, char* argv[])
{
for (int signo = 31; signo >= 1; --signo)
signal(signo, CatchSig);
while (1)
{
cout << "我在运行, pid = " << getpid() << endl;
sleep(1);
}
return 0;
}
只有9号信号可杀死进程(9号进程由os直接控制,禁止对9号信号做捕捉)
四、信号保存
- 执行信号的处理动作称之为信号递达
- 信号从产生到递达之间的状态叫做信号未决
- 进程可以选择阻塞某个信号
- 被阻塞的信号永远不会递达,除非进程接触阻塞
- 阻塞和忽视是有不同的,忽视是递达之后的一种处理动作
1、信号阻塞
每个信号都有两个标志位表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作
如果一个信号没有产生,可以预先将它设为阻塞状态
2、信号捕捉流程
信号产生的时候,不会被立即处理,而是在从内核态返回用户态的时候进行处理
用户想要访问内核或硬件资源,必须通过系统调用完成访问
CPU中存在CR3表态寄存器,表示当前运行的运行级别:0表示内核态,3表示用户态
每一个进程都有自己的地址空间(用户空间独占)、内核空间(被映射到每一个进程的3~4G)
进程要访问OS的接口,只需要在自己的地址空间上进行跳转
每一个进程都会共享一个内核级页表,无论进程如何切换,不会更改内核空间
系统调用接口,起始位置会将我们的用户权限更改为内核态(陷入内核)
特定的身份调用特定的代码,内核态也无法直接调用用户态状态
信号捕捉流程图
五、信号递达
sigprocmask读取或更改进程的信号屏蔽字
sigpending获取进程的pending位图
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <vector>
using namespace std;
static vector<int> sigarr = {2, 3};
void ShowPending(const sigset_t& pending)
{
for (int signo = 31; signo >= 1; signo--) {
if (sigismember(&pending, signo))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void MyHandler(int signo)
{
cout << signo << " 号信号已经被递达\n" << endl;
}
int main(int argc, char* argv[])
{
for (const auto& e : sigarr)
signal(e, MyHandler);
// 1. 屏蔽指定的信号
sigset_t block, oblock, pending;
// 1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
// 1.2 添加要屏蔽的信号
for (const auto& e : sigarr)
sigaddset(&block, e); // 2号信号
// 1.3 开始屏蔽
sigprocmask(SIG_SETMASK, &block, &oblock);
// 2. 遍历打印pending信号集
int cnt = 10;
while (1)
{
sigemptyset(&pending);
sigpending(&pending);
ShowPending(pending);
sleep(1);
if (cnt-- == 0)
{
cout << "恢复对信号的屏蔽\n" << endl;
sigprocmask(SIG_SETMASK, &oblock, &block);
}
}
return 0;
}
将2号、3号信号屏蔽
ctrl C + ctrl \ 发送2号信号和3号信号,信号未决,pending位图2号和3号bit位置1
10s后重置对信号的屏蔽,os直接将重置的信号递达,捕捉到2号、3号信号
sigaction
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <vector>
using namespace std;
void ShowPending(const sigset_t& pending)
{
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void MyHandler(int signo)
{
cout << "get a signo: " << signo << endl;
}
int main(int argc, char* argv[])
{
struct sigaction act, oact;
act.sa_handler = MyHandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oact);
while (true);
return 0;
}
进程串行处理同类信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
void Count(int cnt)
{
while (cnt)
{
printf("cnt: %d\n", cnt);
fflush(stdout);
cnt--;
sleep(1);
}
}
void MyHandler(int signo)
{
cout << "get a signo: " << signo << endl;
Count(10);
}
int main(int argc, char* argv[])
{
struct sigaction act, oact;
act.sa_handler = MyHandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); //在2号信号捕捉期间,屏蔽3号信号
sigaction(SIGINT, &act, &oact);
while (true) sleep(1);
return 0;
}
- 当进程正在递达某一个信号期间,同类信号无法被递达
- 当当前信号正在被捕捉,系统自动将当前信号加入到进程的信号屏蔽字,当捕捉完成,系统又自动解除对该信号的屏蔽
- 一般一个信号被解除屏蔽的话,会自动递达当前屏蔽信号
- 进程处理信号的原则是串行处理同类信号,不允许递归处理