您的当前位置:首页正文

【Linux】进程的地址空间

2024-10-28 来源:个人技术集锦

思维导图

学习内容

学习目标

  • 通过一些奇怪的现象来引入进程的地址空间
  • 进程地址空间的概念
  • 地址空间的理解
  • 为什么要有地址空间
  • 如何理解虚拟地址
  • Linux的调度算法

一、进程地址空间的引入

1.1 一个奇怪的现象

       进程 = 内核数据结构 + 代码(只读) + 数据,所以父子进程是具有独立性的。父子进程有一个进程退出不会影响其他进程。

#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.2 看一看内存空间分布

二、进程地址空间的概念

2.1 对上面的代码进行解释

       子进程要继承父进程的数据,但是进程是具有独立性的, 当子进程需要修改数据时,需要使用写时拷贝。

2.2 写时拷贝 

2.2.1 写时拷贝的概念

2.2.2 写时拷贝的原理

       写时拷贝实际上是运用了一个“引用计数”的概念来实现的,在开辟的空间中多维护了四个字节来存储引用计数。

有两种方式来存储引用计数:

  • 多开辟四个字节(pCount)的空间,用来记录有多少个指针指向这片空间。
  • 在开辟空间的头部预留四个字节的空间来记录有多少个指针指向这片空间。

       当我们多开辟一个空间时,让引用计数 + 1,如果有释放空间,那么就让引用计数 - 1,但是此时不是真正的释放,是假释放,等到引用计数为0时,才是真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。

三、进程地址空间的细节问题

3.1 地址空间的理解

3.1.1 什么是划分区域

struct area
{
    int start;
    int end;
};

struct destop
{
    struct area left;
    struct area right;
};

在操作系统内核中,我们可以找到:

3.1.2 地址空间的理解

3.2 为什么要有地址空间

3.3 如何进一步理解页表和写时拷贝

3.3.1 页表

3.3.1.1 虚拟地址与物理地址的转化
3.3.1.2 页表的rwx权限

       举一个例子:我们在C语言中如果修改常量字符串,编译器会阻止你,不会使你修改常量字符串的,就是因为在页表中有一个rwx权限,常量字符串中的权限只有r,所以不能进行修改。在出现异常情况时,由于页表的存在,不会将系统内部进行破坏。

3.3.1.3 进程挂起

       在页表中还有一个标志位:如果是0,表示进程挂起,将程序加载到磁盘中;如果是1,表示进程运行。

3.3.2 写时拷贝

由于页表将父子进程的代码的权限设置为r,当OS识别出错误时,我们可以进行分情况讨论:

  1. 是不是数据不在物理内存——缺页中断
  2. 是不是数据需要进行写时拷贝——进行写时拷贝
  3. 如果都不是,进行异常处理

3.4 如何理解地址空间

objdump -S xxxxx
objdunp -s xxxxx > xxxxx.s // 将反汇编文件重写入.s文件中

3.5 如何解释学习内容中关于fork函数的疑惑??

       因为fork()函数在创建子进程后,将id的值发生改变,那么我们将会发生写时拷贝,就会使id的值返回两份。

3.6 如何理解申请内存

       进程 = 内核数据结构 + 自己的代码和数据,我们在申请内存空间时,我们一般是如何进行申请的?

操作系统这样做的好处是:

  1. 充分保证内存的使用率,不会空转
  2. 提升了new或者malloc的速度

四、Linux的进程调度队列

4.1 一个CPU中只有一个运行队列

       Linux系统中每一个CPU中拥有一个运行队列,如果有多个CPU就要考虑进程个数的父子均衡问题。Linux系统是分时操作系统,讲究公平。

4.2 优先级

queue数组的下标说明:

  • 普通优先级:100~139
  • 实时优先级:0~99

       在之前的优先级中讲过nice值的取值范围是:-20~19,而这里的普通优先级下标正好也是40个。在Linux系统中,我们的进程是普通的优先级,所以一一对应queue数组的100~139。

       而还有一个系统:实时操作系统。实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心。

       由于队列的数组元素过多,我们可以使用位图来判定哪个优先级对应有进程。因为有140的元素,所以我们可以使用5个整形数组的元素可以将整个元素表示出来。我们可以通过数字的二进制来发现哪个优先级上有进程运行,搜寻次数大大减少。

4.3 活动队列(只出不进)

队列中的三个元素:nr_active、bitmap[5]、queue[140]

  • nr_active:代表总共有多少个运行状态的进程;
  • bitmap[5]:位图,表示哪个优先级上有运行状态的进程;
  • queue[140]:表示的是优先级,分为普通优先级和实时优先级。

       时间片还没有结束的所有进程都按照优先级放在活动队列当中,相同优先级的进程按照FIFO规则进程排队调度。

调度过程如下:

  • 从0下标开始遍历bitmap[5]。
  • 找到第一个非空队列,该队列必定为优先级最高的队列。
  • 拿到选中队列的第一个进程,开始运行,调度完成。
  • 接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。
  • 继续向后遍历bitmap[5],寻找下一个非空队列。

4.4 过期队列(只进不出)

  • 过期队列和活动队列的结构相同。
  • 过期队列上放置的进程都是时间片耗尽的进程。
  • 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算。

4.5 active指针和expired指针

  • active指针永远指向活动队列
  • expired指针永远指向过期队列

       当活动队列上的进程随着时间片的轮转,数量会越来越少;而过期队列的进程会越来越多;直到时间片到期,活动队列的进程为0,过期队列的进程数量最大。此时,操作系统只需交换一下active指针和expired指针,将指针内容进行交换,继续进行时间片的轮转即可,如此循环往复。

显示全文