1.可重入函数
情景:

在一个结点插入时,由于信号中断调用了一个自定义动作的原因使在同一个位置插入了一个结点,最终使一个结点被浪费了,造成内存泄露。
如果我们将在运行时的main与信号中断分别视为一个执行流,此时就会发现一个函数同时被两个以上的执行流进入了,此时这个场景就是函数被重入了。
不可重入函数:一个函数运行时被重入时会使功能发生异常的函数
可重入函数:一个函数运行时被重入时不会使功能发生异常的函数
大部分的函数都是不可重入的
不重入函数的特点:函数内有全局变量或全局的数据结构。
可中重入的特点:函数内只有自己的临时变量。
2.volatile
cpp
int flag = 0;
void handler(int signal)
{
flag = 1;
}
int main()
{
signal(2,handler);
while(!flag);
return 0;
}
编译器其实是以局部看待函数的,也就是以上面的函数为例,编译器其实是识别不到handler函数的存在的,也就是意识不到flag又被修改的可能的。
又有在编译器优化程度比较高的情况下,flag的值会被存储在寄存器中。
CPU在运算时有两种方式:算数运算与逻辑运算
计算过程:将数据从内存导入到CPU中,然后CPU进行计算,最后选择性的写回内存中。
liunx中的优化
gcc和g++中还有一种选项 -O,-O0,-O1,-O2分别代表了不同程度的优化级别,不设置时默认-O0。
上面的程序在-O0运行时使用ctrl + c后flag可以正常修改为1,但使用了-O1优化后CPU就一直使用寄存器中存储的值使flag即使被修改为了1,CPU依旧使用寄存器(0)中的值时其无法正常退出进程。
而volatile作为一个关键字,可以修饰一个变量或函数,用于说明不允许这个变量或函数参与任何优化,如变量就要求编译器老老实实从内存中读取该变量的数据。
3.SIGCHLD
子进程在中止时会给父进程发送SIGCHLD信号,该信号的默认动作是忽略。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
//id == 0是说明此时没有子进程退出,WHOHANG保证接收完此时的子进程就退出
while( (id = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0)
{
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1)
{
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
上面是一种用SIGCHLD回收子进程的操作,此时waitpid用WNOHAHG就能解决只有部分子进程退出使waitpid处于阻塞的情况。
liunx允许将SIGCHLD的默认处理动作设为SIG_IGN,子进程结束后就自动回收了。(这是一种特例)
从OS的角度上看,知道使用默认动作和知道默认动作是什么为两码事,例:
SIGCHLD的默认动作是忽略,但其的动作是SIG_DFL,只是SIG_DFL后该信号的动作是忽略,而使用SIGCHLD的动作就是忽略。
4.线程概念和控制
1.不同角度区分进程和线程
|------|--------------|-------------|
| | 进程 | 线程 |
| 不同 | 内核数据结构+代码和数据 | 进程内部的一个执行分支 |
| | 消耗系统资源的基本单位 | CPU的调度单位 |
| 系统角度 | 强调独占,部分共享 | 强调共享,部分独占 |
| 相同 | 都是执行流 | |
2.概念阐述
进程访问大部分资源都是通过虚拟地址空间访问的,可以说虚拟地址空间是进程访问资源的窗口。
不同进程访问的资源不同,本质是窗口的不同。
线程的本质就是一个独立的task_struct,多个线程允许访问同一个窗口。也就是进程之间彼此独立而形成之间可以共享。
用途:将进程的地址空间划分给多个线程,从而让一整个进程由多个小执行流进行运作。
3.结论
(1)linux线程可以采用进程来模拟。
(2)对资源的划分,本质是对虚拟地址空间的划分,虚拟地址就是资源的代表。
(3)所有代码本质由多个函数组成,每个函数都有自身的起始地址和地址区间,所有函数的代码块整体采用相对编址,虚拟地址空间又是所有代码块的集合,因此所有函数就是虚拟地址空间的一段连续区域。每个函数占据一段虚拟地址空间,我们就可以通过不同的线程访问不同的函数的其实虚拟地址即可。
(4)之前我们所谈的进程就是内部只有一个线程的进程,这种叫单线程。
(5)windows中采用先描述,再组织的方式管理线程,linux中依旧使用task_struct模拟线程。
从OS的视角看:进程切换的操作使之不能将线程等视为进程。
从CPU的视角看:直接将而者视为同一种执行流了,然后执行流又视为轻量级进程。
(6)线程核心就是task_struct,task_struct就是轻量级进程,因此线程就是轻量级进程或者说其是用轻量级进程模拟实现的。
总结有进程就是用于申请资源而线程是用于是用资源的,线程在进程的地址空间内运行。
5.部分知识的扩展
(1)磁盘以4KB为数据块划分.可执行程序就是文件,文件存在磁盘中,物理内存中也被OS要求以4KB为单位进行划分的,物理内存中的一个4KB的数据块就叫页框或页帧。
(2)OS采用先描述,再组织的方式管理所有页框,管理对应的结构体就是struct page,其中有一个成员unsigned long flags是一个位图,用于管理各种属性 。组织就是用struct page mem\[\],一个数组来管理,所以每个page都会有一个下标,通过这个下标来计算(index*4KB)就能天然知道每一个数据块的起始物理地址了,具体物理地址=起始物理地址+页内偏移量。可以说ELF中section的合并为segment的一个目的就是让每个segment的大小为4KB的整数倍。
(3)struct file中有一个成员struct address_space * f_mapping,内部有一个成员void *slots,指向的就是一些特定的page,这些page管理的内存空间就是文件缓冲区,因此vm_area_struct也是可以通过struct file来间接找到文件的文件缓冲区的。