索引
- volatile
- 1.gcc -O含义及其作用
- 2.证明其内存可见性
- 深入理解SIGCHLD信号
- SIGCHLD总结
- 可重入函数
volatile
保存内存的可见性,告知编译器,该关键字修饰的变量不允许被优化,对该变量的任何操作都必须在内存中操作。
1.gcc -O含义及其作用
gcc提供了为了满足用户不同程度的优化需求,提供了很多优化选项,用来对(编译时间,目标文件长度,执行效率)进行平衡。下面大致介绍几个优化级别
-O0:不做任何优化,这是默认的编译选项。
-O和-O1:对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存,编译器会尝试缩小代码的尺寸以及缩短执行时间,但并不执行需要占用大量编译器的优化。
-O2:比O1高级,进行更多的优化。Gcc将执行几乎所有的不包含时间和空间这种的优化,此时编译器不进行循环打开()以及内联函数,与O1比较而言,O2优化增加了编译时间的基础上,提高了生成代码的执行效率。
O3:在O2的基础上做进一步优化。
优化的代码可能给调试带来问题,可能会改变代码的结构,eg:对分支的合并和消除
还有可能给内存操作顺序改变带来问题。eg:对于某些依赖内存操作顺序而进行的逻辑,需要做严格的处理后才能进行优化
2.证明其内存可见性
此时程序的编译:
g++ -o $@ $^ -std=c++11 -O2
int flag = 0;
void handler(int signo)
{printf("变量的值已经修改了 flag:%d\n", flag);flag = 1;
}
int main()
{signal(2, handler);while (!flag);printf("process 正常退出\n");return 0;
}
上述代码Gcc编译时进行了O2优化,编译器编译的时候看到循环中没有语句,此时其循环的判断条件除第一次从内存中提取之外,其他时间都是从寄存器提取,因此,当我们CTRL+C
时,命名条件的flag = 1
,但程序依旧不会跳出循环。
如果我们在一样的代码上添加volatile
由此可见volatile
的作用其保持内存的可见性.
深入理解SIGCHLD信号
父进程创建子进程后,之前讲述的都是父进程用waitpid()和wiat()函数清理僵尸进程,此时既可以阻塞式等待:此时父进程无法进一步处理自己的工作,直到子进程退出。
非阻塞式等待:父进程采取轮循检测的方式等待子进程退出。
实际上在子进程退出的时候会给父进程发送一个信号:SIGCHLD
信号,只不过父进程对该信号的默认处理方式是忽略。下面验证一下子进程退出的时候,子进程是否会发送SIGCHLD
信号。
实验设计:
父进程fork()创建子进程之后,子进程exit()退出,给SIGCHLD
信号设计自定义捕捉函数。
void ChildHandler(int signo)
{// 此时阻塞式等待任何一个子进程。pid_t id = waitpid(-1, nullptr, 0);if (id > 0){cout << "父进程等待成功" << endl;}
}
int main()
{signal(SIGCHLD, ChildHandler);pid_t id = fork();if (id == 0){cout << "我是子进程" << endl;sleep(3);exit(1);}while (true){cout << "main running..." << endl;sleep(1);}return 0;
}
其实上述的自定义捕捉函数是有bug的,如果此时父进程创建了很多子进程并且子进程同时退出的话,就会有多个SIGCHLD
信号被发送,会导致该信号被堵塞,此时就会产生很多僵尸进程
void ChildHandler(int signo)
{// 此时阻塞式等待任何一个子进程。pid_t id = waitpid(-1, nullptr, 0);if (id > 0){cout << "父进程等待成功" << endl;}
}
int main()
{signal(SIGCHLD, ChildHandler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){int cnt = 10;while (cnt){cout << "我是子进程,pid: " << getpid() << " cnt:" << cnt-- << endl;sleep(1);}cout << "子进程退出进入僵尸状态;" << endl;exit(1);}}while (true){cout << "main running..." << endl;sleep(1);}return 0;
}
由于信号同时在处理时继续发送信号此时信号会被阻塞,所以上述代码会出现僵尸进程。
所以我们循环式读取直到waitpid()调用失败
,此时跳出循环表示已经全部等待完成,就不会出现僵尸进程。
void ChildHandler(int signo)
{while (true){// 此时阻塞式等待任何一个子进程。pid_t id = waitpid(-1, nullptr, 0);if (id > 0){cout << "父进程等待成功" << endl;}}
}
此时并不会出现僵尸进程,但是此情况针对的是子进程同时退出,子进程也有可能不同时退出。如下所示。
void ChildHandler(int signo)
{while (true){// 此时阻塞式等待任何一个子进程。pid_t id = waitpid(-1, nullptr, 0);if (id > 0){cout << "父进程等待成功" << endl;}}
}
int main()
{signal(SIGCHLD, ChildHandler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){int cnt = 10;if (i < 6){cnt = 5;}else{cnt = 50;}while (cnt){cout << "我是子进程,pid: " << getpid() << " cnt:" << cnt-- << endl;sleep(1);}cout << "子进程退出进入僵尸状态;" << endl;exit(1);}}while (true){cout << "main running..." << endl;sleep(1);}return 0;
}
此时就会出现子进程一直在运行,但是父进程不会运行自己的代码,因为此时自定义函数中的等待方式是阻塞式等待,此时只要我们将阻塞式等待变成非阻塞式等待即可。
void ChildHandler(int signo)
{while (true){// 此时非阻塞式等待任何一个子进程。pid_t id = waitpid(-1, nullptr, WNOHANG);if (id > 0){cout << "父进程等待成功 child pid:" << getpid() << endl;}else if (id == 0){cout << "还有子进程没有退出,父进程要去忙自己的事情了" << endl;break;}else{cout << "父进程等待所有的子进程结束" << endl;break;}}
}
int main()
{signal(SIGCHLD, ChildHandler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){int cnt = 10;if (i < 6){cnt = 5;}else{cnt = 20;}while (cnt){cout << "我是子进程,pid: " << getpid() << " cnt:" << cnt-- << endl;sleep(1);}cout << "子进程退出进入僵尸状态;" << endl;exit(1);}}while (true){cout << "我是父进程,我正在运行:" << getpid() << endl;cout << "main running..." << endl;sleep(1);}return 0;
}
SIGCHLD总结
上述是使用该信号的自定义捕捉函数,那么如果我们不使用他呢?
用sigaction
将SIGCHLD
的处理动作设置成SIG_IGN
(忽略),这样fork()出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,系统默认的忽略动作和用户用sigaction
函数自定义的忽略通常是没有区别的,但这是一个特例,该方法只对Linux可用,但不保证在其他的UNIX系统上都可用。
这里就不做模拟了。
但是设置了SIG_IGN后,可不仅仅是自动释放子进程这一个内容,这是一个函数,函数可以修改父进程的相关属性,进而被子进程继承下去,如果没有设置忽略的话,子进程在没有waitpid()的情况下是会产生僵尸进程的,但是设置SIG_IGN就不会产生僵尸进程,子进程会自动释放!
可重入函数
当两个不同的控制流程调用同一个函数,访问同一个局部变量或参数造成程序的错乱,该函数就是不可冲入函数。
符合下列条件之一的就是不可冲入函数:
1.调用了malloc 或 free,因为malloc也是用全局链表来管理堆的
2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。