目录
前言
进程是学习多线程开发的一个重要的前置知识。认识进程的主要内容,对于我们后续理解多线程有着很大的帮助。
一、进程的基本认识
1. 理解进程
当我们在电脑上安装一个软件时,在其安装路径下一般都会产生一个 .exe 文件,这个文件叫做可执行文件。此时该文件只是“ 安静地躺在 ”硬盘上,因此它只是一个可执行文件或可执行程序。而当我们双击该文件,此时它就会被操作系统所加载因而处于运行状态中,此时就会在系统中创建出一个相对应的进程。所以,进程便是一个运行着的、跑起来的可执行程序。
当我们多次双击启动 .exe 可执行文件,就可能会产生多个进程,但是它们都是由同一个可执行文件所启动的。因此,有一种说法便是:进程是可执行程序的一个实例。就如同 JavaSE 中的对象是类的一个实例。
进程是系统分配硬件资源的基本单位。就如同疫情期间,政府给因疫情防控而被封在自家小区的每一户发菜,无论这一户多少人口,反正就发固定的菜品和菜量,而其中的小白菜、菠菜、小青菜等菜品就可以对应着 cpu、硬盘、内存等硬件资源一般,由系统分配给“ 每一户 ”,也即是每一个进程。
2. 进程和程序的区别
平时我们所说的程序,其实指的就是 .exe 可执行文件,它保存在硬盘中,是静止的、没有运行的、只占据硬盘资源的可执行文件。
进程是我们双击可执行程序之后,操作系统将这个可执行程序中的指令和数据加载到内存中之后,再让 CPU 去执行其中的指令,完成后续的一系列工作,此时就会创建出一个进程。而既然进程是运行状态中的,势必会消耗硬件资源,如:内存、硬盘、CPU、网络、带宽……
因此程序和进程并非相同的概念,它们之间的状态、所消耗资源等都存在着差异。
二、进程的管理
当我们启动电脑,即使不打开其他的应用,也会在任务管理器处发现已经运行着几十个甚至上百个进程了。当进程数量一多,操作系统就需要对其进行管理,加以管控,才能提供给应用程序一个稳定的运行环境。
操作系统的管理的思路和步骤其实也和我们日常生活中的大部分简单的管理思路是大致相同的:
1)描述
2)组织
例如:上学读书,刚入学的时候,学校肯定需要收集每一个学生的基本资料,然后汇总成每个人的基本资料表中,这便是第一步中的描述。收集完成之后,便需要将学生有序地组织起来,给每个学生分配一个对应的学号,分配到对应的班级中,这便是第二步中的组织。
操作系统管理进程的核心思路也如上面的两个步骤一般:
1)描述:由于操作系统底层都是使用 C/C++ 来编写的,于是它便使用 C 语言中的结构体来描述进程中所包含的基本信息内容,这个结构体叫做:进程控制块,简称:PCB(Process Control Block)。【C语言中的结构体就相当于 Java 中低配的类,只有属性没有方法】
2)组织:在 Linux 系统中,操作系统是使用双向链表来组织 PCB 的,而对于 PCB 的操作也就转变为了对双向链表的操作了。例如:
查看进程列表:遍历该双向链表;
创建一个进程:创建一个 PCB 结构体并将其插入到双向链表中;
销毁一个进程:删除对应的 PCB 结构体并将其释放;
……
需要注意的是,由于 Windows 并不是开源的系统,因此暂时无法得知 Windows 系统中的 PCB 是如何进行组织的。
三、PCB主要包含的信息
从上文中我们已经知道,操作系统中是使用 PCB 来描述一个进程中所包含的信息的,那么 PCB 中具体包含了哪些内容呢?这便是下文的重点。但是由于篇幅有限以及 Java 无需像 C++ 一样进行深入的研究,因此下文中只是列举中其中较为重要的部分,实际内容远不止如此。
1. pid——进程的标识
pid 就类似于数据库中的主键一般,唯一标识出一个进程。在同一个系统、同一时刻,不同进程的 pid 一定不相同,如果是不同系统或者不同时刻下,就无法保证了。
但是当我们打开任务管理器时,会发现有些进程并没有显示出其 pid 。如下图所示
这种情况其实并不是该进程没有对应的 pid ,而是该进程运行时,还创建了相关联的其他多个进程,呈现出了一个多进程的效果,打开下拉列表就会发现,其中的每一个进程实际还是对应着一个 pid,如下图所示:
2. 内存指针
内存指针是和内存资源相关的。
该内存指针指的是一组指针而非一个指针。内存指针表示了该进程所对应的内存资源大概是怎么样的。内存指针中主要存储的便是从 .exe 可执行文件中加载过来的指令和数据。
指令指的是什么?指令指的是程序猿编写该程序时写的代码逻辑,编译之后转化为 cpu 可识别的二进制指令,这一系列二进制指令就会被存储到该程序的 .exe 可执行文件中。当该程序运行成为进程时,这些二进制指令就会被加载到内存中,下一步就进而交给 cpu 去进行执行。也就是说,指令 指的就是二进制指令,就是程序猿编写该程序时写的代码逻辑转化为的二进制形式。
数据便是指执行指令时所需要依赖的数据,以及进程运行过程中所产生的一些中间数据等。
3. 文件描述符表
文件描述符表是和硬盘资源相关的。
我们都知道,应用程序是无法直接和硬件层直接打交道的,于是乎操作系统便抽象出“ 文件 ”这一概念。应用程序可以直接操作文件,而文件时存储在硬盘上的。因此通过操作文件,应用程序便可以间接操作硬盘了。
每一个进程都有一个文件描述符表,用来记录该进程具体使用了哪些文件。每当操作系统打开一个文件时,都会产生一个对应的“ 文件描述符 ”,来唯一描述该文件,而文件描述符表便是来组织这些文件描述符的。而这些文件描述符只在进程内部生效,进程之外是没有文件描述符这一概念的。
4. 关于进程调度相关的属性
首先我们需要知道进程调度是什么?
上文中已经说到当系统启动时就已经有百八十个进程了,但是 cpu 却只有一个,面对着这么多个进程,就只能轮着来用了。
这就像拍戏一样,拍摄场地只有一个,同一时间却有着许多剧组,因此就只能排队来轮着用了,这一剧组用完了才轮到下一剧组来使用。而 cpu 就像拍摄场地一样,进程就像这许多的剧组。
虽然 cpu 只有一个,但是拍摄场地却可能是划分了多个了。早期的 cpu 是只有一个核心的,但是随着时代的发展,现在的 cpu 有了双核、四核、六核、八核……每一个核心就代表着一块拍摄场地。再者,现在的 cpu 核心指的是物理核心,其实际使用起来可以当作两个核心来使用,这两个核心算是逻辑核心。例如现在常见的八核十六线程,实际作用起来就像是十六个核心在工作一般,如十六块拍摄场地。也就是看起来是一个核心,但是使用起来便当作两个来使用,这便使得拍摄场地再次变多。
但是即便有着十几个核心,面对着百八十个进程,仍然是需要轮着来使用,而这一步“ 轮着用 ”,便是进程的调度。
进程调度分为并行和并发两种方式:
并行指的是:同一时刻,两个进程,在两个逻辑核心上同时运行;
并发指的是:同一时刻,两个进程,在一个逻辑核心上轮流运行。但是由于 cpu 切换进程的速度尤为之快,因此在宏观看来就像是“ 同时 ”运行一样,只在微观层面才能看出这是轮流运行的。
操作系统可以按照上述方式来调度进程,但是在应用程序层,是感知不出具体使用了哪种方式的,由于其间差距甚微,因此我们通常只说“ 并发 ”一词,并且谈到并发时,指的是并行和并发两种方式一起。
了解了什么是进程之间的调度之后,那么下面便是有关于 cpu 进程调度的属性,这些属性就介绍了一个进程所耗费的 cpu 资源的情况:
1)状态
进程的主要状态就有两种:就绪状态和阻塞状态/休眠状态
就绪状态,就说明该进程是一个时刻准备好在 cpu 上执行的状态,即是“ 呼之即来挥之即去 ”;
而休眠状态,或者叫做阻塞状态,即说明该进程现在无法被调度到 cpu 上执行。
这两种状态不是固定的,是可以进行相互转换的。
且进程的状态也不止这两个,只是这两个较为核心且重要,因此就列举出来而已。
2)优先级
当我们玩游戏时,cpu 为了给我们最好的游戏体验,于是就把大量的硬件资源倾斜到游戏的进程中,此时便说明这些进程的优先级较高。我们也可知道,系统分配资源时并不是按照均匀分配,而是按照进程之间优先级的不同来进行分配,优先级高的进程则会得到更多的资源。
3)上下文
由于进程过多,但是又都需要同时运行,于是才有了进程调度这一概念,那么就说明进程是轮流在cpu 上执行的,那么每次执行完,就一定需要记录下来本次执行的进度和相关数据,等到下一次轮到该进程继续执行时,才能够根据上一次执行后的进度继续执行下去。就类似于游戏中的存档读档一样。
具体一点展开,对于操作系统来说,所记录下来的上下文数据到底是什么呢?答案就是 cpu 上的寄存器中的数据,而寄存器中所存储的便是进程运行过程中所产生的一些中间结果、下一条指令的位置、函数之间的调用关系……这些数据都需要进行保存,等到下一次轮到该进程回到 cpu 继续执行时,才能够取出这些数据从而正常地继续往下执行,因此,上下文就显得尤为重要。
4)记账信息
记账信息便是对进程的资源使用情况的一个总结,会统计该进程具体在 cpu 上执行了多久,执行了多少条指令,也是对进程的调度工作的一个“ 兜底 ”。
如果根据记账信息发现由于该进程的优先级较低而导致分到的资源过少时,系统下次分配资源就会适当地进行调整。
实际上有关于进程调度的属性远不止这些,只是列举出这些关键点来展开讲解。感兴趣的可以自行查阅资料进行了解。
四、进程的其他知识点
1. 虚拟地址空间
我们都已经知道了进程运行过程中需要占用一定的内存资源。在早期的操作系统中,就是直接地将物理内存空间分配给各个进程,那么就可能会出现问题:一旦某个进程产生bug而内存越界访问了,就会影响到其他的进程的运行。因此就需要在校验进程的操作是否会导致内存越界,而这一步在当时是需要程序猿来检验,但是我们知道:想要靠人靠谱,本身就是最不靠谱的。因此到了后来,操作系统就引入了“ 虚拟地址空间 ”这一概念。
所谓虚拟地址空间,就是在给进程分配内存空间时,先虚拟出一块内存空间以及对应的虚拟内存地址,然后再转换到物理内存空间和物理内存地址。由于有了转换这一步操作,那么就可以依靠可靠的系统来帮我们进行校验。
虚拟地址并非是在物理内存上存在的,只是存在于进程之中。但是对于应用程序,只能看到虚拟的地址而无法看到其对应到的物理地址。
不同的进程可以有相同的虚拟地址,因为它们最终对应到的仍然是不同的物理内存。操作系统可以将虚拟地址和物理地址进行灵活的转换。
下面就是一个简略的示意图:
有了这样的设定,进程可以操作的虚拟地址空间都只在一定范围内,每个进程内出了 bug 都由该进程自己负责而不再影响到其他进程。且校验任务交给了系统之后,大大提高了系统的稳定性。
举个例子:还是在学校内,每个学生都会有一个自己的学号,该学号在这个学校内是独一无二的每个学生的学号都是不一样的。但是在学生所属班级内,还会有自己的座位号,这个座位号不同班级之间就会有相同的了,如一班会有一个1号的同学,那二班三班四班也都会有一个1号的同学,但是这不妨碍到他们对应到的学号是不同的。分班之后的座位号只是为了更加方便,例如一班的10号同学是小明,一个班级最多30人,那么当老师点名时,喊到的号数要么是本班内的人,要么就是31、32这种超出班级人数限制的无效号数,但是依旧不会影响到其他班级。而如果没有座位号,依靠学号来点名,那么就很可能会点到一个二班或者三班同学的学号了,从而影响到了其他班级的同学。
在虚拟地址空间的加持下,每个进程之间有了一定的独立性,即每个进程都拥有属于自己的一块虚拟地址空间,其他进程无法直接访问,自己也无法影响到其他的进程。
2. 进程之间的通信
第1点中展示了进程的独立性所在,但是有的时候我们又希望它们不要过于独立,要进行相互的联系和合作来完成任务。于是便需要进程之间的通信了。
进程之间的通信方式有很多种,但是究其本质,都是找到一块公共区域来完成数据的交换,而根据公共区域的不同分为了多种方式而已。这种联系方式是有限制的联系,不会过多地相互影响。
常见的方式有:
1)基于文件的通信,即公共区域为硬盘;
2)基于网络的通信,即公共区域为网卡。
总结
进程的内容其实远不止这些,但是由于 Java 语言的特性,学习 JavaEE 时无需过度深究,更多的是知其大概即可,理解进程所涵盖的主要内容即可,因此上述内容只是对进程进行了一个很粗略很简要的介绍,更多内容还需要查阅更多的资料来进行相关学习。