进程概念
一、冯诺依曼系统
我们可以看到输入设备和输出设备并不是完全独立的。比如我们以前的文件操作是从磁盘中读取
为什么能直接把外设的数据加载到CPU中?
现在大部分的体系结构都是冯诺依曼体系,比如我们的电脑和使用的服务器。
二、操作系统
首先要知道,启动的操作系统才有意义,就是只有把操作系统加载到内存中才有意义。
2.1 OS层次图
2.2 操作系统的意义
如果没有操作系统,我们就要和一堆的硬件直接接触,需要了解各种硬件的特性,使用成本太高。
由此就有了操作系统:
OS包括:
操作系统是什么?
是一款专门针对软硬件资源管理工作的软件。
操作系统的功能:
对下管理好软硬件资源
对上为用户提供稳定、高效、安全的运行环境
那OS怎么对用户提供各种功能?
2.2.1 系统调用与库函数的区别
2.3 管理的理解
既然是管理,那么就有管理者和被管理者。
通过例子我们可以得出以下的结论:
而管理者的决策需要有依据,依据就是数据。
而我们的数据怎么被管理者知道呢?
如果一个学校人很少,校长有可能能知道每个人的数据,但是如果人很多呢?该如何聚合这些数据呢?
这个过程,我们通常叫做数据的描述
而如何将这些聚合数据产生关联呢?
这个过程,我们通常叫做数据的组织
由此我们得出重要结论:
计算机执行管理时,先把管理对象描述起来,再把管理对象组织起来
以上讲的都是对硬件的管理,那么对软件呢?
三、进程
3.1 进程的概念
进程=程序对应的文件内容(struct)+相关数据结构(比如优先级队列)
在Windows中任务管理器下的程序就叫进程。
上面我们说过操作系统的本质是管理,而管理是先描述再组织。
所以在进程形成的时候,操作系统会对该进程创建PCB(进程控制块)。
3.2 描述进程-PCB
PCB其实是一个结构体,包含进程的所有属性信息,在linux中,PCB就是
struct task_struct{};
而这两个的关系可以类比类和对象之间的关系。
task_ struct内容分类
操作系统要找找到进程,不是找进程加载到内存的代码数据,是直接找PCB。
有了进程控制块,所有的进程管理与进程对应的程序毫无关系,与进程对应的PCB强相关。
3.3 进程和程序
我们写好了程序,当程序被加载到内存中时,操作系统会创建一个task_struct来描述程序,我们把task_struct 和加载到内存的可执行程序一起叫做进程。
也就是上面说的:进程=程序对应的文件内容(struct)+ 相关数据结构(比如优先级队列)
3.4 PCB内容
3.4.1 查看进程
查看进程
先写一个死循环的程序:
查看进程指令:
有两种指令:
运行后打开新的对话框:
另一种查看进程的方法:
proc是linux默认查看进程的目录,而我们创建的的进程的信息也会再proc中创建一个目录。
pid在后面会讲
3.4.2 标识符
运行后:
3.4.3 状态
退出信号:
我们写的程序末尾会有个return 0
,0就是退出码
3.4.4 程序计数器
永远指向将被执行的下一行指令的地址。
3.4.5 记账信息
OS有一个调度模块,可以较为均匀的调度每个进程,因为进程要获得CPU的资源才能进行,而进程又有很多。他会记录每个进程所使用的时间总和以确保“公平”。
3.4.6 上下文信息❗️❗️
假设有三个进程正在运行:
为了保证公平,操作系统规定了进程单次运行的时间片(单次运行的最长时间),我们以为CPU在同时运行多个进程,其实是CPU的快速切换完成的。
但是进程很有可能没运行完,此时PCB1就拍到了末尾。
进程运行会产生大量的临时数据
但是这样就会引出一个问题:
当PCB1运行完一次时间片后这些临时数据下次还要用,如果不管,PCB2就会覆盖掉PCB1的临时数据。
四、fork系统调用创建进程
4.1 fork的理解
那么如何理解fork创建子进程呢?
首先要知道创建进程的三种方式:
fork的本质是创建一个进程,而这个进程就是task_struct + 进程的代码和数据,task_struct是一创建进程就有的,但是进程的代码和数据怎么办?
写时拷贝:
4.2 fork的返回值
经过上述描述,我们会想到一个问题:
如果只掉用fork,父子就做同一件事,这有意义吗?
那么怎么让他们做不一样的事情呢?
fork的返回值:
如何理解有两个返回值?
pid_t fork()
{
……
return ……
}
为什么要如此设置两个返回值?
补充一点:
五、进程状态
首先想想为什么要有进程状态?
linux中的进程一般有以下状态:
5.1 运行状态
前面我们讲过进程在运行状态不一定占有CPU,而是运行一个时间片后让下一个进程获得CPU资源。
只要在运行队列中,所有的状态都叫做R状态,随时被CPU调用。
5.2 睡眠状态和休眠状态
如果一个程序需要的资源没有准备好,那么程序将会睡眠,等待资源准备就绪了它才会再次运行
假如当程序需要读取磁盘时,此时已经已经有多个进程在等待,这是就会形成等待队列,就是S(睡眠)或者D(休眠)状态。
总结一下:
一个进程可能因为运行的需要,在不同的队列里,每个队列代表不同的运行状态。
那么睡眠状态(S)和休眠状态(D)有什么区别呢?
为什么要有两种“睡眠”状态?
为什么数据刷新这么快,还是S状态?
5.3 停止状态和死亡状态
停止状态跟S状态很像,都可以挂起进程,那么他们有什么区别呢?
死亡状态(X):
我们在创建进程的时候会有PCB和代码和数据,当进程死亡的时候,也要把这些资源回收回去。
5.3.1 前台和后台进程
我们看到他们的状态有的后面带+号有的不带,这表示什么呢?
后面带+号表示前台进程
后面不带+号表示后台进程
5.4 僵尸进程
上面我们说了进程需要退出时,系统会回收这个进程的资源,然后进程就进入了死亡。
但是当进程退出的时候,进程的所有资源不会立即回收,而是进入僵尸状态(Z),把数据暂时保存(要写入退出信息),目的是为了判断退出死亡的原因。
资源是由父进程进行回收。
验证:我们可以让父进程休息,然后杀死子进程就可以看到僵尸状态。
僵尸进程我们需要尽量避免,因为会导致过度占用空间和内存泄漏
5.5 孤儿进程
字面意思,父子进程同时运行,父进程被杀掉,子进程就变成了孤儿进程。
孤儿进程将会被1号进程(OS本身)领养
六、进程优先级
首先要知道为什么有优先级,本质是因为资源过少。
优先级也是PCB中的一个数据。
它决定了程序获得CPU资源的顺序。
6.1 查看优先级
ps
指令
ps指令主要用来显示linux进程信息
选项:
PRI的最终值也取决于NI值(PRI=80+NI)
6.2 调整优先级
调整优先级其实就是调整NI值。
我们知道NI的范围只能是-20 ~ 19,那么为什么不让范围更大呢?
6.3 并行与并发
并行:
并发:
七、环境变量
为什么我们运行自己的程序的时候要加上./
,./
就是指明当前路径,要找到程序才能运行,而我们运行指令的时候却不用加呢?
7.1 环境变量的定义
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性(这就是我们使用某些程序不需要./的原因)
要注意他也是OS在内存中开辟的空间用来保存数据。
常见的环境变量:
7.2 环境变量的操作
env命令可以查看系统所有变量
我们也可以把我们自己的程序路径加到环境变量中
使用export指令:
export PATH=
此时我们就可以像运行指令一样运行我们自己的程序:
但此时我们发现我们使用不了正常的指令了
再查看环境变量:
可以看到原来的被覆盖了。
所以我们添加环境变量不覆盖应该使用:
export PATH=$PATH:
7.3 命令行参数
main函数可以携带两个参数:
int main(int argc, char* argv[])
所以我们知道argv的图:
从这里我们也就知道命令行后面附加的参数是如何起作用的了。
7.4 获取环境变量
上面说过ls -al
这种命令因为有环境变量可以不带路径执行,那么他是怎么得到环境变量的呢?
main函数可以携带第三个参数:char* env[]
。 它可以从父进程中得到。
上面讲的argv指向的是一个个的参数,而env指向的是一个个的环境变量。
而且我们也可以把它打印出来:
可以参考上面的env命令,发现他们一摸一样。
当然我们也可以通过系统调用获得环境变量:
getenv()
7.5 环境变量的全局属性
环境变量通常具有全局属性,可以被子进程继承下去
创建一个本地变量:
导出环境变量:
八、程序地址空间❗️❗️
8.1 物理地址or虚拟地址?
在我们以前学到的内存布局应该是这样的:
但是这是真实的物理内存吗?
现在我们可以编写一段代码验证一下:
这里因为父子进程公用代码和数据,结果正常。
但是当父进程发生写时拷贝时:
我们发现他们的地址没有变化
前面我们讲过如果发生写时拷贝,就会另外开一份空间来存储数据,那么地址肯定会变化。
这只能说明打印的地址不是真实的物理地址。
我们把这个叫做虚拟地址,我们在语言层面下看到的地址,其实全部都是虚拟地址!
8.2 进程地址空间
OS中有物理地址,而每个进程都有自己的虚拟地址。
每个进程都会认为自己是独享整个物理地址,它们都会以统一的方式划分自己的内存。
而每个进程都有自己的地址空间,而操作系统需要管理这些地址空间。
进程地址空间本质是内核的数据类型(struct mm_struct),类似于我们前面讲的PCB。
结构体的模拟如下:
struct mm_struct
{
unsigned int stack_begin;
unsigned int stack_end;
//栈区的界限
unsigned int heap_begin;
unsigned int heap_end;
//堆区的界限
//其他区域也类似
}
通过这些区域划分出不同的界限。每个进程都认为自己的mm_struct代表整个内存(地址从0x000000…000 ~ 0xFFFFFF…FFF),也就是每个进程都认为自己拥有4GB的空间。
但是光有虚拟地址是不够用的,必须要把数据存储在物理内存中。
8.3 页表+MMU
OS通过页表+MMU(内置在CPU中的一个硬件)来对齐进行映射
页表负责把虚拟地址映射到物理地址
那么为什么需要页表映射,不能直接让进程地址空间访问物理内存吗?
举个例子
const char* p = "abcd"
,我们不能通过指针p修改"abcd",因为它在字符常量区。本质是OS给我们的权限只有读权限(通过页表的权限管理)
8.4 地址空间的好处
我们想象一种场景:我们直接申请一块大空间,操作系统会直接全部给我了吗?如果我们没有用那么多,其他的空间不是被浪费了吗?
假设没有地址空间,那么CPU是不是只能遍历一遍物理空间才能找到该进程的起始位置?
总结一下:
为什么要有地址空间?
不管代码数据在内存如何存放,在地址空间中一定是连续存放的,大大减小了内存管理的负担。
通过以上的学习,我们再来谈程序和进程的区别: