【Linux】进程的地址空间

简介: 【Linux】进程的地址空间

思维导图

学习内容

      地址是一个很重要的名词,我们的每一个进程在内存中运行都会有若干个地址。在之前我们学习进程的时候,学过一个函数——fork(),这个函数仅仅被调用一次,却能够返回两次。这是为什么呢?那么这一篇博客将会解释这种现象——进程的地址空间

学习目标

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

一、进程地址空间的引入

      我们可以通过一个特别的代码来看到一个奇怪的现象:就是在同一个地址空间中,对于同一个变量的值是不同的,是不是感到很奇怪。在我们的认知和印象中,对于一个地址空间来说,只会有一个变量的大小,而不会出现多个值在同一个地址空间的现象,所以我们推断出这个地址空间一定不会是真正的物理地址,而是一种虚拟的地址空间。

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

#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;
}

                 

二、进程地址空间的概念

      进程地址空间不能简单的认为是内存,通过上面的现象,其也一定不是物理地址,因此一定是虚拟地址。这段空间中自下而上,地址是增长的,栈是向地址减小方向增长(栈是先使用高地址),而堆是向地址增长方向增长(堆是先使用低地址),堆栈之间的共享区,主要用来加载动态库。

      每一个进程在启动时都会有一个进程地址空间的存在,进程地址空间是操作系统给进程花的“大饼”——告诉进程:操作系统中的所有物理地址都可以使用————使每个进程都认为自己独占系统内存资源。(即虚拟空间)。

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 地址空间的理解

      我们可以将操作系统当做英国的大富翁,将进程看成大富翁的私生子,大富翁告诉私生子:我有10亿的遗产,可以给你使用。每一个私生子都不知道彼此的存在,因此每一个私生子都以为自己会获得10亿的遗产,但是整体来看,这是不正确的。为什么操作系统可以这样做呢??因为进程在申请空间时,是不会将操作系统的物理地址全部申请过来,因此每次申请是够用的。

3.2 为什么要有地址空间

进程地址空间有三个好处:

  1. 可以将无序的地址空间变成有序的虚拟地址空间,让进程以统一的视角看待物理内存以及自己运行的各个区域
  2. 进程管理模块和内存管理模块进行解耦:有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟)
  3. 拦截非法请求

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

3.3.1 页表

      页表是非常复杂的。我们可以通过页表来完成一些操作:虚拟地址与物理地址的转化、页表的rwx权限、进程挂起。

3.3.1.1 虚拟地址与物理地址的转化

      在CPU中,有CR3寄存器,将页表中的虚拟地址 + CR3,我们可以构成物理地址。

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的值返回两份。

四、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指针,将指针内容进行交换,继续进行时间片的轮转即可,如此循环往复。

相关文章
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
310 67
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
231 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
148 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
138 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
211 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
137 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
188 0
|
8月前
|
JavaScript Linux Python
在Linux服务器中遇到的立即重启后的绑定错误:地址已被使用问题解决
总的来说,解决"地址已被使用"的问题需要理解Linux的网络资源管理机制,选择合适的套接字选项,以及合适的时间点进行服务重启。以上就是对“立即重启后的绑定错误:地址已被使用问题”的全面解答。希望可以帮你解决问题。
450 20
|
9月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
366 4
|
9月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。