Liunx 操作系统 进程概念(下)

一.命令行参数和环境变量

1.环境变量的概念

环境变量(Environment Variables)可以理解成:

操作系统提前准备好的一些"全局配置信息",程序运行时可以读取这些信息。

例如:

复制代码
echo $PATH

就是查看 PATH 环境变量。

可以把环境变量想象成:

复制代码
操作系统
   |
   |--- PATH
   |--- HOME
   |--- SHELL
   |--- ...

不同程序都可以读取这些信息。

2.常见环境变量

1. PATH

PATH:指定命令的搜索路径。

当我们在终端输入一条命令(例如 lspythonjava)时,操作系统并不会立刻知道该命令对应的程序存放在哪里。它会按照 PATH 环境变量中记录的目录顺序,依次去这些目录里查找是否存在同名的可执行文件。一旦找到,就立即执行;如果所有目录都找遍了仍然没有,终端就会提示 command not found(命令未找到)。

例如,当我们执行 echo $PATH 时,通常会看到类似下面这样的输出:

复制代码
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

这里每一段路径之间用冒号(:)分隔,表示系统会按照从左到右的顺序依次在这些目录中搜索命令。这种设计带来的好处是:我们不需要记住每个程序安装的完整路径,只要把它的目录加入 PATH,就可以在任何位置直接输入命令名来启动它。

在实际开发中,我们经常需要修改 PATH。比如安装了某个新工具后,如果直接运行命令提示找不到,通常就是因为它的安装目录还没有被加入 PATH。此时可以在终端中临时追加路径,例如:

复制代码
export PATH=$PATH:/自定义/工具目录

这条命令会把新的目录追加到现有 PATH 的末尾,从而让系统也能在该目录中查找命令。需要注意的是,这种方式只在当前终端会话中生效,关闭终端后就会失效;如果希望永久生效,需要把这一行写入 ~/.bashrc~/.zshrc 等配置文件里。

>

复制代码
/bin/ls

就能运行?

因为Linux会去 PATH 中指定的目录寻找 ls

查看:

复制代码
echo $PATH

可能得到

复制代码
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin

这些目录之间用:

复制代码
:

分隔。

Linux执行:

复制代码
ls

大致会:

复制代码
ls
 ↓
去PATH中的目录找
 ↓
/usr/local/bin/ls
 ↓
没找到
 ↓
/usr/bin/ls
 ↓
找到了
 ↓
执行

2.HOME

复制代码
echo $HOME

例如普通用户可能得到:

复制代码
/home/whb

root可能是:

复制代码
/root

HOME表示: ****当前用户的主目录

3.SHELL环境变量

查看:

复制代码
echo $SHELL

可能得到:

复制代码
/bin/bash

表示:当前用户默认使用的Shell。

例如:

复制代码
/bin/bash

就是 Bash。

测试:

假设你写了:

复制代码
#include <stdio.h>

int main()
{
    printf("hello world!\n");
    return 0;
}

编译:

复制代码
gcc hello.c -o hello

当前目录:

复制代码
hello

方法一:./hello

执行:

复制代码
./hello

这里:

复制代码
.

表示:当前目录

所以:

复制代码
./hello

就是执行当前目录下的hello程序。

因此可以运行。


方法二:直接输入 hello

如果输入:

复制代码
hello

Linux会去:

复制代码
PATH

里面寻找 hello

例如:

复制代码
/usr/local/bin
/usr/bin
/bin
...

但是你的 hello 在:

复制代码
当前目录

而当前目录通常不在PATH中。

所以:

复制代码
hello

找不到。

于是报:

复制代码
command not found

3.和环境变量相关的命令

1. echo ------ 查看某一个环境变量

语法:

复制代码
echo $变量名

例如:

复制代码
echo $PATH

输出:

复制代码
/usr/local/bin:/usr/bin:/bin

表示查看 PATH 的值。

再比如:

复制代码
echo $HOME

可能输出:

复制代码
/home/whb

为什么前面有 $

复制代码
$PATH

表示:把 PATH 这个变量里面保存的值取出来。

所以:

复制代码
echo PATH

和:

复制代码
echo $PATH

是不一样的。


2. export ------ 设置/导出环境变量

例如:

复制代码
export MYNAME=hello

然后:

复制代码
echo $MYNAME

得到:

复制代码
hello

这里相当于:

复制代码
MYNAME = hello

并且通过 export,它成为环境变量,子进程也可以继承。

例如:

复制代码
export TEST=123
./a.out

a.out 这个程序就可以读取 TEST

要区分:

复制代码
MYNAME=hello

和:

复制代码
export MYNAME=hello

前者只是 Shell 本地变量。

后者是 环境变量,可以传给子进程。

可以简单记: export = 把 Shell 变量"导出"给子进程。

3. env ------ 查看所有环境变量

直接输入:

复制代码
env

会显示很多环境变量,例如:

复制代码
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/whb
SHELL=/bin/bash
USER=whb
PWD=/home/whb/test

所以:

复制代码
echo $PATH

是只看 PATH

而:

复制代码
env

是看所有环境变量


4. unset ------ 删除环境变量

例如:

复制代码
export MYNAME=hello

现在:

复制代码
echo $MYNAME

得到:

复制代码
hello

然后:

复制代码
unset MYNAME

再:

复制代码
echo $MYNAME

就没有值了。

所以:

复制代码
unset 变量名

就是删除这个变量。

注意这里不加 $

复制代码
unset MYNAME       # 正确
unset $MYNAME      # 不应该这样写

因为 unset 操作的是变量本身,不是变量的值。


5. set ------ 查看 Shell 变量 + 环境变量

输入:

复制代码
set

会显示当前 Shell 中定义的变量,内容通常很多。

可以简单理解成:

复制代码
set
 ↓
Shell变量 + 环境变量

而:

复制代码
env
 ↓
主要查看环境变量

4. 环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以 '\0' 结尾的环境字符串。

环境表

假设系统有这些环境变量:

复制代码
PATH=/usr/bin:/bin
HOME=/home/whb
SHELL=/bin/bash
USER=whb

程序运行时,这些环境变量可以组织成:

复制代码
环境表
   |
   ↓
+---------------------+
| 指针1 ───────────────┼────→ "PATH=/usr/bin:/bin\0"
+---------------------+
| 指针2 ───────────────┼────→ "HOME=/home/whb\0"
+---------------------+
| 指针3 ───────────────┼────→ "SHELL=/bin/bash\0"
+---------------------+
| 指针4 ───────────────┼────→ "USER=whb\0"
+---------------------+
| NULL                |
+---------------------+

这就是:字符指针数组 char *env[]

例如:

环境变量:

复制代码
PATH=/usr/bin:/bin

实际上是一个完整字符串:

复制代码
P A T H = / u s r / b i n : / b i n \0

意思就是:

复制代码
env[0] ───→ PATH=/usr/bin:/bin\0
env[1] ───→ HOME=/home/whb\0
env[2] ───→ SHELL=/bin/bash\0

所以每个指针指向一个以 '\0' 结尾的环境字符串。

假设有:

复制代码
char *env[] =
{
    "AAA=111",
    "BBB=222",
    "CCC=333",
    NULL
};

最后的:

复制代码
NULL

作用是:告诉程序:环境变量已经结束了。

类似字符串使用:

复制代码
'\0'

表示结束。

环境表使用

复制代码
NULL

表示结束。

所以:

复制代码
字符串:

字符 → 字符 → 字符 → '\0'
                     ↑
                    结束


环境表:

env[0] → 字符串
env[1] → 字符串
env[2] → 字符串
env[3] → NULL
         ↑
        结束

可以这样遍历环境表

Linux程序中通常可以使用:

复制代码
extern char **environ;

environ 指向环境表。

例如:

复制代码
#include <stdio.h>

extern char **environ;

int main()
{
    int i = 0;

    while(environ[i] != NULL)
    {
        printf("%s\n", environ[i]);
        i++;
    }

    return 0;
}

运行后可能输出:

复制代码
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/whb
SHELL=/bin/bash
USER=whb
...

5.通过系统调⽤获取或设置环境变量

getenv()

函数原型:

复制代码
char *getenv(const char *name);

作用:根据环境变量的名字,找到它对应的值。

比如我们之前在命令行中:

复制代码
echo $PATH

可以查看 PATH。

在 C 程序里面,就可以:

复制代码
getenv("PATH")

获取 PATH 的值。

返回类型:

复制代码
char *

也就是说:返回一个指向环境变量值字符串的字符指针。

例如:

复制代码
char *p = getenv("PATH");

那么:

复制代码
p
↓
"/usr/bin:/bin\0"

因此可以:

复制代码
printf("%s\n", p);

6.环境变量通常是具有全局属性的

环境变量通常具有全局属性,可以被子进程继承下去

例如:

复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *env = getenv("MYENV");

    if(env)
    {
        printf("%s\n", env);
    }

    return 0;
}

直接运行:

复制代码
./test

程序执行:

复制代码
char *env = getenv("MYENV");

但是当前环境中没有:

复制代码
MYENV=hello world

所以:

复制代码
getenv("MYENV")

返回:

复制代码
NULL

于是:

复制代码
if(env)

相当于:

复制代码
if(NULL)

条件不成立,所以:

复制代码
printf("%s\n", env);

根本不会执行,因此没有输出。

执行 export

复制代码
export MYENV="hello world"

现在运行:

复制代码
./test

./test 是 Shell 启动的一个新进程。

关系可以简单画成:

复制代码
Shell进程
   │
   │ 创建/启动
   ↓
 test进程

而 Shell 本身已经有:

复制代码
MYENV=hello world

所以启动 test 时,Shell 会把自己的环境变量传递给新程序。

于是:

复制代码
Shell环境:
MYENV=hello world
       ↓
       ↓ 继承
       ↓
test进程环境:
MYENV=hello world

所以 test 中:

复制代码
getenv("MYENV")

就能找到:

复制代码
hello world

于是输出:

复制代码
hello world

子进程继承的是父进程的环境,但之后它们是相互独立的。

例如:

复制代码
父进程:
MYENV=hello

      ↓ 创建子进程

子进程:
MYENV=hello

如果子进程把自己的环境改成:

复制代码
MYENV=world

一般不会导致父进程变成:

复制代码
MYENV=world

父进程仍然是:

复制代码
MYENV=hello

可以理解为创建子进程时,子进程获得父进程环境的副本。

必须使用 export

假设你只写:

复制代码
MYENV="hello world"

然后:

复制代码
./test

可能还是没有输出。

因为:

复制代码
MYENV="hello world"

只是当前 Shell 的本地 Shell 变量,不一定会放进传给子进程的环境表。

而:

复制代码
export MYENV="hello world"

相当于:

复制代码
Shell变量
   ↓
export
   ↓
环境变量
   ↓
加入环境表
   ↓
子进程可以继承

这就是导出环境变量

二.程序地址空间

1.虚拟地址

示例1:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.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;
}

可能输出:

复制代码
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8

因为 fork() 后,子进程会以父进程为模板,拥有相同的虚拟地址空间布局。

所以父子进程看到的:

复制代码
g_val

对应的虚拟地址都是:

复制代码
0x80497d8

示例2:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int g_val = 0;

int main()
{
    pid_t id = fork();

    if(id < 0)
    {
        perror("fork");
        return 0;
    }
    else if(id == 0)
    {
        // child 子进程
        g_val = 100;

        printf("child[%d]: %d : %p\n",
               getpid(), g_val, &g_val);
    }
    else
    {
        // parent 父进程
        sleep(3);

        printf("parent[%d]: %d : %p\n",
               getpid(), g_val, &g_val);
    }

    sleep(1);

    return 0;
}

可能输出:

复制代码
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

问题来了:

如果:

复制代码
0x80497e8

真的是物理地址,

那么父进程和子进程访问的应该是同一块物理内存。

子进程:

复制代码
g_val = 100;

改了它。

那么父进程再读取:

复制代码
g_val

应该也得到:

复制代码
100

但实际却是:

复制代码
父进程:0
子进程:100

所以: &g_val 打印出来的地址不是物理地址。

这个地址叫:虚拟地址

包括我们平时写 C/C++:

复制代码
int a = 10;

printf("%p", &a);

看到的地址:

复制代码
0x7ffffff...

是:虚拟地址

不是内存条上的真实物理地址。

真正的物理内存可以简单理解成:

复制代码
物理内存
+----------------+
| 真实的内存空间 |
|                |
|                |
+----------------+

但是普通用户程序不能直接随意操作物理地址。

Linux 操作系统负责管理物理内存。

程序看到的是:

复制代码
程序
 ↓
虚拟地址
 ↓
操作系统 + 页表
 ↓
物理地址
 ↓
真正的物理内存

也就是:

复制代码
虚拟地址  ──────→  物理地址

OS必须负责将 虚拟地址转化成 物理地址

为什么 fork() 后一开始还可以共享?

这里就要引出一个非常重要的机制:写时拷贝(Copy-On-Write,COW)

fork() 刚创建子进程的时候,操作系统并不会马上把父进程所有内存完整复制一份。

而是先让父子进程:

复制代码
父进程虚拟地址 ─┐
                ├──→ 同一块物理内存
子进程虚拟地址 ─┘

例如:

复制代码
父进程                 子进程
0x80497e8              0x80497e8
     │                      │
     └────────┬─────────────┘
              ↓
        物理内存 A
        g_val = 0

此时父子进程都没有修改:

复制代码
父:0
子:0

所以完全没问题。


当子进程执行 g_val = 100 呢?

这时候事情发生变化。

子进程要:

复制代码
g_val = 100;

操作系统发现:"这块内存目前和父进程共享,子进程现在要写它。"

于是触发:写时拷贝

操作系统给子进程准备一块新的物理内存:

复制代码
修改之前:

父进程虚拟地址 ──┐
                 ├──→ 物理内存A
子进程虚拟地址 ──┘       g_val=0

修改之后:

复制代码
父进程:
0x80497e8
     │
     ↓
物理内存 A
g_val = 0


子进程:
0x80497e8
     │
     ↓
物理内存 B
g_val = 100

注意:

虚拟地址没变!

仍然都是:

复制代码
0x80497e8

但是:

映射到的物理地址变了!

父:

复制代码
虚拟地址 → 物理地址A

子:

复制代码
虚拟地址 → 物理地址B

这就是同一个变量,地址相同,其实是虚拟地址相同;内容不同,其实是被映射到了不同的物理地址。

2.为什么要设计虚拟地址?

如果没有虚拟地址,所有程序直接使用物理地址:

复制代码
程序A ──→ 物理内存
程序B ──→ 物理内存
程序C ──→ 物理内存

那么不同程序之间很容易互相覆盖、破坏。

有了虚拟地址之后:

复制代码
进程A虚拟地址空间
        ↓
      页表A
        ↓
    物理内存


进程B虚拟地址空间
        ↓
      页表B
        ↓
    物理内存

每个进程都有自己的进程地址空间。

这样可以实现:

  • 进程之间相互隔离
  • 提高内存管理效率
  • 实现虚拟内存
  • 实现写时拷贝
  • 程序不需要知道真实物理地址

3.虚拟内存管理

描述linux下进程的地址空间的所有的信息的结构体是 个mm_struct结构,在每个进程的 mm_struct (内存描述符)。每个进程只有⼀ task_struct 结构中,有⼀个指向该进程的mm_struct结构体指针。

例如:

复制代码
struct task_struct
{
    /* ... */

    struct mm_struct *mm;

    /* ... */
};

这里:

复制代码
struct mm_struct *mm;

是一个指针。

它指向:

复制代码
mm_struct

也就是这个进程对应的虚拟地址空间描述信息。

可以画成:

复制代码
task_struct
┌────────────────────┐
│ PID                │
│ 状态               │
│ 优先级             │
│ ...                │
│                    │
│ mm ────────────────┼────→ mm_struct
└────────────────────┘       │
                             ↓
                      虚拟地址空间

每⼀个进程都会有⾃⼰独⽴的 mm_struct , 这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。

例如:

复制代码
进程A                         进程B

mm_struct A                   mm_struct B
     ↓                             ↓
地址空间A                       地址空间B
     ↓                             ↓
虚拟地址0x1000                 虚拟地址0x1000
     ↓                             ↓
物理内存A                      物理内存B

虽然

复制代码
进程A:0x1000
进程B:0x1000

可能是一样的虚拟地址,但是可以通过不同的页表映射到不同的物理内存。

因此不同进程拥有独立的虚拟地址空间,从而实现进程之间的内存隔离。

4.mm_struct

复制代码
struct mm_struct
{
    struct vm_area_struct *mmap;
    struct rb_root mm_rb;

    unsigned long task_size;

    unsigned long start_code, end_code;
    unsigned long start_data, end_data;

    unsigned long start_brk, brk;
    unsigned long start_stack;

    unsigned long arg_start, arg_end;
    unsigned long env_start, env_end;

    /* ... */
};

mm_struct 就像一张"进程虚拟地址空间的地图"。

它记录了:

复制代码
代码在哪里?
数据在哪里?
堆在哪里?
栈在哪里?
参数在哪里?
环境变量在哪里?
有哪些虚拟内存区域?

例如:

复制代码
unsigned long start_code, end_code;

表示代码段的开始和结束位置。

复制代码
unsigned long start_data, end_data;

表示数据段的开始和结束位置。

复制代码
unsigned long start_brk, brk;

和堆(heap)有关


复制代码
 
复制代码
unsigned long start_stack;

表示栈的起始位置。

复制代码
 
复制代码
unsigned long arg_start, arg_end;

表示:命令行参数区域。

比如:

复制代码
./test hello 123

这里的:

复制代码
hello
123

就是命令行参数。

复制代码
 
复制代码
unsigned long env_start, env_end;

表示:环境变量所在区域。

5.vm_area_struct

linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚 拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进 程快速访问

一个进程的虚拟地址空间不是一整块完全一样的区域。

例如:

复制代码
┌───────────────────┐
│       栈          │
├───────────────────┤
│                   │
│       ...         │
├───────────────────┤
│       堆          │
├───────────────────┤
│       数据段      │
├───────────────────┤
│       代码段      │
└───────────────────┘

代码、数据、堆、栈的:

  • 权限
  • 用途
  • 管理方式

都可能不同。

所以 Linux 使用:

复制代码
struct vm_area_struct

来描述一个独立的虚拟内存区域

简称:VMA(Virtual Memory Area)


一个进程有很多 VMA

比如一个进程:

复制代码
进程A
 │
 ↓
mm_struct
 │
 ├── VMA 1 → 代码段
 │
 ├── VMA 2 → 数据段
 │
 ├── VMA 3 → 堆
 │
 ├── VMA 4 → 共享库
 │
 ├── VMA 5 → 栈
 │
 └── ...

所以:

复制代码
mm_struct
    ↓
管理整个地址空间

vm_area_struct
    ↓
描述其中一个具体的虚拟内存区域

vm_area_struct 里面最重要的两个成员

复制代码
unsigned long vm_start;
unsigned long vm_end;

分别表示:

复制代码
vm_start → 虚拟内存区域开始地址

vm_end   → 虚拟内存区域结束地址

比如:

复制代码
VMA
┌──────────────────────┐
│                      │
│      某个区域         │
│                      │
└──────────────────────┘
↑                      ↑
vm_start              vm_end

所以一个 VMA 本质上就是:从虚拟地址 A 到虚拟地址 B 的这一段区域,它是什么、有什么权限、怎么管理。


vm_nextvm_prev

复制代码
struct vm_area_struct *vm_next;
struct vm_area_struct *vm_prev;

这两个就是:前后指针。

因此可以把多个 VMA 连接起来:

复制代码
VMA1 ←→ VMA2 ←→ VMA3 ←→ VMA4

这就是双向链表。

而:

复制代码
struct vm_area_struct *mmap;

可以指向这个链表。

所以当虚拟区间较少时,可以通过链表管理。

如果一个进程有很多 VMA:

复制代码
VMA1
VMA2
VMA3
...
VMA100
VMA101
...

如果每次寻找某个地址对应哪个 VMA,都从头开始链表查:

复制代码
VMA1 → VMA2 → VMA3 → ...

效率可能比较低。

所以还可以使用:红黑树

复制代码
struct rb_root mm_rb;

就是红黑树的根。

可以简单理解:

复制代码
                 mm_struct
                /         \
             链表          红黑树
              ↓              ↓
        vm_area_struct    vm_area_struct

6.为什么一定需要虚拟地址空间?

如果程序直接操作物理内存,会怎么样?

假设电脑有:

复制代码
128MB物理内存

程序A需要:

复制代码
10MB

程序B需要:

复制代码
110MB

可以分:

复制代码
物理内存
0MB
│
├──────── A 10MB ────────┤
│
├──────── B 110MB ──────────────────────┤
│
└────────────────────────────────────────┘
128MB

看起来能运行。

但是这种方式问题非常大。


第一个问题:安全性

如果程序直接操作物理地址:

复制代码
程序A
  ↓
物理内存

那么理论上程序A可能去访问:

复制代码
程序B的内存

甚至:

复制代码
操作系统内核的内存

那就非常危险。

例如恶意程序:

复制代码
修改其他程序数据
        ↓
修改系统数据
        ↓
系统崩溃

所以必须进行隔离。

有了虚拟地址:

复制代码
进程A
 ↓
虚拟地址空间A
 ↓
页表A
 ↓
物理内存

程序不能随意访问别人的虚拟地址空间。


第二个问题:地址不确定

假设程序里面有:

复制代码
int a = 10;

如果直接使用物理地址。

第一次运行:

复制代码
物理地址:
0x1000

第二次运行,由于内存已经被其他程序占用了:

复制代码
物理地址:
0x5000

那么程序每次运行实际地址都可能不同。

这对程序非常麻烦。


有虚拟地址之后

程序只需要认为:

复制代码
a → 0x1000

每次都可以使用这个虚拟地址。

操作系统再负责:

复制代码
虚拟地址0x1000
        ↓
      页表
        ↓
物理地址可能是0x5000

下一次:

复制代码
虚拟地址0x1000
        ↓
      页表
        ↓
物理地址可能是0x9000

程序完全不需要知道。


第三个问题:提高内存利用率

假设物理内存不够。

没有虚拟内存时:

复制代码
整个进程
     ↓
一起搬到磁盘

这样效率很低。

有虚拟内存和分页机制以后,可以:

复制代码
进程的一部分页面
       ↓
物理内存

暂时不用的页面
       ↓
磁盘

不需要把整个进程一次性搬走。

所以效率更高。

7.malloc 申请的是虚拟空间

在 C/C++ 中使用 mallocnew 申请空间时,本质上是向进程的地址空间申请。

例如:

复制代码
int *p = malloc(100 * sizeof(int));

你可能以为:"操作系统马上给我一块真实物理内存。"

实际上不能简单这么理解。

可以理解成:

复制代码
malloc
 ↓
申请虚拟地址空间
 ↓
操作系统建立/准备相应的内存管理关系

真正访问页面时,如果页面尚未准备好,操作系统可能通过缺页异常等机制分配物理页并建立映射。

这就是延迟分配。


8."进程管理"和"内存管理"解耦

以前如果程序直接使用物理地址:

复制代码
程序
 ↓
直接占用物理内存

程序和物理内存的位置强相关。

有了虚拟地址以后:

复制代码
进程
 ↓
虚拟地址空间
 ↓
页表
 ↓
物理内存

于是:进程只需要关心自己的虚拟地址空间,物理内存由操作系统统一管理。

这就是进程管理模块和内存管理模块解耦。


9.为什么程序看到的地址可以是"连续"的?

这是虚拟地址非常厉害的地方。

例如程序认为:

复制代码
虚拟地址:

0x1000
0x2000
0x3000
0x4000

是连续的。

但实际物理内存可能:

复制代码
虚拟地址       物理地址

0x1000   →     0x8000
0x2000   →     0x2000
0x3000   →     0xF000
0x4000   →     0x5000

物理内存可以是分散的。

但是从进程角度看:

复制代码
0x1000
 ↓
0x2000
 ↓
0x3000
 ↓
0x4000

依然是连续、有序的。

所以在进程视角,所有的内存分布都可以是有序的。

相关推荐
风静如云1 小时前
VMWare:Ubuntu(26.04)共享后看不到
linux
为思念酝酿的痛2 小时前
传输层协议UDP
linux·网络·udp
Elnaij2 小时前
Linux系统与系统编程(16)——日志、线程池的实现与线程安全
linux
杨云龙UP2 小时前
DB2 HADR 主备架构活动日志参数优化操作手册
linux·运维·服务器·数据库·db2·db2 hadr·日志参数调整
csdn_aspnet2 小时前
如何从电脑主机拷东西到VWmare虚拟机
linux·运维·服务器·windows·vmware·虚拟机
Vcaker2 小时前
Linux学习28-Kubernetes service
linux·运维·学习
有梦想的咕噜3 小时前
Newtonsoft.Json (Json.NET) 常用方法汇总
linux·json·.net
码农小韩3 小时前
Linux应用开发(五)——线程
linux·操作系统·linux驱动·嵌入式软件开发·嵌入式操作系统·linux应用
Polevne3 小时前
C# GRPC 一元与双向流
linux·算法·c#