当前位置:   article > 正文

【Linux后端服务器开发】信号量与信号_信号和信号量

信号和信号量

目录

一、信号量概述

二、信号概述

三、信号产生

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、终端按键产生信号

  1. #include <iostream>
  2. #include <unistd.h>
  3. using namespace std;
  4. int main()
  5. {
  6. while (1)
  7. {
  8. cout << "this is a process, pid = " << getpid() << endl;
  9. sleep(1);
  10. }
  11. return 0;
  12. }

键盘按键 ctrl C ---> os将ctrl+C解释为2号信号(SIGINT) ---> 进程终止

自定义捕捉方法

signal函数的调用,并不是自定义捕捉方法函数的直接调用

仅仅设置了对目标信号的捕捉方法,该方法并不一定会被调用

这个自定义捕捉方法只有收到了捕捉信号的时候才会被调用

  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. using namespace std;
  5. void Handler(int signo)
  6. {
  7. cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
  8. }
  9. int main()
  10. {
  11. signal(2, Handler);
  12. while (1)
  13. {
  14. cout << "this is a process, pid = " << getpid() << endl;
  15. sleep(1);
  16. }
  17. return 0;
  18. }

当自定义2号信号(SIGINT)的捕捉方法之后,进程收到2号信号并不会终止,而是执行自定义捕捉方法的打印函数

2、调用系统函数产生信号

  • kill() 可以向任意进程发送任意信号
  • raise() 可以向自身进程发送任意信号 ----- kill(getpid(), signal)
  • abort() 给自己发送指定的信号SIGABRT ----- kill(getpid(), SIGABRT)

mykill.cc

  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. #include <cstdio>
  5. using namespace std;
  6. static void Usage(const string& proc)
  7. {
  8. cout << "\nUsage: " << proc << "pid signo\n" << endl;
  9. }
  10. void Handler(int signo)
  11. {
  12. cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
  13. }
  14. int main(int argc, char* argv[])
  15. {
  16. if (argc != 3)
  17. {
  18. Usage(argv[0]);
  19. exit(1);
  20. }
  21. pid_t pid = atoi(argv[1]);
  22. int signo = atoi(argv[2]);
  23. int n = kill(pid, signo);
  24. if (n != 0)
  25. perror("kill");
  26. return 0;
  27. }

test.cc 一个死循环进程

  1. #include <iostream>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. using namespace std;
  5. int main()
  6. {
  7. while (true)
  8. {
  9. cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
  10. sleep(1);
  11. }
  12. return 0;
  13. }

myraise.cc

  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. #include <sys/types.h>
  5. #include <cstdio>
  6. using namespace std;
  7. int main()
  8. {
  9. int cnt = 0;
  10. while (true)
  11. {
  12. printf("cnt: %d\n", cnt++);
  13. sleep(1);
  14. if (cnt >= 5)
  15. {
  16. raise(9);
  17. //abort();
  18. }
  19. }
  20. return 0;
  21. }

5s后进程被自己发送的信号杀死(或放弃)

3、硬件异常产生信号

SIGFPE

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. using namespace std;
  6. int main()
  7. {
  8. while (true)
  9. {
  10. cout << "我在运行中..." << endl;
  11. sleep(1);
  12. int a = 10;
  13. a /= 0;
  14. }
  15. return 0;
  16. }

除0 浮点数错误SIGFPE

获取信号编号

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. using namespace std;
  6. void CatchSig(int signo)
  7. {
  8. cout << "获取一个信号,信号编号是:" << signo << endl;
  9. sleep(1);
  10. }
  11. int main()
  12. {
  13. signal(SIGFPE, CatchSig);
  14. while (true)
  15. {
  16. cout << "我在运行中..." << endl;
  17. sleep(1);
  18. int a = 10;
  19. a /= 0; //除0 浮点数报错
  20. }
  21. return 0;
  22. }

  • os通过状态寄存器当中的溢出标记位判断cpu是否发生运算异常,若产生异常,os发送信号
  • 进程收到信号,不一定会立即执行
  • cpu每次切换进程时进行上下文保存和恢复,os都会检测一次异常

4、软件条件

设置闹钟,进程1s之后结束,统计cnt累加次数

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. using namespace std;
  6. int main()
  7. {
  8. alarm(1);
  9. int cnt = 0;
  10. while (1)
  11. {
  12. cout << "cnt: " << cnt++ << endl;
  13. }
  14. return 0;
  15. }

将cnt定义为全局重新,用捕捉信号重新定义

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. using namespace std;
  7. int cnt = 0;
  8. void CatchSig(int signo)
  9. {
  10. cout << "cnt: " << cnt << endl;
  11. }
  12. int main(int argc, char* argv[])
  13. {
  14. signal(SIGALRM, CatchSig);
  15. alarm(1);
  16. while (1)
  17. {
  18. cnt++;
  19. }
  20. return 0;
  21. }

由此可见计算机将数据从内存打印到外设时间损耗巨大,IO效率很低

闹钟是一次性闹钟,执行之后不再执行

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. using namespace std;
  7. static int cnt = 0;
  8. void CatchSig(int signo)
  9. {
  10. cout << "cnt: " << cnt << endl;
  11. alarm(1);
  12. }
  13. int main(int argc, char* argv[])
  14. {
  15. signal(SIGALRM, CatchSig);
  16. alarm(1);
  17. while (1)
  18. cnt++;
  19. return 0;
  20. }

任何一个进程都可以通过alarm()系统调用设置闹钟,操作系统如何管理闹钟?先描述,再组织

核心转储

部分服务器默认关闭了核心转储

  1. #include <iostream>
  2. using namespace std;
  3. int main(int argc, char* argv[])
  4. {
  5. //核心转储
  6. while (1)
  7. {
  8. int a[10];
  9. a[100000] = 100;
  10. }
  11. return 0;
  12. }

越界访问,不仅报了段错误,还生成了core.30202文件

以Term退出的没有核心转储

核心转储:当进程出现异常的时候,将进程在对应的时刻,在内存中的数据转储到磁盘中

核心转储意义:支持调试(事后调试)

对所有信号做自定义捕捉测试

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. using namespace std;
  8. void CatchSig(int signo)
  9. {
  10. cout << "收到一个信号,信号编号是:" << signo << endl;
  11. }
  12. int main(int argc, char* argv[])
  13. {
  14. for (int signo = 31; signo >= 1; --signo)
  15. signal(signo, CatchSig);
  16. while (1)
  17. {
  18. cout << "我在运行, pid = " << getpid() << endl;
  19. sleep(1);
  20. }
  21. return 0;
  22. }

只有9号信号可杀死进程(9号进程由os直接控制,禁止对9号信号做捕捉)

四、信号保存

  • 执行信号的处理动作称之为信号递达
  • 信号从产生到递达之间的状态叫做信号未决
  • 进程可以选择阻塞某个信号
  • 被阻塞的信号永远不会递达,除非进程接触阻塞
  • 阻塞和忽视是有不同的,忽视是递达之后的一种处理动作

1、信号阻塞

每个信号都有两个标志位表示阻塞(block)未决(pending),还有一个函数指针表示处理动作

如果一个信号没有产生,可以预先将它设为阻塞状态

2、信号捕捉流程

信号产生的时候,不会被立即处理,而是在从内核态返回用户态的时候进行处理

用户想要访问内核或硬件资源,必须通过系统调用完成访问

CPU中存在CR3表态寄存器,表示当前运行的运行级别:0表示内核态,3表示用户态

每一个进程都有自己的地址空间(用户空间独占)、内核空间(被映射到每一个进程的3~4G)

进程要访问OS的接口,只需要在自己的地址空间上进行跳转

每一个进程都会共享一个内核级页表,无论进程如何切换,不会更改内核空间

系统调用接口,起始位置会将我们的用户权限更改为内核态(陷入内核)

特定的身份调用特定的代码,内核态也无法直接调用用户态状态

信号捕捉流程图

五、信号递达

sigprocmask读取或更改进程的信号屏蔽字

sigpending获取进程的pending位图

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. #include <vector>
  8. using namespace std;
  9. static vector<int> sigarr = {2, 3};
  10. void ShowPending(const sigset_t& pending)
  11. {
  12. for (int signo = 31; signo >= 1; signo--) {
  13. if (sigismember(&pending, signo))
  14. cout << "1";
  15. else
  16. cout << "0";
  17. }
  18. cout << endl;
  19. }
  20. void MyHandler(int signo)
  21. {
  22. cout << signo << " 号信号已经被递达\n" << endl;
  23. }
  24. int main(int argc, char* argv[])
  25. {
  26. for (const auto& e : sigarr)
  27. signal(e, MyHandler);
  28. // 1. 屏蔽指定的信号
  29. sigset_t block, oblock, pending;
  30. // 1.1 初始化
  31. sigemptyset(&block);
  32. sigemptyset(&oblock);
  33. // 1.2 添加要屏蔽的信号
  34. for (const auto& e : sigarr)
  35. sigaddset(&block, e); // 2号信号
  36. // 1.3 开始屏蔽
  37. sigprocmask(SIG_SETMASK, &block, &oblock);
  38. // 2. 遍历打印pending信号集
  39. int cnt = 10;
  40. while (1)
  41. {
  42. sigemptyset(&pending);
  43. sigpending(&pending);
  44. ShowPending(pending);
  45. sleep(1);
  46. if (cnt-- == 0)
  47. {
  48. cout << "恢复对信号的屏蔽\n" << endl;
  49. sigprocmask(SIG_SETMASK, &oblock, &block);
  50. }
  51. }
  52. return 0;
  53. }

将2号、3号信号屏蔽

ctrl C + ctrl \ 发送2号信号和3号信号,信号未决,pending位图2号和3号bit位置1

10s后重置对信号的屏蔽,os直接将重置的信号递达,捕捉到2号、3号信号

sigaction

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <vector>
  5. using namespace std;
  6. void ShowPending(const sigset_t& pending)
  7. {
  8. for (int signo = 31; signo >= 1; signo--)
  9. {
  10. if (sigismember(&pending, signo))
  11. cout << "1";
  12. else
  13. cout << "0";
  14. }
  15. cout << endl;
  16. }
  17. void MyHandler(int signo)
  18. {
  19. cout << "get a signo: " << signo << endl;
  20. }
  21. int main(int argc, char* argv[])
  22. {
  23. struct sigaction act, oact;
  24. act.sa_handler = MyHandler;
  25. act.sa_flags = 0;
  26. sigemptyset(&act.sa_mask);
  27. sigaction(SIGINT, &act, &oact);
  28. while (true);
  29. return 0;
  30. }

进程串行处理同类信号

  1. #include <iostream>
  2. #include <signal.h>
  3. #include <unistd.h>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. using namespace std;
  7. void Count(int cnt)
  8. {
  9. while (cnt)
  10. {
  11. printf("cnt: %d\n", cnt);
  12. fflush(stdout);
  13. cnt--;
  14. sleep(1);
  15. }
  16. }
  17. void MyHandler(int signo)
  18. {
  19. cout << "get a signo: " << signo << endl;
  20. Count(10);
  21. }
  22. int main(int argc, char* argv[])
  23. {
  24. struct sigaction act, oact;
  25. act.sa_handler = MyHandler;
  26. act.sa_flags = 0;
  27. sigemptyset(&act.sa_mask);
  28. sigaddset(&act.sa_mask, 3); //在2号信号捕捉期间,屏蔽3号信号
  29. sigaction(SIGINT, &act, &oact);
  30. while (true) sleep(1);
  31. return 0;
  32. }

  • 当进程正在递达某一个信号期间,同类信号无法被递达
  • 当当前信号正在被捕捉,系统自动将当前信号加入到进程的信号屏蔽字,当捕捉完成,系统又自动解除对该信号的屏蔽
  • 一般一个信号被解除屏蔽的话,会自动递达当前屏蔽信号
  • 进程处理信号的原则是串行处理同类信号,不允许递归处理

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/785387
推荐阅读
相关标签
  

闽ICP备14008679号