目录
- 1.临界资源
- 2.临界值
- 3.原子性
- 4.互斥
- 5.什么是信号量
- 6.什么是信号
- 1.信号概念
- 2.信号的处理方式
- 3.信号阶段
- 1.信号使用前,信号的产生
- 2.为什么进程会崩溃
- 3.信号产生中
- 4.信号产生后
1.临界资源
被多个进程能够看到看到额资源叫做临界资源
如果没有堆临界资源进任何的保护,对于临界资源的访问,双方访问都是乱序的,可能会因为读写交叉导致的各种乱码,废弃数据/访问控制方面的问题
2.临界值
对多个进程而言,访问临界资源的代码叫做临界区,比如二个访问共享内存的那一句代码
3.原子性
我们把一件事情,要么没做,要么做了,没有中间状态,我们叫做原子性
4.互斥
任何时刻,只允许一个进程,访问临界资源,这个我们叫做互斥
5.什么是信号量
信号量的本质是计数器
比如你去电影院看电影,电影院可以给多个人访问,我们叫临界资源,电影院只有100个座位,代表了只能有100张票,,因为多了的话就会的座位发生冲突
那么你怎么证明,放映厅里面特定的座位是你的,那么肯定是我买到了票,这个座位就是我的,那么买票的本质是对放映厅中特定的座位的一种,预定机制,那么你不去也没人坐,理想状态,人可能一样会坐,但是计算机里面的数据不会,那么怎么防止卖票卖多了,只要我们设定了一个值int count = 100,卖到了一张就count–,如果count为零就不卖,等有没有人退票或者电影看完下一场,买票的过程要保证原子性,比如一开始有个进程执行操作,操作到一半,发生进程切换换成另一个进程直接买50张票,买完50张票count-- 50次进程退出,回到最开始的那个进程,他又把count改回了100,那么这样就会因为进程交叉,而出现的数据错误,所以我们要保证原子性
总结
信号量是一个计时器,这个计时器对应的操作是原子的
6.什么是信号
1.信号概念
比如红绿灯,闹钟等等,我们要先能识别信号的能力,比如绿灯行,红灯停,闹钟醒了就起床,所以我们要能识别信号所表达的意思,还有闹钟,我们设置闹钟的时候我们早就知道了信号产生之后要做什么,即便当前信号还没产生,也就是闹钟还没响,而这个是叫做提前知道信号的处理方法,而我们知道信号所表达的意思和提前知道信号的处理方法,具备了这二种我们就有了处理特定信号的能力
那么我们把现实情况带入进程和信号
信号是进程发送得,所以进程要具备处理信号的能力
1.该能力一定是预先早就已经有了
2.进程能够识别对应的信号
3.进程能够处理对应的信号
#include<iostream>
#include<unistd.h>
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
我们来运行下面代码
ctrl + c 把进程终止掉
./myproc & //把当前进程添加到后台进程
jobs查看后台进程
fg 1 查找后台任务,把1放在前台任务,我们在ctrl+c,那么他就没终止了
如果你想要关闭后台进程,但是你后悔了那么就ctrl + z 这个时候我们调用jobs命令看到进程属于停止状态,我们bg 加进程号,他就恢复了运行状态了
2.信号的处理方式
因为信号产生时异步,当信号产生的时候,对应的进程可能正在做更重要的事情,我们进程可以暂时不处理这个信号,但是你必须要记住这个信号已经来了,等他完成了手上的事在来处理这个信号,
而信号的处理分分三种处理方式
1.默认动作 你送给你女朋友一个小礼物(处于热恋中),你女朋友表示很高兴
2.忽略 你和你女朋友发生了冷战,那么你再送给他礼物,他忽略你
3.自定义动作 和不同的人有不同的对应方式
而上面这种叫做信号的处理,专业点叫信号的捕捉
3.信号阶段
1.信号使用前,信号的产生
ctrl c ctrl z ctrl d 等等就是产生信号 ctrl + c就是向前台产生2信号
kill -l看信号
那么我们怎么证明是发送2号信号
signum,给特定信号设置捕捉动作,第二个参数是函数指针,可以自定义动作的函数指针
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号: "<< signo <<endl;
}
int main()
{
signal(SIGINT,handler);
sleep(3);
while(true)
{
cout<<"我是一个正在运行的进程 "<< getpid() <<endl;
sleep(1);
}
return 0;
}
可以看到我输入了ctrl + c,他就发出了信号是二,这里要注意的是,handler这里只是设置了,是SIGINT出信号了,才调用handler函数,如果没有信号了,那么永远都不会使用handler函数
用户层产生信号的方式,键盘产生,操作系统给进程发送(写入)信号
除了使用键盘发送信号,我们还能通过函数来发送信号
通过kill函数来发送信号
kill有二个参数,第一个是要发送的pid,第二个参数是发送的信号
发送信号失败错误返回-1
mykill.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
static void Usage(const std::string &proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
int main(int argc ,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
{
cerr << "kill: " << strerror(errno) << endl;
exit(2);
}
return 0;
}
运行一个不会中断的进程,用自己写的可执行文件,向进程发送9信号,终止掉进程
2.为什么进程会崩溃
进程崩溃的本质是,进程收到硬件的异常而导致OS向目标进程发送信号,进而导致进程终止的现象!
比如除零错误:cpu内部,状态寄存器,当我们出0的时候,cpu内的状态寄存器会被设置成为,有报错,浮点数越界,cpu的内部寄存器(硬件)会记录错误,os就好识别到cpu内存有报错,识别后就好处理
- 谁干的?2. 是什么报错(OS->构建信号)->目标进程发送信号->目标进程在合适的时候->处理信号->终止进程
越界&&野指针: 我们在语言层面使用的地址(指针), 其实都是虚拟地址->物理地址->物理内存->读取对应的数据和代码的,如果虚拟地址有问题,地址转化的工作是由(MMU(硬件)+页表(软件)), 如果转化过程就会引起问题->表现在硬件MMU上->OS发现硬件出现了问题1. 谁干的?2. 是什么报错(OS->构建信号) -> 目标进程发送信号->目标进程在合适的时候->处理信号->终止进程
那么了解了上面,进程报异常一定会终止程序吗?
代码
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号: "<< signo <<endl;
}
int main(int argc ,char* argv[])
{
for (int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);
}
int a = 10;
a/=0;
return 0;
}
可以看到没有终止进程,因为我们把它捕获了,因为原本的信号处理做法是终止进程,而我们把他捕获了自定义处理了,而我们的处理方式只是打印一串字啊,所以异常没有得到处理,所以就发出错误信号
3.信号产生中
1.当实际执行信号的处理动作称为信号递达,而这个其实也叫做信号处理,而信号处理,就是前面讲的三点 1.默认处理 2.忽略 3.自定义处理
2.信号从产生到递达之间的状态,称为信号未决(Pending),就是信号还没处理的时候
3.进程可以选择阻塞 (Block )某个信号,如果阻塞了,如果你取消了堵塞了,要不然就一直处在未决状态,无论你怎么发信号都不会做处理
忽略信号和堵塞信号有什么不用?
忽略信号是处理信号的一种,只不过他的做法是忽略它也就是什么都不做
而阻塞时拦截信号,不让信号做抵达
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。当信号产生操作系统会在进程控制块上修改控制块的未决标志,比如你是1号信号就把你置1,二号就置1,直到信号递达的时候才会清除比特位
上面图片的1号SIGHUP 里面的block和pending为0说明没有收到处理信号代表默认
上面的图片2号SIGINT 里面的pengdng为1说明了已经收到了2号信号但是因为block为1正在处在堵塞状态,所以你没有实现抵达,除外你把block为1,要pendding一直为1
上面的图片3号SIGQUIT,从来没有产生过,但是如果信号来了,我们一样要拦截你
所以上面是横着看的,先看block再看pending再看handler,如果已经收到了信号,再收到同样的信号,剩下的信号是直接丢弃的,linux是这么实现的,普通信号因为位图也只有一个,当然也有的会记多次,多次实现的,就是一个链表,收到信号把他连起来
int sigemptyset(sigset_t *set);://将比特位图全置为0
int sigfillset(sigset_t *set);//将比特位图全置为1
int sigaddset(sigset_t *set, int signum);//将该set位图,多少号信号置为1
int sigdelset(sigset_t *set, int signum);//将该set位图,多少号信号置为0
int sigismember(const sigset_t *set, int signum);//信号signum是否是set位图中的信号
sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
下面代码我们来查看pending信号集,如果发送2信号就处理进程打印
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号: "<< signo <<endl;
}
static void Usage(const std::string &proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
static void showPending(sigset_t *pendings)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main(int argc ,char* argv[])
{
signal(2,handler);
// 1. 不断的获取当前进程的pending信号集
sigset_t pendings;
while (true)
{
// 1.1 清空信号集
sigemptyset(&pendings);
// 1.2 获取当前进程(谁调用,获取谁)的pending信号集
if (sigpending(&pendings) == 0)
{
// 1.3 打印一下当前进程的pengding信号集
showPending(&pendings);
}
sleep(1);
}
return 0;
}
可以看到前面的都是0,这样说明了是默认状态,是可以处理信号的,也可以看到他处理了我向进程发出的信号2,打印了一串字
下面代码和前面不同的是,把2号信号添加进堵塞状态
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号: "<< signo <<endl;
}
static void Usage(const std::string &proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
static void showPending(sigset_t *pendings)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main(int argc ,char* argv[])
{
sigset_t bsig, obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
// sigfillset();
// 添加2号信号到信号屏蔽字中
sigaddset(&bsig, 2);
// 2. signal
// 设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
sigprocmask(SIG_SETMASK, &bsig, &obsig);
signal(2,handler);
// 不断的获取当前进程的pending信号集
sigset_t pendings;
while (true)
{
// 清空信号集
sigemptyset(&pendings);
// 1.2 获取当前进程(谁调用,获取谁)的pending信号集
if (sigpending(&pendings) == 0)
{
// 打印一下当前进程的pengding信号集
showPending(&pendings);
}
sleep(1);
}
return 0;
}
可以看到一开始的信号集是为0的,我们现在发2信号,因为前面我们把它添加进了堵塞状态,所以可以看到,可以看到,但是不能处理,然后也看到了信号集改成了1
下面代码是全部信号自定义,再堵塞全部信号,过了20秒后全部结束堵塞状态,并处理信号,且信号集全部变为0
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号: "<< signo <<endl;
}
static void Usage(const std::string &proc)
{
cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
static void showPending(sigset_t *pendings)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(pendings, sig))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main(int argc ,char* argv[])
{
cout << "pid: " << getpid() << endl;
// 3. 屏蔽2号信号
sigset_t bsig, obsig;
sigemptyset(&bsig);
sigemptyset(&obsig);
// sigfillset();
for (int sig = 1; sig <= 31; sig++)
{
// 3.1 添加2号信号到信号屏蔽字中
sigaddset(&bsig, sig);
// 2. signal
signal(sig, handler);
}
// 3.2 设置用户级的信号屏蔽字到内核中,让当前进程屏蔽到2号信号
sigprocmask(SIG_SETMASK, &bsig, &obsig);
// 1. 不断的获取当前进程的pending信号集
sigset_t pendings;
int cnt = 0;
while (true)
{
// 1.1 清空信号集
sigemptyset(&pendings);
// 1.2 获取当前进程(谁调用,获取谁)的pending信号集
if (sigpending(&pendings) == 0)
{
// 1.3 打印一下当前进程的pengding信号集
showPending(&pendings);
}
sleep(1);
cnt++;
if(cnt == 20)
{
cout << "解除对所有信号的block...." << endl;
sigprocmask(SIG_SETMASK, &obsig, nullptr);
}
}
return 0;
}
4.信号产生后
进程处理信号不是立即处理的,是在合适的时候处理的,是在当前进程从内核态切换成用户态的时候,进行检测和处理
页表分为二种,一种是用户层页表以32位为例1,他只负责3G的空间,还有另一个页表叫做内核页表,他负责的是内核空间,都知道页表是能映射到物理内存的,所以os是在内存里面加载的
当前进程判断是否具有权力,访问内核页表,他是要进行身份切换,
进程如果是用户态只能访问,用户级页表,只能访问自己的用户页表
进程是内核态那么他就能访问内核级页表和用户级页表 权限更高
那么怎么知道我是内核态还是用户态,cpu有对应的寄存器cr3,有比特位标识当前进程的状态0内核态, 3用户态,当系统调用的时候,时间片到了,进程间切换,当然肯定还有其他的复杂操作
你现在写了一串代码,代码里面有open要有打开的文件,你的代码属于用户态,open的代码那一部分属于内核态
我们之前想的是open直接返回值,但是都来到内核态了,所以打开后的文件要查看PCB信号,看看信号集看看block有没有被置为1,pending有没有收到信号的处理方法做检查,如果属于属于默认状态,就是block表和pending为0那么就是直接返回,继续处理代码,如果block表和pending都为1那么也直接返回,因为你收到了处理信号,但是你被堵塞了,不给解决,如果block为0,pending为1那么就执行我们的handler表,handler表如果是SIG_IGN,那么就是忽略它,把pending置为0,返回对应处,handler表如果是SIG_DFL,默认处理方法,通常都是终止掉这个进程,并所有代码释放掉,保留PCB,并设为僵尸状态,并把PCB填充成我所收到了信号,比如说2,那么也不需要返回了,还有最后一种情况就是自定义处理方式,这个是属于用户态的,因为是用户提供的方法
上面的自定义方法不能直接返回到你的代码,因为open的打开的文件还没返回提供返回值,而这个实行自定义方法是由用户态实现的,因为如果是内核态实现的话,你等于有了内核的权限(极高的权限),因为这个是用户写的所以这个自定义方法是某些恶意代码之类的,那么就完蛋了,他会造成了不可逆转的破坏,上面的图可以看到有跟线挡着,上面的是用户态,下面是内核态,我想说的是,你的代码到open的代码,进行了一次用户态转内核态,到了调用自定义方法的时候,进行了内核态转用户态,自定义方法返回,又进行了一次用户态转内核态
上面的那么多话,总结起来过程就是
进程的信号在被合适的时候处理,从内核态返回到用户层的时候->检测->处理