Linux-地址空间

目录

1.介绍

2.理解

3.Linux早期的内核调度队列


1.介绍

这是32位的程序空间地址图:

为了更好地理解这段图,我们来写一段代码编译运行:

复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int g_val=100;

int main()
{
    pid_t id = fork();
    int cnt=3;
    if(id == 0)
    {
        
        while(1)
        {
            printf("I am child, pid=%d, ppid=%d,g_val=%d &g_val=%p\n", getpid(), getppid(),g_val,&g_val);
            sleep(2);
	    cnt--;
	    if(cnt==0)
	    {
	      g_val=500;
	      printf("I am child,change pid=%d->%d\n", 100,500);

	    }
        }
    }
    else 
    {
        while(1)
        {
            printf("I am father, pid=%d, ppid=%d,g_val=%d &g_val=%p\n", getpid(), getppid(),g_val,&g_val);
            sleep(2);
        }
    }
    return 0;
}

我们可以看见子进程修改 g_val的地址后,父进程 的地址和子进程的地址是一模一样的,一个地址为什么会有两个不同的值?

答案是这是个虚拟地址,不是物理内存地址,我们在用C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,下面我们从操作系统来理解地址空间

地址空间的本质是内核中的结构体对象。

未写时拷贝(初始共享)

父进程 fork 创建子进程,虚拟地址空间、页表 "逻辑复制" :父子进程虚拟地址(如 g_val 地址 0x56ab5ceac010)一致,页表均映射到物理内存同一块数据页(g_val = 100 )。此时代码、数据物理页共享,不实际拷贝内存,快速创建进程,节省空间。

写时拷贝(触发拷贝)

当子 / 父进程尝试修改共享数据(如子进程改g_val值),操作系统检测到写操作:

为写操作进程(如子进程)新分配物理页;

把原共享物理页数据(100 )拷贝到新页;

更新写操作进程页表,使其指向新物理页(此时子进程 g_val500 )。父进程页表不变,仍访问原物理页(g_val保持100 ),实现 "写时才真正拷贝内存",避免冗余开销

核心逻辑:读共享,写拷贝,平衡进程创建效率与数据独立性 。

2.理解

1.地址空间的本质是struct里面的一个结构体,内部很多属性都是表示 startend 的范围。

2.虚拟地址无序变为有序,让进程从统一的角度看待物理内存以及自己运行的各个区域。

3.进程管理模块内存管理模块相互解耦。

在计算机系统(尤其是操作系统、分布式框架)中,进程管理模块(负责进程的生命周期管理、调度、状态维护、权限控制等)与内存管理模块(负责内存分配、回收、地址映射、虚拟内存管理等)是核心功能模块。

二者的 "相互解耦" 是指通过设计隔离模块间的直接依赖,使它们能独立完成各自功能,仅通过标准化接口协作,从而提升系统的可维护性、扩展性和容错性。

虚拟地址页表是实现两者解耦的核心机制之一。

4.拦截非法请求

虚拟地址页表通过地址合法性验证权限检查进程地址空间隔离,构建了一层硬件级别的保护机制。它能有效拦截非法的内存访问请求(如越界、权限违规、访问未分配内存等),防止物理内存被错误或恶意操作破坏,是操作系统保障内存安全的核心手段之一。

之前我们介绍Linux进程的时候讲过一段代码

复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
 
int main() {
    pid_t pid = fork();
 
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
          while(1)
          {
        printf("Child process: ID=%d PID = %d, PPID = %d\n", pid , getpid(), getppid());
          sleep(2);
          }
    } else {
          while(1)
          {
        printf("Parent process: ID=%d PID = %d, Child PID = %d\n",pid , getpid(), pid);
          sleep(2);
          }
    }
 
    return 0;
}

fork() 对子进程进行了写时拷贝,所以才返回了两个不同的值。

3.Linux早期的内核调度队列

一个 CPU 拥有一个 runqueue

  • 如果有多个 CPU 就要考虑进程个数的负载均衡问题

运行队列优先级

queue[140]

之前我们介绍进程优先级的时候,我们介绍过进程默认优先级是80 ,nice的范围为**[20,-19]。**

进程队列的优先级为:

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

我们的进程值+40就能建立和进程队列的映射

位图:

long bitmap[ 5 ]

我们的队列优先级有140个,要是一个个逐一检测,会增加时间。

bitmap 就是为了节约时间的 **long bitmap[5]**有 32*5=160 足够包含这么多的优先级,我们只要看位图的数字就能找到哪个优先级还存在进程。

这就是大 O (1) 调度算法****, 大 O (1) 调度算法指的是无论输入规模(比如进程数量、任务数量等)如何变化,算法执行所需的时间保持恒定,不随输入规模的增大而增加。

活动队列(Active Queue)(只出不进)

  • 作用:用于存放时间片尚未耗尽的进程,这些进程会依据优先级进行组织,系统优先调度优先级高的进程,以此保障系统能够高效响应任务需求。
  • 调度逻辑:利用 bitmap 快速查找出优先级最高的非空队列,然后选取该队列的队首进程执行。不管系统中进程的总数是多少,查找和调度进程所花费的时间始终固定,其时间复杂度为 O(1) ,确保了调度过程高效、稳定。

过期队列(Expired Queue)(只进不出)

  • 作用:用来存放时间片已经耗尽的进程,它的结构和活动队列完全一样,可看作是进程时间片管理的 "过渡区域"。
  • 特点:当活动队列中的进程把自身时间片用完后,就会被转移到过期队列中。而当活动队列为空(意味着所有进程的时间片都已耗尽 )时,系统会交换 activeexpired 指针,此时过期队列就转变为新的活动队列,同时重新计算该队列中进程的时间片,让这些进程能够再次参与到系统调度中,以此实现 "批次轮换" 的调度机制,保障进程获取调度的公平性。

active 指针和 expired 指针

  • active 指针:始终指向当前可供调度使用的 活动队列,系统会从该队列里选取进程来执行任务。
  • expired 指针:始终指向 过期队列,用于暂时存放那些时间片已经耗尽的进程。
  • 核心机制:在系统运行过程中,活动队列里的进程会因为时间片不断消耗而逐渐减少,与之相对,过期队列里的进程数量会相应增多。当活动队列为空时,交换这两个指针,过期队列就 "变身" 为新的活动队列,原本过期队列中的进程会重新获得时间片,继续参与系统调度。这种方式无需实际去搬运进程,就能瞬间重置调度资源池,保障调度持续高效地进行。
相关推荐
dessler27 分钟前
Hadoop HDFS-部署和基本操作
linux·运维·hdfs
风静雪冷41 分钟前
find命令解读
linux
DavieLau1 小时前
C#项目WCF接口暴露调用及SOAP接口请求测试(Python版)
xml·服务器·开发语言·python·c#
超勇的阿杰1 小时前
gulimall项目笔记:P54三级分类拖拽功能实现
android·笔记
运维行者_1 小时前
使用Applications Manager进行 Apache Solr 监控
运维·网络·数据库·网络安全·云计算·apache·solr
饕餮争锋1 小时前
设计模式笔记_行为型_策略模式
笔记·设计模式·策略模式
小米里的大麦1 小时前
026 inode 与软硬链接
linux
rainsc2 小时前
Swap卡I/O导致D状态幽灵化处理思路
运维
星哥说事2 小时前
如何将堡塔云WAF迁移到新的服务器
服务器·git·github
一心0922 小时前
tomcat 定时重启
运维·tomcat·定时任务