linux--进程学习

1.冯诺依曼体系结构

常见的计算机,笔记本电脑,服务器等,大部分都遵守冯诺依曼体系。

  • 输入设备,输出设备都为外设
  • 上图中的存储器只的是:内存

运算器:算数运算,逻辑运算

  • **算数运算:**数学运算
  • **逻辑运算:**推理

1.1计算机为什么要有内存?(理解分层存储)

1.存储分级

以下都为具有存储功能的设备

2.体系结构效率

简要解释:

  • 磁盘等外部存储器:存储的数据量大,但传输数据的速度慢(相同时间内传输数据的次数少)。
  • **cpu:**存储和处理数据的量极小,但处理数据的速度极快(相同时间内成功处理数据的次数多)。
  • 由此可见,如果cpu直接从外设中读取数据,那么大多数时间cpu都在等,这就导致整体的运算速度和外设一样低,浪费了cpu的运算能力。
  • **内存,高速缓存,寄存器的存在:**这些存储设备的存储数据量依次减小,传输数据的速度依次加快,这些设备把外设的存储量和传输速度递进式的逼近cpu的存储量和cpu的处理数据的速度,使得整体处理数据的速度接近cpu,充分利用cpu的运算能力。

详细解释:

这个问题本质是CPU、存储设备之间的 "速度鸿沟" 导致的必然设计,我们可以从 "矛盾根源""分层解决方案的必然性""各层级的具体作用" 三个维度,进一步细化,让整个体系更清晰:

一、核心矛盾:CPU 与外部存储的 "速度 - 容量" 极端失衡

计算机的核心诉求是 "高效处理数据",但 CPU 和外部存储(硬盘、SSD 等)的物理特性存在无法调和的矛盾,这是内存(以及高速缓存、寄存器)存在的根本原因:

|------------|-------------------------------------|-------------------------------|---------------------------------------------|
| 对比维度 | cpu运算核心 | 外部存储(硬盘 / SSD) | 差距量级 |
| 数据处理速度 | 极快(纳秒级,如 3GHz CPU 每秒可执行 30 亿次操作) | 慢(硬盘毫秒级 / SSD 微秒级) | CPU 速度是硬盘的100 万倍 以上,是 SSD 的1000 倍以上 |
| 单次处理量 | 极小(每次仅处理寄存器 / 缓存中的少量数据,如 4 字节、8 字节) | 极大(单次可读写 MB 级数据,但 "启动传输" 耗时久) | 外部存储单次容量是 CPU 单次处理量的百万倍以上 |
| 存储容量 | 极小(无法内置大容量存储,成本极高) | 极大(TB 级常见,成本低) | 外部存储容量是 CPU 内置存储的亿倍以上 |

简单说:CPU "吃得快但吃的少",外部存储 "装得多但送得慢" ------ 如果让 CPU 直接从外部存储读取数据,CPU 会 99.9% 的时间都在 "等数据"(即 "CPU 空闲等待"),整体效率会被拉到和外部存储一样低,完全浪费 CPU 的运算能力。

二、分层存储:用 "多级缓冲" 弥合速度鸿沟,实现 "效率接力"

为了解决上述矛盾,计算机设计了 **"寄存器→高速缓存(Cache)→内存(RAM)→外部存储"** 的四级分层存储架构。这四层的核心作用,是让数据从 "大容量、低速" 的外部存储,通过每一级的 "提速、减容",逐步 "适配" CPU 的 "小容量、高速" 需求,本质是一场 "数据接力赛":

1. 第一层:寄存器(Register)------ 直接对接 CPU 的 "贴身仓库"

  • 定位:CPU 内部的 "临时货架",与 CPU 运算单元直接相连,是速度最快的存储。
  • 特点:速度与 CPU 完全同步(纳秒级),但容量极小(一个 CPU 通常只有几十到几百个寄存器,总容量仅几 KB)。
  • 作用:存放 CPU "正在处理" 的指令和数据(比如当前计算的数值、下一步要执行的命令地址)。因为 CPU 每次运算只需要极少量数据,寄存器能让 CPU "零等待" 获取数据,避免运算中断。

2. 第二层:高速缓存(Cache)------CPU 的 "家门口仓库"

  • 定位:介于寄存器和内存之间,通常集成在 CPU 内部(也有外部 Cache),是 "寄存器的扩展缓冲"。
  • 特点:速度接近 CPU(比寄存器慢一点,但远快于内存,约 1-10 纳秒),容量中等(几 MB 到几十 MB,如常见的 L3 Cache 为 12MB、24MB)。
  • 作用:解决 "寄存器容量太小" 的问题。CPU 处理数据时,会先把 "可能马上用到的一批数据"(比如一个程序的核心代码、一段连续的计算数据)从内存加载到 Cache 中。因为程序存在 "局部性原理"(即 "最近用的数据,接下来大概率还会用"),Cache 能让 CPU 不用频繁去内存取数据,进一步减少等待。

3. 第三层:内存(RAM,随机存取存储器)------ 衔接外部存储的 "中间仓库"

  • 定位:计算机的 "主力临时仓库",直接与 CPU 通过总线连接,是分层架构的核心 "中转站"。
  • 特点:速度中等(比 Cache 慢,约几十到几百纳秒),容量较大(几 GB 到几十 GB,如 16GB、32GB 内存)。
  • 作用 :解决 "外部存储速度太慢" 的问题。当我们运行一个程序(如 Word、游戏)时,计算机会先把程序的所有核心数据(不是整个程序,而是当前需要运行的部分)从硬盘 / SSD 加载到内存中 ------ 因为内存速度远快于外部存储,CPU 后续从内存取数据的等待时间会大幅缩短。
    (补充:内存是 "易失性存储",断电后数据丢失,这也是为什么电脑断电后未保存的文件会消失 ------ 数据只在内存中,没写回外部存储。)

4. 第四层:外部存储(硬盘 / SSD、U 盘等)------ 长期存放数据的 "仓库"

  • 定位:计算机的 "长期储物柜",用于永久保存数据(程序、文件、系统等)。
  • 特点:速度最慢(硬盘毫秒级,SSD 微秒级),容量最大(TB 级,成本低),非易失性(断电数据不丢失)。
  • 作用:负责 "海量数据的长期存储",但不直接参与 CPU 的实时运算 ------ 只有当程序需要运行、或数据需要处理时,才会把 "必要部分" 加载到内存,再通过 Cache、寄存器传递给 CPU。

总结:分层存储的本质是 "用空间换时间,用时间换空间"

整个体系的设计逻辑可以概括为两句话:

  1. 从外部存储到内存:用 "内存的大容量 + 中速度",承接外部存储的 "超大容量 + 低速度",把 "慢速的海量数据" 转化为 "中速的可用数据",避免 CPU 直接面对外部存储的 "漫长等待";
  2. 从内存到寄存器:用 "Cache 的小容量 + 高速度"、"寄存器的极小容量 + 极速",逐步把 "中速的批量数据" 压缩为 "CPU 能直接用的高速小数据",让 CPU 的运算能力完全释放。

如果没有内存,CPU 每次运算都要直接从硬盘读数据 ------ 假设 CPU 处理一个数据需要 1 纳秒,从硬盘读数据需要 1 毫秒(1 毫秒 = 100 万纳秒),那么 CPU 每处理 1 次数据,就要等待 100 万次运算的时间,整体效率会暴跌到原来的 1/100 万,这在现实中完全无法使用。

因此,内存(及高速缓存、寄存器)不是 "可选配件",而是计算机为了平衡 "CPU 高速运算" 与 "外部存储海量低速" 的矛盾,所必须的 "效率桥梁"。

1.2冯诺依曼体系的再认识

  • 有些设备只进行输入(键盘,麦克风等),有些设备只进行输出(显示器,音响),有些设备既有输入也有输出(网卡)。
  • 我们口中的输入输出都是站在内存角度(硬件),站在加载到内存中的程序的角度。
  • input:输入设备到内存的过程
  • output:内存到输出设备的过程

1.2.1重谈效率问题

  • 数据从输入设备到内存的过程,从内存到输出设备,本质就是拷贝。
  • cpu处理数据:cpu读取内存中的数据,其实就是把内存中的数据搬到cpu的寄存器中,cpu计算完后把数据再写回内存中,都是一种拷贝。
  • 计算机数据流动的过程,本质就是拷贝。
  • 计算机的效率问题,由设备的拷贝效率决定。
  • 存储设备的效率:拷贝效率。

1.2.2以线上聊天为例的数据在各个设备上流动的流程

两个人拿电脑在线上聊天,本质是两个冯诺依曼体系结构的数据交流

站在冯诺依曼体系结构解释现实问题

总结冯诺依曼体系:

  • 根据冯诺依曼体系结构:在数据层面,cpu只与内存打交道,不与外设打交道。
  • 内存的速度快,但是造价高,磁盘的速度慢,造价便宜,只有内存的计算机,造价高,且需要一直通电,无法普及,只有磁盘的计算机效率低。
  • 冯诺依曼体系结构为计算机提供性价比,不仅速度快,造价不高,能够被人们广泛使用,形成了全球的网民数量,孵化出了如今的互联网。
  • 根据冯诺依曼体系结构:程序(逻辑和数据)想要运行,就必须要先加载到内存中。

2.操作系统

2.1操作系统的概念

任何一台计算机都有一个基本的程序集合,称为操作系统(os),操作系统的工作包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等等)

计算机每次开机启动的第一个软件就是操作系统:按下电源键时要等待一段时间才能完全开机,这个过程就是在启动操作系统

操作系统内核(狭义):管理硬件资源(驱动管理,内存管理,文件管理),进程/任务/线程管理。

操作系统外壳:比如linux配的外壳时shell,windows系统配的就是"图形化界面",手机上就是安卓或ios那套图形化界面,只有这些也不够方便,还需要一些自带的软件,比如刚买的手机自带的应用商店,电脑自带的浏览器等。

内核4大功能:任何计算机设备都需要这4个功能

  1. 内存管理
  2. 文件管理
  3. 驱动管理
  4. 进程管理

linux内核的外壳程序显示层面上设计出一套显示库,让我们更好的去操作,那么这就叫做安卓。

如果我们把操作装在笔记本或服务器上,外壳程序给一个ubuntu图形界面,那么它就是ubuntu系统,所以安卓系统 其实就是内核操作系统 外面的一层外壳程序。

总结:

广义上:内核加外壳程序

狭义上:只指内核

操作系统为什么要有?

(我们并不适合直接与硬件打交道,所以我们需要一款操作系统把计算机中的硬件管理好,总不能说我们打开一个软件,需要我们用户去跟内存说:"你给我开点空间,我运行一个程序")

1.操作系统把软硬件资源管理好!(手段)

给用户提供一个良好的使用环境(稳定的,高效的,安全的)

以人为本

操作系统是一款软件,是一款进行软硬件资源管理的软件。

2.2理解操作系统管理的本质(对下层硬件的管理)

生活中我们做事情无非两种:

1.做决策

2.做执行

操作系统也一样

只需要获取软硬件的信息即可

操作系统获取信息,管理软硬件

  • 本质上是对软硬件的核心数据进行管理
  • 数据是提供做决策的依据

如何理解管理:先描述,再组织

操作系统管理软硬件的细节:

  • 把软硬件资源的数据整合到一个结构体中(方便管理)
  • 使用结构体描述软硬件的信息

操作系统就可以在自己的内部定义一个struct,然后就描述清楚这个设备的类型是什么,状态是什么,制造厂商,是否可以被访问等等。。。

  • 通过链表结构把软硬件的结构体对象连接起来
  • 接下来用户想要对软硬件对象做什么,只需要添加一些函数,和算法即可。

所以操作系统要管理软硬件,需要把软硬件的信息描述起来(整合一个结构体),形成一个个的软硬件对象,把这些对象再通过数据结构组织起来,最后操作系统对软硬件的管理就变成了对链表的增删查改。

所以要真正进行管理只需要:先描述,再组织。

先描述再组织就是这个世界的真相

由此可得:C++中的类和stl就是把现实世界的问题进行计算机建模的核心规律

所以写C++代码,往往要做的第一件事就是先写类,先把要描述的主体描述起来,然后再选择stl容器,就是先描述再组织。

2.3操作系统对用户提供安全稳定的服务

操作系统内存在大量的数据结构,如果操作系统的属性信息,代码和数据允许用户直接访问,这种过程是特别不安全的。

所以操作系统要保证自身数据安全的前提下给用户提供服务,怎么做呢?

  • 不允许用户直接访问操作系统的代码和数据
  • 操作系统提供软件层(system call系统调用)
  • 系统调用相当于操作系统提供的一个个的函数
  • 未来如果有用户想要"创建进程","打开文件","申请内存",用户不能直接和系统申请,需要调用系统提供的操作系统接口,然后在操作系统内完成用户的请求

系统调用介绍:

  • 任何操作系统都有系统调用
  • c语言/c++中都封装了系统调用的库(比如fopen封装的open)
  • 库函数封装了系统调用,库函数是上层,系统调用是下层。
  • 在开发⻆度,操作系统对外会表现为⼀个整体,但是会暴露⾃⼰的部分接⼝,供上层开发使⽤这部分由操作系统提供的接⼝,叫做系统调⽤。
  • 系统调⽤在使⽤上,功能⽐较基础,对⽤⼾的要求相对也⽐较⾼,所以,有⼼的开发者可以对部分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤⼾或者开发者进⾏⼆次开发。

3.进程

3.1进程的概念

课本概念:进程:运行起来的程序,内存中的程序。

  • 程序/可执行文件是一样的,指令的本质也是程序
  • 程序和可执行就是磁盘上的普通文件
  • 程序想要运行起来需要先加载到内存,加载到内存的程序就是进程。

什么叫做进程?

我们的操作系统内可以同时运行很多程序,每一个程序运行前都要加载到内存,每一个程序加载到内存都叫进程。

所以操作系统内一定会同时存在很多进程

在电脑的任务管理器中正在运行的任务都是进程:

在我们还没有启动进程之前,电脑启动的第一款软件就是操作系统

操作系统也在内存里

进程都会被操作系统管理起来,那么操作系统怎么管理进程?

先描述,在组织

  • 要先描述一个进程,那么在内核里就一定有一个描述进程的struct的结构体
  • 那么结构体内就一定有进程属性和指向下一个进程结构体的指针。

操作系统内存在多个进程,就存在多个进程结构体对象,然后用链表连接起来,最后操作系统对进程的管理就变成了对链表的增删查改,所以就完成了对进程的建模。

如何证明一个进程就是一个进程?

光进程的数据和代码在内存里不行,还要在操作系统中有描述该进程的结构体。

(一个学生怎么证明是一个学生,不仅需要身体在学校里,还需要描述这个学生信息的结构在学校的系统里)

在操作系统里把进程排队组织起来,真正组织起来的不是进程的数据代码,而是把描述进程的结构体组织起来 ,然后cpu要选择一个进程去执行,遍历进程结构体链表,选择一个最合适的进程去执行

总结:

进程 = 内核数据结构 + 自己程序的代码和数据

3.1.2描述进程-PCB

在硬盘中的可执行程序不是进程,把程序加载到内存中的代码和数据也不是进程,对进程来讲最重要的是描述该进程的结构体信息,和属性。

描述进程的结构体在操作系统角度叫做 PCB(进程控制块) PCB是描述所有操作系统进程控制块的统称**。**

在具体的liinux系统中这个描述进程的结构体叫做 struct task_struct.

虽然目前还不知道这个结构体里面有什么,但是一定能保证:进程的所有属性,都能通过该结构体直接或间接的找到。

例子:

  • 进程执行是要被调度的,就像人们是要找工作一样,真正找工作的并不是人去找工作,而是描述你这个人的简历去找工作,一家公司在招聘时会收到很多简历,他们会对这些简历进行管理,对简历进行排队一个一个的筛选,所以管理的本质是对数据进行管理。
  • 进程调度也一样,cpu要选择进程去执行,需要先把这些PCB进行管理,对PCB进行排队筛选,选择的是描述这个进程的PCB。

2.在Linux系统中的./ win中的双击 手机中的启动app ->本质上都是在启动进程。在进行这些操作后操作系统就会给这些进程创建PCB,这些PCB就会使用链表连接起来,当一个进程执行差不多后,就会换下一个进程去执行。

命令行上我们执行的命令本质上是特定路径下的文件,执行命令本质上是启动进程。

执行命令,启动自己的程序,在系统层面都被转换为了进程

3.1.3task_struct

内部属性和结构信息

1.标识符:描述本进程的唯一标识符(pid)

2.状态:描述进程,任务状态,有时候处于运行状态,有时处于暂停状态。

所谓标识符,状态其实就是结构体中的一个变量,要么是int,要么是double或float,比如0

表示暂停,1表示运行。

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

进程非常多,但cpu只有一片,所以进程就需要排队,谁在队头谁就先被cpu运行(进程排队的本质就是描述进程的结构体PCB在排队

例:学生中午去食堂窗口打饭,学生会自主的排队,排在前面的就会优先吃的饭,谁先去的谁的优先级就高,优先级高的就优先享受服务。

其实优先级就是一个整形变量:往往数字越小优先级越高

为什么存在优先级:资源少人多,岗位少求职者多,cpu少进程多。

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

进程在执行过程中可能会被中断,中断后要保存再次执行该程序时的执行位置。

5.内存指针:每个进程都会有一个PCB,cpu在执行进程时,总不能去执行PCB,所以需要找到进程的代码和数据,内存指针指向的就是该进程的代码和数据在内存中的存储位置。

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

7.记账信息:cpu累计执行多长时间,优先级是多少,目前进程总共被调度了多长时间。

操作系统在选择调度时,当两个优先级相同的进程在被筛选时,会比较被调度时长。

8.上下文数据:进程执行过程中处理器的寄存器中的数据。

进程执行过程中并不是一个进程完全跑完才执行下一个,当代计算机,都会给每一个进程分配一个时间片(1s执行100个进程每一个进程执行10ms,10ms执行结束换下一个进程),时间片执行完毕,进程会自动让出cpu,让下一个进程去执行。

基于时间片的轮转调度(一台计算机一般都只有一个cpu,单核处理,但进程非常多)。

一个进程没有执行完,就可能会把cpu让出去。

因为时间片到达,就会存在进程切换和调度动作。

所以在进程切换时,被切换的进程PCB要保存cpu中寄存器的数据,便于下次cpu再次运行该进程时接着上次运行的位置继续运行(保存进程运行位置,进程的临时数据)

cpu内寄存器硬件--只有一套

cpu内,寄存器硬件,可以在不同时间段,保存不同进程的数据!!

详细解释task_struct

1.标识符

由此可见,进程标识符就是一个int类型的变量

一个进程如何获取自己的标识符?

获取自己的task_struct结构内部的属性值->操作系统会提供一个系统调用->获取自己的pid

系统调用getpid

cpp 复制代码
#include<iostream>                                                                                                                              
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4  
  5 using namespace std;
  6  
  7 int main()
  8 {
  9   while(1)
 10   {
 11     pid_t id=getpid();
 12     cout<<"我是一个进程:pid: "<<id<<endl;
 13     sleep(1);
 14   }
 15   return 0;
 16 }

同一个程序每次创建程序的pid都不一样,在我们的操作系统中会维护一个全局的计数器,每创建一个进程这个计数器就会++

  • 查看进程的指令 ps axj
  • head -1 表头
  • grep 筛选

进程中除了存在pid还有ppid值

ppid:父进程的id值

task_struct结构体中还存在指向父进程的指针

在linux系统中,新的进程,往往是通过父进程的方式,创建出来的,父进程怎么创建的子进程:

我们每次执行我们的程序创建出来的进程的pid都是变化的,但是ppid却不变,那么这个父进程是谁?--bash

自己启动的程序,或者执行命令,最终都是把程序名,命令交给bash(命令行解释器)

它是所有命令行执行命令,创建进程的父进程

3.1.4创建进程

如何通过代码的方式创建子进程去执行特定的任务?

创建子进程的本质是OS内部多了一个子进程->task_struct + 代码和数据。

用户没办法自己在内存中new task_struct创建子进程,所以就需要系统调用来完成创建

创建进程的系统调用:fork
  • 在fork代码后的打印代码被执行了两次。
  • 第一次打印的pid:5514 ppid: 4090
  • 第一次打印的进程是bash创建出来的子进程
  • 第二次打印的pid: 5515 ppid: 5514
  • 第二次打印的ppid是第一次打印的pid,第二次打印的子进程是5514创建的进程

fork前只有一个执行流,在fork之后第一个执行流创建了一个子进程,变为了两个执行流。

fork可以让我们通过自己的代码创建一个子进程的

父进程bash

bash这个程序在没有成为进程之前,它就是磁盘中的一个文件。

并且bash程序是使用c语言写的,bash创建子进程,就是使用的fork创建的

查看进程命令:/proc

在proc中有很多以数字命名的文件/文件夹,这些数字就是系统中正在运行的进程的pid

进程会实时的在proc目录下显示。

进程目录下属性:

进程创建后在proc的进程pid目录下的内容全是进程的属性

ps命令查到的结果,都是从进程目录中筛选出来的

一个进程目录的属性里有一个文件:cwd(当前进程的工作路径)

cwd的内容就是当前进程的工作路径,例如在调用fopen时,只写一个文件名,为什么会在源文件所在路径下创建文件,因为当一个进程被创建时,进程的工作路径默认和exe文件在同一个路径下,所以创建文件的工作是进程来做,进程在哪个路径下工作,文件就被创建在哪个路径下

修改进程的工作路径(系统调用chdir)

修改进程的工作路径,就是把进程PCB中的cwd修改,本质就是修改内核数据结构

所以修改进程工作路径必定也有系统调用

参数:绝对路径/相对路径

修改进程的工作路径为path

fork的一般用法:

  • fork之后代码是共享执行的
  • 创建子进程都是为了完成任务
  • 需要将代码分流执行,父做父的,子做子的
  • 由此就需要学习fork的返回值
fork的返回值
  • 创建子进程,创建成功把子进程的pid返回给父进程,把0返回给子进程
  • 如果创建失败,-1返回给父进程
  • fork使用来创建进程的函数调用
把子进程的pid返回给父进程的原因:
  • 默认情况下,fork之后,代码和数据,一般是父子共享的
  • 把子进程的pid返回给父进程是为了方便父进程去管理(进程等待)子进程
  • 同时父子进程返回值不同也方便将父子进程进行分流
  • 当一个函数执行到renturn这句代码时,证明该函数已经完成了它的工作
  • fork也一样,当fork运行到return时,就已经完成创建子进程的任务,父子共享return代码,此时就有两个执行流去执行return代码,父进程执行流收到的返回值是子进程的pid,子进程收到的返回值是0.
为什么一个id值能接收两个不同的值?
  • 每一个进程都有自己的代码和数据
  • 创建子进程PCB时直接拷贝父进程的PCB,PCB中有指向代码和数据的指针,此时父子进程的这个指针中的地址是相同的。
  • 每个进程中的指针指向的都是虚拟地址空间,每个进程都有自己的虚拟地址空间。
  • 操作系统将进程中的虚拟地址空间通过页表映射到物理内存。
  • 在此之前,父子进程的虚拟地址空间以及映射后的物理内存都是相同的
  • 直到有一方修改代码中变量的数据后,操作系统会在物理内存中再开辟一块和修改的变量大小相同的空间,是谁修改了这个变量,操作系统就会修改谁的页表中的值,将映射后的值修改为新开辟的空间
  • 所以在进程看来id的名字还是相同的但是在物理内存中的位置已经发生变化,所以就看上去是一个变量接收两个不同的值。
  • 这样就能做到,修改前,共享代码数据,修改后,不发生改变的代码数据同样共享,发生改变的数据各自指向自己的。
  • 实现了同一变量接收不同值,同时随用随开辟,节省内存空间

返回值可以用来父子分流,一份代码包含了两个进程

3.2进程状态

3.2.1操作系统教材中的状态说明

运行,阻塞,挂起

一个cpu一个调度队列

运行状态
  • cpu先执行队头进程
  • 新创建的进程PCB进队尾
  • 在队列中排队的是进程PCB,不是代码和数据
  • 凡是在运行队列(runqueue)中的进程都是运行状态
阻塞状态
  1. os角度,理解阻塞
  2. 代码运行到cin时,进程在等待硬件(键盘)就绪,os最先识别到硬件就绪,os是硬件的管理者,如何管理->先描述再组织
  3. 进程进入硬件的等待队列后,把进程状态由R(运行)变为Block(阻塞),不在运行队列,不会再被cpu调度,不会再运行
  4. 当os识别到硬件中有数据后,会去看硬件的等待队列,发现有进程在等待,os就会把等待队列的队头进程的进程状态变为R状态。
  5. 把进程PCB再移到运行队列,等待cpu调度
  6. 进程状态的转变就是进程PCB从一个链表转移到另一个链表的过程,进程处于什么状态本质上就是进程PCB在哪个队列中。

阻塞与运行的本质:是看进程的PCB在谁的队列中

挂起状态

阻塞挂起

一个进程从阻塞状态变为挂起状态的详细过程。

  • 操作系统会给我们的磁盘中划分一个**swap分区,**该分区物理上是处于磁盘中,但实际是给操作系统作为内存空间来用的。
  • 当一个进程需要键盘输入数据,该进程就会处于键盘的等待队列中,处于阻塞状态。
  • 阻塞状态的进程不会被cpu调度,阻塞进程的代码数据还处于内存中,不仅不会被调度还会占用内存空间。
  • 当内存空间不足,但还有更重要的进程需要内存空间时
  • 操作系统会识别内存中的各个进程,发现一个进程在等待键盘输入,处于阻塞状态,不会被cpu执行,还占用内存资源,os就会把该进程的代码和数据放到磁盘的swap分区中存储,把内存中的释放掉,多出来的空间先给其他进程使用。
  • 代码和数据被转移到磁盘的swap分区中的进程就会处于挂起状态,该进程是从阻塞状态转移到挂起状态,被称为阻塞挂起。
  • 当过一段时间后键盘中有数据输入,键盘硬件就绪,os会把该阻塞挂起的进程转移到运行队列中,当进程被cpu调度时,os就会把该进程的代码和数据从swap分区中重新加载到内存中。

在实际情况下,内存中会有大量的进程处于阻塞状态,只要发生内存空间不足的情况,这些进程的代码和数据都会被换到磁盘的swap分区中

task_struct在内存中,因为内存资源不足,导致进程对应的代码和数据,被交换到swap分区,我们称这样的进程被挂起(阻塞挂起)。

swap分区的大小一般和内存一样大,也有1.5倍/2倍的情况,os在磁盘中划分出这样一个分区,做到拓展内存。

内存数据换入swap分区的缺点

  • 当一个进程在内存空间不足时代码和数据被换出,当该进程被cpu调度时,代码数据再被换入,换出换入的操作是内存和磁盘之间的数据拷贝,该拷贝速度要向下兼容,磁盘与内存之间的拷贝速度远不及内存与cpu之间的速度。
  • 虽然swap分区看上去是拓展了内存,实际上是操作系统将时间换空间。
  • 过度的swap,会使操作系统速度变慢。
  • 所以swap分区不建议太大,如果太大,操作系统会过度依赖swap分区,只要内存空间不足,就把进程代码数据换到swap,导致被挂起的进程越来越多,系统运行速度就会越来越慢,电脑就会越卡

运行挂起

  • 如果把阻塞状态的进程的代码和数据交换到swap分区后,内存空间还是不够,操作系统就会把,处于运行队列中且还没有被调度的进程的代码和数据也换到swap分区中,这样的进程状态为运行挂起
  • 如果系统把运行的进程交换到swap分区后,内存还是不够,os就会选择性的把一些进程杀死,这就是会出现我们打开一个软件,该软件突然闪退的原因。

3.2.2具体的操作系统进程状态-linux

linux系统源码中定义的具体进程状态

cpp 复制代码
/*
*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 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};

该代码中的进程状态是被#define出来的,后面的数值是define后的值,"0,1,2,4,8,16,32"在32个二进制bit位中只有一个bit位是1

R(running)状态
s(sleeping)状态
  • **s :**休眠状态,可被中断休眠(浅度睡眠)
  • 可以使用kill -9 杀掉程序
  • 前台进程带+
  • 在运行时使用"./test &",进程会变为后台(比如后台的下载任务)
  • 后台进程不能被ctrl+c结束,使用kill+pid杀掉进程
D:(disk sleep):Linux特有的状态,休眠状态(不可被中断的休眠状态,深度睡眠)
  • D状态不能被信号,操作系统杀掉
  • 进程进行磁盘io,会处于D状态
T:(stopped)状态
  • kill -19:暂停进程
  • S状态例如等待硬件就绪,就绪后进程会自动进入r状态,继续运行
  • T往往是出现了某种错误导致暂停,需要人为(比如18号信号)使进程继续运行

出现场景:

后台进程和前台进程的区别就是:后台进程不能接收到键盘输入,如果后台进程需要从键盘中获取输入,进程就会被操作系统暂停,做正确的事情叫休眠,做错的事情叫暂停

t(tracing stop):追踪暂停

当我们使用gdb去运行我们的代码时,gdb是创建了一个子进程运行代码,当运行到我们打的断点时,gdb会向子进程发送19号信号,使进程停在断点的位置(此时子进程就处于t状态,gdb追踪子进程,子进程就处于被追踪同时暂停)

x状态(死亡状态)和 Z(僵尸状态)

进程死亡了,不能直接处于x状态,要先处于Z状态,因为创建进程的最初目的是让进程去完成任务,当任务做完时,进程就会退出,那么进程的任务完成的如何,成功还是失败,需要被操作系统知道。

当进程退出时,处于Z状态,操作系统不能直接释放掉进程的task_struct,操作系统需要读取task_struct的信息,读取完毕之后再把Z变为X,此时才能释放进程的PCB

1.进程退出,退出信息是什么。

main函数的返回值,进程退出时收到的信号值。

2.进程退出,进程信息保存在哪。

进程退出时进程的退出信息会保存在task_struct中。

3.检测到Z状态,回收Z状态进程,本质是在做什么

Z状态指的是只保留进程的task_struct,未来让父进程或OS帮我们获取到进程的退出信息

进程处于Z状态,我们仍能看到进程的pid,就是因为进程的task_struct还存在

如果父进程或OS不回收处于Z状态的子进程,子进程就会永远处于Z状态,task_struct不会被释放,占据内存空间,导致内存泄露。

父进程可以使用系统调用(OS)waitpid回收子进程。

3.2.3孤儿进程

出现孤儿进程的原因:父进程先退,子进程继续运行

父进程退出没有观察到Z状态的原因:父进程的父进程是Bash,父进程在退出后,bash把父进程回收了。

子进程的父进程退出,该子进程变为孤儿进程,同时每一个进程都要有父进程,所以该孤儿进程被1号进程(操作系统)领养。

为什么孤儿进程要被领养?

  • 如果孤儿进程不被领养,当孤儿进程退出时,没有父进程回收,孤儿进程就会一直处于Z状态,一直占用内存,导致内存泄露。
  • 系统领养孤儿进程,孤儿进程退出,系统自动回收孤儿进程

孤儿进程被领养后会变为后台进程

  1. 后台进程不能从键盘获取数据。
  2. 后台进程可以向显示器打印数据

区分前后台进程:谁能从键盘获取数据,谁就是前台进程,键盘只有一个,所以同一时间前台进程只能有一个

3.3进程优先级--理论

3.3.1进程优先级是什么?

优先级和权限的区分:

  • 权限是能不能的问题
  • 优先级是已经能,得到资源的先后的问题

优先级是进程已经能得到某种资源的前提下,得到某种资源的先后顺序。

3.3.2为什么要有优先级?

内存有限,cpu有限,资源不足,分配资源,设置优先级:决定进程,获得某种资源的先后顺序。

3.3.3Linux下,是怎么设计的?

使用ps -l命令查看进程:

PRI和NI就是优先级信息
  • 优先级就是task_struct中两个整形变量。

pid -l显示的几个重要信息,有下:

  • UID:代表执行者的身份
  • PID:代表这个进程的代号
  • PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行
  • NI:代表这个进程的nice值

3.3.4PRI和NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI就是nice值,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别。
使用top命令修改进程优先级:

修改前:

修改过程:r选项->输入pid->输入修改后的值

修改后:

nice值的范围---优先级的变化范围

nice的取值范围[-20 , 19 ],pir取值范围[ 60 , 99 ],优先级共40个梯度

为什么是这个范围,为什么不能随便改
  • 优先级的变化是有限的
  • 我们用的操作系统都是分时操作系统,给进程分配时间片,相对公平公正的调度策略,较为均衡的让不同的进程都能在一段时间内,都能得到cpu资源。
  • 分时操作系统比较符合人和互联网的需求(在相同时间段内所有进程都能得到推进)
  • 所以改变优先级,改变的范围不能过大,
  • 与分时操作系统相对的是实时操作系统
  • 实时操作系统:只有一个任务执行完才能执行下一个(单片机,工业控制领域)。
进程优先级的变化范围是多少? old pri 是什么?高频修改优先级?

更改优先级不会是一个高频的动作

PRI(new)=PRI(old)+nice

更改优先级时,oldpri在linux系统默认是80,默认是80便于优先级计算

3.3.5 补充概念-竞争,独立,并行,并发

  • 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

3.4进程切换

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

寄存器是共享的,但寄存器里面的数据,本质是进程私有的,叫做进程上下文

3.5 linux2.6内核进程O(1)调度队列

重新设计双链表

cpp 复制代码
struct link
{
    struct link*next;
    struct link*prev;
}

struct task_struct
{
    //进程属性
    struct link Node;
    //...
}
  • 在task_struct结构体中,包含Link结构体,其中的next和prev指向的是下一个task_struct中的link成员变量。
  • 该链表用来连接的变量和其中的数据是分开的
  • 通过task_struct中的link变量获取task_struct中的其他成员变量的数据:
cpp 复制代码
(next-((int)&(((task_struct*)0)->node))).pid
  • 将数据和连接关系变量分开的原因:复用,管理进程在PCB中包含一个link变量即可,管理其他链表也可以在其他的结构体中包含双链表link变量,增加链式管理的扩展性,链式管理的代码只需要维护一份。
  • 在操作系统角度选择这样的方法的优点:
  • 1.Linux内核,会将所有的进程task_struct,统一放在一个双链表中。
  • 2.通过这种方法,一个task_struct既可以在全局的双链表中,也可以处在运行链表中
cpp 复制代码
struct task_struct
{
    //...
    struct link_head tasks;
    //..
    struct link_head run;

}
  • 3.不同的结构体对象也能连接在同一个链表中
  • 4.内核中的二叉树和hash表也可以用这种方法

3.5.1内核调度

每一个cpu都有一个调度队列: struct runqueue();

  • 在运行队列中有一个数组(struct list_head queue[140]),该数组的下标就是优先级。
  • 普通优先级:100~139(优先级的取值范围是[60,99],一共40个与之对应)
  • 实时优先级:0~99
  • 优先级数组本质就是数组下标,[100,139]中每一个下标对应的就是一个进程链表
  • 例:下标为100的位置存储的是一个链表头节点,该链表中的节点都是优先级为60的进程结构体
  • 每一个相同优先级的进程链表遵循先进先出的规则
  • 根据优先级选择进程的时候,就是一个hash的过程,当cpu拿着优先级去选进程的时候,直接把优先级+40作为数组下标,直接取出该下标位置存储的链表的头节点。
  • 进程加入运行队列也是如此,把进程优先级+40得到的数字为该数组下标,遍历该下标位置的链表,把该进程插入到链表的尾部。
  • cpu在选择进程调度时,可以选择遍历队列,直到不为空,选择出一批优先级相同的进程,对这批进程进行先进先出调度。
  • 相比于遍历队列,linux选择了使用位图,第一个比特位为0代表下标为0的位置为空,比特位为1代表下标为0的位置不为空,同时比特位的总数必须大于等于140,这140个比特位用5个32比特位的变量来存储,把遍历140次优化为只需遍历5次,根据该位图变量的值和位操作符第一个不为空的下标是多少,O(1)的时间复杂度选择进程调度。
进程调度与优先级的关系

问题:假如所有进程的优先级都是61,但不断有优先级为60的进程加入队列,操作系统如何调度?

(如果操作系统当前正在调度优先级为61的这一批进程,当优先级为60的进程不断插入,操作系统优先调度优先级为60的进程,那么61优先级那一批进程将永远得不到cpu资源,导致进程饥饿问题)

分时系统需要较为公平的方式,选择一个进程,一段时间内让所有进程都能得到cpu资源。

当我们把一个进程的优先级修改后,进程在数组中的位置也需要改变

linux系统的具体做法:

  • 在运行队列中有两套一模一样的hash结构
  • nr_active bitmap[5] queue[140]这三个成员被放在一个结构体中
cpp 复制代码
struct prio_array_t
{
    nr_active;
    bitmap[5];
    queue[140]
}

struct prio_array_t array[2];
  • 在运行队列中含有两个 struct prio_array_t* 结构体指针(active , expired)
  • active指向array[0],活跃140队列
  • expired指向array[1],过期140队列

cpu选择进程调度的详细过程:

  1. cpu选择进程只会在active指针指向的队列中选择进程
  2. cpu检查位图,找到优先级最高的进程开始调度,当该进程时间片结束后,根据优先级把该进程插入到过期的140队列中(活跃140队列中的进程越来越少,直至减少到0)
  3. 新产生的进程也会被插入到过期队列,当这一轮进程调度结束后,在下一轮中该进程就会被调度。
  4. 当活跃队列中的进程减少到0时,交换active和expried指针,cpu就会在这两个队列中来回切换的选择队列。
  5. 保证了每个进程都会得到cpu资源。
  6. 如果一个进程处于活跃队列中,此时修改该进程的优先级,不仅仅优先级数字被修改,该进程会插入到过期队列对应下标位置。
  7. nice值的作用:修改进程的优先级实际改的是nice值,假设该进程正处于140的活跃队列中,修改nice值后不会立刻修改该进程PCB位置,而是等该进程在当前位置被cpu调度结束后,在把该进程移入过期队列的141下标那一批进程中。

新来的优先级再高也得等到下一轮才会被调度。

该调度算法有效的解决了进程的饥饿问题,因为正在被cpu调度的队列中进程个数是有限个,并且在一直减少,不存在进程无法获得cpu资源的情况。

0-99下标的实时进程

在运行队列中0-99下标位置的进程都是实时进程,cpu在调度这些进程时,需要把这些进程完全执行完毕才会调度下一个进程。实时进程没有活跃指针和过期指针的交换,只有一个数组。

3.6命令行参数

main函数也可以带参数

  • argv:指针数组,命令行参数列表
  • argc:argv中元素的个数,命令行参数个数
  • 在命令行中执行程序的命令:./code a b c d 实际上是一个字符串
  • 该字符串在被shell拿到后,会以空格为间隔拆分该字符串
  • 拆分出来多少个子串,argc就是几
  • 每一个子串都会分别存储在argv中

为什么要有命令行参数?

命令行参数的本质应用,是为了实现一个命令,可以根据不同的选项,实现不同的子功能。

1.命令行参数,至少是1,argc>=1,argv[0]一定会有元素,指向的就是程序名!

2.选项,是以空格分割的字符串,一个字符也是字符串

3.argv[argc-1]是最后一个元素,argv[argc]==null

3.7环境变量

为什么在执行我们自己的程序时需要加上"./",执行系统的命令就不用?

  • linux系统的命令就是特定路径下的二进制文件
  • 我们自己写的程序系统是找不到的,在运行可执行程序时,系统默认去usr/bin/路径下寻找。
  • 如果我们把自己的程序cp到usr/bin路径下,在执行时就不需要加"./"。
  • 运行可执行程序,需要将该程序加载到内存中,而在加载之前需要找到这个文件,所以默认情况下linux不会再当前路径下找。
  • 加上"./"就是为了告诉系统我要运行的程序在哪。

linux系统自己怎么知道应该去哪个路径下找可执行程序?

除了/usr/bin/路径还会不会去其他路径下找?

  • 环境变量(path)会告诉系统去哪个路径下找可执行程序。
  • 环境变量是在Linux系统中全局有效的变量,有变量名和内容,其中一个环境变量就是path。
  • 环境变量的内容是以:作为分割符。
  • path的作用:告诉linux系统,如果用户执行一个可执行文件,没有指明路径,系统默认去path中的各个路径下找(指定程序的搜索路径)。
  • 执行自己的程序不加路径的另一种方法: 把自己的可执行程序所在的路径添加到path环境变量中。

3.7.1windows中的环境变量

windows中也有path环境变量且作用与linux相同。

3.7.2其他环境变量介绍

查看环境变量指令:env

histsize环境变量:系统记录最近用户输入的histsize个指令。

$HOSTNAME:主机名

cd -该命令可以回到上一个所处的路径,操作系统通过OLDPWD环境变量,知道上一个路径。

$OLDPWD:记录上一次所处的路径

每次终端路径改变,环境变量中的PWD环境变量也会跟着改变。

$HOME:记录家目录的环境变量

3.7.3用代码获取环境变量及其作用

  • main函数的第三个参数就是环境变量表,本质是把环境变量表传递给进程。
用函数,全局变量获取环境变量
1.C语言和C++提供的全局变量 environ
  • environ是一个全局的指针,指向环境变量表。
2.最常用,最便利的获取环境变量(函数)

getenv

  • 用法:根据传进来的环境变量名返回与其对应的内容。
获取环境变量的作用
  • 可以限制我们的可执行程序只能在特定的电脑,特定的路径下运行,也可以限制运行该程序的用户。
  • 在代码中获取用户名,主机名,工作路径等,判断判断该环境变量是否是程序所需要的环境,从而限制程序的运行。
  • 不同的环境变量有不同的应用场景。
环境变量的传递过程
  • argv和env这两张表默认是存在bash中的,bash是一个进程,在内存中,所以argv,argv都是存储在内存中的两个临时的表
  • 我们自己启动的程序和命令行程序都是bash进程的子进程。子进程都是可以看到父进程定义的全局变量,父子进程数据共享(只要子进程不修改)。
  • 所以不在main函数传这两张表,也是可以拿到环境变量的。
  • 以bash为根创建子进程,子进程再创建子进程,这棵进程树的所有子进程都能看到环境变量,所以环境变量有全局属性。

环境变量是如何被加载到内存的?

操作系统的第一个进程就是bash进程,bash进程在加载时,会读取配置文件,把配置文件中的环境变量加载到内存。

配置文件./bashrc在家目录

在xhsell中修改环境变量后,重启xshell环境变量会重置,原因:

我们在xshell中修改的环境变量是在内存中的临时环境变量,配置文件并没有被改变,所以在下次启动xhsell再次读取配置文件,还是最初的环境变量。

3.7.4环境变量,本地变量,内建命令

本地变量:自己在命令行中定义变量

无法被子进程继承,不具有全局性,只能在bash内部使用

类似于c/c++中的局部变量和全局变量。

将本地变量变为环境变量:export

新建环境变量:export aaaa=123456

删除环境变量:unset + 环境变量名

以上操作都是内存级的不会修改配置文件

查看环境变量和本地变量:set

内建命令

在shell内部自己定义,bash内部的一次函数调用,不依赖第三方路径。

3.8进程虚拟地址空间

3.8.1验证空间布局

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}

栈区空间向下增长指:栈区中的一个个变量是从上往下创建,但是其内部的地址还是从下往上增长,变量的地址还是最下面的地址。

以上地址划分并不是内存,是进程地址空间/虚拟地址空间,是系统的概念。

3.8.2虚拟地址

cpp 复制代码
#include<iostream>
#include<unistd.h>

using namespace std;

int g_val = 100;

int main()
{
    cout<<"g_val:"<<g_val<<" &g_val"<<&g_val<<endl;
    pid_t pid = fork();

    if(pid==0)
    {
        while(1)
        {
            cout<<"我是子进程pid: "<<getpid()<<" ppid:"<<getppid()<<" g_val:"<<g_val<<" &g_val:"<<&g_val<<endl;
            sleep(1);
            g_val++;
        }
    }
    else
    {
        while(1)
        {
            cout<<"我是父进程pid: "<<getpid()<<" ppid:"<<getppid()<<" g_val:"<<g_val<<" &g_val:"<<&g_val<<endl;
            sleep(1);
        }
    }
    return 0;
}

对比两次运行结果

  • 父子进程数据是共享的,只要子进程不进行修改,它们看到的就是一份数据。
  • 第二次运行结果显示,即使子进程修改了g_val的值,子进程和父进程的g_val的地址居然一样,并且地址一样的情况下,父子进程的g_val的值却不一样!

解释现象:

以上代码打印的地址并不是物理地址,而是虚拟地址,c语言和C++中的指针也不是物理地址,都是虚拟地址。

3.8.3虚拟地址空间解析

每一个进程都有自己的虚拟地址空间和页表(记录虚拟地址和物理地址的映射),子进程在创建时是以父进程为模板,创建起初,子进程的PCB,虚拟地址空间和页表与父进程相同(类似浅拷贝),直至子进程对某些数据进行修改(写时拷贝),父子进程才有各自独立的物理地址和映射关系

  • 代码是只读的,代码共享
  • OS规定:父子进程中,任何一个进程,尝试对共享的变量进行修改,不能直接修改,而要发生"写时拷贝",开辟新的物理地址,写入修改后的值。

虚拟地址空间

cpp 复制代码
struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	struct vm_area_struct * mmap_cache;	/* last find_vma result */
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
	void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long task_size;		/* size of task vm space */
	unsigned long cached_hole_size;         /* if non-zero, the largest hole below free_area_cache */
	unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */
	pgd_t * pgd;
	atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	int map_count;				/* number of VMAs */
	struct rw_semaphore mmap_sem;
	spinlock_t page_table_lock;		/* Protects page tables and some counters */

	struct list_head mmlist;		/* List of maybe swapped mm's.  These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */

	/* Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	mm_counter_t _file_rss;
	mm_counter_t _anon_rss;

	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm, locked_vm, shared_vm, exec_vm;
	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
	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;

	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

	unsigned dumpable:2;
	cpumask_t cpu_vm_mask;

	/* Architecture-specific MM context */
	mm_context_t context;

	/* Token based thrashing protection. */
	unsigned long swap_token_time;
	char recent_pagein;

	/* coredumping support */
	int core_waiters;
	struct completion *core_startup_done, core_done;

	/* aio bits */
	rwlock_t		ioctx_list_lock;
	struct kioctx		*ioctx_list;
};
  1. **本质:**虚拟地址空间只是一个结构体对象,地址编号从全0到全F。
  2. **构建:**Linux系统的虚拟地址空间,本质是操作系统为每个进程抽象出来的"假内存地址",其来源是操作系统通过硬件(MMU)和软件机制共同构建的逻辑空间。
  3. **硬件支持:**CPU中的内存管理单元(MMU)是基础,负责将虚拟地址"翻译"成实际的物理内存地址(虚拟地址到物理内存地址的映射关系)。
  4. **软件管理:**操作系统通过页表记录虚拟地址与物理地址的映射关系,同时划分出内核空间(高地址)和用户空间(低地址),让每个进程都以为自己独占一块连续内存。
  5. **独占假内存:**每个进程都有自己的虚拟地址空间,并且它们各自虚拟地址空间和实际的物理内存一样大一样拥有2^32个地址编号,让进程认为自己拥有整个物理地址,但进程申请内存也会受到操作系统的监视。

虚拟地址空间划分:

cpp 复制代码
unsigned long start_code, end_code;  // 代码段起始和结束地址
unsigned long start_data, end_data;  // 已初始化数据段起始和结束地址
unsigned long start_data, end_data;  // 已初始化数据段起始和结束地址
unsigned long start_brk, brk;        // BSS段起始地址和堆当前结束地址
unsigned long start_stack;           // 栈起始地址(通常是高地址向下增长)
unsigned long arg_start, arg_end;    // 命令行参数区域边界
unsigned long env_start, env_end;    // 环境变量区域边界
unsigned long task_size;             // 用户态虚拟地址空间总大小
unsigned long mmap_base;             // mmap区域的基地址(通常从高地址向下分配)

这些边界都是一个个的unsigned long 操作系统在分配虚拟地址内存时,在这些边界范围内选取地址。

如果区域是变化的(堆,栈)只需要改变对应的start和end的值即可。

页表介绍:

页表就是一个映射表,左边是虚拟地址右边是物理地址。

  1. **页表是真实物理存在的:**它以数据结构的形式存储在实际物理内存中,由操作系统合建和维护。
  2. **虚拟地址不直接"存储"在页表中:**页表存储的是"虚拟地址片段(如页号)"与"物理地址片段(如物理页框号)"的映射关系,而非所有虚拟地址的完整副本。
  3. 简单类比: 虚拟地址空间是"小区的门牌号范围(1~100号)",页表是"门牌号(虚拟)与实际住户房间号(物理)的对照表",对照表(页表)真实存在,而"门牌号范围"本身只是一套编号规则。

为什么全局变量和static变量的生命周期是全局的?

生命周期随进程,全局变量和static变量是保存在初始化数据区和未初始化数据区,这两个区域的虚拟地址和物理地址开辟结束后直到进程结束都不会变,栈和堆区的虚拟地址是变化的所以变量是临时的,比如函数栈帧中的变量。

常量字符串和代码为什么是只读的?

页表中包含标志位,而标志位中包含权限位,如果一个虚拟地址对应的物理地址权限是"r"(只读)的,但进程要对该地址进程"w"(写)操作,操作系统就会杀死进程。

所以要想让一个字符串是常量字符串,操作系统只需要让该字符串的虚拟地址对应页表的权限位置改为"r"即可

如果一个变量在页表的"是否存在标志位"为"0"说明改变量被挂到磁盘中,如果要访问,就需要操作系统重新将改变量加载到内存中。

**查表工作:**硬件MMU

页表的核心作用是实现虚拟地址到物理地址的精准翻译,同时作为操作系统管理内存、隔离进程的"核心工具"。

总结:

我们所学的编程中的栈,堆,已初始化数据,未初始化数据,代码区等等并不是真正存在物理内存条上的,而是人们为了方便管理内存和数据,抽象出了虚拟内存地址,同时各种区域的划分也都是针对虚拟地址空间的划分。

3.8.4为什么要有虚拟地址空间,它解决了什么问题

在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。
那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存大小是128M,现在同时运行两个程序A和B,A需占用内存10M,B需占用内存110。计算机在给程序分配内存时会采取这样的方法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B。

为什么不直接把物理地址暴露给用户和进程?

1.安全原因

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

2.地址不确定

  • 众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中

    去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝

    的实际内存地址每一次运行都是不确定的,比如:第一次执行a.out时候,内存当中一个进程

    都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经有10个进程

    在运行了,那执行a.out的时候,内存地址就不一定了

3.效率低下

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

地址空间和页表是OS创建并维护的!是不是也就意味着,凡是想使用地址空间和页表进行映射,

也一定要在OS的监管之下来进行访问!!也顺便保护了物理内存中的所有的合法数据,包括各个

进程以及内核的相关有效数据!

因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置

的加载!物理内存的分配和进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完

成了解耦合

因为有地址空间的存在,所以我们在C、C++语言上new,malloc空间的时候,其实是在地址

空间上申请的,物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问

的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这

是由操作系统自动完成,用户包括进程完全0感知!!

因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的

虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的。

1.解决了内存碎片化的问题

  • 可以将物理不连续的内存地址映射到连续的虚拟地址上,进程在使用时并不会意识到内存不连续

  • 虚拟地址是连续的,但物理内存可以随机分布。

2.解决了多进程内存冲突的问题

  • 父子进程共享变量,在子进程修改后,看上去父子进程该变量的虚拟地址空间是相同的,但是它们映射到物理内存的不同位置。

虚拟内存让计算机从一个"大家共用一个大黑板,谁都可以乱写乱擦"的混乱状态,变成了"每人有独立的白板,还有管理员维护"的秩序状态。虽然多了一个管理员(操作系统)的开销,但换来了安全、稳定和高效,这是完全值得的!

const:本质是错误的提前发现,常量被修改在编译时就报出来,而不是在运行时。

3.9进程控制

3.9.1进程创建

fork的常规用法:

一个父进程希望复制自己,父子进程同时执行不同的代码段,例如:父进程等待客户端请求,生成子进程来处理请求。

一个进程要执行一个不同的程序,例如:子进程从fork返回后,调用exec函数。

父子写时拷贝
  • 父进程如果不创建子进程,那么父进程的页表中代码段和数据段的权限位是读写的。
  • 当父进程进入fork后页表中的权限位变为只读的,子进程也会是只读的。
  • 如果父子进程任意一方需要对数据进行修改时,因为数据是只读的,所以操作系统就会识别到,用户要对数据段进行写入,所以fork创建程序把页表权限改为只读的是为了修改数据段操作让系统知道。
  • 当系统识别到你要修改数据段时,会将原数据在物理内存中再拷贝一份,将子进程的页表指向新物理内存,这是子进程再修改数据就不会影响到父进程的数据了。

3.9.2进程终止

内核工作

进程终止的几种情况

  1. 代码跑完结果正确
  2. 代码跑完结果不正确
  3. 代码没跑完,进程异常了

代码跑完结果对不对?

进程的退出码,默认返回0,表示代码跑完,运行成功;返回其他数字表示运行失败产生错误;

cpp 复制代码
#include<iostream>

using namespace std;

int main()
{
    return 0;
}

#include<iostream>

using namespace std;

int main()
{
    return 10;
}

C语言有一套自己的退出码

输入一个错误码,把错误码对应的错误用字符串表示

cpp 复制代码
#include<iostream>
#include<string.h>

using namespace std;

int main()
{
    for(int i=0;i<200;i++)
    cout<<"["<<i<<"]->"<<strerror(i)<<endl;
    return 0;
}

errno错误码

cpp 复制代码
FILE *fp=fopen("./log.txt","r");//失败
    if(fp==NULL)
    {
        cout<<errno<<"->"<<strerror(errno)<<endl;
        
    }
进程信号操作
bash 复制代码
kill -9 [进程pid]
  • 代码跑完:(代码运行期间,没有收到信号)0 && return 0 ->signumber:0 && 退出码: 0
  • 代码跑完结果不正确:signumber: 0 && 退出码:!0;
  • 代码没跑完:signumber: !0 && 退出码无意义。
  • 以上为进程执行的结果状态,可以用两个数字表示:sig , exit_code.
  • 进程退出时,OS会把进程退出的详细信息写入到进程的task_struct结构体中,所以进程退出后需要僵尸状态维持自己的退出状态
exit
cpp 复制代码
int main()
{ 
    cout<<"我是一个进程"<<" pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
    exit(123);
}
cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>


using namespace std;

void Print()
{
    cout<<"hello world"<<endl;
    exit(12);
}

int main()
{ 
    cout<<"我是一个进程"<<" pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
    Print();
    exit(123);
}

如何让进程退出?

  • main函数:return
  • 在任意位置调用函数exit,非main函数return,函数结束,非main函数exit进程结束
_exit
cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>


using namespace std;

void Print()
{
    cout<<"hello world"<<endl;
    _exit(21);
}

int main()
{ 
    cout<<"我是一个进程"<<" pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
    Print();
    exit(123);
}

在终止进程方面等价

区别:exit终止进程会强制刷新缓冲区,但_exit不会

结论:exit是库函数,_exit是系统调用,缓冲区和刷新缓冲区的操作是不在内核中的,是C/C++维护的

3.9.3进程等待

进程等待的必要性
  • 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法

wait:

cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;


int main()
{
    pid_t id =fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt--)
        {
            cout<<"我是子进程 pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(0);
    }
    else
    {
        // while(1)
        // {
        //     cout<<"我是父进程pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
        //     sleep(1);
        // }
        //回收子进程,等待子进程的僵尸状态

        pid_t rid =wait(NULL);
        if(rid ==id)
        {
            cout<<"等待成功"<<endl;
        }
    }
}

结论:

  • 如果父进程wait子进程,但是子进程没有退出,父进程就会阻塞在wait函数中

验证获取子进程退出信息waitpid

status参数是一个输出型参数

cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;


int main()
{
    pid_t id =fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt--)
        {
            cout<<"我是子进程 pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(1);
    }
    else
    {
        // while(1)
        // {
        //     cout<<"我是父进程pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
        //     sleep(1);
        // }
        //回收子进程,等待子进程的僵尸状态
        sleep(10);
        int status=0;
        pid_t rid =waitpid(id,&status,0);
        if(rid ==id)
        {
            cout<<"等待成功status:"<<status<<endl;
        }
        sleep(5);
        exit(0);
    }
}
  • 进程的退出信息:退出信号和退出码
  • status本质上是要获取子进程退出时的两个数字
cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;


int main()
{
    pid_t id =fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt--)
        {
            cout<<"我是子进程 pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(1);
    }
    else
    {
        // while(1)
        // {
        //     cout<<"我是父进程pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
        //     sleep(1);
        // }
        //回收子进程,等待子进程的僵尸状态
        sleep(10);
        int status=0;
        pid_t rid =waitpid(id,&status,0);
        int exit_code=(status>>8)&0xFF;
        int exit_sig=status&0x7F;
        if(rid ==id)
        {
            cout<<"等待成功exit_code:"<<exit_code<<"exit_sig:"<<exit_sig<<endl;
        }
        sleep(5);
        exit(0);
    }
}

退出状态位操作宏

cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;


int main()
{
    pid_t id =fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt--)
        {
            cout<<"我是子进程 pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(1);
    }
    else
    {
        // while(1)
        // {
        //     cout<<"我是父进程pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
        //     sleep(1);
        // }
        //回收子进程,等待子进程的僵尸状态
        int status=0;
        pid_t rid =waitpid(id,&status,0);
        int exit_code=(status>>8)&0xFF;
        int exit_sig=status&0x7F;
        if(rid>0)
        {
            if(WIFEXITED(status))
            {
                cout<<"等待成功exit_code:"<<WEXITSTATUS(status)<<endl;
            }
            else
            {
                cout<<"异常退出"<<endl;
            }
        }
        sleep(5);
        exit(0);
    }
}

options参数

当option位置的参数传WNOHANG宏时,父进程为非阻塞轮询等待。

cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;


int main()
{
    pid_t id =fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt--)
        {
            cout<<"我是子进程 pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(1);
    }
    else
    {
        while(1)
        {
            int status=0;
            pid_t rid=waitpid(id,&status,WNOHANG);
            if(rid>0)
            {
                cout<<"等待成功 exit_code:"<<WEXITSTATUS(status)<<endl;
                break;
            }
            else if(rid==0)
            {
                cout<<"子进程正在运行"<<endl;
            }
            else
            {
                perror("waitpid");
            }
            sleep(1);
        }
    }
}
创建多进程样例:启停多进程
cpp 复制代码
#include<iostream>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<vector>

using namespace std;

void task()
{
    int cnt=5;
    while(cnt--)
    {
        sleep(1);
        cout<<"我是一个子进程:pid:"<<getpid()<<" ppid:"<<getppid()<<endl;
    }
}
void createchildprocess(int num,vector<pid_t> *subs)
{
    for(int i=0;i<num;i++)
    {
        pid_t id=fork();
        if(id==0)
        {
            task();
            exit(0);
        }
        subs->emplace_back(id);
    }
}

void waitall(const vector<pid_t> &subs)
{
    for(const auto& e:subs)
    {
        int status=0;
        pid_t rid =waitpid(e,&status,0);
        if(rid>0)
        {
            cout<<"等待成功,子进程:pid: "<<rid<<" exit_code:"<<WEXITSTATUS(status)<<endl;
        }
    }
}


int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        exit(1);
    }
    int num=std::stoi(argv[1]);
    vector<pid_t> subs;
    createchildprocess(num,&subs);
    waitall(subs);
    return 0;
}

3.10进程程序替换

3.10.1看现象

cpp 复制代码
#include<iostream>
#include<unistd.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    
    execl("/usr/bin/ls","-a","-l",NULL);

    cout<<"代码运行中"<<endl;

    return 0;
}

execl后的代码没有被执行

3.10.2介绍原理

  • 我们的程序在调用execl之前,物理内存中的代码段保存的是我们自己写的C++代码
  • 调用execl进行程序替换后,内存中的老的代码段会被替换为新的代码(替换目标程序的代码)
  • 程序替换的目标程序的数据段也会从磁盘中加载到内存覆盖掉原程序的数据段
  • 程序替换修改的基本上都是页表的右部分,进程PCB不变,所以不会创建新进程。

程序替换的本质:把代码和数据拷贝到内存中,OS完成该任务(系统调用)。

exec系列的函数成功的时候没有返回值

3.10.3fork版本,替换函数的介绍

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        execl("/usr/bin/ls","-a","-l",NULL);
        exit(0);
    }
    wait(NULL);
    cout<<"代码运行中"<<endl;

    return 0;
}
  • 创建子进程,让子进程进行程序替换,只有子进程的代码段和数据段会被替换。父进程的代码和数据不变继续执行后续代码

介绍程序替换函数

  1. 程序替换函数都有"exec"前缀。
  2. pathname参数,要替换的目标程序的位置,绝对或相对路径。(我们要先找到这个程序/usr/bin/ls,再加载该程序)
  3. 后续参数都为目标程序的执行方式(执行选项:"ls -a -l")
  4. 命令行上怎么执行的,参数就怎么填
  5. 所有程序替换函数的参数最后都要以NULL结束,替换成功都没有返回值,并且不会执行后续代码
  6. 替换出错会返回-1,错误码会标志

execlp:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        sleep(1);
        execlp("ls","ls","-a","-l",NULL);
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        cout<<"success exit_code:"<<WEXITSTATUS(status)<<endl;
    }

    return 0;
}
  • 相比于execl多了一个'p',同时只有第一个参数不一样
  • 参数:只需要写程序名即可,execlp会自动到环境变量path中找目标程序

execv

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        sleep(1);
        char *argv[]={(char*)"ls" ,(char*)"-a",(char*)"-l",NULL}; 
        execv("/usr/bin/ls",argv);

        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        cout<<"success exit_code:"<<WEXITSTATUS(status)<<endl;
    }

    return 0;
}
  • 相比于execl把'l'改成'v',execl的可变参数变成了线性数组,恰好'v'也有线性数组的意思(vector)。

execvp

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        sleep(1);
        char *argv[]={(char*)"ls" ,(char*)"-a",(char*)"-l",NULL}; 
        execvp(argv[0],argv);
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        cout<<"success exit_code:"<<WEXITSTATUS(status)<<endl;
    }

    return 0;
}
  • "v"表示程序替换的选项用数组形式。
  • "p"表示程序替换不用写路径,会到环境变量里去找。

execvpe

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        sleep(1);
        char *argv[]={(char*)"cmd" ,(char*)"-a",(char*)"-b",NULL}; 
        char* envp[]={
        (char*)"PATH=/home/ubuntu/linux-learning/leasson8",NULL
        };
        execvpe("./cmd",argv,envp);
        
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        cout<<"success exit_code:"<<WEXITSTATUS(status)<<endl;
    }

    return 0;
}
cpp 复制代码
#include<iostream>

using namespace std;

int main(int argc,char* argv[],char* env[])
{
    for(int i=0;i<argc;i++)
    {
        cout<<"argv["<<i<<"]:"<<argv[i]<<endl;
    }

    cout<<endl;
    
    for(int j=0;env[j];j++)
    {
        cout<<"env["<<j<<"]:"<<env[j]<<endl;
    }
}
  • **'e':**表示把自己维护的环境变量传给替换后的程序
  • 因为我们自己的程序没有在系统的环境变量中,所要第一个参数要带上路径。

execve

以上的6个函数是库函数,而execve才是系统调用,以上的6个函数底层都是调用execve。

当我们使用库函数时,没有传递环境变量,库函数会把系统默认的环境变量传递给execve

总结:

前缀:"l"表示执行方式为可变参数

"p"表示第一个参数可以只程序名,函数会自动到系统的环境变量中找程序。

"v"表示执行方式要传数组

"e"表示替换后的程序使用的环境变量是我们传递的第三个参数,如果我们不自己传环境变量,也会使用系统默认的环境变量。

替换自己写的程序

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    cout<<"我是一个普通进程"<<endl;
    pid_t id =fork();

    if(id==0)
    {
        sleep(1);
        char *argv[]={(char*)"ls" ,(char*)"-a",(char*)"-l",NULL}; 
        execl("/home/ubuntu/linux-learning/leasson8/cmd","cmd",NULL);
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        cout<<"success exit_code:"<<WEXITSTATUS(status)<<endl;
    }

    return 0;
}
cpp 复制代码
#include<iostream>

int main()
{
    std::cout<<"我自己写的程序"<<std::endl;
    std::cout<<"我自己写的程序"<<std::endl;
    std::cout<<"我自己写的程序"<<std::endl;

}

只要是可执行文件都可以替换。

main函数的参数都是bash通过创建子进程,进行程序替换,把环境变量和命令行参数传递给了main函数

4.编写自定义shell

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局的命令⾏参数表
char* gargv[argvnum];
int gargc = 0;
// 全局的变量
int lastcode = 0;
// 我的系统的环境变量
char* genv[envnum];
// 全局的当前shell⼯作路径
char pwd[basesize];
char pwdenv[basesize];
// " "file.txt
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
string GetUserName()
{
	string name = getenv("USER");
	return name.empty() ? "None" : name;
}
string GetHostName()
{
	string hostname = getenv("HOSTNAME");
	return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
	if (nullptr == getcwd(pwd, sizeof(pwd))) return "None";
	snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
	putenv(pwdenv); // PWD=XXX
	return pwd;
	//string pwd = getenv("PWD");
	//return pwd.empty() ? "None" : pwd;
}
string LastDir()
{
	string curr = GetPwd();
	if (curr == "/" || curr == "None") return curr;
	// /home/whb/XXX
	size_t pos = curr.rfind("/");
	if (pos == std::string::npos) return curr;
	return curr.substr(pos + 1);
}
string MakeCommandLine()
{
	// [whb@bite-alicloud myshell]$
	char command_line[basesize];
	snprintf(command_line, basesize, "[%s@%s %s]# ", \
		GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
	return command_line;
}
void PrintCommandLine() // 1. 命令⾏提⽰符
{
	printf("%s", MakeCommandLine().c_str());
	fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) // 2. 获取⽤⼾命令
{
	// 我们认为:我们要将⽤⼾输⼊的命令⾏,当做⼀个完整的字符串
	// "ls -a -l -n"
	char* result = fgets(command_buffer, size, stdin);
	if (!result)
	{
		return false;
	}
	command_buffer[strlen(command_buffer) - 1] = 0;
	if (strlen(command_buffer) == 0) return false;
	return true;
}
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
	(void)len;
	memset(gargv, 0, sizeof(gargv));
	gargc = 0;
	// "ls -a -l -n"
	const char* sep = " ";
	gargv[gargc++] = strtok(command_buffer, sep);
	// =是刻意写的
	while ((bool)(gargv[gargc++] = strtok(nullptr, sep)));
	gargc--;
}
void debug()
{
	printf("argc: %d\n", gargc);
	for (int i = 0; gargv[i]; i++)
	{
		printf("argv[%d]: %s\n", i, gargv[i]);
	}
}
// 在shell中
// 有些命令,必须由⼦进程来执⾏
// 有些命令,不能由⼦进程执⾏,要由shell⾃⼰执⾏ --- 内建命令 built command
bool ExecuteCommand() // 4. 执⾏命令
{
	// 让⼦进程进⾏执⾏
	pid_t id = fork();
	if (id < 0) return false;
	if (id == 0)
	{
		//⼦进程
		// 1. 执⾏命令
		execvpe(gargv[0], gargv, genv);
		// 2. 退出
		exit(1);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if (rid > 0)
	{
		if (WIFEXITED(status))
		{
			lastcode = WEXITSTATUS(status);
		}
		else
		{
			lastcode = 100;
		}
		return true;
	}
	return false;
}
void AddEnv(const char* item)
{
	int index = 0;
	while (genv[index])
	{
		index++;
	}
	genv[index] = (char*)malloc(strlen(item) + 1);
	strncpy(genv[index], item, strlen(item) + 1);
	genv[++index] = nullptr;
}
// shell⾃⼰执⾏命令,本质是shell调⽤⾃⼰的函数
bool CheckAndExecBuiltCommand()
{
	if (strcmp(gargv[0], "cd") == 0)
	{
		// 内建命令
		if (gargc == 2)
		{
			chdir(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 1;
		}
		return true;
	}
	else if (strcmp(gargv[0], "export") == 0)
	{
		// export也是内建命令
		if (gargc == 2)
		{
			AddEnv(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 2;
		}
		return true;
	}
	else if (strcmp(gargv[0], "env") == 0)
	{
		for (int i = 0; genv[i]; i++)
		{
			printf("%s\n", genv[i]);
		}
		lastcode = 0;
		return true;
	}
	else if (strcmp(gargv[0], "echo") == 0)
	{
		if (gargc == 2)
		{
			// echo $?
			// echo $PATH
			// echo hello
			if (gargv[1][0] == '$')
			{
				if (gargv[1][1] == '?')
				{
					printf("%d\n", lastcode);
					lastcode = 0;
				}
			}
			else
			{
				printf("%s\n", gargv[1]);
				lastcode = 0;
			}
		}
		else
		{
			lastcode = 3;
		}
		return true;
	}
	return false;
}
// 作为⼀个shell,获取环境变量应该从系统的配置来
// 我们今天就直接从⽗shell中获取环境变量
void InitEnv()
{
	extern char** environ;
	int index = 0;
	while (environ[index])
	{
		genv[index] = (char*)malloc(strlen(environ[index]) + 1);
		strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
		index++;
	}
	genv[index] = nullptr;
}
int main()
{
	InitEnv();
	char command_buffer[basesize];
	while (true)
	{
		PrintCommandLine(); // 1. 命令⾏提⽰符
		// command_buffer -> output
		if (!GetCommandLine(command_buffer, basesize)) // 2. 获取⽤⼾命令
		{
			continue;
		}
		//printf("%s\n", command_buffer);
		ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令
		if (CheckAndExecBuiltCommand())
		{
			continue;
		}
		ExecuteCommand(); // 4. 执⾏命令
	}
	return 0;
}
相关推荐
深蓝海拓20 小时前
PySide6之QListWidget 学习
笔记·python·qt·学习·pyqt
A9better21 小时前
嵌入式开发学习日志46——FreeRTOS之列表与列表项
学习
胡萝卜的兔21 小时前
ubuntu安装,使用
linux·运维·ubuntu
海盗儿21 小时前
(一)TensorRT-LLM 初探(version: 1.0.0)
linux·运维·windows
2301_7811435621 小时前
联考——言语理解与表达笔记(一)
笔记·学习·考公
阿拉伯柠檬21 小时前
传输层协议TCP(二)
linux·服务器·网络·网络协议·tcp/ip·面试
运维帮手大橙子21 小时前
从基础到体系:我的年度技术学习与实战总结
经验分享·学习
HIT_Weston21 小时前
85、【Ubuntu】【Hugo】搭建私人博客:文章目录(四)
linux·ubuntu·html5
0和1的舞者21 小时前
Python编程入门:从基础到实战
开发语言·python·学习·入门