操作系统 :进程概念

操作系统 :进程概念

引言

理解进程的概念不仅有助于我们掌握操作系统的工作原理,还能为后续学习多任务处理、并发编程、内存管理等高级主题奠定坚实的基础。本文将深入探讨进程的方方面面,包括其基本概念、状态管理、优先级调度、地址空间等核心内容,并结合实际示例和代码分析,帮助读者构建完整的知识体系。


目录

  • [操作系统 :进程概念](#操作系统 :进程概念)
    • 引言
    • [1. 冯诺依曼体系结构](#1. 冯诺依曼体系结构)
    • [2. 操作系统概念及定位](#2. 操作系统概念及定位)
    • [3. 进程](#3. 进程)
      • [3.1 进程的基本概念与操作](#3.1 进程的基本概念与操作)
      • [3.2 进程的状态](#3.2 进程的状态)
      • [3.3 进程的优先级](#3.3 进程的优先级)
      • [3.4 进程的切换](#3.4 进程的切换)
    • [4. 环境变量](#4. 环境变量)
      • [4.1 基本概念](#4.1 基本概念)
      • [4.2 常见的环境变量](#4.2 常见的环境变量)
      • [4.3 关于环境变量的命令](#4.3 关于环境变量的命令)
      • [4.4 通过代码获取环境变量](#4.4 通过代码获取环境变量)
      • [4.5 bash的两张表](#4.5 bash的两张表)
    • [5. 程序地址空间](#5. 程序地址空间)
      • [5.1 虚拟地址](#5.1 虚拟地址)
      • [5.2 进程地址空间(重要)](#5.2 进程地址空间(重要))
      • [5.3 虚拟内存管理第一讲](#5.3 虚拟内存管理第一讲)
      • [5.4 为什么要有虚拟内存空间](#5.4 为什么要有虚拟内存空间)

1. 冯诺依曼体系结构

关于冯诺依曼必须要强调的几点:

  • 这里的存储器指的是内存。

  • 不考虑缓存的情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。

  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。

  • 总结:所有设备都只能直接和内存进行交互。

  • 软件运行,必须先加载(CPU执行加载到的代码,访问加载到的数据),而数据是从一个设备"拷贝"到另一个设备的。因此体系结构的效率是由设备的"拷贝"效率决定的

  • 程序运行之前,在磁盘中加载文件。


2. 操作系统概念及定位

  • 概念

操作系统是任何计算机系统都包含的基本程序集合,操作系统也是一款进行软硬件管理的软件。

  • 设计OS的目的

对上,为用户程序提供一个良好的执行环境。

对下,与硬件交互,管理所有的软硬件资源(手段)

小知识:

  • 访问操作系统必须使用系统调用,而系统调用其实就是系统提供的函数。
  • 只要程序访问了硬件,那么它必须贯穿整个软硬件体系结构。
  • 我们所了解的库在底层封装了系统调用。

管理

管理者和被管理者不见面,管理者是根据有中间层获取的"数据"进行管理。

管理方式就是"先描述在组织"------描述被管理的对象,组织被管理对象。

举例:校长管理学生就可以转化为校长对于execl表格中数据的管理。

  • 系统调用和库函数概念

    在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部由操作系统提供的接口,叫做系统调用。可以理解为操作系统要向上提供对应的服务,但是不相信任何用户。

    系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。


3. 进程

3.1 进程的基本概念与操作

  • test_struct

    • 标示符: 描述本进程的唯一标示符,用来区别其他进程。pid

    • 状态: 任务状态,退出代码,退出信号等。

    • 优先级: 相对于其他进程的优先级。

    • 程序计数器: 程序中即将被执行的下一条指令的地址。

    • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

    • 上下文数据: 进程执行时处理器的寄存器中的数据。

    • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

    • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

    • 其他信息

  • 进程的组织形式

    在内核源代码中我们以可以看到,所有运行在系统里的进程都是以test_struct链表的形式存在内核里的。

  • 查看进程

    1. 进程的信息可以通过/proc系统文件夹查看。如:要获取PID为21381的进程信息,你需要查看 /proc/21381 这个文件夹。

      我们可以看到进程信息中有一个cwd(current work dir),其作用是进程会记录下来自己的当前路径,这也就印证了为什么我们在C语言中fopen时,既可以使用绝对路径,也可以直接使用文件名。

    2. 大多数进程信息同样也可以使用top,ps这些用户级工具来获取。

      ps的参数选项:

      • a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
      • x:显⽰没有控制终端的进程,例如后台运行的守护进程。
      • j:显⽰进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
      • u:以⽤⼾为中心的格式显示进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等。
    3. 通过系统调用获取进程标识符

      进程id:pid;父进程id:ppid

      c 复制代码
      #include <stdio.h>
      #include <sys/types.h>
      #include <unistd.h>
      int main()
      {
      	printf("pid: %d\n", getpid());
      	printf("ppid: %d\n", getppid());
      	return 0;
      }

3.2 进程的状态

  • kernel源代码中的进程状态

    c 复制代码
    /*
    *The task state array is a strange "bitmap" of
    *reasons to sleep. Thus "running" is zero, and
    *you can test for combinations of others with
    *simple bit tests.
    */
    static const char *const task_state_array[] = {
    	"R (running)", /*0 */		
    	"S (sleeping)", /*1 */		//可中断休眠,浅睡眠
    	"D (disk sleep)", /*2 */	//不可中断睡眠,深度睡眠(磁盘)
    	"T (stopped)", /*4 */		//进程出错,OS强制暂停
    	"t (tracing stop)", /*8 */	//被debug断点,将进程暂停
    	"X (dead)", /*16 */			//释放PCB的进程
    	"Z (zombie)", /*32 */		//为了获取退出信息,而未释放PCB的进程
    };
    • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
    • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

    运行 :进程在调度队列中,进程的状态都是running

    阻塞:等待某种设备或者资源就绪(键盘,显示器,网卡,磁盘,摄像头,话筒...)。

    进程状态的变化表现之一,就是要在不同的队列中进行流动,本质都是数据结构的增删查改。

  • 僵尸进程

    僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。

    僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

    所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

  • 僵尸进程的危害------内存泄露

    进程的退出状态必须被维持下去,因为他要告诉父进程,你交给我的任务,我办的怎么样了。可父进程如果一直不读取,子进程就会一直处于Z状态。

    维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就一直都要维护。

    如果一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存。

  • 知识点

    进程退出了,内存泄漏问题就不存在了。

    常驻内存的进程所具有的内存泄漏问题,是比较麻烦的。

    内核结构申请中,存在数据结构对象的缓存,是指进程结束后的PCB从调度队列转为垃圾队列中,等待下一次的废物利用。

  • 孤儿进程

    在父子进程关系中,如果父进程先退出,此时子进程就被称为"孤儿进程"。

    孤儿进程会被1号进程(暂且理解为操作系统)领养,防止孤儿进程进一步变成僵尸进程从而导致内存泄露。

    被领养的孤儿进程会变成后台进程,因此使用Ctrl + c是无法结束进程的,且后台进程可以向前台打印消息。


3.3 进程的优先级

  • 概念

    cpu资源分配的先后顺序,就是指进程的优先权(priority),优先权可以解决目标资源短缺需要合理分配的问题

    优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

    优先权也是一种值,值越低,优先级越高。

    还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

    Linux是一种基于时间片的分时操作系统,考虑公平性,优先级可能变化,但是变化的幅度不能太大。

  • 查看系统进程

    在Linux系统中,用ps -l查看系统进程。

    • UID : 代表执行者的身份user id,在Linux中访问任何资源都是进程访问,进程就代表用户。因此系统通过比较UID和文件所属id,就能知道用户是拥有者还是所属组亦或是other。
    • PID : 代表这个进程的代号。
    • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
    • PRI :代表这个进程可被执行的优先级,其值越小越早被执行。(默认值为80)
    • NI :代表这个进程的nice值,其是进程优先级的修正数值(用来更改PRI的)。
  • PRI & NI

    进程的真实优先级 = PRI(默认为80)+ NI

    优先级的极值:如果优先级设置的不合理,会导致优先级低的进程,长时间得不到CPU资源,进而导致进程饥饿。因此nice值只能取到[-20 , 19],那么进程的优先级范围就是[60 , 99]共40个等级。

    top命令更改已经存在进程的nice值:输入top->按r->输入进程PID->输入要修改的nice值

  • 竞争、独立、并行、并发

    竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。

    独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。

    并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。

    并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。


3.4 进程的切换

CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器(CPU内部的临时空间)中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运行, 这⼀过程就是context switch。

死循环进程不会使系统崩溃,因为时间片进程不会一直占有CPU去运行。

进程自己的上下文数据是保存在test_struct中的TSS:任务状态段

  • Linux真实调度算法:O(1)调度算法

    • Linux2.6内核中进程队列的数据结构runqueue------ 一个CPU拥有一个runqueue

      • 时间片还没有结束的所有进程都按照优先级放在活动队列

      • nr_active: 记录总共有多少个运行状态的进程

      • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级![0,99]是实时优先级我们不需要关心,只需关注[100,139]这40个普通优先级即可。

      • 从该结构中,选择⼀个最合适的进程,过程如下:

        1. 从0下表开始遍历queue[140]
        2. 找到第⼀个非空队列,该队列必定为优先级最⾼的队列
        3. 拿到选中队列的第⼀个进程,开始运行,调度完成!
      • bitmap[5]:遍历queue[140]时间复杂度虽然是常数但还是太低效了,因此引入bitmap[5]。⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个比特位表示队列是否为空,这样便可以大大提高查找效率!

      • 过期进程的queue[140]和活跃进程结构一模一样,只不过过期队列上放置的进程都是时间片耗尽的进程。当活动队列的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。

      • *active指针永远指向活动队列;*expired指针永远指向过期队列;由于活动队列上的进程因为时间片的耗尽会在过期队列越积越多,而活动队列的进程会越来越少。这时就需要*active指针和*expired指针交换一下指向的内容即可。

    • 总结:在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!


4. 环境变量

4.1 基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量 通常具有某些特殊用途,还有在系统当中通常具有全局特性,其意味着可以被子进程继承。而本地变量不会被子进程继承,只在bash内部被使用

4.2 常见的环境变量

  • PATH:指定命令的搜索路径
  • HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL:当前Shell,它的值通常是/bin/bash
  • USER:指定当前登录的用户名。
  • LOGNAME:指定当前登录的用户名,但更偏向于 系统登录记录 的原始用户名。因此即使用户通过 susudo 切换身份,LOGNAME 仍保持最初的登录用户名。
  • PWD:指定当前工作目录的路径(由 Shell 动态更新)。
  • OLDPWD:指定上一次所在的工作目录路径(可通过 cd - 切换回去)。
  • HOSTNAME:指定当前系统的主机名(hostname),用于标识网络中的计算机。影响终端提示符、网络通信(如 SSH)、系统日志等场景。
  • HISTSIZE:指定 Shell 历史记录(history 命令)在 内存中 保存的最大命令条数。

4.3 关于环境变量的命令

  • echo:显示某个环境变量值。如echo $VAR_NAME
  • export:设置一个新的环境变量,仅对当前 Shell 及其子进程有效,关闭终端后失效。如export VAR_NAME=value
  • env:显示所有环境变量。
  • unset:删除环境变量。如unset VAR_NAME
  • set:显示所有变量(包括环境变量和Shell本地变量)。
  • 内建命令built-in command:不需要创建子进程,而是让bash自己调用函数或是通过系统调用执行。

4.4 通过代码获取环境变量

  • main函数的第三个参数

    c 复制代码
    #include <stdio.h>
    int main(int argc, char *argv[], char *env[])
    {
    	int i = 0;
    	for(; env[i]; i++){
    		printf("%s\n", env[i]);
    	}
    	return 0;
    }
  • 通过第三方变量environ获取

    c 复制代码
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
    	extern char **environ;//libc中定义的全局变量environ指向环境变量表,
        					//environ没有包含在任何头⽂件中,所以在使⽤时 要⽤extern声明。
    	int i = 0;
    	for(; environ[i]; i++){
    		printf("%s\n", environ[i]);
    	}
    	return 0;
    }
  • 通过系统调用获取

    c 复制代码
    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
    	printf("%s\n", getenv("PATH"));	//可以用来实现一个加密的小程序
    	return 0;
    }

4.5 bash的两张表

  • 每有一个用户登录就会有一个bash,而bash内部会存在两张表。第一张表是命令行参数表char *argv[],第二张表是环境变量表char *env[]

  • 命令行参数表是bash对于输入的命令进行的切分,可以用来分析和实现程序不同子功能(命令参数选项)的方法。

  • 环境变量表是bash最开始从系统的相关配置文件中拷贝过来的,用来帮助用户和操作系统的运行。其是⼀个字符指针数组,每个指针指向⼀个以'\0'结尾的环境字符串。


5. 程序地址空间

程序地址空间并不是真正的内存

5.1 虚拟地址

c 复制代码
#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,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再读取
		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;
}

输出结果:

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

根据上边的代码我们可以看出,父子进程的输出地址是一致的,但是变量内容却不一样。

结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做 虚拟地址。虚拟地址可以相同,但物理地址一定不同。
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
  • OS必须负责将虚拟地址转换成物理地址。

5.2 进程地址空间(重要)

  • 一个进程就会有一个虚拟地址空间(进程地址空间);一个进程就有一套页表,而页表是用来做虚拟地址和物理地址映射的。页表中其实还有一列存储的是内容的访问权限------在地址转换过程中,对你的地址和操作进行合法性判定,进而保护物理内存。
  • 虚拟地址空间本质就是一个数据结构。
  • 其初级作用是为了让每个进程都认为自己有4GB的物理内存,或是让每个进程都认为自己在独占物理内存。
  • 区域划分:对整个虚拟地址空间进行编址,此时地址空间便有了一个个地址。然后只需要记录每块区域的开始地址和结束地址即可。

5.3 虚拟内存管理第一讲

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

c 复制代码
struct task_struct
{
	/*...*/
	struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。
	struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当该进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
	/*...*/
}

可以说,mm_struct结构是对整个用户空间的描述。每⼀个进程都会有自己独立的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_structmm_struct,进程的地址空间的分布情况:

c 复制代码
struct mm_struct
{
	/*...*/
	struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
	struct rb_root mm_rb; /* red_black树 */
	unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
	/*...*/
	// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;
	/*...*/
}

那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:

  1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
  2. 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。

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

c 复制代码
struct vm_area_struct {
	unsigned long vm_start; //虚存区起始
	unsigned long vm_end; //虚存区结束
	struct vm_area_struct *vm_next, *vm_prev; //前后指针
	struct rb_node vm_rb; //红⿊树中的位置
	unsigned long rb_subtree_gap;
	struct mm_struct *vm_mm; //所属的 mm_struct
	pgprot_t vm_page_prot;
	unsigned long vm_flags; //标志位
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;
	struct list_head anon_vma_chain;
	struct anon_vma *anon_vma;
	const struct vm_operations_struct *vm_ops; //vma对应的实际操作
	unsigned long vm_pgoff; //⽂件映射偏移量
	struct file * vm_file; //映射的⽂件
	void * vm_private_data; //私有数据
	atomic_long_t swap_readahead_info;
	#ifndef CONFIG_MMU
	struct vm_region *vm_region; /* NOMMU mapping region */
	#endif
	#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy; /* NUMA policy for the VMA */
	#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

5.4 为什么要有虚拟内存空间

亦或是如果程序直接可以操作物理内存会造成什么问题?

在早期的计算机中,要运行⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。

那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存大小是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B。

这种分配方法可以保证程序A和程序B都能运行,但是这种简单的内存分配策略问题很多。

比如:

  • 安全⻛险:

    每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内存区域,如果是⼀个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。

  • 地址不确定:

    众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷⻉的实际内存地址每⼀次运⾏都是不确定的,⽐如:第一次执⾏a.out时候,内存当中⼀个进程都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经有10个进程在运行了,那执⾏a.out的时候,内存地址就不⼀定了。

  • 效率低下:

    如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷贝时间太⻓,效率低。

有了虚拟地址空间和分⻚机制就能解决了

地址空间和⻚表是OS创建并维护的!也就意味着,凡是想使用地址空间和⻚表进⾏映射,也⼀定要在OS的监管之下来进⾏访问!!也顺便保护了物理内存中的所有的合法数据 ,包括各个进程以及内核的相关有效数据。

因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置的加载!物理内存的分配 和 进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合

因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址空间上申请的,物理内存甚⾄可以⼀个字节都不给你。而当你真正进⾏对物理地址空间访问的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这是由操作系统⾃动完成,用户包括进程完全零感知!!

因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射,在 进程视⻆所有的内存分布都可以是有序 的。

相关推荐
多多*3 小时前
Java设计模式 简单工厂模式 工厂方法模式 抽象工厂模式 模版工厂模式 模式对比
java·linux·运维·服务器·stm32·单片机·嵌入式硬件
Qwertyuiop20164 小时前
搭建开源笔记平台:outline
笔记·开源
南鸳6104 小时前
Linux常见操作命令(2)
linux·运维·服务器
fengwuJ5 小时前
Linux安装Idea
linux·ubuntu·debian·intellij-idea
西北大程序猿5 小时前
linux进程信号 ─── linux第27课
linux·运维·服务器·信号处理
涛ing5 小时前
【Git “fetch“ 命令详解】
linux·c语言·c++·人工智能·git·vscode·svn
想躺在地上晒成地瓜干6 小时前
树莓派超全系列文档--(18)树莓派配置音频
linux·音视频·树莓派·raspberrypi·树莓派教程
默凉6 小时前
win 远程 ubuntu 服务器 安装图形界面
ubuntu
Vic·Tory6 小时前
Go语言学习笔记
笔记·学习·golang
Small踢倒coffee_氕氘氚6 小时前
Python实现3D贴图渲染:解锁数字艺术新维度
经验分享·笔记