这里写目录标题
- 冯·诺依曼体系结构
- 操作系统(Operator System)
- 1.概念
- 2.目的
- 3.管理
- 4.系统调用和库函数概念
- 进程
- 1.概念
- 2.描述进程-PCB
- 3.查看进程
- 4.通过系统调用获取进程标示符
- 5.通过系统调用创建进程-fork
- 进程状态
- 1.Linux内核源代码
- 2.进程状态查看
- 进程优先级
- 1.基本概念
- 2.查看系统进程优先级
- 3.PRI和NI
- 环境变量
- 1.环境变量的基本概念
- 2.常见环境变量及查看环境变量方法
- 3.环境变量相关的命令及组织方式
- 4.通过代码获取环境变量
- 进程地址空间
- 1.内存分布
- 2.什么是进程地址空间
- 3.为什么要有地址空间
- 4.页表
- 5.理解进程地址空间
- 总结
冯·诺依曼体系结构
冯·诺依曼体系结构(Von Neumann Architecture),又称为普林斯顿体系结构,是一种计算机系统的基本设计范式,由数学家冯·诺依曼于1945年提出。这一体系结构成为现代计算机体系结构的基础,并在计算机发展史上具有重要意义。它的主要特点是将计算机的程序和数据存储在同一存储器中,并使用存储程序的概念,使得计算机可以根据指令序列自动执行程序。
冯·诺依曼体系结构包含以下关键组成部分:
- 中央处理单元(Central Processing Unit,CPU):CPU是计算机的核心,负责执行指令、进行算术和逻辑运算。它包括算术逻辑单元(Arithmetic Logic Unit,ALU)和控制单元(Control Unit)。ALU执行各种算术和逻辑运算,而控制单元负责从内存中取指令、解析指令并控制计算机的其他部件执行指令。
- 存储器(Memory):存储器用于存放程序和数据。冯·诺依曼体系结构使用统一的存储器空间,即指令和数据都存储在同一个存储器中。这使得程序可以像数据一样被读取和处理。
- 输入/输出设备(Input/Output,I/O):I/O设备用于将数据和信息输入计算机或将计算机处理的结果输出到外部世界。这些设备可以是键盘、鼠标、显示器、打印机等。
- 控制流(Control Flow):计算机按照存储在存储器中的指令序列顺序执行。控制单元从存储器中读取指令,并根据指令类型执行适当的操作。指令中的条件分支和循环等控制结构使得程序可以根据条件和需要来改变执行流程。
- 存储程序(StoredProgram):冯·诺依曼体系结构中的重要概念之一是存储程序。程序以二进制指令的形式存储在存储器中,可以被顺序执行,也可以根据条件或跳转指令进行非顺序执行。
冯·诺依曼体系结构的优点在于其简单和通用性。由于程序和数据存储在同一存储器中,程序可以自我修改和操作,这使得计算机具有高度的灵活性和可编程性。这一体系结构为现代计算机的发展奠定了基础,并成为绝大多数通用计算机系统的基本结构。然而,随着技术的发展,也出现了一些其他类型的体系结构,如并行计算、向量处理和现代超大规模集成电路(Very Large Scale Integration, VLSI)技术所采用的结构。
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:
- 输入单元:包括键盘, 鼠标,摄像头,话筒,扫描仪, 写板,磁盘,网卡等
- 中央处理器(CPU):含有运算器和控制器等
- 输出单元:显示器,打印机,音响,磁盘,网卡等
- 存储器:内存
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 所有设备都只能直接和内存打交道。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,从上面的硬件组成我们可以知道有些设备既可以做输入设备,又可以做输出设备。比如,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程是怎么样的呢?
首先当然是从键盘输入数据,经过内存,再由内存向cpu发出请求,cpu向内存允许此步操作同时发出指令调用网卡,网卡再通过网线发送数据到聊天软件的云服务器,云服务器发送消息到你朋友电脑上,由网卡进行接收,网卡向内存响应,内存向cpu发出请求,cpu向允许此步操作同时内存发出指令调用显示器,输出消息。
虽然这里表述没有特别详细,但是大概的系统调用的结构,也就是冯·诺依曼体系结构就是这么回事,下面我们再来了解操作系统。
操作系统(Operator System)
1.概念
操作系统是一种软件,是计算机系统中最基本和最重要的系统软件之一。它是连接计算机硬件和应用软件之间的桥梁,负责管理和协调计算机的各种资源,为用户和应用程序提供服务,使得计算机能够高效地运行和执行任务。
操作系统的主要功能包括以下几个方面:
- 资源管理:操作系统管理计算机的硬件资源,包括中央处理器(CPU)、内存(RAM)、硬盘、输入/输出设备等。它决定哪个程序可以占用CPU的时间,将内存分配给不同的应用程序,并控制外部设备的访问。
- 进程管理:操作系统管理计算机运行的进程。进程是指在计算机上运行的程序的实例。操作系统负责创建、终止、暂停、恢复进程,并在它们之间进行切换,以实现多任务处理。
- 内存管理:操作系统负责将内存空间分配给不同的进程,并管理内存中的数据和指令。它实现虚拟内存技术,将部分程序和数据存储在辅助存储设备(如硬盘)上,以扩展可用的内存空间。
- 文件系统管理:操作系统管理计算机的文件系统,包括文件的创建、删除、读取和写入。它通过文件系统提供对数据的持久性存储和访问,使得用户可以保存和获取数据。
- 设备驱动程序:操作系统提供设备驱动程序,用于控制和管理硬件设备。这些驱动程序使得操作系统能够与不同类型的硬件设备进行通信。
- 用户界面:**操作系统提供用户界面,使得用户可以与计算机进行交互。**常见的用户界面包括命令行界面和图形用户界面(GUI)。
操作系统还有一些其他重要功能,例如网络管理、安全管理、调度算法等,这些功能使得计算机能够高效地运行多个任务,提高资源利用率,确保数据的安全性和完整性。
不同类型的计算机和设备都需要适配不同的操作系统。目前,市场上最常见的操作系统包括Windows、macOS、Linux等。每种操作系统都有其独特的特点和优势,可以满足不同用户和应用程序的需求。
2.目的
设计操作系统的主要目的是为了管理计算机的硬件资源并提供一个方便、高效、安全和稳定的运行环境,使得计算机能够有效地执行用户程序和应用软件。以下是设计操作系统的主要目的:
- 资源管理:操作系统负责管理计算机的硬件资源,包括中央处理器(CPU)、内存(RAM)、硬盘、输入/输出设备等。它决定如何分配这些资源给不同的应用程序和进程,以实现多任务处理和资源利用的最大化。
- 多任务处理:现代计算机需要能够同时运行多个任务,例如同时运行多个应用程序或处理多个用户请求。操作系统通过进程管理和调度算法,使得多个任务可以在CPU上交替执行,给用户带来了更好的响应和效率。
- 内存管理:操作系统负责将内存空间分配给不同的程序和进程,并在需要时进行内存交换(虚拟内存),以扩展可用的内存空间。这样可以更好地利用内存资源,使得计算机可以处理更大规模的任务。
- 文件系统管理:操作系统提供文件系统来管理数据的持久性存储和访问。它允许用户创建、读取、写入和删除文件,并确保数据的安全性和完整性。
- 用户界面:操作系统提供用户界面,使得用户可以与计算机进行交互,输入指令并获得计算机的输出。这可以是命令行界面或图形用户界面(GUI),提供了友好的方式来操作计算机。
- 安全性:操作系统需要保护计算机系统和用户数据的安全。它通过访问控制、身份验证和权限管理等方式来确保只有授权用户可以访问特定的资源和数据。
- 错误处理:操作系统需要具备一定的错误处理能力,能够检测和处理硬件和软件出现的异常情况,防止系统崩溃和数据损坏。
- 稳定性和可靠性:设计操作系统时需要追求稳定性和可靠性,使得系统能够长时间稳定运行,不容易出现崩溃和故障。
总的来说,设计操作系统的目的是为了让计算机系统能够高效、安全地运行各种应用程序,提供用户友好的交互界面,并最大限度地利用计算机硬件资源,以满足用户的需求并提高计算机系统的整体性能。
3.管理
在实际的操作系统中,为了更高效地管理硬件资源,通常会使用链表或其他高效的数据结构组织struct结构体灵活地添加、删除和修改数据项,非常适合用于管理动态变化的硬件资源。
以下是一个示例,展示操作系统如何使用链表组织struct结构体来管理CPU、内存和硬盘资源:
#include <stdio.h>
#include <stdlib.h>
// 定义CPU的结构体
struct CPU {
int id; // CPU编号
int cores; // CPU核心数
double clock; // CPU主频
// 其他CPU相关的属性和方法
};
// 定义内存的结构体
struct Memory {
int id; // 内存编号
int size; // 内存大小(单位:字节)
// 其他内存相关的属性和方法
};
// 定义硬盘的结构体
struct Disk {
int id; // 硬盘编号
int size; // 硬盘大小(单位:字节)
// 其他硬盘相关的属性和方法
};
// 定义操作系统管理的硬件资源链表节点
struct HardwareNode {
struct CPU cpu;
struct Memory memory;
struct Disk disk;
struct HardwareNode* next; // 指向下一个节点的指针
};
// 定义操作系统的结构体
struct OperatingSystem {
struct HardwareNode* hardwareList; // 硬件资源链表的头节点
// 其他操作系统相关的属性和方法
};
// 初始化操作系统硬件资源链表
void initHardwareList(struct OperatingSystem *os) {
os->hardwareList = NULL; // 初始化为空链表
}
// 添加硬件资源节点到链表
void addHardwareNode(struct OperatingSystem *os, struct HardwareNode* node) {
if (os->hardwareList == NULL) {
os->hardwareList = node;
node->next = NULL;
} else {
node->next = os->hardwareList;
os->hardwareList = node;
}
}
// 使用链表中的硬件资源进行任务处理
void processTasks(struct OperatingSystem *os) {
// 遍历链表,处理每个硬件资源节点中的任务
struct HardwareNode* currentNode = os->hardwareList;
while (currentNode != NULL) {
// 处理当前节点中的任务
// ...
currentNode = currentNode->next; // 移动到下一个节点
}
}
// 释放链表资源,避免内存泄漏
void freeHardwareList(struct OperatingSystem *os) {
struct HardwareNode* currentNode = os->hardwareList;
while (currentNode != NULL) {
struct HardwareNode* temp = currentNode;
currentNode = currentNode->next;
free(temp); // 释放当前节点
}
os->hardwareList = NULL; // 确保链表头指针为空
}
int main() {
struct OperatingSystem os;
initHardwareList(&os);
// 创建CPU、内存、硬盘节点并添加到链表中
struct HardwareNode* cpuNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
cpuNode->cpu.id = 1;
cpuNode->cpu.cores = 4;
cpuNode->cpu.clock = 2.6;
addHardwareNode(&os, cpuNode);
struct HardwareNode* memoryNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
memoryNode->memory.id = 1;
memoryNode->memory.size = 8192; // 8 GB
addHardwareNode(&os, memoryNode);
struct HardwareNode* diskNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
diskNode->disk.id = 1;
diskNode->disk.size = 102400; // 100 GB
addHardwareNode(&os, diskNode);
// 使用链表中的硬件资源进行任务处理
processTasks(&os);
// 释放链表资源,避免内存泄漏
freeHardwareList(&os);
return 0;
}
在上述示例中,我们使用了一个硬件资源链表,其中每个链表节点都包含一个CPU结构体、一个Memory结构体和一个Disk结构体,分别表示不同的硬件资源。通过链表,操作系统可以动态地添加和管理硬件资源,更加灵活高效地处理任务。同时,在程序结束时,通过释放链表资源,避免了内存泄漏问题。实际的操作系统会更复杂和完善,但这个示例演示了如何使用链表组织struct结构体来管理硬件资源。
4.系统调用和库函数概念
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程
进程是计算机中运行的程序的实例。它是操作系统进行资源分配和调度的基本单位,是计算机系统中最基本的执行单元之一。
1.概念
- 程序:程序是一组指令的有序集合,用于完成特定的任务或解决特定的问题。它通常存储在磁盘等存储介质上,是静态的。
- 进程:进程是程序的一次执行过程。当程序被加载到内存中并开始执行时,就会形成一个进程。进程是动态的,具有运行状态、内存空间、寄存器集合、程序计数器等信息。
- 多进程:操作系统支持同时运行多个进程。多进程使得计算机可以同时执行多个任务,每个任务运行在独立的进程中,相互之间互不干扰。
- 进程状态:进程可以处于多种状态,如运行态、就绪态、阻塞态等。运行态表示进程正在执行,就绪态表示进程已准备好运行但还未被调度,阻塞态表示进程由于某种原因(如等待输入/输出完成)而暂时无法执行。
- 进程控制块(PCB):每个进程都有对应的数据结构,称为进程控制块。PCB包含了进程的所有信息,如进程状态、程序计数器、寄存器内容、内存分配、优先级等。操作系统通过PCB来管理和控制进程的执行。
- 进程间通信(IPC):不同的进程可能需要相互通信和共享数据。为了实现进程间的交互,操作系统提供了各种进程间通信机制,如管道、消息队列、共享内存等。
- 进程调度:多个进程争夺CPU时间执行,操作系统需要进行进程调度,决定当前应该执行哪个进程。进程调度算法的设计影响着系统的响应性能和资源利用率。
总结:
进程是程序的一次执行过程,是操作系统资源管理的基本单位。多进程使得计算机可以同时运行多个任务。每个进程有其特有的状态、PCB和执行信息,操作系统通过进程调度来决定如何分配CPU时间和内存。进程间通信允许不同进程之间进行数据交互。进程的概念是操作系统中理解多任务处理和资源管理的重要基础。
2.描述进程-PCB
PCB(Process Control Block)是一种数据结构,用于表示操作系统中的进程。每个进程都有对应的PCB,PCB包含了与该进程相关的所有信息,以便操作系统能够管理和控制进程的执行。PCB通常存储在内核的地址空间中,而不是进程的用户空间。
PCB中包含的信息可以因操作系统的设计而异,但通常包括以下一些关键信息:
- 进程ID(PID):唯一标识操作系统中每个进程的整数值,用于在系统中识别和管理进程。
- 进程状态:表示进程当前的执行状态,常见的状态有运行态(正在执行)、就绪态(准备执行但还未被调度)、阻塞态(等待某事件完成,如输入/输出)等。
- 寄存器集合:保存了进程在被切换出运行时的寄存器内容,包括程序计数器(PC)等。
- 进程优先级:用于调度算法中,表示进程被分配CPU时间的优先级。
- 内存管理信息:包括进程的内存分配情况、页表信息等。
- 文件描述符表:记录了进程打开的文件和管道等的状态和信息。
- 进程统计信息:如运行时间、累计CPU时间、IO操作次数等。
PCB 中包含了进程的状态、标识信息、资源管理信息、上下文信息、调度信息、进程间通信信息等。它是操作系统进行资源分配和调度的基本单位,是操作系统了解和管理进程的关键数据结构。
每当操作系统进行进程切换时,会保存当前进程的上下文信息到其 PCB 中,然后加载下一个进程的上下文信息到 CPU 寄存器中,使得下一个进程能够继续执行。
在Linux操作系统中,进程控制块(PCB)使用的数据结构是task_struct。task_struct是Linux内核中表示进程的数据结构,定义在头文件<linux/sched.h>中。task_struct包含了大量的信息,用于管理进程的所有方面。
在task_struct中,包含了上述提到的一些关键信息,同时还包含了与Linux进程管理相关的其他信息,如调度信息、信号处理信息、进程所属的用户和组等。由于Linux是开源操作系统,task_struct的结构非常复杂,并且在不同版本的内核中可能会有所不同。
总的来说,PCB(或task_struct)是操作系统用来管理进程的重要数据结构,它会被装载到RAM(内存)里且包含了与进程相关的所有信息,帮助操作系统管理进程的执行和资源分配。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
注:
- 在计算机体系结构中,CPU(中央处理器)中的PC(程序计数器)指向当前正在执行的指令的地址。
- PC是一个特殊的寄存器,它用于存储即将被执行的指令的地址。PC在CPU的运行过程中起着至关重要的作用。当CPU执行一个指令时,它会从PC指向的地址处读取该指令,并根据指令的操作码和操作数执行相应的操作。执行完当前指令后,PC会自动递增,指向下一条即将执行的指令的地址,以便继续执行下一条指令。这样,CPU可以顺序地执行一系列指令,从而完成程序的执行过程。
- PC的值在指令执行期间不断变化,使得CPU可以按照程序的顺序依次执行指令,实现程序的流程控制。在条件分支和循环等控制结构中,PC的值会根据指令执行的结果来修改,从而跳转到不同的指令地址,实现程序的跳转和转移。
- PC的保存和切换是实现多任务处理的关键。在多任务操作系统中,每个进程都有自己的PC值。当操作系统进行进程切换时,会将当前进程的PC保存到其进程控制块(PCB)中,然后将下一个进程的PC加载到CPU的PC寄存器中,以便继续执行该进程的指令。
- 总之,CPU中的PC指针是用于存储当前正在执行的指令的地址,并且它在指令执行过程中不断变化,控制着程序的流程和执行顺序。
3.查看进程
进程的信息可以通过ls /proc
系统文件夹查看
如:要获取PID为1的进程信息,你需要查看/proc/1
这个文件夹。
大多数进程信息同样可以使用top
和ps
这些用户级工具来获取
top
top经常用来监控Linux的系统状况,是常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况。
ps
但是ps命令通常我们不这么使用,首先建立下面的一段死循环代码,方便我们查看该进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
生成可执行程序后我们执行,同时输入命令ps axj | head -1 && ps ajx | grep 'test'
4.通过系统调用获取进程标示符
首先我们要了解两个系统调用函数getpid()
和getppid()
,通过man工具我们可以看到以下定义:
函数所需头文件为<sys/types.h>
和<unistd.h>
getpid()
返回进程ID,即PID
getppid()
返回父进程ID,即PPID
接下来我们调用这两个函数
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 while(1){
7 printf("pid: %d\n", getpid());
8 printf("ppid: %d\n", getppid());
9 sleep(1);
10 }
11 return 0;
12 }
我们在程序运行时输入指令ls /proc/PID(PPID) -al
查看进程信息
子进程
父进程
我们可以看到,父进程是bash,也就是Linux系统的shell程序,如果我们输入命令kill -9 PID
杀死进程,父进程并不会受影响,但是输入命令kill -9 PPID
,杀死父进程也就是shell进程,不但子进程直接被杀死,且将会导致整个所有系统指令瘫痪,使用不了。
碰到意外将shell进程杀死的情况不用慌张,我们只需重新对服务器进行连接就ok了,而且我们可以看到,右侧窗口杀死的shell进程并不会影响左侧窗口,这是因为Linux系统在建立不同的连接时,会创建不同的shell进程,所以两个连接互不干扰。我们多建立几个连接后查询bash进程,就会看到多个bash进程在运行,且为不同PID。
命令ps axj | head -1 && ps ajx | grep 'bash'
5.通过系统调用创建进程-fork
首先我们来了解一下fork函数
下面我们来看这段神奇的代码:
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 int ret = fork();
7 if(ret < 0){
8 perror("fork");
9 return 1;
10 }
11 else if(ret == 0){ //child
12 printf("I am child : %d!, ret: %d\n", getpid(), ret);
13 }
14 else{ //father
15 printf("I am father : %d!, ret: %d\n", getpid(), ret);
16 }
17 sleep(1);
18 return 0;
19 }
我们在学习C/C++语言的时候,都知道函数只能返回一个值,但是fork函数却可以有两个,我们看下面的运行结果
命令while :; do ps axj | head -1 && ps ajx | grep test | grep -v grep;sleep 1;echo "--------------------------";done
那么这是为什么呢,这是因为在执行fork函数时,建立了子进程,也就是说这里同时执行了两个相同的进程,且为父子关系,子进程和父进程共享代码,数据各自开辟空间,具体要在后面我们讲到虚拟地址才能更好的解释。
进程状态
1.Linux内核源代码
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- Z僵尸状态(zombie):僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
2.进程状态查看
R运行状态
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 while(1)
7 {
8
9 }
10 return 0;
11 }
S睡眠状态
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 while(1)
7 {
8 sleep(1);
9 }
10 return 0;
11 }
T停止状态(stopped)
代码同上,运行时我们输入暂停进程命令kill -19 PID
,再恢复进程kill -18 PID
Z僵尸状态
首先我们来一个创建维持30秒的僵死进程例子:
1 #include <stdio.h>
2 #include <stdlib.h>
3 int main()
4 {
5 pid_t id = fork();
6 if(id < 0){
7 perror("fork");
8 return 1;
9 }
10 else if(id > 0){ //parent
11 printf("parent[%d] is sleeping...\n", getpid());
12 sleep(30);
13 }else{
14 printf("child[%d] is begin Z...\n", getpid());
15 sleep(5);
16 exit(EXIT_SUCCESS);
17
18 }
19 return 0;
20 }
输入监控指令while :; do ps aux | grep test | grep -v grep; sleep 1;echo "----------------------------"; done
僵尸进程是已经结束执行的子进程,但其进程控制块(PCB)仍保留在系统中,等待父进程调用系统调用 wait() 或 waitpid() 来获取其退出状态。僵尸进程的存在虽然不会再消耗 CPU 时间或其他资源,但它们仍然具有一定的危害和潜在问题,主要表现在以下几个方面:
- 资源浪费:僵尸进程的 PCB 仍然占用系统内核资源,尤其在大量产生僵尸进程的情况下,会浪费系统内存和进程表的空间。
- 进程数限制:系统对同时存在的进程数量有限制,如果有太多僵尸进程积压,可能会导致系统达到进程数限制,进而影响系统的正常运行。
- 父进程资源泄漏:如果父进程没有正确处理僵尸进程,不调用 wait() 或 waitpid() 来回收子进程资源,可能导致父进程的资源泄漏,尤其是文件描述符等资源没有得到释放。
- 可能导致资源耗尽:如果僵尸进程过多且父进程不进行处理,可能会耗尽进程表资源,导致其他合法进程无法创建。
解决僵尸进程问题的方法是,父进程在子进程退出后调用 wait() 或 waitpid() 等系统调用,回收子进程的资源并获取其退出状态。通过这样的处理,操作系统会将僵尸进程的 PCB 从进程表中移除,避免资源浪费和潜在问题。如果父进程不关心子进程的退出状态,也可以使用 waitpid() 函数的 WNOHANG 参数,让 waitpid() 变为非阻塞模式,即使子进程还未退出,也不会阻塞父进程的执行。
总的来说,虽然僵尸进程不会对系统造成巨大的直接危害,但它们可能会导致资源浪费、进程数限制和父进程资源泄漏等问题,因此在编写程序时,应该正确处理子进程的退出状态,避免产生过多的僵尸进程。
注:
- X死亡状态(dead)是在程序结束的一瞬间的状态,不好演示。
- D磁盘休眠状态同样不方便展示,在正常情况下,D 状态的进程数应该是较少的。不过我们可以通过命令
ps aux | grep "^D"
来显示系统所有的D状态进程。- 进程是R状态,不一定是在CPU上运行,进程在运行队列中,就是R状态(进程已准备好,等待调度)
- S状态为浅度休眠(对外部事件可以做出反应),大部分情况下都是这种状态
- D状态为深度休眠(不可以被杀掉,即便是操作系统,只能等待D状态进程自动醒来,或者是关机重启(可能被卡死))
- S状态和D状态称为等待状态
- 进程退出,一般不是立刻让OS回收信息,释放进程的所有资源
进程退出后,操作系统通常不会立刻回收进程的所有资源。相反,它会留下一段时间,让父进程或其他相关机制有机会处理退出的进程的信息,可能进行资源回收和善后工作。
在进程退出时,操作系统会做以下一些操作:
--------终止进程的执行:操作系统会立即停止进程的执行,不再分配 CPU 时间给该进程。
--------将退出状态保存在 PCB 中:操作系统会将进程的退出状态(退出码)保存在进程控制块(PCB)中,以便父进程或其他进程可以查询和处理。
--------进程资源回收:操作系统会在一段时间内保留进程的资源,包括内存、文件描述符等,以便其他进程或父进程可以获取退出的进程信息或进行善后处理。这段时间也称为"僵尸状态"(Zombie State)。
--------向父进程发送信号:操作系统会向父进程发送一个 SIGCHLD 信号,通知父进程子进程已经退出,并且可以查询退出状态。
父进程通常会调用系统调用 wait() 或 waitpid() 来等待子进程的退出,并获取其退出状态。一旦父进程调用这些函数,操作系统将回收僵尸进程的资源,并从进程表中移除该进程的 PCB。
在父进程没有处理子进程退出状态时,如果子进程退出,但父进程没有调用 wait() 或 waitpid(),则子进程会进入僵尸状态,PCB 仍然保留在系统中,但资源没有被回收,这会导致资源浪费。
因此,父进程应该在子进程退出后及时处理其退出状态,确保子进程的资源被正确回收,避免僵尸进程的产生。
普通状态与+状态的区别
后台运行时,我们可以输入命令jobs
查看后台进程状态fg+对应序号
将后台转为前台
kill命令
用于删除执行中的程序或工作
指令:kill [-s <信息名称或编号>][PID] 或 kill [-l <信息编号>]
接上数字即为对应功能,比如kill -9 PID
即为直接终止该进程,要注意的是D状态和Z状态用kill命令是杀不掉的。
进程状态图
孤儿进程
**孤儿进程(Orphan Process)**是指在父进程终止或意外退出后,子进程还在继续运行,但此时其父进程已经不存在了。孤儿进程会被操作系统接管,并由systemd进程(centos6.5中的1号进程为initd)(通常是 PID 为 1 的进程)成为其新的父进程。
通常情况下,当一个进程创建子进程后,父进程会等待子进程完成,并调用 wait() 或 waitpid() 等系统调用来获取子进程的退出状态。但是,如果父进程意外退出或提前终止,子进程就会成为孤儿进程。
孤儿进程的创建机制主要涉及以下几个步骤:
- 父进程创建子进程。
- 父进程意外退出或被终止,无法处理子进程的退出状态。
- 子进程继续在系统中运行,但其父进程已经不存在,成为孤儿进程。
- 操作系统将孤儿进程的父进程 ID(PPID)设置为systemd进程的进程 ID,systemd进程成为孤儿进程的新父进程。
由于 init 进程会定期调用 wait() 或类似系统调用来处理已终止的子进程,所以孤儿进程的退出状态最终会被处理,其资源会被回收。因此,孤儿进程并不会像僵尸进程一样导致资源泄漏。
总而言之,孤儿进程是指在其父进程终止后,仍然在系统中运行的进程,其父进程已经不存在。操作系统会将孤儿进程的父进程 ID 设置为systemd进程的进程 ID,使systemd进程成为孤儿进程的新父进程,并最终处理孤儿进程的退出状态。
示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
我们使用top命令查看PID为1的进程为操作系统,所有孤儿进程被1号systemd进程领养,当然也由systemd进程回收
进程优先级
1.基本概念
进程优先级是操作系统中用于调度进程执行的重要概念。每个进程都有一个相应的优先级,它决定了进程在多任务环境下获得 CPU 时间片的优先级别。进程优先级的调整可以影响进程的执行顺序,从而对系统的性能和响应性产生影响。
在多任务操作系统中,CPU 时间被划分为小的时间片,并按照一定的调度算法分配给不同的进程。调度算法根据进程的优先级决定在给定时刻应该运行哪个进程。较高优先级的进程在调度时会优先获得 CPU 时间,而较低优先级的进程则可能需要等待更长时间才能得到执行的机会。
进程优先级通常由操作系统根据一些规则来设置和调整。一些常见的优先级调整规则包括:
- 静态优先级:在进程创建时指定,通常由程序员或系统管理员指定,表示进程的固定优先级。
- 动态优先级:根据进程的行为和资源使用情况动态调整。比如,某些操作系统可能会根据进程的 CPU 使用时间、I/O 请求等来动态调整优先级,以实现公平的资源分配。
- 实时优先级:适用于实时系统,用于确保特定任务的响应时间。实时进程通常具有较高的优先级,以保证其能够及时响应特定事件。
进程优先级的合理调整可以对系统的性能和响应时间进行优化。例如,在响应用户交互的操作时,可以将用户界面相关的进程设置为较高优先级,确保系统对用户的操作能够及时响应。而对于一些资源密集型任务,可以将其优先级设置较低,以免其占用过多的 CPU 时间影响其他任务的执行。
需要注意的是,优先级调整需要慎重进行,过高的优先级可能会导致其他任务得不到足够的执行时间,而过低的优先级可能会导致某些任务无法及时响应,影响系统的整体性能。因此,在进行优先级调整时,需要仔细考虑不同任务的重要性和资源需求,以平衡系统性能和资源利用率。
2.查看系统进程优先级
首先输入指令ps -l
我们很容易注意到其中的几个重要信息:
名称 | 作用 |
---|---|
UID | 代表执行者的身份 |
PID | 代表这个进程的代号 |
PPID | 代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 |
PRI | 代表这个进程可被执行的优先级,其值越小越早被执行 |
NI | 代表这个进程的nice值 |
3.PRI和NI
在Linux系统中,优先级由pri和nice值共同确定(优先级的数值越小,优先级越高;优先级的数值越大,优先级越低)
PRI(Priority):表示进程的静态优先级,是一个整数值,基准默认值为80,默认范围是 60 到 99(普通进程而言)。数值越小,优先级越高。负数表示高优先级,正数表示低优先级。对于普通用户创建的进程,默认的优先级为 0。如果进程的优先级值是负数,表示它是实时进程。
NI(Nice Value):表示进程的动态优先级,也是一个整数值,在-20到+19之间。数值越大,优先级越低。NI 值可以在运行时通过用户或管理员调整来影响进程的调度优先级。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化,可以理解nice值是进程优先级的修正修正数据
示例:
首先我们生成并运行一个死循环的可执行程序,查看其PID
第二步进入top
后按“r”–>输入进程PID–>输入nice值
退出查看PRI
可以看到我们输入的100,但是普通我们最多加到99,提升优先级也是一样,最低只能到60,但需要注意的是,不管在什么时候进行加减,都是在基准PRI(80)上进行加减,且提升优先级(即NI为负值)需要root权限。
环境变量
1.环境变量的基本概念
**环境变量(environment variables)**是在操作系统中用于配置程序运行环境的一种机制。它是一些由操作系统或用户定义的键值对(Key-Value pairs),其中键表示环境变量的名称,而值表示环境变量的内容。环境变量通常在操作系统的全局范围内生效,被用于存储程序运行所需的配置信息、路径、语言设置、系统资源等。
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 比如我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
环境变量的优点在于它们可以用于传递配置信息给程序,而无需修改程序的源代码。这使得程序更加灵活,可以在不同的环境中运行而不需要重新编译。许多程序和操作系统本身都使用环境变量来配置其行为和设置。例如,PATH 环境变量用于指定系统的可执行程序所在的路径,使得用户可以在终端中直接运行程序而不需要指定绝对路径。
2.常见环境变量及查看环境变量方法
常见的环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
查看环境变量
echo $环境变量名称
在我们创建一个可执行文件时,为什么还要加./
才能运行,而其他指令和程序却不需要,那是因为我们所处的路径不在环境变量PATH中,当我们输入指令export PATH=$PATH:程序所在路径
时,就将程序所在路径添加到了PATH中,这样就可以不带./
就能直接运行了。
3.环境变量相关的命令及组织方式
命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
4.通过代码获取环境变量
①main函数第三个参数
首先我们先介绍前两个参数int argc, char *argv[]
其中:
argc(Argument Count):表示命令行参数的数量,包括程序本身。即 argc 的值至少为 1。
argv(Argument Vector):是一个指向指针的数组,每个指针指向一个表示命令行参数的字符串。
比如下面的代码:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Number of command-line arguments: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}
运行时我们输入命令./program arg1 arg2 arg3
输出将会是:
Number of command-line arguments: 4
Argument 0: ./program
Argument 1: arg1
Argument 2: arg2
Argument 3: arg3
下面我们看第三个参数char *env[]
env(Environment Variable):它一个指向指针的数组,每个指针指向一个环境变量字符串。
通过 env 参数,程序可以获取和修改当前进程的环境变量。环境变量通常用于设置程序的配置信息、路径等。在一些特定的编程语言和操作系统中,main 函数支持这样的参数形式。
以下是一个简单的例子,展示了如何在 C 语言中使用 env 参数获取环境变量:
#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获取
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
代码如下:
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
结果同第一种
③通过系统调用获取环境变量
getenv()函数
代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
环境变量通常具有全局属性,可以被子进程继承下去
比如下面这段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
直接运行肯定是没有用的,因为没有这个全局变量,但是我们输入指令export MYENV="hello world"
导出环境变量,再次运行,就有输出结果了,这说明环境变量是可以被子进程继承下去的!
进程地址空间
基于Linux kernel 2.6.32
32位平台
1.内存分布
正常我们在学习C/C++时,了解到的内存分布如下图
我们再通过下面的代码深入了解一下内存的构造
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int g_val=100;
5 int g_unval;
6
7 int main(int argc,char* argv[],char* env[])
8 {
9 printf("code addr :%p\n",main);
10 const char* p="hello";
11 printf("read only :%p\n",p);
12 printf("global val :%p\n",&g_val);
13 printf("global uninit val:%p\n",&g_unval);
14 char* q1=(char*)malloc(10);
15 char* q2=(char*)malloc(10);
16 char* q3=(char*)malloc(10);
17 char* q4=(char*)malloc(10);
18 printf("heap addr :%p\n",q1);
19 printf("heap addr :%p\n",q2);
20 printf("heap addr :%p\n",q3);
21 printf("heap addr :%p\n",q4);
22
23 printf("stack addr :%p\n",&q1);
24 printf("stack addr :%p\n",&q2);
25 printf("stack addr :%p\n",&q3);
26 printf("stack addr :%p\n",&q4);
27
28 static int i=0;
29 printf("static addr :%p\n",&i);
30 printf("args addr :%p\n",argv[0]);
31 printf("env addr :%p\n",env[0]);
32 return 0;
33 }
2.什么是进程地址空间
我们看下面的代码
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int g_val = 100;
5 int main()
6 {
7 pid_t id = fork();
8 if(id < 0)
9 {
10 perror("fork");
11 return ;
12 }
13 else if(id == 0)
14 { //child
15
16 g_val = 50;
17 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
18 }
19 else
20 { //parent
21
22 sleep(3);
23 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
24 }
25 sleep(1);
26 return 0;
27 }
我们建立子进程时,将子进程中的全局变量值修改,再让其父进程运行,我们看输出结果
然后我们发现一个惊人的结果,子进程先运行的情况下,修改了全局变量,父进程后运行,但全局变量还是初始值,最让人惊讶的是,他们是相同的地址。
在这里我们要提出一个概念:进程地址空间(即我们常说的虚拟内存)
其实在我们生成每一个C/C++程序时,他就已经为所有的内容分配好了地址,我们可以输入指令objdump -afh 程序名
虚拟内存是计算机操作系统中的一种技术,它扩展了物理内存(RAM)的大小,使得进程能够访问比实际物理内存更大的地址空间。虚拟内存允许多个进程同时运行,每个进程都有自己独立的地址空间,从而实现了进程之间的隔离和保护。
虚拟内存的主要思想是将物理内存和磁盘空间结合起来,形成一个连续的地址空间,称为虚拟地址空间。这个虚拟地址空间对于每个进程都是独立的,每个进程都认为自己在独占使用整个地址空间,而无需关心其他进程的存在。
当进程访问虚拟地址空间时,操作系统会通过一个称为内存管理单元(MMU)的硬件部件将虚拟地址映射到物理内存或磁盘上的相应位置。如果所需的数据在物理内存中,那么访问就会立即完成。如果所需的数据不在物理内存中,操作系统会将不常用的数据从物理内存中置换(换出)到磁盘上的一个特定区域,然后将所需的数据从磁盘读取到物理内存中(换入)。这个过程是透明的,对于进程来说是不可见的。
所以在每个进程的眼里,整个内存中只有自己的存在。
3.为什么要有地址空间
地址空间的存在是为了实现内存管理和进程隔离。
- 内存管理:地址空间允许操作系统有效地管理计算机的内存资源。通过地址空间,操作系统可以跟踪哪些内存地址被使用,哪些是空闲的,以及哪些是属于不同进程的。当进程需要访问内存时,操作系统通过地址映射将虚拟地址转换为物理地址,确保进程访问的内存是合法的,并且不会与其他进程的内存冲突。
- 进程隔离:地址空间实现了进程之间的隔离。每个进程都有自己独立的地址空间,使得进程之间的数据和指令相互隔离,互不干扰。这样,即使一个进程出现错误或崩溃,也不会对其他进程产生影响。进程之间的隔离还提供了更高的系统稳定性和安全性。
- 多道程序设计:地址空间使得操作系统能够在多个进程之间进行切换,实现多道程序设计。通过将进程的地址空间保存在内存中,操作系统可以暂停一个进程的执行,将其状态保存下来,然后恢复另一个进程的执行,从而实现多个进程的并发执行,提高了系统的利用率和响应性。
- 虚拟化:地址空间实现了虚拟化,使得每个进程都认为自己在独占整个内存空间。进程访问的地址是虚拟地址,而不需要关心实际物理内存的布局。这种虚拟化使得编程和应用程序开发更加简单,不需要考虑实际的物理内存位置。
总的来说,地址空间是计算机系统中内存管理和进程隔离的基础,它为操作系统提供了一种有效的内存管理机制,同时保障了不同进程之间的数据安全和隔离。地址空间的存在使得计算机系统能够同时运行多个进程,提高了系统的效率和灵活性。
地址空间在Linux内核中的定义
struct mm_struct {
struct vm_area_struct * mmap; /* 映射区域列表的头部 */
struct rb_root mm_rb; /* 存储映射区域的红黑树 */
struct vm_area_struct *mmap_cache; /* 最后查找的映射区域 */
unsigned long (*get_unmapped_area)(struct file *filp, unsigned long addr,
unsigned long len, unsigned long pgoff,
unsigned long flags);
pgd_t * pgd; /* 页全局目录(Page Global Directory) */
atomic_t mm_users; /* 进程共享的计数器 */
atomic_t mm_count; /* 进程描述符的引用计数 */
int map_count; /* 映射区域的计数器 */
rwlock_t page_table_lock; /* 页表的自旋锁 */
struct rw_semaphore mmap_sem; /* mmap_sem 用于对进程的映射区域进行保护 */
spinlock_t page_table_lock; /* 页表锁,保护页表修改 */
struct list_head mmlist; /* 进程的链表节点,链接到所有进程的mm_struct列表 */
unsigned long start_code, end_code;/* 进程代码段的起始地址和结束地址 */
unsigned long start_data, end_data;/* 进程数据段的起始地址和结束地址 */
unsigned long start_brk, brk; /* 进程堆的起始地址和当前堆顶地址 */
unsigned long arg_start, arg_end; /* 进程参数的起始地址和结束地址 */
unsigned long env_start, env_end; /* 进程环境变量的起始地址和结束地址 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 辅助向量 */
unsigned long rss; /* resident set size, 进程使用的物理页数量 */
unsigned long total_vm; /* 进程总共的虚拟页数量 */
unsigned long locked_vm; /* 锁定的虚拟页数量 */
unsigned long pinned_vm; /* 固定的虚拟页数量 */
unsigned long shared_vm; /* 共享的虚拟页数量 */
unsigned long exec_vm; /* 程序映像的虚拟页数量 */
unsigned long stack_vm; /* 栈的虚拟页数量 */
unsigned long def_flags; /* 默认页标志 */
unsigned long nr_ptes; /* 被映射页表项的数量 */
unsigned long nr_pmds; /* 被映射页中间表项的数量 */
unsigned long dummy_page; /* 空页表项指针 */
struct vmacache vmacache_seqnum; /* 虚拟页的页表缓存 */
struct vmacache vmacache; /* 虚拟页的页表缓存 */
struct rw_semaphore pagetable_rwsem; /* 用于页表的读写信号量 */
struct page ** pmd_huge_pte; /* Huge PMD页表项 */
int map_count; /* 映射区域的计数器 */
struct list_head mmlist; /* 进程的链表节点,链接到所有进程的mm_struct列表 */
unsigned long start_code, end_code;/* 进程代码段的起始地址和结束地址 */
unsigned long start_data, end_data;/* 进程数据段的起始地址和结束地址 */
unsigned long start_brk, brk; /* 进程堆的起始地址和当前堆顶地址 */
unsigned long arg_start, arg_end; /* 进程参数的起始地址和结束地址 */
unsigned long env_start, env_end; /* 进程环境变量的起始地址和结束地址 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 辅助向量 */
unsigned long rss; /* resident set size, 进程使用的物理页数量 */
unsigned long total_vm; /* 进程总共的虚拟页数量 */
unsigned long locked_vm; /* 锁定的虚拟页数量 */
unsigned long pinned_vm; /* 固定的虚拟页数量 */
unsigned long shared_vm; /* 共享的虚拟页数量 */
unsigned long exec_vm; /* 程序映像的虚拟页数量 */
unsigned long stack_vm; /* 栈的虚拟页数量 */
unsigned long def_flags; /* 默认页标志 */
unsigned long nr_ptes; /* 被映射页表项的数量 */
unsigned long nr_pmds; /* 被映射页中间表项的数量 */
unsigned long dummy_page; /* 空页表项指针 */
struct vmacache vmacache_seqnum; /* 虚拟页的页表缓存 */
struct vmacache vmacache; /* 虚拟页的页表缓存 */
struct rw_semaphore pagetable_rwsem; /* 用于页表的读写信号量 */
struct page ** pmd_huge_pte; /* Huge PMD页表项 */
struct mm_rss_stat rss_stat; /* 进程内存使用的统计信息 */
unsigned int def_flags; /* 默认页标志 */
pgd_t *pgd; /* 页全局目录表项指针 */
struct mm_struct *mm; /* 进程所属的mm_struct */
unsigned long start_brk; /* 堆的起始地址 */
unsigned long brk; /* 当前堆的顶部地址 */
unsigned long start_stack; /* 栈的起始地址 */
unsigned long arg_start; /* 参数区的起始地址 */
unsigned long arg_end; /* 参数区的结束地址 */
unsigned long env_start; /* 环境变量区的起始地址 */
unsigned long env_end; /* 环境变量区的结束地址 */
unsigned long saved_auxv[AT_VECTOR_SIZE];
4.页表
在虚拟内存中的进程要映射到物理内存中,是通过页表来实现的,那什么是页表呢?
页表(Page Table)是计算机操作系统中用于实现虚拟地址到物理地址转换的一种数据结构。在使用虚拟内存的系统中,每个进程都有自己的虚拟地址空间,而虚拟地址需要通过页表映射为实际的物理地址才能在内存中定位数据。
当进程访问虚拟地址时,操作系统的内存管理单元(MMU)会使用页表来完成地址转换。页表记录了虚拟地址和对应的物理地址之间的映射关系。它将虚拟地址划分成固定大小的页面(通常是4KB或2MB),然后将这些页面映射到物理内存中的对应位置。
页表通常以表格的形式存储,其中每一行对应一个虚拟页,包含了虚拟地址和物理地址之间的映射关系。虚拟地址的高位部分用于查找页表中的行,而低位部分则用于在对应页中定位实际的物理地址。
页表通常存放在计算机的主存(物理内存)中,具体来说,页表存放在进程的内核地址空间中。每个进程在运行时都有自己的页表,用于将其虚拟地址空间中的虚拟地址映射到实际的物理地址。
在 32 位系统中,页表一般存储在操作系统内核的地址空间中的某个特定区域,例如 Linux 内核中常见的位置是从 0xC0000000(3GB)开始的一段地址空间。这个区域被称为内核空间,用于存放操作系统内核的代码、数据和数据结构,包括页表。进程的用户空间通常从 0 开始,虚拟地址从 0 到 3GB 是用户空间的范围。
在 64 位系统中,由于地址空间更为宽广,页表的存储方式可能有所不同。不过一般仍然是在内核地址空间中存放页表,而用户空间则会占用更大的范围。
页表通常是由操作系统动态地创建和管理的,而且对于每个进程都有独立的页表。当进程切换时,操作系统会切换对应的页表,以便为每个进程提供独立的虚拟地址空间。页表的管理和切换是操作系统内存管理的核心功能之一,它使得虚拟内存系统能够高效地实现进程之间的隔离和内存共享。
在虚拟内存系统中,常见的页表结构包括:
- 单级页表:使用一个单独的表来存储所有虚拟页和物理页的映射关系。这种方法简单直接,但对于大型地址空间,需要较大的页表,可能会造成空间浪费。
- 多级页表:将页表分为多个级别,每个级别包含一个部分虚拟地址和物理地址的映射。通过多级页表,可以有效地减少页表的大小,节省内存空间。常见的多级页表结构有两级页表、三级页表等。
- 反向页表:通常用于处理大型的物理内存。反向页表将物理页映射到虚拟页,从而可以更快速地查找物理地址对应的虚拟地址。
虚拟地址到物理地址的转换是操作系统内存管理的核心部分。页表的使用允许操作系统在虚拟内存和物理内存之间实现透明的地址转换,使得进程感觉自己在独占整个内存空间,提高了系统的灵活性和资源利用率。
在 Linux 内核中,页表是通过多级页表的方式来实现的。每个进程都有自己独立的页表用于将虚拟地址映射到物理地址。页表的数据结构主要包括以下几个层次:
pgd_t(Page Global Directory):页全局目录,最顶层的页表,用于将虚拟地址映射到中间页表 pmd。
pmd_t(Page Middle Directory):页中间目录,第二层的页表,用于将虚拟地址映射到底层页表 pte。
pte_t(Page Table Entry):页表项,最底层的页表,用于将虚拟地址映射到物理地址。
这些页表数据结构的定义可以在 Linux 内核的头文件 linux/pgtable.h 中找到。
5.理解进程地址空间
通过上面这幅图结合前面的知识我们不难理解,为什么子进程和父进程相同代码同一内存地址可以有不同的结果,亦或是之前的fork函数为什么可以有两个返回值。同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
所以在每一个进程建立时,其中内容都有其对应的虚拟地址,然后通过页表再访问内存,页表通过映射找到合适的物理地址再运行,页表存在的进程虚拟地址及对应物理地址可以做到更好的交互,比如我们使用动态内存分配函数申请的空间未使用,造成的浪费空间,页表并不会直接映射,而是有延迟分配,等待你使用后再映射,从而达到提高整机效率,让物理内存真正做到100%有效使用。
总结
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!