目录
一.什么是进程
进程和程序的区别
Linux查看进程
进程的信息
fork函数
二.进程状态
操作系统上进程状态的概念
运行
阻塞
挂起
Linux中的进程状态
R状态
S状态和D状态
T状态
t状态
X状态
Z状态
三.进程的优先级
修改进程优先级
四.环境变量
常见的环境变量
PATH
HOME
PWD
模拟pwd命令
getenv函数
main函数的两个参数
编辑
main函数的第三个参数
五.进程地址空间
什么是进程地址空间
为什么要有地址空间
一.什么是进程
根据书本上的概念来解释,一个运行起来(被加载到内存中)的程序称为进程。
光这样一句话很难体会到什么是进程,所以我们来展开讲一下。
进程和程序的区别
进程是被加载到内存上的,而程序是我们写好的一份二进制文件存在磁盘上的。
那么到底什么是进程呢?
首先我们来讲一下PCB进程控制块,PCB进程控制块在Linux中就是一个名叫task_struct的结构体,在这个结构体里面,它可以存放进程的各种信息,当一个程序被加载到内存中的时候,操作系统都会为这个程序生成一个PCB进程控制块,这个控制块里面存放了这个程序的各种信息,以便于维护这个进程。
所以进程可以理解成,一个二进制程序文件被加载到内存中,并且操作系统会为其构建一个PCB结构体来存放这个程序的信息,以便来维护它。
Linux查看进程
在Linux当中,我们可以使用 ps ajx 命令查看进程。
也可以直接查看/proc文件夹。
如下图所示,使用ps ajx来查看进程。
进程的信息
在查看进程信息中,我们可以发现有两栏:PPID、PID,这两个分别是父进程id和进程id。
那具体又是什么意思呢?
当我们运行一个程序的时候,它自己的进程id就叫做PID,它的父进程id就叫做PPID。
那为什么进程有会分为进程和父进程呢?
首先我们来了解一下操作系统,操作系统是一款管理软件,它可以管理好我们的软硬件资源,当我们要运行我们的程序的时候,操作系统会帮助我们去运行它,但是注意的是,操作系统不会亲自去运行该程序,而是操作系统会去派生一个子进程来运行我们的程序,为什么呢?
如果是操作系统亲自去运行我们的程序,那要是程序运行崩溃了,这就会导致我们的操作系统也一起崩溃,所以为了防止这种情况,操作系统会去派生一个子进程来运行我们的程序,即使程序运行崩溃了,最多就这个子进程崩溃了,而不会影响到我们的操作系统。
fork函数
在我们的Linux里面,提供了一个fork函数,这个函数可以在程序中创建子进程。
fork函数是去创建一个子进程,同时它会有两个返回值,这两个返回值分别给到父进程和子进程,父进程会返回子进程的id,子进程会返回0,如果子进程创建失败,则返回小于0的数。所以根据这两个返回值,我们可以很好的分开父进程和子进程所运行的代码。
如下代码:
当我们的程序运行起来的时候,会有两个进程在运行,子进程运行else if处的代码,父进程运行else处的代码。
运行结果如下所示。
二.进程状态
知道了进程是什么后,那么每个进程都会有很多种状态,那各种状态又代表什么意思?
操作系统上进程状态的概念
进程的状态有很多:运行,新建,就绪,阻塞,挂起等等,我们在这里挑一下讲解。
运行
有的人认为,一个进程正在被CPU处理,就叫做运行,其实这并不完全正确,因为一个进程没有被CPU处理,也可以有运行的状态。
假设内存中有多个进程,而同时又有多个进程需要被CPU处理,那么它们是怎么做的呢?
操作系统会为CPU创建一个CPU运行队列,凡是要被CPU处理的进程都会进到这个队列中,而当进程被放入到CPU运行队列中,则称为运行状态。
同时CPU处理运行队列是一种轮转的方式,假设现在我们的CPU只有一个核,但是这个时候我们的CPU运行队列很长,有20个进程都需要被运行,但是我们的CPU只有一个核,也就是说我们的CPU一次只能处理一个进程,但是我们在用电脑的时候,并没有因为CPU一次只能处理一个进程而导致其他的进程无法运行,那是因为我们的CPU有一个轮转的机制,对于CPU运行队列上的进程,我们的CPU会轮流去执行所有进程,且轮转的速度非常快,快到我们感受不到,所以当我们的电脑有很多进程的时候并没有感觉到其他进程卡死,那是因为CPU的轮转速度很快。
阻塞
在一个进程中,不仅会有计算的部分,还会有输出的部分,比如我们写了个代码,将程序中的所有数据写入到磁盘中,那么在这个进程中,不仅需要CPU去处理,还需要用到磁盘。
那么如果此时有别的进程正在占用磁盘资源又会怎么样呢?操作系统也会给每个硬件创建一个运行队列,将需要用到此硬件资源的进程按顺序的放入队列中,此时CPU正在处理的进程而在磁盘队列中排队,就叫阻塞。
挂起
当进程的阻塞状态较多的时候,会占用非常多的内存空间,当内存空间满了的时候,为了能使时计算机正常运行,操作系统会将还没排队到的阻塞进程暂时的放入磁盘中,这个进程的状态叫做挂起。
Linux中的进程状态
上面介绍的都是大部分操作系统下的解释,那么接下来看看Linux中,状态是如何表示的。
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状态
R状态就是对应上面的运行状态。
S状态和D状态
S状态可以对应上面的阻塞状态或者是挂起状态,至于是哪一个,由操作系统决定。
D状态时Linux下特有的,叫做深度睡眠状态,这个状态在这里不做讲解。
T状态
T状态就是暂停的意思,指这个进程停止运行了,但它还在。
t状态
t状态表示该进程正在被追踪,简单的理解就是,当我们使用gdb调试器对这个进程的代码进行调试打断点的时候,就是这个状态。
X状态
X状态就是死亡状态,就是简单的指该进程已经被结束掉了。
Z状态
Z状态是僵尸状态的意思,为什么会有僵尸状态呢?
因为当一个进程结束的时候,不会立马消失,它的父进程或者操作系统会对该进程进行收集信息,简单的理解就是,走了也得有个交代,是怎么走的,要调查清楚。
三.进程的优先级
所谓的进程优先级就是cpu资源的分配顺序,优先级高的进程会优先使用cpu资源,反之。
在linux系统中,我们可以使用ps -l命令来查看进程的优先级。
进程的优先级由两部分组成,分别是PRI和NI。
PRI就是进程的优先级,该值越小则优先级越高,反之。
而NI是该进程的优先级可修改数值,也就是说,我们可以通过效果NI值来修改进程的优先级。
进程优先级=PRI+NI值
修改进程优先级
若要修改进程的优先级,我们可以用top命令、再按r,再输入进程的pid、最后输入NI值来修改进程的优先级。
注意,NI的值调整只在[-20,19]这个区间中,超过了,会默认选边界的值。
四.环境变量
对于环境变量来说,可能大部分同学都知道,比如说,要写Java的代码时,需要配置jdk的环境变量,又或者使用vscode时,也需要配置环境变量,但是环境变量到底是什么,为什么要配置环境变量?
首先,我们来看一个现象。
我们在使用Linux时,会使用到各种各样的命令,同时,Linux也有着一个理念:Linux下一切皆文件,也就是说,Linux命令其实也是一个文件,只不过是用C\C++编写的后编译而成的可执行程序,同时,我们也可以编写自己的可执行程序。
pwd命令的路径
但是现在有一个问题,我们在运行自己的可执行程序的时候,通常要在前面加上./,而命令不用。如下:
那么是什么原因导致的呢?首先,我们来看一下Linux系统中的环境变量,通过env命令查看。
把全部环境变量打印出来有很多,但不用怕,我们把我们需要的截图出来即可。
我们可以看到,在Linux系统中,有一个这样的环境变量,这里面正好有我们命令的路径。
同时在这里再引入另一个问题,当我们要运行我们的程序的时候,操作系统怎么知道我们的程序在那,它是怎么找到我们的程序的,那既然涉及到找程序,那必须要有该程序的路径,路径又有绝对路径和相对路径,当我们运行我们的程序的时候,也是去找我们程序的位置,即./test中的./就是该程序的相对路径,所以操作系统会根据这个路径去找我们的程序,并运行起来。
所以当我们的操作系统要去运行pwd命令时,它也要去找该命令的路径,但是这个路径不需要我们提供,因为有一个环境变量,这个环境变量有这些命令的路径,操作系统会根据这个环境变量的路径去找到我们的命令。
所以说,这就是环境变量的意义,当我们在vscode中配置C语言的环境变量时,就是给一个路径,让我们的程序去这个路径中找到C语言的库。
常见的环境变量
接下来介绍一下常见的环境变量,以及如何查看单个环境变量。
PATH
是Linux系统中命令的路径
HOME
用户的家目录
PWD
用户所在目录
说到底,环境变量就是一堆字符串的全局变量,这些字符串的作用是,当我们要去使用某些命令或者功能时,需要通过这些环境变量去找,而且这个环境变量是内存级别的变量,当我们启动机器时,它才会被加载到内存中,关机后就会消失。
模拟pwd命令
既然环境变量中存放着我们需要用到的信息,那么我们可以使用这些环境变量来模拟实现pwd命令。
getenv函数
在Linux中,有一个函数,可以去获取我们的环境变量。
getenv函数,给一个环境变量的名字,返回它的环境变量的内容。使用方法如下。
我们编写的这个程序,是去PWD这个环境变量中获取我们所在的目录,并将其输出,所以我们自己写的pwd命令就简单的完成了。效果如下。
main函数的两个参数
有一些同学可能就会见过,在main函数的参数里,有两个参数。
那么这两个参数分别又是什么呢?
char* argv[]:这是一个指针数组,每个位置里面放着一个char类型的指针。
int argc:记录着argv[]这个数组的大小。
那么这个指针数组里面的char指针又指向什么内容呢?我们来运行下面的代码来看看。
通过运行这个代码我们可以看到,当我们输入./test是它就输出./test
当我们输入./test -a -l时,它就输出./test -a -l。
这就是我们在使用命令时候的选项,比如说:ls -a -l
它就是通过这种方法来识别我们的选项,然后根据你所输入的选项来输出对应的信息。
main函数的第三个参数
main函数不仅有前两个参数,还是第三个参数。
第三个参数是char* env[],这也是一个指针数组,这个数组里面每一个指针都指向一个环境变量。我们来运行看看。
所以说,要在程序中获取环境变量,除了可以使用getenv函数外,还能用char* env[]参数来获取,不过一个第一种方法用的比较多。
五.进程地址空间
什么是进程地址空间?
在进行解释前,我们先来看一段程序。
下面是这段程序的代码。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int global=10;//定义一个全局变量
5
6 int main()
7 {
8 pid_t id=fork();//创建子进程
9 if(id<0)
10 {
11 printf("创建子进程失败\n");
12 return 1;
13 }
14 else if(id==0)//说明是子进程
15 {
16 int cnt1=3;
17 while(cnt1--)
18 {
19 printf("我是子进程,全局变量global:%d,它的地址是:%p\n",global,&global);
20 sleep(1);
21 }
22
23 global=20;//在子进程中改变global的值
24
25 int cnt=3;
26 while(cnt--)
27 {
28 printf("我是子进程,全局变量global:%d,它的地址是:%p\n",global,&global);
29 sleep(1);
30 }
31 }
32 else//说明是父进程
33 {
34 int cnt2=6;
35 while(cnt2--)
36 {
37 printf("我是父进程,全局变量global:%d,它的地址是:%p\n",global,&global);
38 sleep(1);
39 }
40 }
41 return 0;
42 }
在这段代码中,我做的事情是:创建了一个全局变量global,且创建了一个子进程,在子进程和父进程中,刚开始我都进行了相同的操作,将全局变量的值和地址都进行一个输出,3秒后,我在子进程中将全局变量global的值进行改变,再进行一个输出。
现在我们来看一下运行的结果。
在前3秒的运行结果中,我们可以看到,父进程和子进程都进行了相应的输出,它们打印出来的global的值都是相同的,这应该没什么问题,且它们打印出来的地址也是相同,这样看起来也确实没什么问题。
但是当在子进程中将global的值进行改变的时候,只有子进程的global改变了,而父进程的global没有改变,这看起来也很合理,但是这个时候发现,父进程和子进程的global的地址竟然也是相同的???
看到这个现象难免会觉得很奇怪,我们来分析一下:
我们知道,当一个程序运行起来的时候,它会被加载到内存中,相应的它所创建的变量也会在内存中,同时我们知道内存在电脑中其实是一个硬件,即内存条,就是说这些变量和程序都是放在内存条中的。
这时我们在看上面的代码结果,发现父进程和子进程global这个变量的地址是相同的,但是值不同,我们在把这个结果套入我们的内存条中分析,它们的地址相同,说明在这个内存条上,父进程和子进程的global的位置是相同的,但是值不同,说明在这个位置的内存上的值不同,也就是说相同的位置却又两个不同的值?
这看上去显然不合理,所以说上面结果所打印出来的值其实不是真实的地址,而是虚拟的。
什么是进程地址空间
上面的问题,我们先放一放,我们来解释一下什么是地址空间。
在学习C/C++的时候,应该都见过这个图,
为什么这个图叫做C/C++的内存分布,而不是内存条的内存分布?
那既然有C/C++的内存分布,那是不是说明还有其他语言的内存分布,为什么同一块内存条上,不同的语言会有不同的内存分布呢?
那是因为这个C/C++内存分布其实是虚拟地址,这个内存分布的地址不是真实的,这个虚拟地址就叫做进程的地址空间。
那么进程的地址空间到底是什么?
在linux中,进程的地址空间其实就是一个结构体变量,当一个程序被运行起来的时候操作系统会给它创建这个结构体的变量,在这个结构体变量中,它记录了栈区、堆区、未初始化数据区,初始化数据区的起始位置和终止位置,也就是虚拟地址,且当程序运行起来的时候,会通过页表将这个结构体变量上的虚拟地址映射到物理内存中,光看这句话很难理解,我们来看一下图。
当我们把一个程序运行起来的时候,会将这个程序加载到内存中,同时,操作系统会为这个进程创建一个PCB进程控制块,同时也会创建这个进程的地址空间,最后通过页表将地址空间中的虚拟地址映射到物理内存中。
为什么要有地址空间
知道了什么是地址空间后,那么为什么要有地址空间呢?
那是因为要保证进程的独立性,同时也是为了安全考虑的,如果没有地址空间,让进程直接去访问物理内存,那要是越界了怎么办,有了地址空间的存在,要是越界访问了能很好的进行判断并阻止。
那么进程的独立性又是如何保证的?
我们接着来看开始那段程序:
首先我们要先知道,父进程是如何创建子进程的。
当我们调用fork函数去创建子进程的时候,操作系统会帮我们把父进程的数据和代码全盘拷贝一份,同时PCB进程控制块和进程的地址空间和页表也会全盘拷贝下来。这个时候子进程就创建好了,这对于前3秒的运行结果来看,没什么问题,但是当我们更改全局变量global的时候,这里会发生一个写时拷贝,操作系统会为子进程的global变量开一块新空间,将新的值20写进去,写完后,再改变子进程页表的映射关系来保证进程的独立性,使父进程和子进程的global变量互不影响。
光看这段看也很难理解,我们来看一下图: