进程的虚拟地址空间

简介: 进程的虚拟地址空间

每个程序运行起来后,都拥有一个自己的虚拟地址空间(注意是虚拟的,不是实际存在的),这个虚拟地址空间的大小由计算机的硬件平台

关于虚拟的概念,当时IBM给出了一种说法很形象生动:

它存在,你能看得见,它是物理的

它存在,你看不见,它是透明的

它不存在,你却看得见,它是虚拟的

它不存在,你也看不见,它被删除了!

决定,具体地说是由CPU位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如32位的硬件平台决定了虚拟地址空间的地址为0到2^32 - 1,即0x00000000~0xFFFFFFFF,也就是我们常说的4GB虚拟空间大小,也就是它只有4GB的寻址能力。

现在让我们来看一下x86体系下32位Linux环境中进程虚拟地址空间:

了解了地址空间,我们就能对程序有更深入的理解。

这里介绍一下每一节中存放的内容:

.text:已编译程序的机器代码
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
.data:已初始化的全局和静态变量。注意局部变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中
.bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量。这个节不占据实际的空间,它仅仅是一个占位符。
区分已初始化和未初始化变量是为了空间效率,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0

一种记住.data和.bss节之间区别的简单方法是把“bss”看成是“更好地节省空间(Better Save Space)的缩写”(.bss中的值都为0)

举个例子:

int gdata1 = 10;
int gdata2 = 0;
int gdata3;
static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;
int main()
{
  int a = 12;
  int b = 0;
  int c;
  static int e = 13;
  static int f = 0;
  static int g;
    cout << c << g << endl;
    //c不为零,g为零
  system("pause");
  return 0;
}

根据上面的规则,各个变量存放在哪块内存呢?

gdata1和gdata4初始化且不为0,存放在.data段
gdata2、gdata3、gdata5、gdata6未初始化或初始化为0,存放在.bss段
a,b,c不会产生符号,编译成指令,指令存放在.text段
mov dword ptr[a], 0ch
e初始化了,且不为0,存放在.data段
f和g初始化为0,或未初始化,存放在.bss段
注意一点,虽然a变量是局部变量,存放在栈上,
但有一点需要明确a = 12编译后生成指令,存放在.text段,只是在运行时,在栈上开辟空间

知道了每节存放的内容,或许就能更好的理解程序

比如,为什么有时会发生栈溢出(stack overflow)?

栈向低地址增长,栈空间是有限的,当递归函数递归太深就会爆栈(关于函数调用是如何开辟栈空间的之后再谈)

为什么程序常常会出现”段错误(segment fault)“或”非法操作,该内存地址不能read/write“的错误信息?

这往往是因为指针指向一个不允许读或写的内存地址,而程序却试图利用指针来读写该地址的时候,就会出现这个错误。在Linux或Windows的内存布局中,有些地址是始终不能读写的,例如0地址。

还有这样的程序为什么报错?

char *p = "hello, world";
*p = 'a';

因为”hello, world“作为字符串常量存放在只读数据段中,修改只读数据段当然不可以。

等等,当我们之后遇到此类问题时,都可以想想它在内存中是怎么存储的,从而明白为什么出错。

一些小点:

1.栈通常只有数兆字节的大小,而堆一般比栈大很多可以有几十至数百兆字节的容量。程序使用mallocnew分配内存时得到的内存都来自堆里。

2.Windows在默认情况下会将高地址的2GB空间分配给内核(也可配置为1GB),而Linux默认情况下将高地址的1GB空间分配给内核。

思考,数据什么时候可以放在栈上,什么时候又需要放在堆上呢?

栈是程序运行的基础,函数调用的过程中,通过移动栈指针开辟足够的空间,用来存放函数使用到的局部变量,以及函数中使用到的通用寄存器的副本(以便在函数调用结束后,通过副本可以恢复到函数调用之前)。编译器编译并优化代码时,一个函数就是一个最小的编译单元,所以在编译阶段,编译器就得知道要存放哪些局部变量和寄存器,以便预留空间。所以编译期可以确定大小的”放在“(预留,运行时开辟)栈上,对于无法确定大小或大小可以改变的数据,最好放在堆上。除了动态大小的需要放在堆上,动态生命周期的内存也需要分配到堆上。因为栈上的内存在函数调用结束后,会被回收(实则是改变栈指针,数据并不会被清空),对于栈上内存的生命周期是不受开发人员控制的,局限在当前调用栈中。而堆上开辟的内存需要显式释放,因此堆上的内存就有更加灵活的生命周期,可以在不同的调用栈之间共享数据。总结下来,栈上的数据是静态的,大小固定,生命周期固定,而堆上的数据是动态的,大小不固定,生命周期不固定。

以上便是进程的虚拟地址空间的介绍,这里只关注了几个常用的节,关于这部分知识点很多,之后继续总结。

正文结束,这里通过一个小例子来测试指针在不同平台下、不同类型下的大小:

一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为4字节,64位平台下的指针为8字节。

这里有一个经典的面试题,就是问指针大小,比如32位操作系统下,int *p和char *p,p的大小是多大?
都是4字节,因为p是指针类型,p存放的是内存地址,与类型无关。

32位环境下:

64位环境下:

参考资料:

[1] 龚奕利,贺莲译.深入理解计算机系统[M].北京:机械工业出版社,2016.

[2] 俞甲子,石凡,潘爱民著.程序员的自我修养:链接、装载与库[M].北京:电子工业出版社,2009.4

相关文章
|
6月前
|
安全 Linux 编译器
Linux进程——进程地址空间
Linux进程——进程地址空间
56 2
|
5月前
|
Java Linux 编译器
【Linux】深挖进程地址空间
【Linux】深挖进程地址空间
44 0
|
6月前
|
存储 算法 Linux
【Linux】进程的地址空间
【Linux】进程的地址空间
|
5月前
|
Linux
【Linux】进程地址空间
【Linux】进程地址空间
|
6月前
|
Linux 调度 C++
【linux】进程的地址空间
【linux】进程的地址空间
51 0
|
7月前
|
消息中间件 Java C++
"Java多线程基础-2:简介虚拟地址空间——保障进程间独立性的机制 "
如何保障进程之间这样的独立性?操作系统采用了“虚拟地址空间”的方式。
51 0
|
7月前
|
存储 算法 Linux
【Linux】程序地址空间 -- 详解 & Linux 2.6 内核进程调度队列 -- 了解
【Linux】程序地址空间 -- 详解 & Linux 2.6 内核进程调度队列 -- 了解
|
5月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
5月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
192 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
4月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。