目录

一、冯诺依曼体系结构

1.冯诺依曼体系结构的五大构成

(1)存储器

(2)输入设备和输出设备

(3)运算器和控制器

2.CPU执行

(1)CPU数据来源

(2)内存、CPU和磁盘的数据交换

3.远端数据传输过程

二、操作系统

1.操作系统的概念

2.理解硬件管理

(1)管理的本质

(2)数据获取

(3)管理的方法

3.对软件的管理

4.系统调用和库函数的关系

三、进程

1.进程的概念

(1)内存中的进程

(2)PCB(进程控制块)

(3)进程管理

2.查看进程

(1)windows下的进程查看

(2)linux下的进程查看

(3)结束进程

3.进程的常见调用

(1)通过系统调用获取进程标示符(进程id和父进程id)

(2)通过系统调用创建进程

4.进程的状态

(1)程序与内存与CPU的关联

(2)状态种类

(3)运行状态

(4)阻塞状态

(5)挂起状态

5.linux中的进程状态查看

(1)运行(running)状态

(2)休眠(sleeping)状态

(3)停止(stopped)状态

(4)深度睡眠(disk sleep)状态

(5)追踪(tracing stop)状态

(6)死亡(dead)状态

(7)僵尸(zombie)状态

(8)linux下的进程状态后的“+”含义

6.僵尸进程与孤儿进程

(1)僵尸进程

(2)孤儿进程

7.进程优先级

(1)进程优先级的作用

(2)linux下的进程优先级特点

(3)linux下的进程优先级修改

8.进程相关概念

(1)竞争性

(2)独立性

(3)并发

(4)并行

9.进程切换

四、环境变量

1.PATH环境变量

(1)将程序路径添加进该环境变量

2.添加环境变量

(1)添加本地变量

3.取消环境变量和本地变量

4.查看环境变量内容

(1)查看环境变量

(2)查看本地变量和环境变量

5.windows下的环境变量

6.int main()主函数中的参数

(1)int argc和char* argv[]

(2)char* env[]

五、进程地址空间

1.C/C++地址空间

2.虚拟地址空间

(1)进程认为自身独占系统资源

(2)系统如何使进程认为自己独占所有资源

3.虚拟地址

(1)进程地址空间的区域划分

(2)虚拟地址

4.页表

5.虚拟地址空间存在原因

(1)保护其他进程和用户信息

(2)保证进程的独立性

(3)便于编译器以统一视角编译代码

5.线性地址与逻辑地址

(1)线性地址

(2)逻辑地址


一、冯诺依曼体系结构

在了解linux下的进程之前,我们要先对计算机的构成有一个基本的认识。计算机的组成是由一位名叫冯·诺依曼的数学家提出的。由此,计算机的构成也被叫做“冯诺依曼体系结构”

冯诺依曼体系结构的构成如上图所示。主要由“输入设备”“输出设备”“存储器”“运算器”“控制器”五个组件构成。因为本段落并不是主讲计算机构成,而仅仅是为linux下的进程做铺垫,因此不会过于细致深入的讲解。如果想要更加深入的理解计算机,可以去看《深入理解计算机系统》

1.冯诺依曼体系结构的五大构成

(1)存储器

存储器,简单来讲就是存储数据的地方。而在计算机中,存储器主要指的是“内存”,计算机中的程序一般都是要加载到内存中去运行。这个“内存”要与我们日常说的磁盘空间的容量相区分。

磁盘空间的容量是外存,但是因为日常中大部分人对计算机的内存和外存区分不清楚,因此统一都是叫“内存”磁盘空间的外存的特点是有“永久性存储能力”,即存储的数据不因计算机的关闭而丢失。而内存有一个特点,就是“掉电易失”。指的是如果计算机断电了,内存中的数据就会丢失

(2)输入设备和输出设备

输入设备输入设备,简单来讲就是计算机的外设。鼠标、键盘、磁盘和网卡等都属于计算机的输入或输出设备。注意,一个外设既可以是输入设备,也可能是输出设备。例如磁盘,我们可以从外部输入数据到磁盘上,也可以从磁盘上读取数据

(3)运算器和控制器

运算器是用于处理计算机中对数据的数学计算和逻辑运算等的组件。控制器则是完成指令相关操作的组件。这两个组件没必要多说,我们只需要知道“运算器 + 控制器 + 部分其他组件 = CPU”,即中央处理器即可

2.CPU执行

在日常编写代码的过程中,不知道大家有没有想过,CPU究竟是如何识别代码中所要传递的信息的。要知道,CPU虽然计算和运行速度非常快,但是其本身只能被动的接受传输过来的数据和命令,并按照传过来的命令执行对应的操作。就如同我们在高中生活中只能被动接受老师的指令进行学习一般。

但是CPU要执行对应的命令,就必须先明白我们传过去的数据的意思。为此,CPU自身存在一个指令集,该指令集中就是CPU所能执行的命令。同时我们知道计算机底层是由0,1信号组成的。而我们写代码编译的本质其实就是一个翻译的过程,即将我们的代码翻译为“二进制可执行程序”。计算机将该程序传给CPU,CPU再根据指令集进行对应的操作

(1)CPU数据来源

同时,CPU在读取写入数据时,为了提高计算机的运行效率只和“内存”打交道。但是,内存中在一开始是没有数据的内存中的数据是来源于磁盘,也就是外存的。我们的计算机中的各种程序、文件等数据都是存在磁盘之中,当内存需要时再从磁盘加载到内存中

但是,我们说了在CPU中读写数据是为了提高效率。如果每次需要数据都从磁盘先加载内存,再从内存加载到CPU,那么效率其实并不会有什么提升。为此,磁盘中的部分数据其实是会提前加载到内存中的。比如我们的操作系统。其实我们的操作系统也是一个软件,其数据在关机状态下存于磁盘中,当我们开机时就会将操作系统的信息加载到内存中。所以其实我们计算机开机的过程就是在加载数据到内存。因此,一个程序要运行必须加载到内存

(2)内存、CPU和磁盘的数据交换

由此,内存也可以看做一个容量很大的缓存,用于与CPU和磁盘之间数据交换的适配。CPU和磁盘都会预先将数据加载到内存,在需要时从内存中刷新数据到磁盘或CPU中。我们经常说的IO过程,其实就是数据从外设中加载到内存和从内存加载到外设中的过程

而内存中需要刷新数据,在有些数据无用时需要删除数据等等。这些操作内存自身是无法完成的,而是交由操作系统完成。

3.远端数据传输过程

在日常生活中,我们肯定都有过通过QQ或微信给别人发送信息的经历。但是大家有没有想过这个从自己的设备发送信息到他人的设备的过程是怎么样的呢?这个问题当我们了解了冯诺依曼体系结构后就能有一个初步的认知

假设你和你朋友的QQ已经处于登录状态,已经加载到了内存当中。现在你和你的朋友之间发了一条“你好”的信息。此时这条信息会先从你的输入设备,也就是键盘或触摸屏传输数据到内存中的QQ的程序中,然后从内存里加载到输出设备,也就是网卡中。然后这条信息通过网络传输到你朋友的输入设备网卡中,再从输入设备中加载到你朋友的内存的QQ程序中,最后从内存中加载到你朋友的输出设备显示器上。上述过程不考虑网络传输过程和内存中的数据处理等问题。由此,远端的数据传输可以看成如下图所示:

二、操作系统

因为操作系统太过庞大,在这里想要完全讲完是不可能的,因此此处主要是对操作系统的一个初步理解

1.操作系统的概念

操作系统,简单来讲就是一个进行软件硬件资源管理软件。那么操作系统为了要对软硬件进行管理呢?原因是为了通过合理的管理软硬件资源(方法),为用户提供良好的(稳定、高效、安全)执行环境(目的)。

而操作系统的管理主要涉及四个方面:“进程管理”“文件系统”“内存管理”“驱动管理”

2.理解硬件管理

(1)管理的本质

在理解操作的管理之前。我们要先对管理由一个初步的认知。

为了更好的理解,我们在这里举一个例子。我们在学校里都有一个共同的管理者,也就是校长会对我们进行管理。在这里,校长是管理者,我们则是被管理者。但是,大多数学校里面的人,其实和校长并没有交集,甚至可能没有见过面。但是校长依然能够将整个学校里的人管理的井井有条。这就是说明:“管理者不需要和被管理者直接交互,依然能够管理好被管理者”

然后,在学校里我们也有辅导员和班长,有人可能认为辅导员和班长也是管理者。但是他们在本质上其实并不是管理者,仅仅只是管理者下的执行者,执行管理者下达的管理方法。因为他们并没有对管理上重大事宜决策的权力。由此,管理者还需要具备一个因素,就是“拥有对重大事宜的决策权”

同时我们要知道,管理不是乱管理,每项决策的通过执行都需要有对应的依据。以学校管理为例,学校要对学生进行管理,就需要有学生对应的身份信息,学习成绩等各类信息。由此,学校的管理就是“对数据进行管理”。而管理的本质也就是“对数据进行管理”

(2)数据获取

既然管理者要对被管理者进行管理的本质是对数据进行管理,但是管理者和被管理者又不直接接触,那么被管理者的数据管理者要怎么拿到呢?由此,在管理者和被管理者之间就还有一个执行者

当然,这里是进行了简化的,实际情况会更加复杂,但是大框架就是这样。

现在有了执行者这层关系,管理者想要获得数据,就是下达指令给执行者执行者与被管理直接接触拿到对应的数据,然后执行者再将获取的数据交给管理者管理者再通过得到的数据进行决策

将这层关系推到操作系统上,就可以得出操作系统对硬件进行管理时,会通过一层执行者,也就是驱动程序进行管理:

有人可能不太理解什么是驱动。驱动简单来讲,就是使我们的各类硬件可以正常使用的程序。我们的键盘、鼠标等硬件并不是插入接口后就能直接使用的。有些人可能就遇见过硬件已经插入接口了,但是无法正常使用,并且计算机中显示“未查找到驱动程序”“驱动程序损坏”等提示。这就是因为硬件的使用需要操作系统通过驱动程序来进行管理,驱动程序损坏就会导致对应的硬盘无法被正常管理使用。

(3)管理的方法

我们依然以学校为例子。一个学校少则上千人,多则上万人。校长作为管理者,如果单凭一个人管理数据,无疑是不太现实的。但是学生虽然很多,但是每个学生的数据都是有共同点,无非就是包括姓名、地址、学号、身份证号等各类个人信息。由此,通过这些个人信息我们就可以抽象出一个struct结构体

我们让学生将自己数据输入到该结构体中,并利用指针的形式形成一个链表。由此,对学生数据的管理就演变成了对链表的管理。当我们需要对应学生数据时,直接操作该链表即可。通过这种方式,就大大简化了操作的复杂度。

而这个通过学生的信息特点进行描述的过程就是一个对管理对象的信息进行建模的过程。同时,这个构建结构体的过程在C++中就可以看做是“面向对象”的过程。

通过上述例子,其进行管理的过程其实就是在先对信息进行描述,再根据描述的内容进行建模。因此,管理的方法其实就可以看做是“先描述,再组织”

3.对软件的管理

首先我们要知道,软件不仅可以管理硬件,也可以管理软件。就好比学校里的校长作为一个人,不仅可以管理学校内的各种硬件设备,也可以管理人一样。

操作系统作为一个软件,既可以管理计算机内的硬件,也可以管理计算机内的软件。但是,操作系统 内部其实也是有一定的保护措施的。就好比现实中的银行,你要去银行办理业务,业务员坐在服务窗口后面,你与业务员之间隔着一块玻璃。这块玻璃其实就是为了在有危险分子破坏银行时用于保护业务员的安全的。

操作系统也是同理。但是就像业务员需要为用户办理业务,因此开了一个小窗口进行服务

操作系统也是需要为上层用户提供服务的。但是为了避免上层用户对操作系统底层数据的破坏,也是提供了一个“操作系统接口”来为用户提供服务。要注意的是,这个接口其实就是系统调用接口,因为操作系统是用C语言写的,这个系统调用接口其实也是“C式接口”

当然,虽然操作系统提供了各式各样的系统调用接口,但是对于大部分用户来说,直接调用系统接口进行操作还是过于复杂和困难,由此又出现了“用户操作接口”。这个接口由shell外壳C/C++的lib部分指令组成。用户使用计算机就是通过各种指令等操作去调用“用户调用接口”,再用用户调用接口去调用“系统调用接口”

4.系统调用和库函数的关系

(1)开发角度来看,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用

(2)系统调用在使用上,功能比较基础,对用户的要求也相对较高。所以,有心的开发者可以对部分系统调用进行适度封装,从而形成“库”。有了库,就很有利于更上层用户或者开发者进行二次开发

三、进程

1.进程的概念

进程,指的就是一个运行起来(即加载到了内存中)的程序,就叫做进程。也可以简单理解为在内存中的程序就是一个进程。

而我们所说的程序,其本质就是文件,这些文件存放在磁盘上

(1)内存中的进程

在之前我们已经了解到,磁盘中的程序会加载到内存中,但其过程并不是我们所想象的那么简单。因为加载到内存中的程序,一般来讲并不会只有一个,都是多个进程同时存在于内存中

如果有疑惑,可以打开电脑上的任务管理器,这里面就有进程查看窗口

并不是说如果你打开电脑什么软件都不开,就不会有进程。只要你的电脑处于开启状态就会有系统进程在内存当中,少则十几个数十个,多则上百个

对于如此多的进程,操作系统则需要对这些进程进行管理,包括分配空间的大小,分配的空间的位置等各种各样的信息都需要操作系统进行管理。而我们之前说过,管理的方法是“先描述,再组织”

(2)PCB(进程控制块)

为了方便对这些进程进行管理,操作系统也根据这些进程的一些共同信息进行整理归类,形成了“PCB”,也就是“进程控制块”。进程控制块我们现在可以简单的看为就是一个结构体,这个结构体里面包含了进程的各类属性和该进程对应的代码、属性地址(这些属性在磁盘中的可执行程序中是没有的,会在加载到内存时自动形成):

由此,每一个进程在内存都拥有一个“struct task_struct*”。至于为什么是结构体而不是类,是因为操作系统是由C语言编写的,C语言中并没有类的概念

(3)进程管理

当我们理解的“进程控制块”后,就可以更好的理解进程在系统中的交互了。

当一个程序从磁盘加载到内存中时(注意加载的仅仅只是程序的执行代码等内容),会对应生成了一份“进程控制块”,该控制块会指向内存中对应的进程。当存在多进程时,这些控制块以链表的形式相互连接。如果CPU想调用某个进程,内存就会通过控制块链表找到对应的控制块,再由控制块找到对应进程的执行代码发送给CPU:

由此,所谓的对进程进行管理,就变成了对进程对应的“PCB”进行相关管理,进而变成了对链表的增删查

进程控制块的内核当中的struct task_strcut内核结构体,是用于描述进程的结构体。当我们形成进程时,该内核结构体会为我们创建一个内核对象:”task_struct 对象”。然后再将该结构与我们的代码和数据相关联起来,进而完成“先描述,再组织”的工作

由此,我们可以得到进程其实就是“进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码”

2.查看进程

(1)windows下的进程查看

在windows下要查看进程是很简单的,直接打开任务管理器就可以看见:

(2)linux下的进程查看

为了便于查看,我们现在写以下一个会循环打印“i'm a process”这句话的程序:

我们先生成一个“myprocess”程序,此时该程序还没有运行,仅仅只是一个在磁盘上的二进制文件:

我们执行该程序,此时它就变成了一个进程:

但是我们现在虽然知道它是一个进程,但却无法查看该进程。要查看该进程主要用以下方法。

1.执行“ps ajx | grep 程序名”命令

该命令中的“ps ajx”是显示系统中的所有进程。整条命令是用于筛选带有对应“程序名”字样的进程。我们执行后就可以找到对应的进程:

如果不理解这里面显示的信息,可以输入“ps ajx | head -1 && ps ajx | grep ‘程序名’”。该命令会输出对应的数据的标题:

(3)结束进程

要在linux下结束一个进程非常简单,我们直接执行“kill -9 PID号”即可:

3.进程的常见调用

(1)通过系统调用获取进程标示符(进程id和父进程id)

1.进程id和父进程id

在linux下,每个程序都有自己的进程id和父进程id

在了解进程id和父进程id之前,我们要先了解两个函数:getpid()getppid()。第一个函数是用来获取进程id的, 而第二个函数则是用来获取父进程id的:

这里的返回值是“pid_t”,我们暂时将其理解为返回一个整数即可。

为了方便演示,我们用以下程序演示:

该程序的运行结果如下所示:

可以看到,此时就将该进程的pid和ppid打印出来了。但是,如果我们多执行几次:

我们就会发现,该程序的进程id会不断改变。这很容易理解,因为进程结束后重新加载到内存中都会有一个新的进程id。就好比你高考考上了一个大学,你入学后会给你一个学号,但是你不满意该学校,复读了一年又考上了同一个大学,此时又会给你一个学号,但是这两个学号是不一样的。

进程id改变我们能理解,但奇怪的是父进程id却没有改变。既然如此,我们现在就输入“ps ajx | head -1 && ps ajx | grep 29513”命令:

可以看到,在进程名字这一块,29513显示的是“bash”。这其实就是一个“总进程”。在操作系统启动时,操作系统就已经为我们指派了一个进程,我们所有的进程都是在该进程下执行的“子进程”。而该机制的目的就是为了避免我们的进程有问题时,对系统使用产生影响

而这里的这个“bash”进程,其实就是我们的shell。我们执行“kill -9 进程id”命令来结束该进程:

可以看到,当我将该“bash”进程结束掉后,我的linux账户就直接退出了。当然,在某些云服务器上可能不会退出,但也会导致命令无法使用

注意:命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash

(2)通过系统调用创建进程

1.fork()函数调用

要通过系统调用创建进程,直接调用fork()函数即可,该函数的作用就是创建一个进程

虽然我们知道了fork()函数,也知道了该函数的作用,但我们不知道该函数执行后会出现什么现象。因此,我们用以下程序来进行测试:

在执行该程序前,我们要先知道,在fork()函数调用前,该进程只有一个父进程。而在fork()函数调用后,才会新形成一个进程,此时就是“父进程 + 子进程”两个进程。

现在我们来执行该程序:

可以看到,我的程序里面仅仅只是打印一句话,但是这里却打印了两句话,并且第一句话中的子进程id就是第二句话的父进程id

这就是因为fork()创建了一个进程。第一个进程的父进程很明显是“bash”,而第二个进程由于是在该程序中创建的,因此它的父进程就是进程10755。这也就说明了fork()确实能创建进程

2.fork()函数的返回值

从上面的程序中我们确实可以证明fork()函数可以创建进程。但是这种使用方式却没有什么意义,因为将fork()函数之后的代码执行两次并没有什么用。

因此,现在我们来看看该函数的返回值。

首先是函数的返回值类型:

可以看到,这个函数的返回值是“pid_t”,前面也说过,这个返回值看成返回一个整数即可,没什么好奇怪的。

接下来我们看看该函数对返回值的解释:

在这里我们只用看成功时的返回值,失败的现象我们就先不考虑。可以看到,在这里的返回值解释中说到,“如果成功,就把子进程的pid返回给父进程,并将0返回给子进程”。那么我们修改一下程序如下图来测试一下:

然后我们执行该程序:

可以看到,执行结果确实是将子进程的pid返回给了父进程,并将0返回给了子进程。一眼看上去没有什么问题,和手册里面的返回值解释的一模一样。

但是,我们在学习C或C++时说过,“一个变量在同一时间只能有一个返回值”。而在这里,在该程序没有对返回值进行任何修改操作的情况下,竟然让变量在同一时间拥有了两个返回值。这就是问题所在。

3.fork()的使用场景

对于上述情况,我们现在是无法理解的,因为我们对进程的理解还不够深入。但我们理解了进程控制之后才能对上述情况有个更清晰的认识。

因此既然我们无法理解现象,但我们可以用它。现在我们写如下一个程序:

如果id == 0,我们就循环打印子进程。如果id > 0,就循环打印父进程。注意,我这里写的是两个死循环

然后来执行该程序:

可以看到这里程序在正常运行,这种同一程序中多个进程同时运行的现象就叫做“多进程”

但是我们要注意到,该程序不仅打印了子进程,也打印了父进程。我们的程序写的是一个if判断,并且里面的内容是死循环打印。按道理来讲同一时间只会有一个if else判断成立。但是,在这里这两个判断条件同时成立并且都在死循环打印

这种现象其实就说明了两个点:

(1)fork()执行后,会有父进程和子进程两个进程在执行后续的代码

(2)fork()之后的代码,会被父进程和子进程共享

意思就是,在这里是两个不同的进程在执行相同的代码,根据fork()返回值的不同执行了各自不同的部分。而这种通过返回值的不同,使父子进程执行后续共享代码不同部分的编程方式就叫做“并发式编程”

4.进程的状态

在理解进程状态前,我们要先理解CPU是如何调度进程和进程时如何存在于内存中的

(1)程序与内存与CPU的关联

首先我们要知道,我们的各类硬件都是通过驱动程序来支持运行的,每个驱动程序在磁盘中都会一个对应的“PCB进程结构体”,这个结构体中包含了对应的硬件的所有属性信息

而磁盘中的程序要运行也是同样的。磁盘中的程序启动后会进入内存中变成进程,此时该进程中包含了运行程序的代码等内容。而当一个程序变成一个进程后,内存中就会对应生成该进程的“PCB进程结构体”,该结构体中包含了对应进程的各类属性对应进程的代码地址

当这些进程要运行时,就必需要由CPU进行调度执行。但是我们的CPU只有一个,哪怕你是多核,你的CPU的数量依然无法与进程的数量相比。并且一个CPU一般来讲只能同时运行一个进程。那么有人就会奇怪了,既然一个CPU同时只能运行一个进程,那么为什么我们的计算机上能有数十上百个进程在同时运行呢?

这是因为CPU虽然同时只能运行一个进程,但是它的运行速度非常快,运行一个进程的速度是以纳秒甚至微秒为单位的。因此,CPU在运行进程时可以看成是在不间断的循环跑所有进程,一个进程跑完后立刻下一个进程。就好比我们在公司面试时一次面试只能进去一个人,面试官就是CPU,而我们就是那一个个进程。同时因为其速度非常快在感官上就会给我们所有进程在同时运行的错觉,其实这些进程都是一个一个的运行的。

既然进程需要循环跑,那么就必定需要按照某个顺序来跑,不然CPU无法知道自己应该接着运行哪个程序。由此,又有了“运行队列”的概念。从名字就可以知道,这是一个队列。在这个队列中包含了指向进程结构体的指针和一些其他需要的属性。运行队列按一定顺序将这些结构体链接在一起,CPU运行进程时就按照该运行队列中的顺序运行

要注意,在运行队列中的是“PCB结构体”,而不是进程本身。就好比我们找工作投简历时,发给公司的是我们的简历,公司在对简历排序筛选时也是对我们的简历进行排序,而不是让我们本人过去对我们进行排序。

现在有了对上述的内存中的进程与CPU的关联的知识后,我们就能很容易的理解各类进程状态了

(2)状态种类

进程有很多种状态,如运行新建就绪挂起阻塞等待停止挂机死亡等各类状态。在这里不会将所有状态都讲解一遍,而是选择其中比较常见的几种状态讲解

(3)运行状态

运行状态,指的就是“处于运行队列中的进程的状态”。换言之,只要一个进程在CPU的运行队列中,那么该进程就处于运行状态,都是“R”状态无论它是否真的在CPU中运行。就好比我们面试时,只要在面试房间外排队的人都将自己调整为了面试状态准备面试,无论这些人是否是在面试房间内接受面试

(4)阻塞状态

我们要知道,CPU在运行进程时,这些进程不仅需要CPU中的资源,也可能需要硬件资源。就比如我们在QQ上和别人聊天,此时QQ处于运行状态,需要占用CPU资源,同时我们发送信息也需要网卡,显示器等硬件资源。

当然,有人可能会觉得既然进程需要硬件资源直接去用就行了。但是硬件资源的调用速度非常慢,当然这个慢是与CPU的速度相比。因此就可能出现CPU在调用某个进程时,该进程还需要用到硬件资源而使CPU不得不停下等待的状况。为了应对这个问题,在我们的硬件的“PCB结构体”中就还有一个“等待队列”。该队列和CPU的“运行队列”差不多,是用于等待硬件进行调用完成进程资源需求的:

假如现在在CPU中有一个正在运行的进程想要向磁盘写入数据,那么CPU就会将该进程从运行队列中剥离出来,将其放到磁盘的等待队列中,然后继续执行其他进程。如果磁盘此时正在为其他进程服务,该进程就会在等待队列中等待。就好比我们去银行办理业务,业务窗口的工作人员告诉我们说我们需要先去另一个窗口填一个表才能帮我们办理对应业务,此时我们就直接去另一个窗口填表,如果没人就直接填,有人就排队等待。但是我们原来的那个业务窗口不可能等我们填完表帮我们办理好后再帮其他人办理业务,而是在我们去另一个窗口时接着帮其他人办理业务。这种进程在硬件的等待队列中等待硬件资源就绪,就叫做进程处于“阻塞状态”

当该进程的资源使用完成后,操作系统就会将该进程从磁盘中调出来,将其状态改为“运行状态”后重新加入CPU的运行队列运行。

从这里我们也可以看出来,其实进程的不同状态指的就是“进程在不同的队列中”

注意,进程在不同队列中指的是进程的“PCB结构体”在不同队列中,而不是进程本身

(5)挂起状态

我们说过阻塞状态就是一个进程因为需要使用硬件资源而进入对应硬件的等待队列时的状态。

但是如果此时一个硬件的等待队列中存在多个等待进程,那么就会导致这些进程虽然无法进入CPU的运行队列运行,但也无法使用硬盘资源,只能等待。

此时对应进程的“PCB结构体”虽然在硬盘的等待队列中无法运行,但是其本身的数据依然在内存中占据空间。因此对于这种占着磁盘空间却什么都没干的进程,内存为了腾出容量供其他进程使用,就会暂时将该进程的代码、执行进度等数据从内存中挪到磁盘的一块专门存放这种未运行进程数据的空间内

而这种在内存中的一个进程的数据被从内存中暂时转移到磁盘内的行为,就叫做“挂起”。而此时的进程就是处于“挂起状态”

当对应进程的进程结构体进入硬件使用完资源被重新放入运行队列时,该进程的资源就会被从磁盘内重新转移到内存中。这种对进程数据的转移就叫做“内存数据的换入换出”

当然,与挂起相关的还有“阻塞挂起状态”“准备挂起状态”等。有兴趣的可以自行了解,基本除了“运行状态”外,其他的状态都可以与“挂起状态”相结合

5.linux中的进程状态查看

如果大家有兴趣,可以去linux中的内核源代码中搜索“task_state_array”来查看liunx下的进程状态的表示。这里就不去查找了,linux下的进程状态表示就如下图:

(1)运行(running)状态

在linux下,进程的运行状态用“R”表示。

为了方便看到进程状态,我们现在写以下一个死循环程序:

注意,因为这里程序是在死循环运行,除非我们停下程序,不然在该窗口下是无法执行命令的。因此我们要先复制窗口,然后在另一个窗口下执行命令

右击你的账户选项卡,选择里面的复制会话即可

我们将该程序运行起来后,输入“ps axj | grep 程序名”命令:


此时我们可以看到,在该myprocess程序下的状态栏下,就有一个“R+”的符号,这就表示该程序处于“运行状态”。至于“+”号,我们暂时不用管。下面的那个进程时grep命令的进程,也不用管

(2)休眠(sleeping)状态

睡眠状态在linux下的符号是“S”

我们现在准备以下一个死循环打印程序:

现在我们来运行该程序:

此时该程序在不停的打印数据。然后我们在另一个窗口输入“ps ajx | grep 程序名”命令:

可以看到,现在该程序的运行状态就变成了“S+”,即“睡眠状态”。那大家就会觉得很奇怪,明明该程序一直在运行,为什么这里显示的不是“运行状态”而是“睡眠状态”

这其实是因为该程序需要循环打印一条消息,要打印该消息就需要使用显示器的资源。但是显示器作为一个硬件,其运行速度比CPU的速度慢很多。可以说在该程序中有99%的时间都在访问显示器,只有1%的时间是CPU运行该进程。当我们查看该进程时,极大可能查看到的都是该进程在访问显示器时的状态,而非在CPU中运行的状态。因此当我们查看一个需要访问硬件的进程时,基本都是睡眠状态。“睡眠状态”也可以看做就是“阻塞状态”

(3)停止(stopped)状态

停止状态一般被视作挂起状态或阻塞状态的一种,但是linux将该状态单独拿出来了

我们继续运行以下程序:

然后我们在另一个窗口中输入“ps axj | grep 程序名”命令:

此时它依然是“休眠状态”。然后我们输入“kill -l”命令,该命令可以查看kill命令所能带的选项

此时我们可以看到,在该命令的选项中有一个“19”号命令,可以用于暂停。然后我们输入“kill -19 进程pid”命令。如果不知道哪个是进程pid,可以输入“ps axj | head -1 && ps axj | grep 进程名”进行查看:

当我们输入暂停命令后,可以看到,此时我们的程序下方就出现了“Stopped”提示,表示该进程已被暂停。然后我们输入“ps axj | grep 进程名”命令:

此时我们就可以看到,该进程的进程状态时“T”,即暂停状态 

当然,既然能暂停,就可以继续,如果我们想进程继续运行,就可以输入“kill -18 进程pid”来运行:

此时该进程又重新变成了“S”,即睡眠状态

(4)深度睡眠(disk sleep)状态

深度睡眠状态是一种特殊的状态,在linux中用“D”标识,与“睡眠状态”相对应。简单来讲,“睡眠状态”的进程我们是可以将它终止的,但是“深度睡眠状态”的进程无法被终止。这种状态是为了防止如磁盘这类进程被操作系统终止导致数据丢失用的。

要更好理解“深度睡眠状态”,在这里我们举一个场景:假设某一天,我们有一个进程A,该进程需要向磁盘写入数据。于是进程A就带着自己的数据地址来到磁盘,让磁盘帮它处理。但是此时我们的内存中存在着许多进程,使得整个内存已经快满了面临内存不足的情况。操作系统作为管理这些的存在,为了腾出内存空间供其他进程使用,于是将很多进程都“挂起”了。

但是挂起很多进程后还是无法解决内存不足的问题,于是操作系统就开始删除那些占着内存不干事的进程。而进程A刚好就在等磁盘写入数据,无所事事。于是操作系统来到进程A身边,将进程A结束了。过了一会儿磁盘写入数据结束了,但是不知道什么原因写入失败,磁盘就想将写入失败的事实返回给进程A,但是进程A此时已经被结束了,磁盘无法返回数据。但是磁盘不能因为无法返回数据就停下不解决其他进程的问题,于是磁盘就将这些失败数据扔掉了。

等到用户需要这些数据时,就发现磁盘中没有对应的数据,并且了解到是因为操作系统为了解决内存不足的问题将进程A结束了,导致无法及时发现数据未写入。于是用户就将进程A设置为了“深度睡眠状态”,该状态下的进程无法被操作系统删除,同时也无法被用户删除

“深度睡眠状态”我们在平时是很少见到的,基本只有在“高IO”“高并发”状态下才会看见。当然,不要觉得这个进程状态很好用,如果一个公司的服务器中的进程出现了大量的“D”状态,就说明该服务器正处于“崩溃”的边缘,也就是我们平时听到的一些公司在高访问下的“服务器崩溃”“宕机”。这种情况解决起来是非常困难的,因为无论是操作系统还是用户都不会干涉这种状态的进程。要想解决只能慢慢减轻服务器的负担使服务器缓过来或者等程序自己醒来甚至断电重启

如果大家想看看该状态,可以输入“dd”命令。该命令会形成一个几十上百g临时文件。大家最好不要轻易尝试该命令,因为如果你的机器空间不够,执行该命令后你的系统很有可能会直接崩溃挂掉

(5)追踪(tracing stop)状态

追踪状态在linux下是用“t”表示,意思是该进程正在被追踪

我们写如下一个程序,该程序会打印4次“追踪测试”:

然后我们对该程序进行调试,并在第8行处打上断点:

现在我们运行该程序,该程序就会在第8行停下:

此时我们再在另一个窗口查看该进程的状态:

可以看到,该进程的进程状态显示的是“t”,就是我们的追踪状态,此时该进程的运行正在被追踪。而该状态的存在就是我们能够调试程序的原因。该状态下程序停止,等待用户查看此时产出的数据后后续操作

(6)死亡(dead)状态

该状态在linux中用“X”表示,意思是一个进程的死亡。理解起来很简单,就是一个进程的结束

但是我们很难去验证该状态的存在,因为在linux下只要一个进程死亡,系统就会立刻或者延迟回收该进程的空间,虽然有可能延迟,但是是相对于系统速度而言的,对我们来讲就是一瞬间的事

(7)僵尸(zombie)状态

僵尸状态是linux下一个非常特殊的状态,用“Z”表示,用于表示一个进程已经退出了,但是它占用的资源还未释放

举个例子,你是一个小区的住户,你每天都会出门锻炼,你出门的时候都会和你的邻居打个招呼问个好。但是有一天,你去给你这个邻居打招呼时无论怎么喊都没人理你,于是你推开门进去,发现你的邻居已经不动了。看到这个情况你赶紧打电话给110和120,120的人来后查看了以下,确认这个人已经去世了。此时警察来了,封锁了整个现场并对你的邻居进行调查看看是什么情况。等到警察和医院把你的邻居的死因等等信息都获取后,才会通知他的家属让他们将人带去埋了。

在这个例子中,我们的邻居就好比是进程110和120就好比是该进程的父进程操作系统邻居去世就是该进程结束了,但是进程虽然结束了,它的数据还会在内存中保留下来供父进程或操作系统获取。等它们信息获取完后,才会由父进程或操作系统将该进程所占用的资源释放掉。而这个进程已经结束但资源未回收的状态,就叫做“僵尸状态”

要查看僵尸状态也很简单,我们写一个子进程已经结束但父进程未结束,无法回收空间的程序即可:

该程序会创建一个进程,并根据返回值执行不同的操作,子进程会在5s后退出,但是父进程会死循环执行

等5s子进程结束后我们再查看该子进程的状态:

此时可以看到,该子进程的状态就是“Z+”,处于僵尸状态。同时,该进程的名字后面还有“defunct”提示符,表示“该进程已失效”

僵尸状态是在一般情况下是每个进程在结束时都会短暂存在的状态,这里的短暂是以系统角度来说的,正常情况下我们是看不到的。

(8)linux下的进程状态后的“+”含义

我们以以下一个死循环程序为例:

该程序运行后是休眠状态“S+”,没什么好说的:

然后现在输入“kill -19 进程pid”将该程序暂停

此时该程序进入了暂停状态。然后我们再输入“kill -18 进程pid”命令让该程序继续运行后并查看它的进程状态:

这时我们就会发现,该进程虽然继续运行了,但是它的进程状态不是显示的“S+”,,而是“S”。然后我们在该进程运行的窗口下按下“ctrl c”其他各类指令

此时我们会发现,无论我们按下多少次“ctrl c”都无法结束该进程,并且在这个窗口下我们还能执行各种命令,但是这个打印进程就是不会停止

原因就是因为此时该进程已经变成了一个后台进程,一直都在后台运行“ctrl c”命令是结束前台进程的命令,无法结束后台进程。因此在进程符号中有“+”的就是前台进程没有“+”的就是后台进程

如果我们想结束该后台进程,就要使用“kill -9 进程pid”命令来结束

6.僵尸进程与孤儿进程

(1)僵尸进程

僵尸进程,简单来说就是处于僵尸状态的进程的资源一直未被回收。虽然已经退出的进程的信息都被保存在“PCB结构体”中,但结构体的维护也是需要资源的

如果一个进程的已经退出了,但是它的父进程或操作系统一直都没有将它所占用的资源回收,就会导致应该释放的资源无法释放。如果一个父进程创建了很多子进程却不回收它的资源,就会使得内存中的可用空间越来越小,进而造成内存泄漏问题。

所以僵尸进程是我们必须要避免和解决的问题。

(2)孤儿进程

僵尸进程父进程还未结束时,子进程先结束导致子进程的资源无法释放。而孤儿进程则是子进程未结束,父进程先结束,导致子进程没有人管,就可能出现当子进程结束时,其资源无法释放。为此,linux下在这种子进程未结束而父进程先结束的情况下,会让操作系统领养子进程,待子进程结束后的资源由操作系统来释放

我们写以下一个程序来测试:

现在运行该程序,该程序会循环打印子进程和父进程的pid及其ppid。然后我们再在该程序运行时查看它的状态:

此时两个进程都是“睡眠状态”。现在我们输入“kill -9 进程pid”命令,将父进程结束,并查看进程的状态:

可以看到,此时父进程就结束了,但是子进程还在。有人可能就会奇怪,子进程结束时父进程未结束都会导致该子进程成为“僵尸进程”,但这里的父进程结束后就直接结束了,没有成为“僵尸进程”。原因是所有的进程都是在父进程“bash”下运行的,上面的那个父进程也不例外。但是“bash”父进程和普通的父进程不一样,它比较负责任,当它的子进程结束时,会自动释放子进程的资源

我们可以注意到,该子进程的状态从“S+”变成了“S”。就这说明当子进程的父进程先结束时,未结束的子进程会变成“后台进程”。此时的子进程无法用“ctrl c”的方法结束,只能用“kill -9 进程pid”命令来结束

如果眼尖的人就会发现,剩下的子进程的ppid变成了“1”

我们输入“ps ajx | head -1 && ps ajx | grep systemd”查看一下操作系统:

可以看到,操作系统的pid就是1,这就说明“当一个父进程先结束,但其子进程未结束时,该子进程会被操作系统领养”。对于该子进程后续的资源释放也就都是有操作系统来进行

7.进程优先级

进程优先级这一概念比较简单,并且用户能对进程优先级的操作和干涉也是比较少的。因此进程优先级不必过多了解

(1)进程优先级的作用

优先级的概念就简单,就是确定谁先谁后的问题。进程优先级也是同样的。因为在计算机中,资源总归是少数, 而要使用资源的进程才是多数。如果不确定进程优先级,将哪个进程先执行哪个进程后执行划分出来,就可能出现混乱,导致资源被随意占用

(2)linux下的进程优先级特点

在一般情况下,系统的进程优先级都是用一个数字来确定的。但是在linux系统中的进程的优先级是用两个数字来设置。

至于为什么用数字来设置,就好比我们去学校食堂吃饭,当你点餐后每个窗口都会给你一个写着你的号码的小票,这个号码就是厨师做你的饭的次序,也就是厨师做饭的优先级

(3)linux下的进程优先级修改

linux下的进程优先级是可以修改的。在实际修改之前,我们先写以下一个死循环程序:

我们将这个程序运行起来后,再在另一个窗口输入“ps -la”查看进程的优先级

在这里面的“PRI”表示priority,即“优先级”“NI”表示“nice”,是用于修改的优先级的。在默认情况下,linux下的普通进程的优先级“PRI”都是80“NI”0

并且在linux下,进程的优先级 = 老的优先级 + NI值

现在我们输入“top”命令修改优先级(如果使用top修改的权限不足,可以尝试用“sudo”提权再修改):

输入“top”命令后会出现以上界面(该界面未截全,下面还有很多进程)。在这个界面按下键盘上的“r”

就会弹出这个输入行。在这里输入你要修改的进程的pid(注意,要用你的字母键盘上面的那一行数字输入,右边的数字键盘可能无法输入):

有以上输入行后,就可以输入你要设置的优先级了。在这里我们将优先级设置为-100

设置好后按下“q”退出该界面。然后在输入“ps -la”命令查看优先级:

可以看到,虽然我们设置的优先级是“-100”,但是该进程的优先级“PRI”仅仅变成了“60”,而“NI”则变成了“-20”

然后我们再将该进程的优先级设置成100后再来查看该进程优先级:

此时“PRI”变成了“99”,而“NI”则变成了“19”。这就说明,在linux下用户所能设置的优先级仅仅是40个维度,即“60~99”。这同时也说明了用户能对进程中的优先级干涉是很少的

同时要记住,在linux下,虽然说进程的优先级 = 老的优先级 + NI值,但是这个“老的优先级”其实并不是我们设置完后的优先级值,而是其默认的80

比如此时我们的程序优先级是99,我们再将其设置为1

此时的优先级是“80 + 1 = 81”,而非“99 + 1 = 100”

8.进程相关概念

(1)竞争性

一个系统的进程是非常多的,动则数十上百个。但是在我们的计算机中,CPU都是很少的,一般只有h3一个。所以进程之间为了使用资源,是具有竞争属性的。为了更高效的完成任务,更合理的竞争相关资源,便有了优先级

(2)独立性

在多进程运行下,每个进程独享各种资源,多进程运行期间互不干扰。

比如我们的手机上我们可以同时打开如QQ、微信、b站等各类软件。但是一个软件的退出或崩溃不会影响到其他软件。这就是因为各个进程之间具有独立性

(3)并发

多个进程在一个CPU下采用“进程切换”的方式,在一段时间内,让多个进程都得以推进,称之为并发

在我们的计算机中,一个CPU在同一时间只能有一个进程运行。但是CPU并不是在运行完一个进程后再运行下一个进程。而是设置了一个“时间片”来限制。假如这个时间片是10毫秒,那么就说明每个进程都只能在CPU中运行10毫秒。时间一到,无论该进程有没有运行完,都必须切换成下一个进程。当轮到同一个进程时就从其上次运行的地方开始再运行10毫秒,如此循环往复

(4)并行

虽然一般的计算机只有一个CPU,但有些计算机是会有2个甚至更多的CPU的。而一个CPU中同一时间只能有一个进程运行,当存在多个CPU时,就会出现多个进程同时运行的情况,这就叫做“并行”

要将“多CPU”“多核”相区分。“多核”中的“核”指的是CPU中的内核处理器,而非CPU本身。也就是说,“多核”指的是一个CPU中有多个“内核处理器”,而非多个CPU

9.进程切换

在并发中我们说了, 进程在CPU中运行时,并不是一个进程运行结束后才运行下一个进程。而是设置一个“时间片”每个进程都会跑时间片所规定的时间。如果时间片是10毫秒,那么每个进程都跑10毫秒,无论该进程有没有结束

该机制的作用就是为了防止某些进程长期占用CPU导致CPU无法执行其他进程。举个例子,我们有时会写一个死循环程序。当执行该程序时,如果CPU要跑完该进程才切换为其他进程的话,我们就无法进行其他任何操作,因此此时CPU已被该死循环进程占用,且CPU同一时间只能运行一个进程,无法切换为其他进程。但实际上并不会出现上述情况,原因就是有时间片的存在,导致该死循环进程执行一定时间后就被切换成其他进程了

现在我们知道了CPU会进行进程切换。但是CPU是怎么知道上一个进程执行到什么地方了呢?其实,在CPU中是存在一套寄存器的,注意是一套,而不是一个。在这套寄存器里面有着保存各类数据的寄存器。当我们的进程要切换为下一个进程时,CPU中的寄存器会将该进程执行的指令的下一条执行的地址保存下来,与此同时被保存下来的还有该进程运行所产生和需要的各类临时数据,这些数据被存放在“PCB结构体”中,当然这一说法并不准确“PCB结构体”中并没有空间来保存这一数据,但现在我们暂时可以理解就保存在结构体中。

寄存器中的数据,每当一个进程加载进CPU时都会被覆盖,因此需要有这种恢复机制。就好比我们定义一个变量,这个变量只能有一个值,当有其他值给这个变量时,原来的值就会被覆盖

因此,当进程进行切换时,将执行到的指令地址和各类临时数据保存下来的操作,叫做“上下文保护”。当进程在恢复运行时,通过PCB结构体将上一次执行的指令和数据恢复到寄存器中并继续运行的操作叫做“上下文恢复”

有人可能会说,一个寄存器的空间也就4个字节8个字节,虽说CPU中不止一个寄存器,但是我们写程序时会定义很多个变量,有很多的值。甚至我们电脑上几十个G的软件都有,寄存器怎么保存的下呢?其实寄存器虽然空间小,但是其速度非常快。并且当一个程序运行时,它在某个时间段只会执行固定数量的指令或某一行代码。而寄存器的内部只会保存当前进程在当前时间所执行的指令和需要的数据,在这些指令之前和之后的那些不需要使用的指令,寄存器都不会保存。因此,寄存器中的数据是一直变化的

四、环境变量

环境变量其实就是指“操作系统为了满足不同的应用场景而预先在系统内设置的一大批的全局变量”。同时我们要有一个认识:“环境变量其实就是字符串”。

在了解环境变量之前,我们要先理解一个问题,就是在linux下我们自己写的程序和程序中的指令有什么不同?我们输入分别输入“file /usr/bin/ls”“file myprocess”查看系统命令ls和我们自己写的myprocess程序:

可以看到,系统命令其实也是一个程序,我们使用系统命令时其实就是在运行一个程序。当我们运行一个程序我们自己写的程序时,我们必须要让系统知道该程序所在路径,因此“./”的作用其实就是提供“相对路径”,告诉系统该程序就在当前路径内

但是系统命令作为一个程序,却不需要带上“./”。这不是因为系统不需要去找它的路径,而是因为系统帮我们到默认路径上去找了

因此,如果你想让你的程序可以不带“./”运行,就可以执行“sudo cp 程序名 /usr/bin/”命令,该命令会把你的程序放到默认路径

可以看到,在上图中,我们用程序名去执行会报错

但是当我们执行cp命令后(如果是普通用户就需要加sudo,root用户则不需要):

此时我们就无需带“./”就可以执行对应程序。这也就说明了linux下的系统命令其实就是存在于“/usr/bin/”路径下的程序

当然,并不建议大家将自己写的程序添加到该默认路径下,因为你自己写的程序不够安全,也没有经过检测,很可能会污染命令池

如果你想删除在默认路径下的程序,执行“sudo rm -f /usr/bin/程序名”即可

1.PATH环境变量

环境变量应用于系统的不同场景,我们以下面的一个例子来理解环境变量中的“PATH”

现在我们知道了为什么系统命令无需带“./”。但是系统是如何找到对应的默认路径的呢?这其实就是因为系统中定义了一个叫做"PATH"的环境变量

我们输入“echo $PATH”进行查看:

可以看到,在该环境变量中存在很多路径,我们的系统其实就是从这些路径里面去找对应的命令,如果没有找到就会报错。我们的ls等命令无需带“./”也是这些命令的所在路径在该环境变量中

(1)将程序路径添加进该环境变量

我们也可以将自己的程序的路径添加到该环境变量中。执行“export PATH=$PATH:程序路径”即可将自己的程序添加进PATH环境变量

此时我们再执行自己的程序时,无需带“./”就可以执行了:

注意,最好不要执行“export PATH=程序路径”,该命令会将环境变量中的路径覆盖,而非添加路径。执行后我们linux系统下的很多指令就会无法使用

当然,就算执行了也关系,因为该环境变量是一个“内存级”的环境变量,我们重登linux账户该环境变量就会恢复成原来的内容

当然,系统中还有许多环境变量,这里的PATH环境变量只是用来举个例子,让大家认识一下环境变量。其他的还有如“USER”环境变量用于识别用户身份“HOME”环境变量用于表示用户家目录“PWD”环境变量记录用户当前所在路径等等

2.添加环境变量

(1)添加本地变量

本地变量,就是指只在本地shell存在的变量。简单来讲,就是我们定义的本地变量只能在自己的进程中使用,无法被进程继承。而环境变量,则是在该linux机器下的所有进程中都可以生效,会被进程继承

如果我们想添加一个本地变量,可以在命令行上输入“变量名="内容”(双引号可带可不带,但建议带上,避免你的变量中有空格等字符导致系统识别错误):

可以看到,通过这种方式,我们就可以直接定义一个本地变量。但是这仅仅是一个本地变量,如果我们用“env”这种搜索环境变量的命令是搜索不到的:

该指令没有搜索到对应的环境变量

然后再来写一个程序测试一下:

其中的myenv()函数会识别环境变量,如果该环境变量存在,就返回它的内容,否则就返回NULL

现在运行该程序:

此时输出找不到该环境变量,这也说明了我们此时定义的仅仅是本地变量而非环境变量

1.本地变量转环境变量

如果我们想将本地变量改成环境变量,直接输入“export 本地变量名”即可:

此时我们就可以搜到该本地变量了

3.取消环境变量和本地变量

如果我们不想要某个环境变量或本地变量,想取消掉,直接输入“unset 变量名”即可:

4.查看环境变量内容

(1)查看环境变量

如果想查看当前系统中的环境变量,直接输入“env”命令即可:

这里并没有截全,实际上在下面还有很多环境变量

如果你想查看单个环境变量的内容,就可以输入“echo $环境变量”来查看指定的环境变量内容:

该环境变量中说明了我们命令行中上下翻动可记录的最多的命令个数。如果你想看你历史上使用过的命令,输入“history”命令即可查看:

(2)查看本地变量和环境变量

如果我们还想看系统中的本地变量和环境变量,就可以输入“set”命令。假设我们现在有一个“MYENV”本地变量,用“env”命令是搜索不到的。此时我们用“set”来搜索:

5.windows下的环境变量

其实不止linux有环境变量,windows也是有环境变量的

我们右击电脑上的“此电脑”图标,选择属性,点进去就可以看到有“高级系统设置”选项:

点进去后我们就可以看到“环境变量”选项:

点进“环境变量”选项就可以看见我们的电脑上的环境变量:

注意,如果没有相关知识和技能,千万不要尝试修改或删除这里的内容

6.int main()主函数中的参数

(1)int argc和char* argv[]

很多人可能不知道,我们每次写代码是的主函数int main()中其实是有参数的。其中一共有三个参数。我们先来介绍其中的前两个参数

之前也说过,linux下的命令其实就是程序。但是,这些命令大家在使用过程中可以知道,是可以带其他选项的。如“ls -l”、“ls -a”等等。既然这些程序都可以带选项,那么按道理来讲,我们自己写的程序也是可以带选项的

因此,我们先写下面一个程序来看看参数“char*argv[]”中有什么:

我们执行该程序后可以看到打印了如下内容:

argv[0]上的内容就是我们刚才执行的命令。此时我们在带上几个选项来运行该程序:

可以看到,此时argv[]中就是我们输入的命令行中的参数。那此时“int argc”“char* argv[]”的作用就很明显了。argc是用来表明命令中参数的数量的,而argv则是用来存储参数的。在此时我们的输入在命令行的内容如“./myprocess -a -b -c”被看做是字符串,当进入主函数后,这个字符串被分割为“./myprocess”“-a”“-b”“-c”一个个小字符串

既然argv中保存了选项,那我们再修改下程序:

此时我们执行该程序并带上对应的选项:

可以看到,该程序成功的帮我们把对应的内容打印出来了。这也就进一步的说明了,其实linux中的命令就是用C语言写的程序,并且我们自己写的程序也是可以带上选项执行对应功能的,只是我们以前没有场景使用罢了

(2)char* env[]

对于这个参数,我们从名字上来看就很眼熟,因为查看环境变量的命令就是“env”。那么我们可以推测该参数是不是和环境变量有关。

我们写以下一个程序来测试一下:

这里因为env[]没有标识其内容个数的变量,因此直接用的是"env[i]"当判断条件。但是这是因为env[]的最后一个字符默认指向“NULL”才能这样用

我们现在来运行一下程序:

其结果我们也很眼熟,我们再执行“env”查看环境变量:

它们的内容一模一样。这也就说明了,主函数中的“env[]”是用来存储环境变量的

因此,每个进程都会收到一份环境表,环境表是一个字符指针数组,每个指针指向一个以'‘\0’结尾的环境字符串。同时存储这些环境变量的是char*数组。这就是为什么我们说“环境变量其实就是字符串”

五、进程地址空间

1.C/C++地址空间

在了解进程地址空间之前,我们先来回顾下C/C++的地址空间。大家应该都知道,C和C++的地址空间被分为代码区、堆区、栈区等多个区域:

在堆区和栈区之间是有一个公共空间的,供双方使用

当时我们可能以为这些地址空间就是内存上的内存。但是实际上它们并不是内存。在了解它们就是是什么之前,我们先来写一个以下内容的程序:

该程序会创建一个子进程,并根据I变量“id”的返回值执行不同的操作。同时这里面定义了一个全局变量“global_value”,该变量在3s后会被子进程修改为300。并且父子进程选项中会打印其自己的pid、ppid和全局变量“global_value”的值及其地址

现在我们来执行该程序:

根据打印内容我们可以发现,在3s之前,父子进程打印的全局变量的值和地址都是相同的。这很正常。但是当3s后子进程将全局变量修改后,我们发现子进程打印的值变成了300,但是父进程打印的值依然是100。并且更其奇怪的是,此时父子进程打印的全局变量的地址竟然是一样

我们之前说过,同一个地址上是不能同时存在不同的值的。但此时在同一个地址上却出现了不同的值。这就说明,此时我们打印出来的地址并不是内存上真正的空间地址,而是“虚拟地址”。我们以前学习C/C++语言时打印的所有地址其实都不是内存上的物理地址,而是虚拟地址。“虚拟地址”也被叫做“线性地址”“逻辑地址”

而我们以前所说的C/C++地址空间其实就是“虚拟地址空间”。虚拟地址空间的存在也使我们更好的支持了并发

2.虚拟地址空间

(1)进程认为自身独占系统资源

现在我们知道了在内存中存在虚拟地址空间。在理解虚拟地址空间之前,我们要先知道一个概念,“进程会认为它独占系统资源”。当然,在实际上进程并没有独占系统资源

举个例子,假如现在有一个有钱人,身价上百亿。而他自己比较喜欢花天酒地,在外面有三个私生子。这三个私生子之间互不认识。有一天这个有钱人分别对他的三个儿子说,“你好好学习好好工作,等我去世以后就把遗产全部给你”。此时它的三个都非常高兴,因为此时他们都认为自己会继承父亲的所有遗产。于是这三个儿子需要用钱时都会向付钱要钱。虽然他们知道自己将来能继承全部遗产,但现在还没有继承,所以不会无限度的要钱。而父亲为了不让他们乱花钱,就设置了一个限度,超过这个限度就不会给他们钱。

在这个例子里面,三个儿子就好比是“进程”有钱人就好比是“内存”。三个儿子都认为自己会继承全部遗产就好比是“进程认为自己独占内存所有资源”儿子向父亲要钱就是“进程向内存申请空间”父亲设置的给钱的额度就是“内存给进程设置的申请空间的最大值”。而父亲给三个儿子画的继承遗产的大饼就是“进程地址空间”

因此简单来讲,进程地址空间就是“操作系统给进程画的大饼”

(2)系统如何使进程认为自己独占所有资源

既然操作系统要让进程误认为自己独占所有系统资源,那肯定要有一个方法。我们之前讲过操作系统的管理方式是“先描述,再组织”,并且也了解了“PCB进程结构体”。而操作系统误导进程的方法也是相似的。在操作系统中,会构建一个struct结构体,该结构体中就包含了操作系统对进程画的饼的所有内容。

同时在系统中有几十上百个进程,操作系统为了避免遗忘或弄错给进程画的饼,就会在每个进程中都放入一个结构体,用该结构体来记忆操作系统给进程画的饼

因此进程地址空间的本质就是“是内核的一种数据结构,叫做mm_struct”

3.虚拟地址

在之前我们说进程地址空间的本质是“内核的一种叫做mm_struct的数据结构体”。既然是数据结构体体,那肯定就有结构体成员

在以前的学习中,想必大家都知道,地址空间描述的基本空间大小是字节。即一个地址代表一个字节

我们今天以32位系统来举例,假设我们现在有一个32位的系统,那么在这个系统下, 就会有2^32次方个地址。而一个地址代表一个字节,就说明在32位的系统下有4GB的空间。而这2^32次方个地址就是用unsigned int类型来表示的。因为该类型所占空间大小为4字节,共32个bit位,就能够保证2^32个地址都有唯一性

同时要注意,在系统中地址是从低地址向高地址使用的

(1)进程地址空间的区域划分

1.区域划分

我们说过在内存中划分有堆区和栈区。但是我们并不了解堆区和栈区是如何划分的。在这里,就举一个例子来理解堆区与栈区的划分

假设今天有小李和小王两个人是同桌,他们之间经常玩闹,有一天小李把小王惹生气了,于是小王和小李绝交并拿起了笔,在桌子上画了一条线,说这就是他们之间的“三八线”,各自用各自的区域不准越界。小李此时这种在双方之间划分使用区域的方式就叫做“区域划分”

2.区域调整

当小李和小王过了一段时间后,小李安耐不住想和小王玩,但又苦于这条三八线,于是找小王提建议说能不能在中间划分一个公共区域,大家就在这个公共区域玩。小王此时也没有那么生气了,也想和小李玩,就答应了小李的请求。于是小王拿起笔,在双方直接划了一个公共区域,之前是小李小王各50cm,现在就改成小李小王各45cm,中间的10cm作为他们两个之间的公共玩耍区域。此时这种划分公共区域的方法就叫做“区域调整”

3.区域扩大

小李和小王和好一段时间后,小李逐渐得意忘形,有一天又把小王惹生气了, 并且比第一次还严重。于是小王此时怒不可遏,又拿出尺子来划分“三八线”。这次小王就没有再用评分的原则,而是直接缩减小李的空间,改成小李30cm,自己70cm。这种扩大自己空间的方式就叫做“空间扩大”

在系统中,要实现对空间的划分,同样会形成一个结构体。我们假设这个结构体叫做“struct Destop”,那么该结构体内就是保存了各个空间的起始地址和结束地址

由此,我们的地址空间也是用同样的方法进行了划分。在32位系统的进程地址空间中有4GB空间,这些空间就大致如下划分:

当然,这里并没有全部写完, 只是写了部分区域划分

(2)虚拟地址

现在我们知道了在系统中有一个叫“struct mm_struct”的内核结构体。那么进程在运行时,就会需要依靠这个结构体来生成一份进程地址空间。假设我们现在有一个进程,该进程的PCB结构体“task_struct”中有一个“mm”指针,该指针指向一块malloc出来的“struct mm_struct”类型的空间:

而这块空间中,当然要有对应的区域划分,我们假设区域是如下方式划分的:

在这里面的如0x1111 1111到0x1222 2222这种区域划分出来的地址就叫做“区域起始地址”“区域结束地址”,而这些地址全部都是“虚拟地址”。并且这些虚拟地址的数量一定要是2^32

也就是说,我们的进程中都有一块虚拟空间,这些虚拟空间中全部都是虚拟地址,而这些虚拟地址就是给我们的代码、数据、堆区、栈区等各个空间使用的。要注意的是,如代码区、数据区这些区域的大小是固定不变的。但是栈区和堆区的大小是会改变的。而结合上面所说的,堆区和栈区等的空间变化其本质上就是修改结构体中对应区域的起始地址和结束地址

而我们之前说的操作系统会进程画饼,让操作系统误以为自己独占所有资源。这个饼其实就是我们上面的mm,即我们的虚拟地址空间

4.页表

虽然进程中用的是虚拟进程空间和虚拟地址,但是进程最终还是需要存在内存中,使用物理地址的。要使用物理地址,就需要用虚拟地址找到物理地址,而虚拟地址找到物理地址的媒介,其实就是页表

假设现在我们有一个磁盘,在这个磁盘中有一个test.exe程序,该程序加载到内存中要占用1k字节的空间。这个进程的在进入内存时,会对应生成一个“PCB结构体”,该结构体中的mm指针指向一块malloc出来的进程地址空间。假设此时这个进程想要定义一个char ch = 1,需要一个字节的空间。此时该空间会先有一个虚拟地址,然后再通过页表,将虚拟地址映射到物理地址上,此时才完成了定义

当然,实际的页表并不是一个虚拟地址对应一个物理地址。因为我们假设一个地址占四个字节,找一次地址就要2个地址,也就是8个字节。而32位系统下一共有2^32个地址,再乘以8,就需要32GB空间,这样仅仅一个页表就比内存都大了。使用在系统中的页表实际是非常复杂的,采用了树状结构来减少空间使用。此处只是为了方便认识页表才这样画

注意,页表每个进程都有的,而非在系统只有一个页表:

5.虚拟地址空间存在原因

有人可能认为,直接让进程访问物理地址而不是从虚拟地址通过页表映射到物理地址上会更加方便。诚然,这样确实更方便,但是也会存在一定问题

(1)保护其他进程和用户信息

首先虚拟地址空间就是为了保护其他进程和用户信息。假设我们现在没有虚拟地址空间,进程可以直接访问物理内存。如果该进程中存在越界访问的问题,并且这个进程的旁边是另一个进程的数据,此时就会导致其他进程的数据有遭到修改的风险

同时,如果你的电脑上有一个恶意程序,该程序运行起来直接就访问了你的物理内存,而你的物理内存上存在许多信息,包括你的各类用户信息。那么此时该进程就可以随意访问你的信息,并将其返回到程序中供他人非法获取

有人就很奇怪了, 虽然进程是通过虚拟地址找到物理地址的,但最终还是要到物理地址上去,那虚拟地址如何保护我们的数据呢?其实这一保护功能不是由虚拟地址完成,而是由“页表”完成。不要简单的认为页表只能提供映射,其实它还存在许多其他功能,就包括对进程行为的识别

举个例子,我们小时候都得到过压岁钱,这些压岁钱在我们手里,我们可以直接用,想买什么买什么。但是这个时候我们的父母通常会过来,告诉我们说把压岁钱交给他们保管。我们上交之后,每次想买点零食时,父母都会从我们的压岁钱里面拿点出来给我们。有一天,我们想买本漫画书,找父母要钱,此时他们就以妨碍学习为由,不给我们钱。

在这里面,我们就是进程,父母就是页表。父母把压岁钱收管,根据我们的需求来决定是否给我们钱就是页表在对进程的行为进行识别,判断是否合法

(2)保证进程的独立性

我们之前写过这样一个程序:

该程序重复打印子进程和父进程的pid、ppid和一个全局变量"global_value"的值。并且子进程会在3s后修改global_value的值

虽然全局变量“global_value”的值被改变了,但是在父子进程打印的全局变量的地址并没有改变的情况下,父子进程打印的gobal_value的值却不同。导致这个结果的原因就是“进程具有独立性”

在内存中,每个进程都有其独立的内核数据结构,父子进程也不例外:

我们说了父子进程因为其代码都是相同的,因此是“共享代码”的。也就是说,这两个进程在全局变量"gobal_value"未被修改时父子进程通过虚拟地址找到的都是同一块空间上的同一个值

但是在过了3s后,此时子进程会修改“gobal_value”的值,如果修改原空间上的值,势必会导致父进程的值也会被修改。就无法保证父子进程的独立性

因此某一个进程要对共享代码中的数据进行修改时,会先将原内存中的内容拷贝一份,然后在物理内存中重新找一块空间将内容拷贝进去。再修改页表将对应虚拟地址映射到物理地址,然后再修改这块空间上的值

这种任何一方尝试写入或修改数据, 操作系统先进行数据拷贝,更改页表映射然后再让程序进行修改行为,叫做“写时拷贝”。父子进程就是通过“写时拷贝”的方式来实现不同进程的数据分离。这是由操作系统来帮我们做的

对于那种毫不相干的进程,他们之间的内核数据结构和进程对应的代码和数据都是独立的,这也就保证了数据之间的独立性

因此,地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证进程之间的独立性

(3)便于编译器以统一视角编译代码

我们一直说每个程序加载到内存中会有一个PCB结构体,里面保存了关于该进程的各类属性和代码数据地址。但是,不仅仅是程序加载到内存中会有一块虚拟地址空间保存虚拟地址,程序在加载到内存之前也是有地址的。这个地址也是虚拟地址,不过我们通常叫做逻辑地址

很简单的一个道理,我们自己写的程序,在我们编译运行时我们说了,函数调用时要通过函数的地址去找到对应的函数声明,而这里的函数地址并不是加载到内存中才有的,而是在我们代码进行编译的时候就有了。换句话说,我们的程序在进入内存变成进程之前它的内部就已经有一套虚拟地址了。这套虚拟地址和进程结构体中的虚拟进程空间采用同样的方式,都是有2^32个地址

举个例子。假设我们现在有一个my.exe程序,这个程序的代码如下图所示:

当我们写好这个程序,将该程序进行编译时,该程序的内部就会自动为每个函数、每行代码乃至每个变量都生成一个虚拟地址,并且里面调用了函数或者使用了其他变量的代码的地址就是其对应的定义处的地址:

当我们的程序运行起来加载到内存里面时,该进程本身就又有了一套地址。注意这里该进程其实有两套地址,一套是进程内部进行跳转的虚拟地址,另一套是标识该进程在物理内存中存在的物理地址

此时我们要意识到,当程序加载进内存时,会形成一个PCB结构体,这个结构体中有一个mm指针指向开辟的进程地址空间,该地址空间中会用多个start、end值来标识各个区域的空间划分。我们以代码段为例,上述程序中的代码都需要保存在代码区中,而代码区的大小就是主函数的代码大小,我们图中的主函数是从0x1111 1111开始的,我们假设该代码的大小是10kb,那么代码区的结束位置就是“0x1111 1111 + 10kb”。

有了这些准备后我们再来进程与CPU的寻址问题。这里要记住,加载到CPU中的是进程的PCB结构体,而非进程本身。假设该程序此时运行到主函数中的func()函数调用处,此时系统就会将func()函数的虚拟地址0x1122 2222加载到CPU中,CPU再通过该地址找到该进程的进程地址空间的对应位置,然后通过进程地址空间与页表的映射找到物理内存中的代码存储位置,然后去调用func()函数。func()中的a变量也是同理。要调用a变量,就要将a变量的虚拟地址加载到CPU中,CPU通过该地址找到该进程中的进程地址空间中的a变量的位置,通过这个地址与页表映射找到物理内存中a的物理地址进行调用

在这整个过程中,CPU都知道对应代码的虚拟地址,并没有见到过它的物理地址。而程序中自行形成的虚拟地址的格式与地址位置和进程中的进程结构体指向的进程地址空间是差不多的。也就是说,程序中的虚拟地址与进程地址空间的虚拟地址在一般情况下是一样的。这样也就便于CPU直接将对应虚拟地址放到进程地址空间去进行页表映射

因此,进程地址空间存在的另一个重要原因就是“让进程以同一的视角,来看待进程对应的代码和数据等各个区域,方便使用。同时也让编译器以统一的视角来编译代码。”这样,代码编译完后,就可以直接使用了

5.线性地址与逻辑地址

(1)线性地址

我们之前说,虚拟地址又叫做线性地址。因为虚拟地址是从0一直到2^32,是连续不断的。因此在一些教材里面,虚拟地址又被叫做“线性地址”

(2)逻辑地址

逻辑地址其实就是在程序内部的用于代码跳转的地址。只不过在linux中我们说的逻辑地址和虚拟地址是一个东西。但是虚拟地址的名字听起来更好理解,所以在文中用的都是虚拟地址,但是在实际中常用的名字是逻辑地址

初识linux之进程相关推荐

  1. 单片机过渡到,对linux的初识(线程到进程)

    大家好,我是小昭,一路在debug调试,代码缓慢地优化中,希望生活也能优化起来-- 前言 为了应对自身专业能力的提升和工作的要求,开始学习linux,过程中遇到一些问题,比如像mmu,为什么一般单片机 ...

  2. Linux内核分析(三)----初识linux内存管理子系统

    原文:Linux内核分析(三)----初识linux内存管理子系统 Linux内核分析(三) 昨天我们对内核模块进行了简单的分析,今天为了让我们今后的分析没有太多障碍,我们今天先简单的分析一下linu ...

  3. Linux笔记001 初识Linux

    下一集:Linux 002用户和用户组管理命令 一.初识Linux 在前面的课程中,我们无论是开发.测试.部署.存储都在Windwos操作系统的环境中,从今天开始我们一起学习下Linux,Linux系 ...

  4. 4.1 Linux之初识Linux

    初识Linux 4.1.1 操作系统 4.1.2 Linux发展历程 4.1.3 Linux简介 4.1.3.1 什么是 Linux 4.1.3.2 Linux的特点 4.1.4 Linux和Unix ...

  5. 第十九天:初识Linux+系统与设置命令+目录管理

    Linux 1 初识Linux 在前面的课程中,我们无论是开发.测试.部署.存储都在Windwos操作系统的环境中,从今天开始我们一起学习下Linux,Linux系统和Windows系统最大的区别就是 ...

  6. Linux之学习目标,初识linux操作系统。(其开发者们之无私奉献与分享合作精神,使我辈敬佩之至,在此由衷地向前辈们表达敬意与感谢。)

    学习目标 能够知道什么是Linux系统以及它的应用场景 能够独立完成安装VMware虚拟机和网络配置 能够独立完成安装CentOS以及远程终端SecureCRT 能够熟练编写账户管理.用户组的增删改查 ...

  7. 初识Linux操作系统及常用的Linux命令

    文章目录 每日一句正能量 前言 一.Linux简介 二.Linux常用命令 每日一句正能量   平淡的生活,会带给你最简单的幸福:忙碌的脚步,会带给你最美丽的风景:真诚的祝福,会带给你最由衷的快乐! ...

  8. Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存

    Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存 参考:<linux编程从入门到精通>,<Linux C程序设计大全>,<unix环境高级编程> ...

  9. Linux守护进程实现

    Linux守护进程 redis版: void daemonize(void) {int fd;if (fork() != 0) exit(0); /* parent exits */setsid(); ...

最新文章

  1. C基础知识小总结(十)
  2. 附录:PyTorch记事本
  3. Bootstrap入门(二十一)组件15:警告框
  4. 主题:log4j详解与实战
  5. iBatis.Net(C#)数据库查询
  6. 第一篇博客,写在颓废之时
  7. 陈伯雄lisp_基于AutoLisp的AutoCAD二次开发自动生成系统图
  8. Android官方开发文档Training系列课程中文版:高效显示位图之加载大位图
  9. python的print怎么输出utf-8的编码_原创反转精度算法:小数的终极编码
  10. 两个整数集合的交集 ———— 腾讯2014软件开发笔试题目
  11. json 例子_json-简单的例子
  12. Stopwatch 类
  13. 2021-09-13强化学 习 原理及技术介绍
  14. Linux环境下编译运行大型C语言项目
  15. php免安装配置方法,mysql免安装版配置步骤详解
  16. [Intellij IDEA] 通过学生认证免费激活IDEA
  17. html总微软雅黑怎么设置,css怎么设置字体为微软雅黑
  18. 计算机专业认知存在的问题,浅谈新形势下计算机专业存在的问题与对策
  19. ThinkPad安装Mac
  20. ASP.NETt运行原理和运行机制

热门文章

  1. 关于redis服务的代码编码
  2. 用UNION的注意事项
  3. (转)深度学习中各种图像库的图片读取方式
  4. 学习方法推荐——时间管理
  5. android 数据线有几种,不止是安卓和苹果线,手机数据线原来还有这几种!
  6. 开关为什么要过零检测?内附带代码
  7. 漫谈核心能力(2) -- 知错能改,善莫大焉
  8. APISpace 号码实时查询API接口 免费好用
  9. 李迅雷+趋势的力量+K型分化时代如何赢取超额收益
  10. python-django(一)