目录
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_val改500 )。父进程页表不变,仍访问原物理页(g_val保持100 ),实现 "写时才真正拷贝内存",避免冗余开销 。
核心逻辑:读共享,写拷贝,平衡进程创建效率与数据独立性 。
2.理解
1.地址空间的本质是struct里面的一个结构体,内部很多属性都是表示 start和 end 的范围。

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)(只进不出)
- 作用:用来存放时间片已经耗尽的进程,它的结构和活动队列完全一样,可看作是进程时间片管理的 "过渡区域"。
- 特点:当活动队列中的进程把自身时间片用完后,就会被转移到过期队列中。而当活动队列为空(意味着所有进程的时间片都已耗尽 )时,系统会交换 active 和 expired 指针,此时过期队列就转变为新的活动队列,同时重新计算该队列中进程的时间片,让这些进程能够再次参与到系统调度中,以此实现 "批次轮换" 的调度机制,保障进程获取调度的公平性。
active 指针和 expired 指针
- active 指针:始终指向当前可供调度使用的 活动队列,系统会从该队列里选取进程来执行任务。
- expired 指针:始终指向 过期队列,用于暂时存放那些时间片已经耗尽的进程。
- 核心机制:在系统运行过程中,活动队列里的进程会因为时间片不断消耗而逐渐减少,与之相对,过期队列里的进程数量会相应增多。当活动队列为空时,交换这两个指针,过期队列就 "变身" 为新的活动队列,原本过期队列中的进程会重新获得时间片,继续参与系统调度。这种方式无需实际去搬运进程,就能瞬间重置调度资源池,保障调度持续高效地进行。
