进程地址空间
内核数据结构task_struct中存放着指向mm_struct的结构体指针,该结构体用来描述进程的内存管理信息,包括但不限于页目录与虚拟内存区域。先来看这样一个程序
#include<stdio.h>
#include<unistd.h>
int main(void)
{
int num =100;
int re = fork();
if(re > 0)
{
printf("The number's address:%p\n",&num);
printf("The number:%d\n",num);
num = 10;
sleep(10);
printf("The number's address:%p\n",&num);
printf("The number:%d\n",num);
}
if(re == 0)
{
printf("The number's address:%p\n",&num);
printf("The number:%d\n",num);
sleep(10);
printf("The number's address:%p\n",&num);
printf("The number:%d\n",num);
}
return 0;
}
这是运行结果:
可以见到,第一次输出,父子进程的值与地址都相同;但在第二次输出,父进程的变量被修改,父子进程的值不一样,但是地址却一样。
所以,这里的地址(也就是语言级别的地址——指针)并不是物理地址,而是虚拟地址——线性地址、逻辑地址(但二者又略有出入)。
进程地址空间通过树状结构的多级页表与物理地址建立联系,即task_struct -> mm_struct -> 页目录 -> 页表,再通过页表找到对应的物理地址,在物理地址取出数据——由外存加载至内存。
基于各进程都有自己的内核数据结构, 这种机制保持了进程的独立性的同时也保护了物理地址——当进程非法访问时,会被页表检测并拦截。
写时拷贝(Copy on Write)
在上述情况中,为了保持进程的独立性,当父子进程任何一方尝试写入数据至同一地址时(在未做出写入操作时,父子进程共享同一物理地址,但也仅是可读),操作系统会先对数据进行拷贝,而后放入新开的物理地址,再改变页表的映射,其后进程才可以进行修改。这种思想就是写时拷贝。
这种技术被广泛地引用在操作系统中,当一个程序运行结束时,操作系统并不会立即将其数据清除,因为这个程序可能还会在运行一次。写文件操作也是,只有我们关闭文件,操作系统才会将其写入外存;但这也意味着,当主机非正常关闭时,文件就会丢失,但为了性能这样做是非常值得的。