[操作系统] 深入进程地址空间
程序地址空间回顾
在C语言学习的时,对程序的函数、变量、代码等数据的存储有一个大致的轮廓。在语言层面上存储的地方叫做程序地址空间,不同类型的数据有着不同的存储地址。
下图为程序地址空间的存储分布和和特性:
使用以下代码来验证一下各个类型的是数据存储是否如图所示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int g_unval;
int g_val = 100; int main(int argc, char *argv[], char *env[])
{const char *str = "helloworld";printf("code addr: %p\n", main);printf("init global addr: %p\n", &g_val);printf("uninit global addr: %p\n", &g_unval);static int test = 10;char *heap_mem = (char*)malloc(10);char *heap_mem1 = (char*)malloc(10);char *heap_mem2 = (char*)malloc(10);char *heap_mem3 = (char*)malloc(10);printf("heap addr: %p\n", heap_mem);printf("heap addr: %p\n", heap_mem1);printf("heap addr: %p\n", heap_mem2);printf("heap addr: %p\n", heap_mem3);printf("test static addr: %p\n", &test);printf("stack addr: %p\n", &heap_mem);printf("stack addr: %p\n", &heap_mem1);printf("stack addr: %p\n", &heap_mem2);printf("stack addr: %p\n", &heap_mem3);printf("read only string addr: %p\n", str);for(int i = 0; i < argc; i++){printf("argv[%d]: %p\n", i, argv[i]);}for(int i = 0; env[i]; i++){printf("env[%d]: %p\n", i, env[i]);}return 0;
}
结果如下:
$ ./a.out
code addr: 0x40055d // 正文代码 main()
init global addr: 0x601034 // 未初始化全局变量
uninit global addr: 0x601040 // 初始化的全局变量
heap addr: 0x1791010 // 堆:向上增长 ↑
heap addr: 0x1791030
heap addr: 0x1791050
heap addr: 0x1791070
test static addr: 0x601038 // static int 类型
stack addr: 0x7ffd0f9a4368 // 栈:向下增长 ↓
stack addr: 0x7ffd0f9a4360
stack addr: 0x7ffd0f9a4358
stack addr: 0x7ffd0f9a4350
read only string addr: 0x400800 // const char *str
argv[0]: 0x7ffd0f9a4811
env[0]: 0x7ffd0f9a4819
env[1]: 0x7ffd0f9a482
env[2]: 0x7ffd0f9a4845
env[3]: 0x7ffd0f9a4850
env[4]: 0x7ffd0f9a4860
env[5]: 0x7ffd0f9a486e
通过地址结果的验证可以明确:
- 堆向上增长,随着创建申请空间,空间地址逐渐变大。
- 栈向下增长,随着创建变量,变量空间地址逐渐变小。
const char*
的最字符串常量地址与正文代码的地址相近,说明在编译的时候会将该类型硬编到正文代码中,所以形成了代码只可读。- 函数内部的
static
类型的变量地址与初始化数据中全局变量地址相近,因为static
类型的变量在编译时就会在初始化数据区域,所以就会作为全局变量。则static
是全局属性。
虚拟地址
实际上程序的地址空间是内存吗?
地址空间不是内存地址,而是虚拟地址!
在语言层上,我们会叫做程序地址空间。但是在系统层面上,会将其称为进程地址空间或者虚拟地址空间。
可以通过以下代码来验证:
#include<stdio.h>
#include <unistd.h>int gval = 100;int main()
{pid_t id = fork();if (id == 0){while (1){printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());sleep(1);gval++;}}else{while (1){printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());sleep(1);}}return 0;
}
结果如图:
父子进程按照代码逻辑进行运行,随着每次睡眠过后子进程的全局变量就会+1
。但是通过结果可以发现,父进程和子进程各自的全局变量地址都是0x601054
。明明是同一个地址空间,为什么全局变量gval
的值不同呢?
这就证明了,进程的地址空间一定不是内存地址,不是物理上的地址,而是虚拟地址!我们在程序中使用指针指向的地址,以及取变量地址等操作,实际上都是在访问虚拟地址。
物理地址一般不会向用户展示,操作系统会将虚拟地址转化成物理地址,虚拟地址由操作系统统一管理。
进程地址空间
基础概念
每个进程都有其虚拟地址空间mm_struct
和页表存在于task_struct
中,每个地址空间1
字节。所以对于32位的机器,在虚拟地址空间中共有2^32
个地址空间,64位机器则有2^32
地址空间。
页表中存储的是虚拟地址和物理地址的映射关系。
程序在运行时实际上管理的是虚拟地址空间中的地址,当程序需要进行管理一个地址的时候,操作系统会将该地址在页表中进行查找,就可以得到与其对应映射的物理内存地址。然后操作系统会对物理内存地址的数据进行访问管理。
子进程会继承父进程的虚拟地址空间和页表。
如何通过一个字节地址访问多个字节大小的数据?
通过地址和类型偏移量确定整个数据。
假设存在一个int
变量a
,当我们通过虚拟地址空间的映射找到物理地址后,会通过int
类型在结构体中的偏移量进行确定整个数据内容,因为所有的数据都是通过先描述后组织进行管理,通过对应的数据结构就可以确定数据的位置。
进程如何独立
子进程的虚拟地址空间和页表会继承父进程,那么进程之间是怎么独立的呢?
假设父进程存在一全局变量int g_val
,在当前父进程虚拟地址在页表中已经与物理地址映射。然后创建子进程,当子进程中尝试对g_val
修改时操作系统会进行以下操作:
- 在物理地址空间中会重新开辟一块
int
大小的空间,在此空间内存储修改后的地址。 - 在页表中查询子进程虚拟地址空间中
g_val
虚拟地址,然后将新开辟的物理地址与虚拟地址重新建立映射关系。 - 此时,因为继承的关系,父进程与子进程中的
g_val
使用的是同一个虚拟地址,但由于子进程对g_val
进行修改,所以同一个变量的虚拟地址映射的是不同的物理地址。
这就是写时拷贝的机制!!
所以发生写时拷贝后,子进程对于修改的数据会重新构建映射关系,而其他的数据、代码、变量等都是共享的物理资源,这也避免了重复拷贝的内存的浪费,减少创建时间。
**通过这种机制就形成了进程的独立! **
虚拟地址与进程地址空间关系
通过上文可知,对于32位的机器来说,每个进程的虚拟地址空间有2^32
字节的大小,也就是4个G。但如果整个内存只有4G的话,那么一个进程就要把所有的内存空间全部占满吗?显然不可能。
如其名,虚拟地址空间并不是真正的内存空间。操作系统会让每个进程都认为他们可以独占物理内存,但是在实际使用的时候会根据真实的需求通过映射关系开辟内存空间。
虚拟地址空间如何从物理内存划分
由于进程不会独占物理内存,那么肯定有相对应的划分管理方法。
虚拟地址的本质:结构体对象,数据结构!
mm_struct
中存储的起始地址和结束地址用int
表示。- 每个区域的范围是
[start_address, end_address]
,这些地址用int
记录下来。例如:
struct mm_struct {int code_start; // 代码段起始地址int code_end; // 代码段结束地址int heap_start; // 堆起始地址int heap_end; // 堆结束地址int stack_start; // 栈起始地址int stack_end; // 栈结束地址
};
虚拟地址通常是用 int(4字节,32位) 类型存储的,而每个 int
值就直接对应一个地址。虚拟地址空间中的地址可以用一个 int
值表示,因为 int
的取值范围足够覆盖整个虚拟地址空间的范围(0 ~ 232−12^{32} - 1232−1,即 4GB)。
在32为机器中虚拟地址由2^32字节空间,每个区域(栈,堆…)都有自己确定的区域,然后堆所有的区域进行编址。虚拟地址空间就是结构体mm_struct
,里面存放的就是每个区域的起始地址和结束地址对应的int
值。
区域调整
既然每个区域的大小是用int
值进行确定,那么当需要对这个区域大小进行调整的时候,区域调整就是对起始和结束的整数范围进行调整。
根据各个区域的特性,例如堆向上增长,栈向下增长,将其对应的start
和end
进行+
或者—
,以此来进行区域的调整。
小结:虚拟地址空间是什么
操作系统需要对进程中的虚拟地址空间进行管理,虚拟地址空间是内核中的一种数据结构mm_struct
,大部分属性都是各个区域的开始和结束地址的int
值。
先描述,在组织。作为数据结构,操作系统不仅会对进程进行管理,也会对mm_struct
进行管理,用链表进行管理。但是实际上通过PCB也可以直接访问到mm_struct
。
相关文章:

[操作系统] 深入进程地址空间
程序地址空间回顾 在C语言学习的时,对程序的函数、变量、代码等数据的存储有一个大致的轮廓。在语言层面上存储的地方叫做程序地址空间,不同类型的数据有着不同的存储地址。 下图为程序地址空间的存储分布和和特性: 使用以下代码来验证一下…...

CVE-2025-0411 7-zip 漏洞复现
文章目录 免责申明漏洞描述影响版本漏洞poc漏洞复现修复建议 免责申明 本文章仅供学习与交流,请勿用于非法用途,均由使用者本人负责,文章作者不为此承担任何责任 漏洞描述 此漏洞 (CVSS SCORE 7.0) 允许远程攻击者绕…...

leetcode151-反转字符串中的单词
leetcode 151 思路 时间复杂度:O(n) 空间复杂度:O(n) 首先将字符串转为数组,这样可以方便进行操作,然后定义一个新的数组来存放从后到前的单词,由于arr中转换以后可能会出现有些项是空格的情况,所以需要判…...
若依 v-hasPermi 自定义指令失效场景
今天使用若依跟往常一样使用v-hasPermi 自定义指令的时候发现这个指令失效了,原因是和v-if指令一块使用,具体代码如下: <el-buttonsize"mini"type"text"icon"el-icon-edit-outline"v-hasPermi"[evalu…...
vue3中自定一个组件并且能够用v-model对自定义组件进行数据的双向绑定
1. 基础用法 在 Vue3 中,v-model 在组件上的使用有了更灵活的方式。默认情况下,v-model 使用 modelValue 作为 prop,update:modelValue 作为事件。 1.1 基本示例 <!-- CustomInput.vue --> <template><input:value"mo…...
使用 Python 和 Tesseract 实现验证码识别
验证码识别是一个常见且实用的技术需求,尤其是在自动化测试和数据采集场景中。通过开源 OCR(Optical Character Recognition,光学字符识别)工具 Tesseract,结合 Python 的强大生态,我们可以高效实现验证码识…...

谈一谈前端构建工具的本地代理配置(Webpack与Vite)
在Web前端开发中,我们在本地写代码经常遇到的一件事情就是代理配置。代理配置说简单也简单,配置一次基本就一劳永逸,但有时候配置不对,无论如何也连不上后端,就成了非常头疼的一件事。在这本文中,我们讨论一…...

CentOS7非root用户离线安装Docker及常见问题总结、各种操作系统docker桌面程序下载地址
环境说明 1、安装用户有sudo权限 2、本文讲docker组件安装,不是桌面程序安装 3、本文讲离线安装,不是在线安装 4、目标机器是内网机器,与外部网络不连通 下载 1、下载离线安装包,并上传到$HOME/basic-tool 目录 下载地址&am…...
Alibaba Spring Cloud 十三 Nacos,Gateway,Nginx 部署架构与负载均衡方案
在微服务体系中,Nacos 主要承担“服务注册与发现、配置中心”的职能,Gateway(如 Spring Cloud Gateway)通常负责“路由转发、过滤、安全鉴权、灰度流量控制”等功能,而 Nginx 则常被用作“边缘反向代理”或“统一流量入…...
+-*/运算符优先级计算模板
acwing3302 知识点一:有关unordered_map的优先级 头文件<unordered_map>,然后进行符号优先级定义 定义方式unordered_map<char,int>pr{ {,1},{-,1},{*,2},{/,2}};其余没定义的默认为0 知识点二:头文件<cctype>中的isdigit()是判断…...

GPT 结束语设计 以nanogpt为例
GPT 结束语设计 以nanogpt为例 目录 GPT 结束语设计 以nanogpt为例 1、简述 2、分词设计 3、结束语断点 1、简述 在手搓gpt的时候,可能会遇到一些性能问题,即关于是否需要全部输出或者怎么节约资源。 在输出语句被max_new_tokens 限制,…...

FastDFS的安装及使用
分布式存储发展历程 前段时间 618 活动火热进行,正是购物的好时机。当我们访问这些电 商网站的时候,每一个商品都会有各式各样的图片展示介绍,这些图 片一张两张可以随便丢在服务器的某个文件夹中,可是电商网站如此 大体量的…...

C++ lambda表达式
目录 1.lambda表达式 1.1什么是Lambda表达式? 1.2Lambda表达式的语法 1.3捕捉列表 1.4函数对象与lambda表达式 1.lambda表达式 1.1什么是Lambda表达式? Lambda表达式是C11标准引入的一种匿名函数,它允许你在需要函数的地方直接编写代码…...
react页面定时器调用一组多个接口,如果接口请求返回令牌失效,清除定时器不再触发这一组请求
为了实现一个React页面使用定时器调用一组多个接口,并在任意一个接口请求返回令牌失效时清除定时器且不再触发这一组请求,可以遵循以下步骤: 1. 定义API调用函数:创建一个函数来处理一组API调用。每个API调用都应该检查响应状态以…...
Python的泛型(Generic)与协变(Covariant)
今天咱们聊聊Python类型标注中的泛型(Generic),与协变(Covariant)。 不了解类型标注的小伙伴,可以先看一看我的上一篇文章 “Python类型检查” Python 类型检查-CSDN博客 例子 这次我开个宠物商店。看下面代码。 class Animal:passclass Dog(Animal):passclass Cat(A…...

Python Typing: 实战应用指南
文章目录 1. 什么是 Python Typing?2. 实战案例:构建一个用户管理系统2.1 项目描述2.2 代码实现 3. 类型检查工具:MyPy4. 常见的 typing 用法5. 总结 在 Python 中,静态类型检查越来越受到开发者的重视。typing 模块提供了一种方式…...

OpenEuler学习笔记(六):OpenEuler与其他Linux服务器的区别是什么?
OpenEuler是一款基于Linux内核的开源服务器操作系统,与其他Linux服务器操作系统(如CentOS、Ubuntu Server等)存在多方面的区别,主要体现在以下几个方面: 一、社区与支持 OpenEuler:由华为发起并开源&…...

如何使用CRM数据分析和洞察来支持业务决策和市场营销?
如何使用CRM数据分析和洞察来支持业务决策和市场营销? 大家好!今天咱们聊聊一个特别重要的话题——如何利用客户关系管理(CRM)系统中的数据进行分析与洞察能够帮助我们做出更好的业务决策以及提升市场营销效果。其实啊࿰…...

MyBatis和JPA区别详解
文章目录 MyBatis和JPA区别详解一、引言二、设计理念与使用方式1、MyBatis:半自动化的ORM框架1.1、代码示例 2、JPA:全自动的ORM框架2.1、代码示例 三、性能优化与适用场景1、MyBatis:灵活的SQL控制1.1、适用场景 2、JPA:开发效率…...

SVN客户端使用手册
目录 一、简介 二、SVN的安装与卸载 1. 安装(公司内部一般会提供安装包和汉化包,直接到公司内部网盘下载即可,如果找不到可以看下面的教程) 2. 查看SVN版本 编辑 3. SVN卸载 三、SVN的基本操作 1. 检出 2. 清除认证数据 3. 提交…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...

关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...

JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...