🔥个人主页 :Quitecoder
🔥专栏:linux笔记仓
目录
01.进程地址空间
c
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<stdlib.h>
6
7 int g_val=100;
8
9 int main()
10 {
11 printf("father is running,pid:%d,ppid:%d\n",getpid(),getppid());
12
13 pid_t id=fork();
14 if(id==0)
15 {
16 while(1)
17 {
18 printf("I am child process,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
19 }
20 }
21 else
22 {
23 while(1)
24 {
25 printf("I am father process,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
26 }
sleep(1);
27 }
28 return 0;
29 }
我们发现,输出出来的变量值和地址是一模一样的,很好理解,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改
现在对代码进行修改
改变子进程中的变量值再输出结果:
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
OS必须负责将 虚拟地址 转化成 物理地址 。
分页和虚拟地址空间
地址空间的本质就是内核中的一个结构体对象,子进程会把父进程的很多内核数据结构全拷贝一份(浅拷贝),当子进程尝试对变量进行修改时,我在物理内存重新开辟一块空间,新的物理地址放到页表当中,重新构建映射
在虚拟内存系统中,每个进程都拥有一块连续的虚拟地址空间,这块空间由操作系统管理,对进程来说,它看起来像是独占的内存。虚拟地址不直接对应物理内存中的实际位置,而是通过一系列的映射过程转换成物理地址
页表是实现虚拟地址到物理地址映射的数据结构。操作系统将虚拟内存分割成固定大小的块,称为"页"(pages),物理内存也被分割成同样大小的"页帧"(page frames)。页表存储着虚拟页和相应物理页帧之间的映射信息。
- 页表项(Page Table Entry, PTE):每个页表项包含对应物理页帧的信息,以及一些状态位(如有效位、修改位、访问位等)
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
内存管理中的写时拷贝
写时拷贝(Copy-On-Write,简称 COW)是一种优化策略,用于进程管理和内存管理中,以减少数据复制的需要,节省资源并提高效率
在操作系统中,写时拷贝主要用于实现 fork()
系统调用时的内存效率优化。当一个进程调用 fork()
创建子进程时,操作系统原本需要复制整个进程的地址空间到子进程中 。然而,通过使用写时拷贝技术,子进程最初会共享父进程的地址空间中的所有页,而不是物理上复制它们。
- 共享内存页 :在
fork()
后,父进程和子进程会共享同一物理内存页,每个页表项被标记为只读。 - 修改触发拷贝 :如果父进程或子进程尝试写入某个共享页,CPU的内存管理单元(MMU)会触发一个保护页异常(page fault),操作系统响应这个异常,进行真正的物理拷贝。这样,只有在需要写入时,才会为修改的页分配新的物理内存,从而避免不必要的数据复制。
02.理解地址空间
地址空间划分
在操作系统的地址空间管理中,地址空间被划分为几个区域,以组织不同类型的数据和代码。这些区域的划分是为了提高内存的管理效率、安全性和程序的运行性能。以下是典型的地址空间中的主要区域:
-
代码段(Text Segment)
代码段,也称为文本段,是地址空间中存储程序的可执行代码的区域。它通常是只读的,以防止程序代码在运行时被意外或恶意修改。只读属性也有助于保护操作系统和用户程序的安全。
-
数据段(Data Segment)
数据段用于存储程序中的全局变量和静态变量。这个区域可以进一步细分为已初始化数据段和未初始化数据段(BSS段):
- 已初始化数据段:存放程序中明确赋了初值的全局和静态变量。
- 未初始化数据段(BSS):用于存储程序中未初始化的全局变量和静态变量。在程序启动时,操作系统通常将此区域清零。
-
堆(Heap)
堆区用于动态内存分配。程序运行时,如需分配额外内存(例如,通过
malloc
,new
等函数),这些内存块将从堆上分配。堆的大小不是静态的,它会根据程序的需求动态增长和缩减。堆通常从低地址向高地址增长。 -
栈(Stack)
栈区用于支持函数调用。每次函数调用时,返回地址、参数、局部变量等都会被推送到栈上。每当函数返回时,这些数据会被弹出。栈是自顶向下增长的数据结构,它通常从高地址向低地址增长。由于栈的大小有限,过深的递归或大量的局部变量可能导致栈溢出。
-
内核区
在用户模式和内核模式的系统中,内核区是专门为操作系统内核保留的地址空间。这部分通常包含内核代码和数据,是保护模式下不允许用户程序访问的。
这些区域的划分允许操作系统更有效地管理不同类型的数据和代码,确保它们正确、高效地运行,并保护程序数据不被非法访问或破坏。通过精确控制这些区域的访问权限(如只读、执行、读写),操作系统提高了整个系统的稳定性和安全性。
地址空间本质是内核的一个struct结构体!内部很多的属性都是表示start , end的范围
理解地址空间的概念涉及到对现代操作系统中如何处理和隔离不同程序和进程的内存资源的基本认识。地址空间基本上是一个抽象的概念,用来表示为一个特定的进程分配的所有可用内存,包括代码、数据、堆和栈等。这里是一些核心点来帮助更好地理解地址空间:
- 虚拟内存与物理内存的区别
- 虚拟内存 :对程序来说,它通过虚拟内存进行操作。每个进程都有自己独立的虚拟地址空间,这个空间是连续的,由操作系统通过页表来映射到实际的物理内存上。虚拟内存抽象层允许每个进程操作好像它拥有全部内存的错觉。
- 物理内存:实际的硬件内存。虚拟地址空间中的地址通过内存管理单元(MMU)映射到物理内存地址。
- 地址空间的作用
- 隔离性 :每个进程有自己的虚拟地址空间,其他进程不能直接访问。这样提高了系统的稳定性和安全性,因为错误或恶意的内存访问不会影响其他进程。
- 安全性:操作系统可以设定不同区域(如代码段、数据段)的访问权限,防止程序非法修改代码或数据。
- 灵活性:虚拟内存系统使得应用程序能够使用比实际物理内存更多的地址空间,通过技术如分页和交换(swapping),扩展了内存的使用。
- 管理和优化
- 分页系统:虚拟内存通常被分割为多个固定大小的页,这些页独立地映射到物理内存的页框中。这种方法简化了内存管理,并可以有效地使用磁盘作为虚拟内存的扩展。
- 写时拷贝 :这是一种优化技术,常用于
fork()
系统调用中。父进程和子进程最初共享相同的物理内存页,仅当其中一个进程尝试修改页时,操作系统才会为该进程创建这个页的副本。
- 实际应用
在程序编写时,开发者不需要处理地址空间的具体细节,这些都由操作系统和编译器自动处理。程序员主要关注的是如何高效地使用内存,例如通过优化数据结构和算法来减少内存的需求和提高缓存的利用率。
地址空间是每个进程独立享有的虚拟内存布局,它包括了程序执行所需的所有类型的内存区域。通过操作系统的内存管理机制,如页表和内存管理单元(MMU),虚拟地址被映射到物理地址,从而实现虚拟内存的抽象。这不仅保证了操作系统的灵活性和应用程序的安全性,还提高了内存使用的效率和程序的可扩展性。
实际的物理内存中,代码区数据区,堆区,栈区,共享区,命令行参数和环境变量是无序的,那么地址空间的第一个作用,就是将无序变成有序,让进程以统一的视角看待物理内存及自己运行的各个区域
虚拟内存技术允许每个进程使用的内存超过实际的物理内存容量。地址空间的使用使得操作系统可以有效地管理内存,将不活跃的页交换到磁盘,将频繁使用的页保持在快速的物理内存中。这种灵活的内存管理策略使得更多的应用能够同时运行,而不受物理内存大小的直接限制
地址空间为每个进程提供了一个独立的内存视图,确保一个进程无法直接读取或修改另一个进程的内存。这种隔离保护了系统的稳定性,防止了错误或恶意的进程干扰其他进程。如果没有地址空间的隔离,一个进程的崩溃可能导致整个系统的崩溃
所有非法访问都不能通过虚拟地址空间访问到物理内存,对物理内存起到保护作用
页表当中每一个条目,有标记位等更多细节
写时拷贝工作机制:
-
共享页 :在 fork() 之后,父进程和子进程的页表都指向相同的物理内存页,并标记为只读。这避免了对内存的实际复制。
-
页面修改检测 :如果父或子进程想要写入某个页面时,写入操作试图改变只读页面会导致页错误(Page Fault)。操作系统截获该错误,将该页面的当前内容复制到新的内存框架中,并更新相应的页表,使该页对于执行写入操作的进程变为可写(同时保持原页面对另一个进程为共享状态)。
-
再写时实际复制 :通过这种机制,只有在页面实际被修改时才从共享复制,这就是"写时拷贝"名称的由来。
程序内部使用的地址都是基于虚拟地址空间,页表负责将这些地址实时映射到实际的物理内存地址,为程序的正确执行提供支撑
03.Linux2.6内核进程调度队列
前面提到的nice值范围在[-20,19]
在 Linux 2.6 内核中,进程调度得到了很大的改进,以提高系统的效率、响应性和可扩展性。Linux 2.6 使用了一种称为 Ø(1)调度器 的调度算法,这种算法通过使用多个调度队列来达到高效调度。以下是对这些调度队列及相关机制的详细解释:
Ø(1)调度器概述
-
设计目标
- Ø(1)调度器旨在提供恒定时间复杂度的进程调度算法,即在最坏情况下,调度决策的计算时间不随系统中进程数量的增加而增加。
- 提高系统响应性,特别是实时性和交互性任务的优先级处理。
-
优先级队列
- 每个 CPU 维护两个优先级数组,每个数组包含 140 个(0-139)给定优先级的链表:
- 活动队列(active array):存放当前的可调度进程。
- 过期队列(expired array):存放时间片用完的进程。
- 优先级低(139)给实时性进程,优先级高(0)给交互式进程。0 到 99 是实时优先级,100 到 139 是普通进程优先级。
- 每个 CPU 维护两个优先级数组,每个数组包含 140 个(0-139)给定优先级的链表:
-
调度过程
- 当一个进程的时间片用尽时,它被移到过期队列,并重新分配一个新的时间片,这个时间片通常根据进程的动态优先级计算。
- 当活动队列中没有可运行的进程时,活动和过期队列会被交换(只是指针交换,不是实际数据移动),从而避免了在复杂和长时间的进程调度中进行长时间的进程切换。
- 优先考虑在活动队列中优先级最高的进程来运行。
-
可扩展性
- Ø(1)调度器的设计使得它能够高效管理大量的进程,而不会因为进程数量增加而导致调度器性能下降。
- 多 CPU 环境中,通过每个 CPU 维护独有的调度数据结构来减少竞争条件。
活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,
数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
一个只出不进,一个只进不出
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 但是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
优先级计算和动态调整
- 实时优先级(0-99)通常不由调度器动态调整。
- 普通进程优先级(100-139)是根据进程的行为(如是否等待IO操作、交互操作频繁等)动态调整的。系统会奖励交互式任务较高的优先级,而使得计算密集型任务可能降低优先级。
多核和多处理器支持
- 每CPU调度域:每个 CPU 维护自己的运行队列,减少 CPU 之间的锁竞争。
- 负载均衡:通过偶尔检查和重新分配进程以确保均衡负载分配在所有可用 CPU 上。