共享内存的原理是两个进程通过页表映射到同一块物理内存上,然后就可以开始通信了,可以手动解除映射关系,也可以等待进程退出,再解除物理内存和进程的映射关系,但是申请好的共享内存的生命周期并不会随着进程的退出而释放,而是要等到手动释放或者重启内核,也就是开关机才可以释放。
插入:C++中的建堆操作
#include <iostream>
#include <vector>
#include <algorithm> //包含算法头文件
using namespace std;
int main() {
vector<int> nums = {4, 5, 1, 3, 2};
// generate heap in the range of numsector
make_heap(nums.begin(), nums.end()); //建堆
cout << "initial max value : " << nums.front() << endl;
// pop max value
pop_heap(nums.begin(), nums.end()); //删除堆顶元素
nums.pop_back();
cout << "after pop, the max vsalue : " << nums.front() << endl;
// push a new value
nums.push_back(6);
push_heap(nums.begin(), nums.end());
cout << "after push, the max value : " << nums.front() << endl;
system("pause");
return 0;
}
============================================================================
消息队列:
消息队列是带类型的队列,每次按照指定的类型来出队列
广义的消息队列;
消息队列也是一个队列,每个元素都带有一个类型
出队列的时候,是按照指定的类型先进先出的
大概模型:
中间件:和业务无关的基础设施。数据库也是一种中间件
信号量是一个计数器,负责进程间的同步和互斥
这个计数器描述了可用资源的个数。
每次有进程想申请一个可用资源的时候,计数器-1 (P)
每次有进程想要释放资源可用资源的时候,计数器+1 ( V)如果计数器已经是0了,这时候再有进程想要申请资源的话,会有两种情况,
1.进程会阻塞等待
2.进程放弃申请资源
============================================================================
信号:
在Linux可以通过kill -l这样的命令去查看所有的信号,信号总共有62种
信号的特点:
1.信号有很多种
2.操作系统对于每种不同信号的处理方式是不相同的。
3.每种信号在还没有发生之前,其实操作系统已经之后对于这种信号的应对措施了。
段错误就是内存访问越界,是11号信号。
信号就相当于是神经系统。帮助操作系统来处理进程运行过程中遇到的各种意外
信号的产生方式:
解引用空指针会发生程序崩溃。
int * p = NULL;
cout<<*p<<endl; //崩溃
int a[10] = { 0 };
cout << a[10] << endl; //这段代码确实是可以对非法内存,进行读的,但是当你进行写操作的时候就会发生错误
解引用空指针是非法的,但是访问数组的越界下标却是可以的
解引用空指针的结果
1.程序崩溃
2.进程异常终止
3.MMU发现进程虚拟空间和实际物理内存映射关系不匹配,然后给操纵系统返回11号信号,操作系统终止进程,提醒用户段错误
信号产生的方式:
1.键盘
CTRL+ C ==>2
CTRL + Z ==>19
CTRL + \ ==>3
2.硬件
MMU
CPU
3.软件触发中断
管道,读端关闭,尝试写,触发信号中断(SIGPIPE),管道破裂
TCP中一定的条件也会触发SIGPIPE信号
4.系统调用产生
kill()函数
前31个信号叫非实时信号,也叫普通信号,后34 - 64叫做实时信号
============================================================================
插入知识:
//在C语言中字符串转数字的方法有两种
//方法1
char array[] = "100";
int num = atoi(array);
printf("%d\n",num);
//方法2
char array[] = "100";
int num = 0;
sscanf(array,"%d",&num);
printf("%d\n",num);
//C++字符串转数字
//方法1
//首先版包含头文件
#include<sstream>
std::stringstream ss;
std::string str = "100";
int num = 0;
ss<<str;
ss>>num;
std::cout<<num<<std::endl;
//方法2
//stoi是C++11中增加的新的方法,atoi,是将char*转数字,而stoi是将string转数字。
std::string str = "100";
int num = std::stoi(str);
std::cout<<num<<std::endl;
kill 命令是也是一个系统调用函数,kill命令底层调用的就是kill()函数
除了使用ps aux | grep 某某进程名 ==== pidof 某某进程名
两个的效果是完全一样的
============================================================================
信号的处理方式:
1.忽略
2.默认处理信号
3.捕捉信号,通过子定制的行为来处理信号
处理信号的函数signed
//首先这个是函数的说明手册,说signal这个函数会在将第一个参数的信号捕捉到之后,不会报给操作系统处理而是
//按照你传入的第二个参数,来处理,又根据typedef可知,第二个参数是typedef void (*sighandler_t)(int);
//第二个参数必须是一个是一个void类型的函数指针
//然后捕捉到函数之后就会按照你函数中指定的方式去进行处理
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//代码如下
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 void signedal(int signo) //函数的参数不用自己传,会自动传进来一个信号的id
5 {
6 printf("我是被%d号信号干掉的\n",signo); //尝试打印信号id
7 }
8 int main()
9 {
10
11 signal(2,signedal); //捕捉到2号信号进行我交给的函数操作
12 while(1)
13 {
14 printf("哈哈\n");
15 sleep(1);
16 }
17
18 return 0;
19 }
打印结果就是这个样子:
哈哈
哈哈
^C2
哈哈
哈哈
^C2
哈哈
哈哈
哈哈
哈哈
^\退出(吐核)
//再来看一个
// 我们使用一个for循环将所有的进程都进行捕捉,看看进程还能不能退出
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 void signedal(int signo)
5 {
6 printf("我是被%d号信号干掉的\n",signo);
7 }
8 int main()
9 {
10 for(int i = 0;i <= 64;++i) //循环捕捉
11 {
12 signal(i,signedal);
13 }
14 while(1)
15 {
16 printf("哈哈\n");
17 sleep(1);
18 }
19
20 return 0;
21 }
其实最后进程还是可以退出的,因为9号进程是无法被捕捉了,要不然也就太容易刀枪不入了吧。
从上面给两端代码看出一个问题就是,捕捉信号这个函数,好像不会因为说,代码执行过了signal函数, 然后就不能捕捉了,而是是在代码的运行过程中全程都可以捕捉
忽略信号
那操作系统怎么来忽略用户所指定的信号呢?
还是上面的函数,只要把第二个参数改成SIG_IGN就自动忽略了
signal(2,SIG_IGN);
信号捕捉过程示意图:
1.信号处理函数并不是由用户函数去调用的,根用户代码没有调用关系,信号处理函数是一个单独的执行流,何应乎代码之间没有相互调用的关系,而是由内核处理的
2.信号处理函数,原来的执行流就在等待,而不会继续往下执行,也就是说用户代码,会等待内核将信号处理函数处理完毕之后,再继续执行
C++中的try catch底层就是基于信号来处理的
============================================================================
信号的阻塞:
有的时候信号不会被立刻处理,而是等到时机何再进行处理,
上面说了,用户的代码,和捕捉信号之后调用的回调函数不属于同一个执行流 。一个是用户的流,一个是内核的流
============================================================================
可重入函数
一个函数如果在多个执行流中被调用,就可能出现问题
一个函数可重入,就意味着在多个执行流中调用是没有问题的
一个函数如果不可重入,在多个执行流中调用就是有问题的
如果过一个函数是用来全局变量/静态变量,不可重入
如果一个函数调用了不可重入函数,也是不可重入的
============================================================================
volatile C语言中的关键字,告诉编译器,这个变量必须每次都从内存中去读取,不能加载到寄存器中,为的就是防止编译器对某些变量的过度优化。
编译器在执行多个执行流的时候是特别容易出问题的
volatile经常使用在多线程程序中,编译器对于这种多执行流的情况时不太会判断的
============================================================================
wait阻塞等待
waitpid阻塞/非阻塞等待
阻塞等待:代码简单,但是效率低
非阻塞等待:代码复杂,但是在进程循环判断等待的时候,可以去干点别的事情,效率高
如何才能实现两者的结合呢?既简单,效率又高,SIGCHLD信号,17号信号
只要有子进程退出,子进程就会自动发送17号信号给父进程,此时父进程进些捕捉17号信号,然后在回调函数中对子进程进行回收,回收完之后,再从内核回到父进程的进程,再继续进行运行父进程中的代码
//这段代码就结合了wait 和waitpid的优点,既简单又高效
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<signal.h>
5 void mysignal(int sig) //在信号捕捉函数的调用函数中,进行回收子进程
6 {
7 printf("%d\n",sig);
8 wait(NULL);
9 }
10 int main ()
11 {
12 signal(17,mysignal);
13 int ret = fork();
14 if(ret > 0)
15 while(1)
16 {
17 sleep(1);
18 }
19 else if(ret == 0)
20 return 0;
21 else
22 perror("fork");
23 return 0;
24 }
前1-31的信号要是同时来多个有可能只处理一次,34-64就不一样了
未决信号集位图,在多个信号同时到达,有可能只被处理一次。
但是上面的代码存在很大的问题,就是假如同时有很多的子进程退出发送了,好几个17号信号,此时在未决信号集中,只记录一个,也就是只执行一次,此时子进程就不能说是完全被回收干净,此时还是会存在僵尸进程的。
//修改上面的代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<signal.h>
5 void mysignal(int sig) //在信号捕捉函数的调用函数中,进行回收子进程
6 {
7 printf("%d\n",sig);
8 while(1)
{
int ret = waitpid(-1,NULL,WNOHANG);
//如果ret > 0已经回收到了一个子进程,返回值就是子进程的pid
//如果ret == 0 还有子进程在,但是子进程还没有结束
//如果ret < 0没有子进程
if(ret > 0)
{
//什么都不用做继续执行循环,看还有没有其他的子进程全部回收。
}
else
{
//退出循环
break;
}
}
9 }
10 int main ()
11 {
12 signal(17,mysignal);
13 int ret = fork();
14 if(ret > 0)
15 while(1)
16 {
17 sleep(1);
18 }
19 else if(ret == 0)
20 return 0;
21 else
22 perror("fork");
23 return 0;
24 }
其实上面这些 都是没有用的,代码又臭又长:
其实还有一个更简单的,忽略子进程退出的信号,让子进程自生自灭
//最终代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<signal.h>
5 int main ()
6 {
7 signal(17,SIG_IGN); //一句话代码。直接忽略信号,此时子进程再退出,发现父进程不管它了,操作系统就把子进程的资源释放了,
8 int ret = fork();
9 if(ret > 0)
10 while(1)
11 {
12 sleep(1);
13 }
14 else if(ret == 0)
15 return 0;
16 else
17 perror("fork");
18 return 0;
19 }
在 man 3 signal 中可以看到
直接忽略的第二个参数
推荐书籍:C++《C++ cookbook》