赞
踩
地址是一个很重要的名词,我们的每一个进程在内存中运行都会有若干个地址。在之前我们学习进程的时候,学过一个函数——fork(),这个函数仅仅被调用一次,却能够返回两次。这是为什么呢?那么这一篇博客将会解释这种现象——进程的地址空间。
我们可以通过一个特别的代码来看到一个奇怪的现象:就是在同一个地址空间中,对于同一个变量的值是不同的,是不是感到很奇怪。在我们的认知和印象中,对于一个地址空间来说,只会有一个变量的大小,而不会出现多个值在同一个地址空间的现象,所以我们推断出这个地址空间一定不会是真正的物理地址,而是一种虚拟的地址空间。
进程 = 内核数据结构 + 代码(只读) + 数据,所以父子进程是具有独立性的。父子进程有一个进程退出不会影响其他进程。
- #include <iostream>
- #include <algorithm>
- #include <string>
- #include <cstring>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- using namespace std;
-
- int g_val = 100;
-
- int main()
- {
- pid_t id = fork();
- if(id == 0)
- {
- while (true)
- {
- cout << "I am child, g_val:" << g_val << ", dist:" << &g_val << endl;
- sleep(1);
- }
- }
- else if(id > 0)
- {
- g_val = 200;
- while (true)
- {
- cout << "I am prant, g_val:" << g_val << ", dist:" << & g_val << endl;
- sleep(1);
- }
- }
- return 0;
- }
进程地址空间不能简单的认为是内存,通过上面的现象,其也一定不是物理地址,因此一定是虚拟地址。这段空间中自下而上,地址是增长的,栈是向地址减小方向增长(栈是先使用高地址),而堆是向地址增长方向增长(堆是先使用低地址),堆栈之间的共享区,主要用来加载动态库。
每一个进程在启动时都会有一个进程地址空间的存在,进程地址空间是操作系统给进程花的“大饼”——告诉进程:操作系统中的所有物理地址都可以使用————使每个进程都认为自己独占系统内存资源。(即虚拟空间)。
写时拷贝的概念:就是等到修改数据时,才真正分配内存空间,这是对程序性能的优化,可以延迟甚至避免内存拷贝,当然目的就是避免不必要的拷贝。
写时拷贝实际上是运用了一个“引用计数”的概念来实现的,在开辟的空间中多维护了四个字节来存储引用计数。
有两种方式来存储引用计数:
当我们多开辟一个空间时,让引用计数 + 1,如果有释放空间,那么就让引用计数 - 1,但是此时不是真正的释放,是假释放,等到引用计数为0时,才是真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。
我们可以通过一个例子来引出进程地址空间的划分区域。在同桌时期,我们同桌双方会约定在桌子上划分割线来确定我们各自的区域。那么在操作系统中,我们应该如何进行划分区域呢??我们知道进程地址空间的本质是操作系统中的一个结构体,在结构体中,我们可以定义变量来表示这个区域的起始点和终止点。
- struct area
- {
- int start;
- int end;
- };
-
- struct destop
- {
- struct area left;
- struct area right;
- };
在操作系统内核中,我们可以找到:
我们可以将操作系统当做英国的大富翁,将进程看成大富翁的私生子,大富翁告诉私生子:我有10亿的遗产,可以给你使用。每一个私生子都不知道彼此的存在,因此每一个私生子都以为自己会获得10亿的遗产,但是整体来看,这是不正确的。为什么操作系统可以这样做呢??因为进程在申请空间时,是不会将操作系统的物理地址全部申请过来,因此每次申请是够用的。
进程地址空间有三个好处:
- 可以将无序的地址空间变成有序的虚拟地址空间,让进程以统一的视角看待物理内存以及自己运行的各个区域
- 进程管理模块和内存管理模块进行解耦:有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟)
- 拦截非法请求
页表是非常复杂的。我们可以通过页表来完成一些操作:虚拟地址与物理地址的转化、页表的rwx权限、进程挂起。
在CPU中,有CR3寄存器,将页表中的虚拟地址 + CR3,我们可以构成物理地址。
举一个例子:我们在C语言中如果修改常量字符串,编译器会阻止你,不会使你修改常量字符串的,就是因为在页表中有一个rwx权限,常量字符串中的权限只有r,所以不能进行修改。在出现异常情况时,由于页表的存在,不会将系统内部进行破坏。
在页表中还有一个标志位:如果是0,表示进程挂起,将程序加载到磁盘中;如果是1,表示进程运行。
由于页表将父子进程的代码的权限设置为r,当OS识别出错误时,我们可以进行分情况讨论:
程序本身内部就有地址,我们可以通过下面的指令来看一看可执行程序的反汇编文件:
程序本身内部就有地址,这些地址就是虚拟地址,页表直接从中加载虚拟地址,在程序加载到内存中会出现物理地址,页表进行匹配,完成写入。
objdump -S xxxxx objdunp -s xxxxx > xxxxx.s // 将反汇编文件重写入.s文件中
因为fork()函数在创建子进程后,将id的值发生改变,那么我们将会发生写时拷贝,就会使id的值返回两份。
Linux系统中每一个CPU中拥有一个运行队列,如果有多个CPU就要考虑进程个数的父子均衡问题。Linux系统是分时操作系统,讲究公平。
queue数组的下标说明:
在之前的优先级中讲过nice值的取值范围是:-20~19,而这里的普通优先级下标正好也是40个。在Linux系统中,我们的进程是普通的优先级,所以一一对应queue数组的100~139。
而还有一个系统:实时操作系统。实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心。
由于队列的数组元素过多,我们可以使用位图来判定哪个优先级对应有进程。因为有140的元素,所以我们可以使用5个整形数组的元素可以将整个元素表示出来。我们可以通过数字的二进制来发现哪个优先级上有进程运行,搜寻次数大大减少。
队列中的三个元素:nr_active、bitmap[5]、queue[140]。
nr_active:代表总共有多少个运行状态的进程;
bitmap[5]:位图,表示哪个优先级上有运行状态的进程;
queue[140]:表示的是优先级,分为普通优先级和实时优先级。
时间片还没有结束的所有进程都按照优先级放在活动队列当中,相同优先级的进程按照FIFO规则进程排队调度。
调度过程如下:
当活动队列上的进程随着时间片的轮转,数量会越来越少;而过期队列的进程会越来越多;直到时间片到期,活动队列的进程为0,过期队列的进程数量最大。此时,操作系统只需交换一下active指针和expired指针,将指针内容进行交换,继续进行时间片的轮转即可,如此循环往复。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。