【Linux】进程地址空间初步理解

目录

进程地址空间

看一个现象

写时拷贝

虚拟地址的认识

区域划分

程序加载的过程

​编辑

为什么会有虚拟地址空间?

缺页中断的理解

补充的理解


进程地址空间

也叫虚拟地址空间

虚拟地址空间不是物理内存,每一个take_struct都要对应一个虚拟地址空间

虚拟地址空间的宽度对应一个字节

在32位机器------2^32个地址=4GB

在32位机器------2^64个地址=8GB

以32位机器为例子

低地址到高地址

上图是一个进程地址空间------虚拟地址空间

在用户方面用的是0-3GB的内存,剩下的都是内核的

如果用户想访问所谓的初始化、未初始化的代码、栈、堆等,都可以通过拿到对应的地址进行访问。

可以看到虚拟地址通过页表的映射对应了物理地址

问题:平常写代码的时候&,打印的时候是访问虚拟地址还是物理地址?------虚拟地址

看一个现象

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
    pid_t id = fork();

    if(id < 0){
        perror("fork");
        return 0;
}
    else if(id == 0){ //child
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{ //parent
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
        sleep(1);
        return 0;
}

结果

bash 复制代码
hzx@VM-0-12-ubuntu:~$ gcc fork_addr.c -o fork_addr
hzx@VM-0-12-ubuntu:~$ ./fork_addr
child[12345]: 0 : 0x60104c
parent[12344]: 0 : 0x60104c

关键现象:

  • 子进程和父进程打印的 g_val 值都是 0

  • 子进程和父进程打印的 &g_val 地址完全一样 (0x60104c)

原因:虽然g_val是不同进程的,但是他们的地址是一样的,当然这也不能说明访问的是虚拟地址还是物理地址。------下文慢慢会深挖,只需要一个现象------访问地址一样。

当创建子进程的时候

会拷贝父进程的页表、take_strcut、和虚拟地址------浅拷贝。

因此父进程和子进程的g_val虚拟地址和物理地址会一样,当我们&访问内存的时候地址就是一样的。

后来,由于各种的需求子进程的g_val需要被修改,会怎么变化?

因为进程具有独立性,就可以猜想到,父进程和子进程的g_val不会相互影响。------怎么变化?------写时拷贝

写时拷贝

当子进程的g_val被修改的时候,操作系统会第一时间知道,然后会在物理内存处增加一个内存空间存放子进程修改的值,把修改后g_val的值放到新的物理内存空间,但是他们的虚拟地址都是一样的------因此平常写代码&,访问的是虚拟地址。

虚拟地址的认识

因此,也有了虚拟地址的一个大概的认识:

虚拟地址是操作系统给每个进程发的"假地址"。进程以为自己独占整个内存,实际上 OS 通过页表把它映射到真实的物理内存。& 取到的地址是虚拟地址,父子进程虚拟地址相同,但通过各自的页表映射到不同的物理内存。

虚拟地址空间也要被管理------先描述再组织------数据结构(strcut mm_struct)

区域划分

把虚拟地址空间分成几个固定的"区域",每个区域放不同类型的数据。

只需要确定开始和结束的位置

cpp 复制代码
struct mm_struct {
    // 1. 区域边界(最核心)
    unsigned long start_code, end_code;   // 代码段起止
    unsigned long start_data, end_data;   // 数据段起止
    unsigned long start_brk, brk;         // 堆起止
    unsigned long start_stack;            // 栈起始
    
    // 2. 参数和环境变量
    unsigned long arg_start, arg_end;     // 命令行参数
    unsigned long env_start, env_end;     // 环境变量
    
    // 3. 组织所有区域
    struct vm_area_struct *mmap;          // 区域链表头
    struct rb_root mm_rb;                 // 区域红黑树
    
    // 4. 页表
    pgd_t *pgd;                           // 页全局目录
    
    // 5. 其他
    unsigned long task_size;              // 地址空间大小
    atomic_t mm_users;                    // 使用该地址空间的进程数
    atomic_t mm_count;                    // 引用计数
    // ...
};

每一个位置的都有对应的地址,那么这个就是统一编制。

程序加载的过程

程序加载时,OS 只建立虚拟地址空间和页表(页表项为空),物理内存不分配。程序执行时,访问虚拟地址触发缺页中断,OS 才分配物理内存、读取文件内容、填充页表。这就是"延迟分配"------虚拟地址先确定,物理内存用到才分配。

那么这个时候,建立虚拟地址空间的过程就是调整区域的划分把上面结构体的_end,start的数字进行调整。

take_strct

为什么会有虚拟地址空间?

问题 1:进程之间互相踩踏

进程A和进程B都想用同一个地址,数据会被覆盖。

问题 2:安全风险

程序可以直接访问内核数据,造成系统崩溃、内存泄漏。

没有保护,任何程序都能访问任何内存。------系统会对你地址的访问和操作进行合法判定

问题 3:地址不确定

每次运行地址都不一样,程序无法预知自己的数据在哪。

问题 4:内存不够用

物理空间可能小于进程的大小

没有虚拟内存,程序不能超过物理内存大小。

从虚拟地址空间角度理解野指针

野指针指向的虚拟地址,要么没有映射,要么映射到了不该访问的地方。------他映射的物理地址被释放掉了,就没有了地址。

从虚拟内存角度理解一行代码

cpp 复制代码
char str* ="hello world";
*str='A';

这个代码如果运行会直接崩溃,为什么?

因为str指向的内容是放在了字符常量区的,当运行的时候,字符常量区的虚拟地址映射到物理内存的时候,由于str不能被修改,会发送权限拦截。

完整的理解

cpp 复制代码
1. 程序编译时,"hello world" 被放入只读数据段
2. 加载时,OS 把该区域映射为"只读"
3. 执行 *str = 'A'
   - CPU 用 str 的虚拟地址查页表
   - 页表显示:该区域权限是"只读"
   - CPU 触发写保护异常
4. OS 接管
   - 检查:这是只读区域,不允许写
   - 向进程发送 SIGSEGV 信号
5. 进程收到 SIGSEGV
   - 默认行为:终止进程
   - 终端显示:Segmentation fault (core dumped)

缺页中断的理解

CPU 访问虚拟地址时,页表里找不到映射,触发异常让 OS 处理。OS 分配物理内存,填充页表,然后程序重新执行。缺页中断是虚拟内存"延迟分配"机制的核心,正常情况下程序感知不到,只有非法访问才会变成段错误。

cpp 复制代码
1. 程序访问虚拟地址 0x4005d6
2. CPU 查页表
3. 页表项为空(或无效)
4. CPU 触发缺页中断
5. OS 接管:
   a. 检查这个地址是否合法
   b. 分配一块物理内存
   c. 从磁盘读取数据(如果是文件映射)
   d. 填充页表:0x4005d6 → 物理地址 X
   e. 标记权限(只读/可写)
6. 返回程序,重新执行刚才的指令
7. 这次页表有映射了,访问成功

因此还有一个原因是为了让进程管理和内存管理进行解耦合

补充的理解

我们可以不加载数据,只有take_struct、mm_struct。

创建进程先有内核数据结构,才陆陆续续加载代码和数据。

进程挂起的时候,OS会查找页表对应的地址,把页表清空,再把代码和数据放到磁盘。

栈不是不只有一个吗,怎么做到malloc的时候是随机的?

mm_struct有多个vm_area_struct,每个代表一个malloc的区域,除了堆区,其他区域其实也是一样的

相关推荐
Julien20041 小时前
Docker 网络(一)
linux·运维·服务器·ssh·学习方法
字节跳动的猫2 小时前
LikeShop 积分体系全链路改造:获取规则、订单抵扣与积分商品兑换二开
运维·数据结构
杨云龙UP2 小时前
一次数据库查询缓慢故障复盘:大表数据增长、SQL全表扫描导致系统响应异常
linux·运维·服务器·数据库·sql·mysql
PellyKoo2 小时前
【linux运维】ubuntu+samba 用户组独占目录权限配置踩坑记
linux·运维·ubuntu
wtblszn2 小时前
空压机在线监测物联网方案
大数据·运维·物联网·自动化·能源
AR-26710-3 小时前
Linux Day15——系统管理复习
linux·运维
高山有多高3 小时前
【Linux笔记】Linux自定义Shell
linux·c++
姜鱼问生3 小时前
HTTPS 部署实战:未备案域名 + 非标端口的完整方案
linux·https
jackletter3 小时前
linux:安装 ubuntu 26.04.1
linux·ubuntu·ubuntu26.04.1