0


Linux进程概念

目录


前言

哈喽,小伙伴们大家好,进程是操作系统中非常重要的一个概念,今天我将在linux系统下带大家了解进程的一些基本知识。事不宜迟,拿好小本本,我们赶紧开始吧~


一、描述组织进程

1、描述进程

进程概念:运行起来程序就是进程。

描述进程过程: 由于冯诺依曼体系要求,我们在执行一个程序的时候,需要先把程序加载到内存中去,CPU在从内存中依次提取代码。那么,我们运行的程序都总只有一个吗?显示不是。大多数情况下,都是很多程序一起运行的,这时候就需要操作系统作进程管理。操作系统不会直接管理可执行程序,而是会管理可执行程序的数据。他会根据每一个可执行程序进行描述,形成一个结构体,这个结构体叫做进程控制块,也叫做PCB(process control block)。然后操作系统再把所有的进程控制块组织到一起,一起加载到内存中去。可以理解成进程是可执行程序和进程控制块的集合,操作系统通过管理进程控制块就可以起到管理进程的作用。

2、task_struct

2.1概念

概念:PCB是一个统称,在Linux下控制进程的结构体叫做task_struct。下面我将介绍一下这个结构体中都包含哪些内容。

2.2内容分类

(1)标识符:标识符是描述一个进程的唯一标识,用来区别其它进程。相当于人的身份证。我们先运行一个进程,然后使用相关命令把进程的信息调出来。

查看系统中所有进程信息: ps aux / ps axj 命令

PID为运行进程的标识符,PPID为父进程的标识符。

用kil -9 ID号可以结束一个进程。

(2)状态

STAY下面对应的是这个进程的状态,R代表正在运行,而加号代表这个进程是前台进程。在同一时间,后台进程有多个,而前台进程只能由一个。在前台进程运行时,我们在终端中输入命令是无效的。如果想要进程在后台运行,则在运行命令的后面加一个&即可。

可以看到进程后台运行依旧可以输入命令行,并且进程状态后面没了加号。

(3)优先级

进程往往有很多个同时进行,但CPU资源是有限的,所以需要对进程进行优先级设置,需要排出放入CPU的先后顺序。对进程的排序本质上是对PCB进行排队。

(4)程序计数器

** 概念:**程序即将指向的下一条指令的地址。

在我们看书看到一半的时候,往往会夹一个书签,方便下次找到整这个位置。进程执行时也同样需要一个标签,方便进程切换出去回来的时候能找到这个位置,所以需要程序计数器(PC指针)把这个位置记下来。

(5)内存指针

task_struct想要找到对应的执行程序,需要一个指针来保存地址,这称为内存指针

(6)上下文数据

我们先来举一个例子来解释什么叫上下文数据。想必大家在上大学的时候都有同学在大一结束的时候去当兵吧,如果拿到了征兵和招募通知不在学校办理相关手续就直接走,那大概率过两年回来的时候已经被学校开除了。在走之前办理相关手续是为了进行信息保留,把学籍、学分、考试成绩等信息都保留下来,这样等当兵回来之后可以继续上大二。这些保留的信息在计算机中就叫做上下文数据。

CPU做的工作有三步:取指令,分析指令,执行指令。一个进程运行的时候会加载到内存中,CPU从内存中按顺序取代码进行计算,假设CPU是单核的,那么在某一时间它只能进行一个进程的计算。既然这样,如果我们也一个死循环,CPU是否会卡死呢?答案是当然不会。 每个进程都有一个时间片,时间片实际就是一个时间段,时间片一到或者是有更高优先级的进程来了,CPU就会进行进程切换。在几个进程之间循环切换就可以实现多个进程并行。

而CPU中只有一套寄存器,用来存放取到的指令和计算过程中的数据。那么如果计算进行到一半时,比如a=b+c;d=a+b;这个指令刚刚计算到一半,恰好需要进行进程切换怎么办呢?难道下次切换回来的时候要进行重新计算吗?答案是当然不会,刚刚计算过程中产生的数据叫做上下文数据,发生进程切换时CPU会把它们保存到task_struct中(不太准确,先简单这么理解),等切换回来的时候再直接把上下文数据提取出来继续运算就可以了。程序计数器也是存在CPU的寄存器中的,也可以理解成是一种上下文数据。

(7)I/O状态信息

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

(8)记账信息

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

3、组织进程

运行在系统里的进程数据以task_struct链表的形式储存起来。

二、查看进程

在根目录中,有proc(process)目录是专门存放进程信息的。可以用/proc查看。

后面跟具体进程的PID可以查看对应进程的信息。

这里的cwd中存放的是进程的工作目录,也就是说进程是知道自己在哪个目录下工作的。所以我们之前进行文件操作时在能在对应的文件夹下完成读写。

三、通过系统调用创建进程

我们之前创建进程的方式都是通过运行可执行程序形成进程。现在我们来讲一下如何通过系统调用创建进程。

1、fork函数

我们可以通过fork函数来创建一个进程。

特点:

  • fork()函数在进程中创建了一个子进程,父进程的代码和数据可能是来自磁盘的,而子进程的代码和数据是来自父进程的。
  • fork()创建子进程,fork之前的代码被父进程执行,fork之后的代码父子都可以执行。也就是说fork之后父子共享代码(那数据呢?之后再说)
  • fork之后就有了父子两个进程,这两个进程谁先被调度是不确定的,完全取决于操作系统的调度算法。

2、父子进程分流执行

父子进程执行的指令往往是不同的,否则子进程就没有存在的必要。

和我们以往接触到的函数不同,fork()函数有两个返回值,它会返回给子进程0,把子进程的PID返回给父进程。

看如下代码,父进程和子进程会同时进行:

四、进程状态

进程包括几个不同状态,一般我们在学校学到的或者书中看到的都会说进程有五个状态,分别是:创建,就绪,执行,阻塞,终止。但对这种叙述很多小伙伴都感觉似懂非懂,原因是这种描述是描述的所有操作系统的共性,所以感觉起来会有点抽象。今天我将以具体的一款操作系统为例,带大家看看Linux的进程状态是怎样的。

1、Linux内核源码

首先我想问大家一个问题,状态可数据化吗?答案是可以的,我们可以把每一个状态和一个数字或字母对应起来,用数字或字母表示处于的状态,就可以实现状态的数据化。数据化后的状态会保存到task_struct中。

下面是一段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 */
};

2、Linux状态的说明

2.1 R运行状态

运行状态并不一定表示进程在运行中,也可以是在运行队列中。

2.2 S睡眠状态

意味着进程在等待某件事情完成(这里的睡眠有时候也可以叫做可中断睡眠)。

2.3 D磁盘休眠状态

也叫不可中断睡眠,在这个状态的进程通常会等待IO的结束。

S状态和D状态有什么区别呢?举个例子,有一个进程叫张三,它负责取硬盘中拿东西,硬盘中和它对接的程序叫李四,李四说让张三等一会,它需要把数据收集一下再给它,于是张三进入了S睡眠状态等待李四。可这时候操作系统来了,操作系统感觉空间已经严重不足了,又看到张三在睡觉,于是就直接把张三杀掉了。过了一会李四收集好数据了开始找张三,结果张三已经挂了,无人应答。这种情况就尴尬了,是很容易引发问题的。于是就有了D状态,D状态和S状态的区别就是D状态时深度睡眠,操作系统无权把它杀掉或唤醒,所以张三只要进入D状态就可以避免上面的问题。

2.4 T停止状态

可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。

kill -l 可以列出所有命令:

2.5 Z僵尸状态

为了方便大家理解僵尸状态,我来举一个例子。假设马路上出现一具尸体,尸体不会被马上抬走,而是要等警察来清理现场,采集尸体上的有效信息,来确认是意外还是他杀,等着一系列的事情都办完之后才会把人拉走。在这个例子里面,死人就好比进程,警察好比操作系统(父进程),等待警察采集尸体上有效信息这个过程就是僵尸状态。

僵尸进程概念:一个进程已经退出了,但它的相关资源没有被回收,这样的进程叫做僵尸进程。

为什么要有僵尸状态呢?

首先我们要清楚,一个进程创造出来是为了完成某个任务,为了判断这个任务有没有完成,进程退出时会释放内存后,然后把退出的相关信息(退出码等)保存到task_struct中,等待操作系统或父进程来读取,如果没有人来读取,该进程的资源就不能被释放。等待父进程取信息时的这个状态就叫做僵尸状态。僵尸进程本身并不占内存,但是用来保存信息的task_struct会占内存。僵尸状态其实已经近似于一具尸体了,没有人能够再杀死它,kill -9 也不行。只有等待父进程读取信息才会释放。

2.6 X死亡状态

死亡状态往往是一瞬间的事,在进程退出后相关信息被父进程读取后立刻进入死亡状态然后被释放。死亡状态很难观测到。

3、僵尸进程的危害

  • 如果子进程的退出信息一直不被读取,就会一直处在僵尸状态,信息储存在task_struct中,需要PCB一直维护。
  • 如果一个父进程创建了很多子进程,但就是不回收,就会造成内存资源的浪费。
  • 如果在子进程还没退出的时候父进程提前退出,子进程就会变成孤儿进程。孤儿进程很容易造成内存泄漏,所以会被操作系统领养,由操作系统来回收。

五、进程优先级

1、基本概念

进程优先级概念:由于CPU的资源是有限的,各个进程需要对CPU资源进行竞争,CPU资源分配的先后顺序称为进程优先级。

其它概念:

竞争性:cpu资源有限,所以进程之间是存在竞争性的。

独立性:多进程运行时,进程与进程保持独立,互不干扰。

并行:多个进程在多个cpu下分别同时进行运行,叫做并行。

并发:多个进程在一个cpu采用进程切换的方式,使多个进程同时推进,叫做并发。

2、优先级的查看与调整

2.1查看系统进程

查看当前shell产生的进程:ps l

其中的信息含义如下:

  • UID:执行者的身份
  • PID:当前进程的标号
  • PPID:父进程的标号
  • PRI:代表这个进程可被执行的优先级,数字越小越先被执行
  • NI:代表这个进程的nice值

**2.2 PRI and NI **

PRI很好理解,代表这个进程的优先级。NI代表nice值,那么什么是nice值呢?

nice值是一个修正优先级的数值,我们无法直接改变PRI,但可以通过改变nice值来改变PRI。 PRI(new)=PRI(old)+nice值。只要把nice值设为负数,PRI就会减小,优先级变高。nice值设为证书,PRI增大,优先级减小。nice的取值范围是-20-19。

2.3调整优先级

调整优先级的本质是调整nice值。我们可以用top命令来更改nice值。

  • top
  • 进入top后按“r”–>输入进程PID–>输入nice值

我们观察myproc的进程信息

通过top命令把nice值调整为10,发现PRI和NI都发生了变化。

六、环境变量

概念:环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数。在系统中通常具有全局特性。

1、常见环境变量

命令:env查看所有环境变量

1.1 PATH

含义:命令的默认搜索路径

通过之前的学习,我们了解到,我们输入的命令行本质上也是一个进程。那为什么输入命令行的时候可以直接输入指令名,而运行其它进程的时候必须指定路径,否则就会找不到呢?

这是因为PATH这个环境变量中存了一系列的路径,每个路径之间用冒号隔开。我们输入指令时如果不指明路径操作系统会默认去这些路径中搜索,而我们平常使用的命令行指令都存在这些路径中,所以可以直接使用。代码如下(用echo输出环境变量的时候记得前面加$,否则会被当成字符串处理):

如果我们想要一个进程不需要指明路径就可以直接运行应该怎样做呢?很简单,只要把它也添加到默认搜索路径中就可以了。例如我们用yum安装软件之后可以使用相应软件的命令或者winodws下点一下快捷方式程序就能启动,本质上都是因为这些软件的路径被安装到了环境变量标定的路径下。但我们自己写的程序一般不建议添加到环境变量标定的路径下,因为我们自己写的程序质量和用途都参差不齐,容易污染系统中的工具集。

如果想要我们自己写的程序可以直接运行,可以把当前程序所在路径添加到环境变量中。

注意:环境变量只是一个变量,我们对它的改变是不会保存的。退出后再登陆系统会重新生成环境变量。

1.2 HOME

含义:用户的主工作目录(即用户登陆到Linux系统的下默认工作目录)

下面分别是普通用户和root用户中HOME的值,可以发现是不同的。

** 1.3 SHELL**

含义:当前操作系统中具体的shell。

2、环境变量的组织方式

2.1主函数的参数

首先我们要先了解一个概念,main函数是可以有参数的。main函数是被操作系统调用的,当我们使用ls -l这种指令时,实际上-l就是在给main函数传参。

c语言规定main函数有两个参数,第一个是整型变量,代表传入参数的个数,第二个是指向字符串的指针数组,里面保存传入指令的地址。有了这两个参数,我们就可以像命令行指令那样,通过改变后缀来产生不同的效果。

2.2通过代码获取环境变量

环境变量的获取方式有两种,一种是通过主函数的第三个参数,另一种是通过第三方变量environ。下面主要演示第二种。

每个程序都会收到一张环境表,环境表是字符指针数组,由二级指针environ调用。

环境变量具有全局属性,我们可以在程序中把他们打印出来。

这样打印出来的是所有的环境变量,我们也可以用函数getenv根据环境变量名调出具体的环境变量。

#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}

七、进程地址空间

1、虚拟地址

在c语言中,我们学过地址的概念,也了解过内存的空间分布,如下图所示。但是我们对这些概念的理解可能并不准确,实际情况可我们想象的往往会有一些出入,今天我将从系统的角度对这些概念进行修正。

在这里插入图片描述

我们先来看一个例子,代码如下:

运行这个进程,发现在子进程对全局变量进行修改后,父进程和子进程打印出来的值并不一样,但地址却相同。

从上面这个例子我们可以得到以下结论:

  • 父子进程的的变量内容不一样,所以父子进程输出的绝对不是同一个变量。
  • 但是他们的地址又相同,说明地址绝对不是真正意义上的物理地址,在linux下,我们把这种地址称为虚拟地址。
  • 我们在c/c++下看到的都是虚拟地址,物理地址一概看不到,由操作系统统一管理。

2、映射关系

实际上我们之前提到的分区都不是真正意义上的数据内存分区,而是虚拟地址对应的分区。虚拟地址和内存中的实际物理地址都存在一张页表中,并且是一一映射关系,可以根据虚拟地址找到物理地址。每个进程创建时,都会形成一个进程地址空间(也就是虚拟地址)和它的task_struct关联起来。刚开始子进程和父进程的映射关系是完全一样的,虚拟地址都指向相同的物理地址,但子进程改变了全局变量的值,由于进程之间具有独立性,父进程中全局变量的值不能随子进程的改变而改变,所以它会在内存中开辟一个新变量来保存改变后的值,并改变页表中的映射关系,使虚拟地址能够找到它。

3、mm_struct

进程地址空间实际上就是一个结构体,称为mm_struct,mm_struct是对实际物理空间的数据化。举个例子,我们上学的时候可能都划过三八线,在画三八线时要保证公平,需要用到尺子来量一下课桌的长度。实际上测量长度这一行为就是数据化的体现,假设量出来桌子长100厘米,那么规定0到50厘米归左边的小明,51到100厘米归右边的小红,仅仅用一个长度区间就很好的描述了课桌的分区。

和划分课桌相同,对一块区域数据化的最好方法就是把他的起始位置和结束位置用数据表示出来,所以mm_struct中保存的是各个分区的始末位置。类似于下图:

实际上不光进程是分段的,保存在磁盘中的可执行程序也是分段的。

注意:mm_struct和task_struct一样,都是保存在内存中的,并且task_struct中存了一个指针,用来指向mm_struct。

4、地址空间的作用

(1)保护内存

有了地址空间,就再也不会有系统层面的越界访问了。我们平常说的指针越界其实都是地址空间的越界,并不会影响到系统。

(2)统一性

每个进程都认为看到的是相同的空间范围(顺序,构成)。

(3)更好的完成进程独立性和合理使用空间

使用虚拟地址的话,每个进程都感觉自己在独占内存,有助于系统更好的分配空间。举个例子,我们每个人都往银行存五万块钱,每个人都觉得自己拥有五万块钱,但大家又不可能同时取这五万块钱,所以银行握着一大笔存款可以随意支配,创造更大的价值。将内存集中到一起由操作系统统一调配更加高效,实现了进程调度和内存管理的解耦。


总结

以上就是本文要讲的全部内容。本文主要介绍了进程的相关概念,希望能给大家带来帮助。在下一章我将介绍进程控制的相关知识,对进程进行进一步的讲解。本篇文章就到这里啦,山高路远,来日方长,期待下次见面~

标签: linux 操作系统

本文转载自: https://blog.csdn.net/weixin_59371851/article/details/125953831
版权归原作者 敲键盘的喵 所有, 如有侵权,请联系我们删除。

“Linux进程概念”的评论:

还没有评论