Linux信号量和信号
1.温故知新
上一篇博客,我们又知道了一种进程间通信的方案:共享内存。它是在物理内存中用系统调用给我们在物理内存开辟一个共享内存,可以由多个进程的页表进行映射,共享内存不和管道一样,它的生命周期是随内核的,而且它的效率高,这是因为共享内存创建出来不需要系统调用,而我们的管道等都需要系统调用进行通信,系统调用由成本,所以共享内存效率比较高,不知道我们学C++的时候还知道内存池吗?SLT它为了提高效率,防止小块内存一直调系统调用,它会提前开辟出一大块内存池让我们的STL去使用,减少内存碎片的问题。new,malloc底层就是系统调用,我们用户肯定无法开辟空间,得让我们的系统帮助我们用户去开辟空间,为什么,系统是管理硬件和软件的软件。共享内存不提供互斥和同步,所以我们用户必须手动来保证访问资源的同步和互斥。
这里简单介绍一下,互斥是什么?顾名思义,就是互斥的,我一个资源在同一时间只能有一个线程来访问,同步就是让不同的线程按照一定的顺序来访问我们的资源,同步必须保证互斥,只不过是有顺序了,所以同步=互斥+顺序。
2.并发编程
介绍信号之前,先了解一个概念叫做并发编程。
多个执行流,能看到同一份资源,如果我们并发访问一份共享资源,会导致数据不安全,我们需要协调我们多执行流,就是保护我们的共享资源,一般就是共享和互斥。
被保护起来的资源,一般是全局变量等全局的,叫做临界资源。
保护的方式一般就是我们上面说的互斥和同步,让多个进程在同一时间只能有一个进程来访问这个资源,同步就是协调进程,让它们按照一定的顺序来访问,避免饥饿问题。
多个执行流,在访问临界资源的时候,具有一定的顺序性,叫做同步。同步保证了互斥,在保证一个执行流访问共享资源的时候同时保证了一定的顺序性。
系统中某些资源同一时间只允许一个执行流访问,这样的资源叫做临界资源。
在执行流中涉及访问互斥资源的程序叫做临界区,你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)。
所谓的对共享资源的保护,其实就是对临界区代码的保护。归根到底我们是写代码来保护资源的,对代码的保护就间接实现了对共享资源的保护。
3.信号量
信号量就是一个计数器。我们知道我们有的时候会去看电影,假如我们电影是一个资源,我们一个电影院一般要容纳很多人进去看电影,所以我们这个时候很多人进去看电影就是抽象为资源分给很多执行流使用,而如果我们一个VIP影院只允许一个人去看,抽象出来就是我们就是把电影院资源作为一个整体去使用了。
我们电影院的经营者要进行卖票,我只要买了票,那个位置就是我的,就是对资源的预定。我们电影院是不能将一个位置的票重复出售的。
未来当我们的所有执行流想要访问一个 公共资源,要先对资源进行申请,也就是买票,相当于对资源的预定机制。
但是所有进程想要看到信号量,信号量本身就是一个共享资源,那么谁来保证信号量的安全嗯?答案是把信号量设计为原子的,不可被中断的。
所以我们的信号量实际就是对一个资源的预定机制,允许多个执行流去访问一个共享资源,所以未来执行流想要访问临界资源就要先申请信号量,对资源进行预定。
我们的二元信号量就是互斥,多元信号量就是同步,说白了我们二元信号量就是只0和1只允许一个执行流申请,多元信号量允许多个执行流对资源进行申请。二元就是把电影院的资源当整体使用,只允许一个人进去看。多元就允许我们多个执行流对资源进行预定,然后进去访问了。多元信号量允许多个线程对资源进行并发访问,如果我们里面有共享资源还要搭配锁来保证我们的线程安全。
信号量可以限制我们执行流访问共享内存的个数,当它为1时,它只允许一个执行流去访问我们的共享内存,会保护我们的共享内存的安全。
4.信号
1.什么是信号
讲完了信号量,我们来讲一讲信号,有人问,信号量和信号一字之差有啥联系吗?答案是没有联系,就和老婆饼和老婆没有半毛钱关系一样。
信号有信号产生,信号保存,信号处理三个阶段。现实生活中我们看到红灯信号就知道不能走,信号在发出之前我们就知道对应的应对方法了。相似的,我们计算机中,我们进程内部都已经内置了对信号的处理方法和处理机制了。我们能识别这些信号是因为被教育过,进程能识别过这些信号是已经被写好了。
还有信号到来时,我们可能正在做更重要的事,不会立即去处理信号,比如快递到达了, 我还正在打游戏,那得等我打完游戏再去取快递吧。
进程对于信号的处理有默认动作,顾名思义,就是默认处理,还有就是忽略,忽略它不管它,还有就是我们自定义捕捉,OS会提供对应的函数帮助我们捕捉信号并对它的处理进行修改。
我们知道前面讲过我们在shell中启动一个命令,如果我们想让它停下来,可以按crtl+c,就可以终止掉这个进程,当我们按下这个键的同时,我们就会想这个进程发送2号信号,让它终止下来,我们看到的就是这个进程被我们终止了。
结论1:我们通过crtl+c这个案例可以明白我们的键盘可以想进程发送信号。
结论2:kill命令可以杀死一个进程。
我们先来说一下crtl+c的详细,就是我们只有前台进程可以通过键盘获取信号,任何时刻,一个登录系统只允许一个进程在前台运行,其他进程都在后台。
我们fork之后,父进程先退出,让子进程变孤儿进程,它就会自动变为后台进程了。
我们有一个signal可以对捕捉的信号做自定义处理可以验证我的说法,有兴趣可以去Linux上试一下。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 自定义信号处理函数
void signal_handler(int signum) {printf("自定义信号处理:捕获信号 %d\n", signum);
}int main() {// 注册信号处理函数signal(SIGINT, signal_handler);printf("程序运行中,按下 Ctrl+C 触发信号...\n");printf("进程ID: %d\n", getpid());// 无限循环以保持程序运行while(1) {sleep(1);}return 0;
}
2.信号的本质
我们说是信号信号的,其实就是一个宏,我们发现我们的信号是没有0号信号的。我们前面说过信号分为大致下面:信号产生,信号保存,信号处理,我们信号产生不会去立即处理,而是在合适的时候处理,那我就得把我的信号保存下来啊,保存下来我才知道他来过啊!那么记录在哪里呢?怎么记录呢?
下面依次回答:记录在结构体力,结构体里各种变量,怎么记录呢?通过位图,我们一个有这么多信号,我们一个整型刚好32个比特位,一个比特位表示一个信号,可以保存所有产生的信号。
下面我有2个问题是:如何理解给进程发信号?有了上面的认识,给进程发信号不就把一个比特位由0变1吗?我们查一下比特位我们就知道这个信号来过了,它就被保存下来了。
如何识别信号呢?这也是我们看它在位图的哪个比特位,看它对应比特位是0还是1我们就可以知道在这个信号在哪里,是否产生过。
但是总而言之,我们发信号是通过操作系统发送的,无论发送的信号方式有多少种,我们用户是无法发送信号的,本质还是我们操作系统去发送信号的。
不知道还记得我们以前介绍过的task struct结构体吗?每个进程创建出来这个结构体里面就描述它的属性,我们的信号存储就是存储在我们的task_struct里了。task_struct本质是内核的数据结构,谁有权利去内核改数据,只有操作系统,所以我们得出上面的结论,既然我们的信号是通过位图在我们的task_struct(描述进程属性的结构体)里存储的,我们修改里面的一个变量可以保存我们的信号,那么就只有操作系统本身可以去内核修改数据,就只有操作系统才可以给我们的进程发送信号!!!