1. 信号量
1.1 进程互斥概念
两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称作进程互斥· 也就是说,一个进程正在访问临界资源,另一个要访问该资源的进程必须等待(任何时刻,都只允许一个进程在进行共享资源的访问)
任何时刻都只允许一个进程在进行访问的共享资源叫做临界资源
临界资源都是通过代码访问的,凡是访问临界资源的代码就叫做临界区
一个程序,它要么完整的被执行,要么完全不执行的特性就叫原子性
1.2 认识信号量
信号量又是什么呢?
- 本质就是一个计数器,用来给资源计数
- 任何一个进程想访问临界资源中的一个子资源的时候都不能直接访问,必须先申请信号量资源;如果有信号量资源,进程在对应的临界区访问临界资源就会申请对应的信号量,类似:count–;使用完后就会释放信号量,类似:count++
信号量是不是共享资源呢?
- 是的,因为进行需要申请信号量,那么进程就必须先看到对应的信号量,那么信号量就是共享资源
什么来保证信号量的资源呢?
- 信号量必须保证++和–操作是原子性的
接口认识(不具体说,这里信号量只是做个了解)
- int semget(key_t key, int nsems, int semflg); —> 获取一个信号量标识符
- int semctl(int semid, int semnum, int cmd, …); —> 信号量控制操作
2. 信号入门
2.1 信号概念
- 生活中的信号到进程信号:
红绿灯、闹钟、下课铃、倒计时、电话等等,这些都是我们生活中的信号。当发生信号的时候,我们就会有对应的行为,比如当红灯亮的时候,我们会停止下来等待,当然信号没有发生的时候,我们也会有知道怎么来处理它。那么我们能处理信号的原因是因为我们可以识别到这些信号。那么进程就相当于是我,信号就相当于一个数字,进程在没有收到信号的时候其实它就已经知道怎么来处理这个信号了!为了能够知道信号处理,那么就需要识别这些信号,那么这些信号怎么来识别呢,操作系统中已经对每个信号进行了设置,如下:(1-31:普通信号,34-64:实时信号,不关心实时信号)
还有一个问题就是,生活中电话这个信号是不是可能随时就来了,但是我们如果正在和老板谈重要会议呢,那么就不会立马处理,但是这个电话挂断后,我们会记住有个电话之前打来过,这里这个信号就被保存到我们的大脑中,此时的过程就是信号产生 -> 信号保存 -> 信号处理,进程也是如此,当一个信号来了的时候,可能这个进程在执行一个优先级很高的任务,那么此时这个信号就会被进程记录下来,等任务执行完后,再对此信号做出处理
- 进程如何记录对应产生的信号?怎么保存这些信号呢?
用结构体对信号进行描述,然后用数据结构对信号管理起来;进程task_struct结构体中存在位图结构来保存信号
2.2 见一见
#include <iostream> #include <unistd.h> int main() { while(true) { std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl; sleep(2); } return 0; }
演示:
- 通过发送信号杀掉进程:
- 前台进程直接ctrl + c终止进程:
后台进程只能通过发送信号杀掉进程:
2.3 signal()系统调用
| 选项 | 内容 |
| 作用 | Signal()将信号信号的处置设置为handler |
| 头文件 | #include <signal.h> |
| 函数声明 | sighandler_t signal(int signum, sighandler_t handler); |
#include <iostream> int add(int x, int y) { return x + y; } void calc(int (*add_fun)(int, int)) { int a = 10; int b = 20; int result = add_fun(a, b); std::cout << "result: " << result << std::endl; } int main() { calc(add); return 0; }
- signal()系统调用获取对信号最处理
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signum) { std::cout << "get a signal: " << signum << std::endl; } int main() { signal(2, handler); //handler函数就是回调函数 while(true) { std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl; sleep(2); } return 0; }
演示:
观察现象:当Ctrl + c时,会执行会执行对应的handler方法
- (9)号信号的特殊
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signum) { std::cout << "get a signal: " << signum << std::endl; } int main() { for(int i = 1; i <= 31; ++i) { signal(i, handler); } while(true) { std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl; sleep(3); } return 0; }
演示:
(9)号信号是管理员信号,是不可被定义的,所以可以直接杀掉进程
2.4 宏定义信号
3. 信号产生方式
3.1 键盘产生信号
当我们命令上按Ctrl+c时就表示终止进程,键盘如何发送这里请查阅资料
3.2 系统调用产生信号
- kill()系统调用
| 选项 | 内容 |
| 作用 | 发送信号给进程 |
| 头文件 | #include <sys/types.h> #include <signal.h> |
| 函数声明 | int kill(pid_t pid, int sig); |
| 返回值 | 成功返回0,失败返回-1 |
systemcall.cc文件
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <assert.h> #include <errno.h> #include <cstring> void manual(std::string process) { std::cout << "manual: \n\t"; std::cout << process << " <number> <process>\n" << std:: endl; } int main(int argc, char* argv[]) { if(argc != 3) //必须带两个选项 { manual(argv[0]); } int signum = atoi(argv[1]); //atoi():字符串转整数 pid_t id = atoi(argv[2]); int ret = kill(id, signum); //kill():发送信号给进程 assert(ret == 0); if(ret != -1){ std::cerr << errno << " : " << strerror(errno) << std::endl; } return 0; }
test.cc文件:
#include <iostream> #include <unistd.h> int main() { while(true) { std::cout << "I am a process, is excuting ..... , PID: " << getpid() << std::endl; sleep(2); } return 0; }
演示:
- raise()C语言接口
| 选项 | 内容 |
| 作用 | 发送信号给调用者 |
| 头文件 | #include <signal.h> |
| 函数声明 | int raise(int sig); |
| 返回值 | 成功返回0,失败返回非0 |
- abort()C语言接口
| 选项 | 内容 |
| 作用 | 导致进程异常终止 |
| 头文件 | #include <stdlib.h> |
| 函数声明 | void abort(void); |
| 返回值 | 无返回值 |
3.3 软件条件产生信号
- abort()C语言接口
| 选项 | 内容 |
| 作用 | 设置一个告警信号 |
| 头文件 | #include <unistd.h> |
| 函数声明 | unsigned alarm(unsigned seconds); |
| 返回值 | 如果在剩余时间内有先前的alarm()请求,则alarm()应返回一个非零值,表示距离前一个的秒数请求将生成一个SIGALRM信号。否则,报警()应返回0 |
- 验证IO效率
#include <iostream> #include <unistd.h> #include <signal.h> int count = 0; int main() { alarm(1); //1秒后发信号 while(true) { std::cout << "count: " << count++ << std::endl; } return 0; } //大概count等于20000左右
#include <iostream> #include <unistd.h> #include <signal.h> int count = 0; void handler(int signum) { std::cout << "get a signal: " << signum << " count:" << count << std::endl; } int main() { signal(SIGALRM, handler); alarm(1); //1秒后发信号 while(true) { ++count; } return 0; } //大概count等于500000000左右
- alarm()返回值理解
#include <iostream> #include <unistd.h> #include <signal.h> int count = 0; void handler(int signum) { std::cout << "get a signal: " << signum << " count:" << count << std::endl; int ret = alarm(10); std::cout << "ret: " << ret << std::endl; } int main() { std::cout << "PID: " << getpid() << std::endl; signal(SIGALRM, handler); alarm(10); //1秒后发信号 while(true) { ++count; } return 0; }
返回的就是上次设置闹钟时间到给闹钟发信号的时间的时间差。也就是假如我设置了一个20分钟的闹钟,此时我的小猫把房间里面的东西给我吵醒了,我起床一看我只睡了10分钟,那么剩余的10分钟就是这里的时间差。这个闹钟是个信号,那么对应的操作系统中就会有对应的闹钟结构体对其描述和管理。
3.4 硬件异常产生信号
test.cc
#include <iostream> int main() { int a = 10; int b = 0; int c = a / b; std::cout << "division operation" << std::endl; return 0; } //运行结果如下:
这里其实是硬件异常导致产生的信号,运行会通过内存把数据加载到CPU来运算,CPU中有一种寄存器是用来记录是否数据溢出的,这里除0操作会溢出,那么这个寄存器就会被置为对应的数值来表示这个状态,最后CPU检测到异常然后给到操作系统,操作就会给该signal进程发送信号(8)号信号SIGFPE。
- 这里可不可以不让这个进程因为SIGFPE信号退出呢?可以的
#include <iostream> #include <signal.h> #include <sys/types.h> #include <unistd.h> void handler(int signalnum) { printf("PID:%d received signal:%d crash\n", getpid(),signalnum); //exit(1); } int main() { signal(SIGFPE, handler); //捕捉信号 int a = 10; int b = 0; int c = a / b; std::cout << "division operation" << std::endl; return 0; } //这段代码的现象:循环打印printf内容,为什么呢? //原因是CPU中这个溢出状态检测寄存器检测到溢出后操作系统接受到错误给该进程发送信号,但是操作系统并没有修复CPU溢出检测寄存器,所以操作系统不断就给进程发送信号,这里进程不断捕捉信号(自定义行为),就死循环了。怎么改正呢?很简单绶捕捉信号后终止掉进程
- 野指针问题同样是硬件异常产生信号
#include <iostream> #include <signal.h> #include <sys/types.h> #include <unistd.h> void handler(int signalnum) { printf("PID:%d received signal:%d crash\n", getpid(),signalnum); //exit(1); } int main() { signal(SIGSEGV, handler); int* p1 = nullptr; //p1 = (int*)100; *p1 = 100; //野指针访问写入 std::cout << "wild pointer!" << std::endl; return 0; } //输出结果:segmentation fault (11号信号:SIGSEGV) //为什么会出现这种错误呢?程序运行会变成进程,进程由操作系统管理,虚拟内存会通过页表建立key/value关系映射到物理内存,硬件上MMU主要完成虚拟地址到物理地址的映射,所以虚表是实现MMU的手段,这里的页表中不仅仅有kv关系,同时也有读写权限,p1=(void*)0,也就是0号地址,这里有两种可能导致异常,可能p1虚拟地址并没有物理地址,也可能p1虚拟地址有映射的物理地址但是并没有写入或者读取权限。,这两种可能都会导致报错。 //这里为什么会出现死循环呢?原因还是因为操作系统并没有修复MMU硬件错误,使得进程不断捕获信号,导致死循环打印。解决方法:捕获后终止进程
3.5 Core dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。虚拟机上是可以直接看到的,但是云服务器上此功能是默认关闭的,使用ulimit -a命令查看:
- 如何打开呢?
使用ulimit -c size(大小)使得这个磁盘核心转储有大小就是把它打开了。
如何验证core file size的存在?
#include <iostream> #include <signal.h> #include <sys/types.h> #include <unistd.h> void handler(int signalnum){ printf("PID:%d received signal:%d termination\n",getpid(), signalnum); exit(1); } int main() { while(true){ std::cout << "PID:" << getpid() << " doing ....." << std::endl; sleep(1); } return 0; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aglLGiPi-1684114794264)(https://typora130.oss-cn-nanjing.aliyuncs.com/QQ截图20230508191812.png)]
又上述观察得到一个结果:Term就是普通终止,没有任何操作;Core终止会先进行核心转储再终止进程,如何验证?
- 核心转储有什么用?方便异常后进行调试
#include <iostream> int main() { std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; int* p1 = nullptr; *p1 = 100; //野指针访问写入 std::cout << "wild pointer!" << std::endl; return 0; }
- 这里有了这个core dump那么调试就很轻松,那为什么云服务器会关闭core dump呢,它有什么坏处呢?
它会占用磁盘空间,一是本身程序就很大,出错后形成core dump文件很大;二是每次程序瓜重启都会形成core dump文件,如果很多次重启程序就会导致形成很多个core dump文件。如何关闭呢?ulimit -c 0命令。
另外,进程退出时可以获取信号,也可以获取退出码,也可以知道core dump是否发生(依靠core dump标记比特位):
#include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t id = fork(); if(id == 0) { std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; int* p = nullptr; *p = 100; std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; std::cout << "wild pointer!" << std::endl; exit(0); } int status = 0; waitpid(id, &status, 0); //阻塞等待 printf("exit code: %d | exit signal: %d | core dump flag: %d\n", \ ((status >> 8) & 0xFF), status & 0x7F, (status >> 7) & 0x1); return 0; } //运行结果如下:












