Linux进程概念

进程

基本概念

课本概念:程序的实例,正在执行的程序等。

内核观点:承担分配系统资源(CPU时间、内存)的实体。

在平时运行代码时,代码进行编译链接后会生成一个可执行程序,这个可执行程序本质上是一个文件,放在磁盘上。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存中,只有加载到内存中,CPU才能对其进行逐行的语句执行,一旦将这个程序加载到内存中,我们就不再将这个程序叫做程序,而改称进程。例如在windows中的任务管理器,我们运行的所有程序都变成进程存在内存中,占用内存空间。

b3d48a15f1f44809a719cb49ac700f15.png

描述进程-PCB

在系统中,可以存在大量进程,在linux系统也不例外,使用ps aux可以查看系统中存在的进程:

7a45e6ee8ae1458083113c2c8a6db3b0.png

进程信息被放在一个叫做进程控制块的数据结构中,进程控制块可以理解成进程属性的集合。

在课本中,进程控制块被称为PCB(Process Control Block)

操作系统将每一个进程都进行描述,形成一个个进程控制块(PCB),并将这些PCB以双链表的形式组织起来。

8084ad0ba00641a694b36384c395185b.png

 当变成双链表的形式之后,操作系统对于进程的控制就了如指掌了,因为对进程的控制就相当于对双链表的控制,例如创建一个进程,先将进程的代码和数据加载到内存,然后再将PCB插入到双链表里,而退出一个进程,就是先将这个PCB删除,然后再将代码和数据进行释放或者置为无效。

task_struct,PCB的一种

PCB是描述进程的,在C++中称之为面向对象,而在C语言中我们称之为结构体,且Linux是由C语言编写的,那么Linux当中的进程控制块必定是用结构体来实现的,那么在Linux中描述进程的结构体叫做:task_struct

PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct

task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息

task_struct内容分类

所以task_struct就是Linux当中的进程控制块,task_struct当中主要包含:

标示符:描述本进程的唯一标示符,用来区别其他进程。

状态:任务状态,退出代码,退出信号等。

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

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

内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

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

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

记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。

其他信息

查看进程

查看进程有两种方法:

1.通过系统目录查看:在根目录下有一个名为proc的系统文件夹,文件夹中包含大量的进城信息,其中有些子目录的目录名为数字

40b1be1fb800417f8522fb3e75f60390.png

这些数字其实是某一些进程的PID,对应文件夹中记录着对应进程的各种信息,如果我们想查看PID为1的进程的进城信息,则查看名字为1的文件夹即可。

6df821bc01af4c17b4ca9f811b257f3b.png

2.通过ps命令查看

使用ps aux命令,可以显示所有的进程信息

92ba2cadc4ed456d912e2b58aae6b517.png

通过系统调用获取进程的PID和PPID

首先要知道:

父进程(PPID)

子进程(PID)

通过使用系统调用函数,getpid和getppid可以分别获取进程的PID和PPID。

dcb767783ccf4235b9fd76ff17c689ff.png

 1becd3fde6884fcca00c38c50353dd27.png

通过系统调用创建进程- fork函数初识

首先,我们可以通过系统的man函数来认识fork函数,也就是 man fork。

fork是一个系统调用级别的函数,其功能就是创建一个子进程。

当运行下列代码:

8ac855fc13314fae81442eb77f76bee5.png

可以和上面没有fork函数的代码进行对比:如果没有fork函数,那么就会循环打印进程的PID和PPID,如果有了fork函数,结果如下:

 ae1bf6d52b1247da9734d917b4b8cd9d.png

可以看到,第二行的PPID是第一行的PID,然后一二行循环打印,说明父进程打印的是第一行,子进程打印的是第二行。也说明了fork函数创建出来的子进程与process进程是父子关系。

另外,fork出来的子进程与别的进程一样,系统也会为其创建PCB。

例如在windows的任务管理器中:

d9b4966124cc4fe2b744ca3c9c3a8fbb.png

这里展开的列表都是子进程。 

我们知道代码是属于父进程的,那么fork函数创建的子进程代码和数据又怎么来呢?

先查看并运行下列代码:

e4b5b0274e4b439b949eca77a27d98ea.png

0c05acffba3e4bf398a1f8dbcc3b20e6.png

可以看到,fork函数调用前,代码都是由父进程进行执行,fokr函数之后的代码,默认情况下父子都会运行,虽然代码是共享的,但是父子进程各自开辟空间(写时拷贝)

需要注意的是,父子两个进程,系统先调用谁是不确定的,取决于操作系统的调度算法。

使用if进行分流

在上面说到,fork之后的代码由父子进程共同运行,但是我们创建出来的子进程做的事和父进程一样的话,那就没有意义的,此时就需要使用if语句进行分流,让父子进程分别做不同的事情。

fork函数的返回值:

1.如果子进程创建成功,在父进程中返回子进程PID,而在子进程中返回0。

2.如果子进程创建失败,则在父进程中返回-1。

此时我们知道父进程与子进程获取fork函数的返回值不同,那么我们就可以利用返回值来使用if语句进行不同的代码,来做不同的事。

如下列代码:

b907ee6404ef4c4bb0e14a0c3203fb7e.png

运行结果: 

7f1c70bf947947689b16de1fae743c19.png

进程状态

我们知道,一个系统中有大量的进程,那么这些进程都是一直在运行的吗?当然不是,如果一直运行,硬件会吃不消,所以进程会拥有很多不同的状态,为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态,一个进程可以有几个状态(在Linux内核里进程有时也叫做任务)。

我们先看看状态在linux内核中的定义:

/*
* 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 *task_state_array[] = {
	"R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};

 进程状态是保存到自己的进程控制块(PCB),在Linux系统中也就是保存在task_struct中

在linux中,可以使用 ps aux或者ps axj命令查看进程状态。

c84c85d9ed0a40ddb8263c1d29ea85a6.png

d1a49515001f48b09b297f853847163e.png

运行状态 -R 

一个进程处于运行状态(Running),并不意味着进程一定在运行中,它表明进程要么是在运行中,要么是在运行队列里,也就是说,可以同时存在多个R状态的进程。

所有处于运行状态的进程,都会被放到运行队列中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

浅度睡眠状态 -S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事件完成,可以随时被唤醒,也可以随时被杀掉(这里的睡眠有时候也叫做可中断睡眠(interrupt sleep))。

深度睡眠状态 -D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复,该状态有时候也叫不可中断睡眠(uninterrupted sleep),处于这个状态的进程通常会等待IO的结束

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(该状态也叫做磁盘休眠状态)

暂停状态 -T

在Linux中,我们可以发送SIGSTOP信号使进程进入暂停状态,发送SIGCONT信号可以让处于暂停状态的进程继续运行。

kill -SIGSTOP PID

kill -SIGCONT PID

死亡状态 -X

死亡状态只是一个返回状态,当一个进程的退出信息被读取之后,该进程申请的资源就会被释放,该进程也就不存在了,所以我们不会在任务列表中看到死亡状态。

僵尸状态 -Z

当一个进程要退出的时候,在系统层面,该进程曾经申请的资源并不是立即释放,而是要暂时存储一段时间,以供操作系统或者其父进程进行地区,如果退出信息一直未被读取,那么相关数据不会被释放,也就是说,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。

僵尸状态的存在是必要的,因为进程被创建的目的是为了完成某一个任务,那么任务完成时,调用方需要知道任务完成情况,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。

我们可以使用 echo $? 来获取最近一次进程退出时的退出码。

echo $?

僵尸状态的危害

1.僵尸进程的退出状态必须要一直维持下去,因为它要告诉其父进程相应的退出信息,可是父进程一直不读取,那么子进程就会一直处于僵尸状态

2.僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。

3.若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构本身就要占用内存。

4.僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。

孤儿进程

Linux当中的进程关系大多是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么该子进程我们称为僵尸进程;若是父进程先退出,将来子进程进入僵尸状态时就没有父进程对其进行处理,那么此时子进程此时称为孤儿进程。

若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程时,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由进行回收处理。

进程优先级

基本概念

什么是优先级?

优先级实际上是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,优先权高的进程有优先执行的权利。

优先级存在的原因

因为CPU的资源是有限的,一个CPU一次只能跑一个进程,而进程是有多个的,所以需要存在优先级来确定进程的先后顺序。

查看系统进程

在linux或者unix系统中,使用ps -l命令会类似输出以下几个内容:

列出的信息当中有几个重要信息,如下:

UID:代表执行者的身份

PID:代表这个进程的代号

PPID:代表这个进程是由哪个进程发展衍生而来的,也就是父进程的代号

PRI:代表这个进程可被执行的优先级,其值越小越早被执行

NI:代表这个进程的nice值

当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。 

ps -al

通过top命令更改进程的nice值

输入top,如下图,类似于任务管理器,可以实时显示系统进程的资源占用情况。

 使用top命令后按“R”键,会要求输入待调整nice值的进程PID:

输入PID并回车后,会要求输入调整后的nice值,输入完成后按“Q”即可退出,此时就成功更改进程的优先级了。 

需要注意的是:如果NI想调整为负值,也就是调高优先级,需要使用sudo命令提升权限。

通过renice命令更改进程的nice值

renice NICE值 PID

输入上述命令即可直接更改进程的优先级信息,如果NICE值为负,也需要sudo提升权限。 

PRI与NI

1.PRI代表进程的优先级,通俗点就是进程被CPU执行的先后顺序,该值越小进程优先级别越高

2.NI代表的是nice值,其表示进程可被执行的优先级的修正数值

3.PRI值越小越快被执行,当加入nice值后,将会使PRI变为:PRI(new)=PRI(old) + NI

4.若NI值为负值,那么该进程的PRI会变小,优先级会变高

5.调整进程优先级,在Linux下,就是调整进程的nice值

6.NI的取值范围是-20到19,一共40个级别

在Linux中,PRI(old)默认为80,即PRI = 80 + NI 

查看进程优先级的命令

top命令更改进程的nice值

renice命令更改进程的nice值

四个重要概念:

竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。

独立性:多进程运行,需要各种独享资源,多进程运行期间互不干扰。

并行:多个进程在多个CPU下分别同时进行运行,这称之为并行。

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

环境变量

基本概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。

例如,我们编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。

常见环境变量

PATH:指定命令的搜索路径

HOME:指定用户的主工作目录(即用户登陆到Linux系统中的默认所处目录)

SHELL:当前shell,它的值通常是/bin/bash

查看环境变量的方法

我们可以通过echo命令来查看环境变量:

echo $NAME //NAME为待查看的环境变量名称

例如查看环境变量PATH:

57bec0aeb2614ce994cf249398668937.png

测试PATH

大家可以想一下,为什么执行ls命令不用带./,而执行自己生产的可执行程序要带./呢?

我们需要知道,执行一个可执行程序必须要找到它在哪里,既然不带./就可以执行ls命令,说明系统可以找到ls的位置,当然系统就无法找到我们自己的可执行程序,所以必须带上./,用来告诉系统该程序在当前目录下

而系统就是通过环境变量PATH来找到ls命令的,查看PATH环境变量:

57bec0aeb2614ce994cf249398668937.png

再查看ls的位置:

 9f81f652efc945fa8832c20f881c2103.png

可以看到,ls在/usr/bin目录下,而PATH包含了该变量,说明ls是在PATH的某个路径之下,所以ls是可以不带路径执行,系统也能找到位置的。

那么我们可以让我们自己的可执行程序也不用带路径就可以执行吗?

当然可以,有两种方式:例如有一个process程序

方式一:将可执行程序拷贝到环境变量PATH的某个路径下

sudo cp proc /usr/bin

方式二:将可执行程序所在的目录导入到环境变量PATH中

export PATH=$PATH:/home/bear/dirforproc/ENV

这里就引出环境变量相关的命令:

和环境变量相关的命令

1.echo:显示某个环境变量值

2.export:设置一个新的环境变量

3.env:显示所有环境变量

4.unset:清楚环境变量

5.set:显示本地定义的shell变量和环境变量

测试HOME

我们需要知道,任何一个用户在运行系统登录时都有自己的主工作目录(家目录(HOME目录)),环境变量HOME当中即保存的该用户的主工作目录。

普通用户示例:

e2b0bf26a37444558ad4a0f833ee033e.png

root用户示例 :

e471c4ace8ed445ab56f3c5990e96709.png

测试shell

我们在Linux系统当中所敲的各种命令,实际上需要由命令行解释器进行解释,而在Linux当中有许多命令行解释器(例如bash、sh),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器种类。

b545c5b2e8094523819bc9be2cf6fae8.png

而该命令行解释器实际上是系统当中的一条命令,当这个命令运行起来变成进程后就可以为我们进行命令行解释。 

e531d30727194737a01b0d5486091b31.png

环境变量的组织方式

在系统中,环境变量的组织方式如下:

每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以‘\0’结尾的环境字符串,最后一个字符指针为空。 

通过代码获取环境变量

在我们平常写代码,总是会带一个main函数,实际上main函数是有参数的。

main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。我们可以在windows下的编译器进行验证,当我们调试代码的时候,若是一直使用逐步调试,那么最终会来到调试main函数的地方。

mainret = main(argc, argv, encp)

可以看到,main函数有三个参数。

argc与argv

在Linux操作系统下,运行下列代码:

运行结果: 

可以看到,在main函数中,argc指的是参数的个数,而argv[]里面包含着我们所给出的若干选项。

 envp[]

这个变量实际上是环境变量表,我们可以通过这个参数来获取系统的环境变量:

运行下列代码

 运行结果:

除了使用main函数的第三个参数来获取环境变量以外,我们还可以通过第三方变量environ来获取。

运行结果:

需要注意的是:libc 中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用environ时要用extern进行声明。

通过系统调用获取环境变量

除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。

getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

例如,使用getenv函数获取环境变量PATH的值:

程序地址空间

我们来一段代码:

代码当中用fork函数创建了一个子进程,其中让子进程先将全局变量g_val从100改为200后,再让父进程先休眠3秒钟,然后再打印全局变量的值。

按道理来说,子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200,但是代码运行结果如下:

可以看到父进程打印的全局变量g_va仍为之前的100,并且父子进程中打印的全局变量g_val的地址是一样的,这就意味着父子进程在同一个地址读出的值不同。

按照正常来说,同一个物理地址获取的值必定是相同的,现在同一个地址获取的值不同,也就意味着这里的地址并不是物理地址,是虚拟地址,物理地址用户是看不到的,由操作系统统一管理。

 需要注意的是:虚拟地址和物理地址之间的转化由操作系统完成。

进程地址空间

 我们将前面的布局图称为程序地址空间,实际上是不准确的,应该叫做进程地址空间,进程地址空间本质上是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现。

进程地址空间类似一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区,堆区,栈区等。而在结构体mm_struct当中,记录了各个边界刻度,例如代码开始的刻度与结束的刻度,如下图:

在结构体mm_struct中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是线性增长,所以虚拟地址又叫做线性地址。

1.堆向上增长以及栈向下增长实际是改变mm_struct当中堆和栈的边界刻度

2.我们生成的可执行程序实际上也被分为了各个区域,当程序运行起来时,系统将对于数据加载到对于内存中,提高了工作效率,而进行科执行程序的"分区"操作实际是由编译器进行的,所以说代码的优化级别实际上是编译器说了算。

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建,而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。

可以看下图:

可以看到,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于自己的task_truct和mm_struct,父子进程空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置。

再看下图:

当子进程刚被创建时,子进程与父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一个空间,只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再修改。

例如,与上面的情况一样,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的映射方法,将相同的虚拟地址映射成不同的物理地址即可。

这种在需要进行数据修改时再进行拷贝的技术,称为写实拷贝技术。

1.为什么数据要进行写时拷贝 

进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

2.为什么不在创建子进程的时候就进行数据拷贝

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配,这样可以高效的使用内存空间。

3.代码会不会进行写实拷贝

大多数情况是不会的,但是例如在进行进程替换的时候,则需要进行代码的写实拷贝。

4.为什么要有进程地址空间?

1️⃣.有了进程地址空间后,就不会有任何系统级别的越界问题存在了,例如进程1不会错误的访问到进程2的物理空间,因为你堆某一地址空间进程操作前需要通过页表映射到物理内存,而页表只会映射属于你自己的物理内存。

2️⃣.有了进程地址空间后,每个进程都认为看到的都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们编写程序的时候只需要关注虚拟地址,无需关注数据在物理内存当中的实际存储位置。

3️⃣.有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好地完成进程的独立性以及合理使用内存空间,并能将进程调度与内存管理进行解耦或分离。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值