Linux进程概念

目录

冯诺依曼体系结构

 操作系统(Operator System)

概念

设计OS的目的

如何理解 "管理"

总结

进程

描述进程-PCB

task_struct-PCB的一种

task_ struct内容分类

组织进程

 查看进程

通过系统调用获取进程标示符

进程状态 

看看Linux内核源代码

进程状态查看

Z(zombie)-僵尸进程

僵尸进程危害

孤儿进程

进程优先级

基本概念

查看系统进程

PRI and NI

用top命令更改已存在进程的nice:

PRI vs NI

其他概念

环境变量

基本概念

常见环境变量

查看环境变量方法

测试PATH

 测试HOME

和环境变量相关的命令

环境变量的组织方式

通过代码如何获取环境变量

1.通过命令行第三个参数

 通过第三方变量environ获取 

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

程序地址空间(不准确)

进程地址空间


冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系
相信了解过计算机的都知道。
下面使冯诺依曼体系的图:

我们所认识的计算机,都是有一个个的硬件组件组成
输入设备:包括键盘 , 鼠标,扫描仪 , 写板等
中央处理器 (CPU) :含有运算器和控制器等
输出设备:显示器,打印机等
关于冯诺依曼,必须强调几点:
这里的存储器指的是内存
不考虑缓存情况,这里的 CPU 能且只能对内存进行读写,不能访问外设 ( 输入或输出设备 )
外设 ( 输入或输出设备 ) 要输入或者输出数据,也只能写入内存或者从内存中读取。
所有设备都只能直接和内存打交道,所以内存十分重要。
为什么在数据层面,cpu在读取或者写入的时候和内存打交道,这是为了提高效率,因为内存的速度远比磁盘要快。
以qq通新为例子:假设你发了一条信息给你的朋友,然后你的朋友接收到了信息,这个过程怎么用冯诺依曼体系来解释呢?
总结:cpu不和外设打交道,而之和内存打交道。这是为了提高效率

 操作系统(Operator System)

概念

任何计算机系统都包含一个基本的程序集合,称为操作系统 (OS) 。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库, shell 程序等等)
操作系统是一个软硬件管理的软件

设计OS的目的

与硬件交互,管理所有的软硬件资源
为用户程序(应用程序)提供一个良好的执行环境

如何理解 "管理"

首先说明,管理的本质就是对数据进行管理。

为什么要管理呢?

通过合理的管理软硬件资源(对下),从而给用户提供稳定,安全,高效的执行环境。(对上)

管理的方法:先描述,后组织

什么意思呢?对于硬件的管理,我们可以抽取出他们之间的性质,归结成一个类,然后通过数据结构链表的知识,我们就可以把他们链接起来,此时,我们管理硬件,不就是对链表的管理了吗,那不就很简单了,我们只需要写上增删查改的算法就能对链表进行很好的管理了。硬件是如此,软件其实也是这样。

但是由于用户并不知道怎么调用系统的接口,所以就有人写了库或者是shell来为用户提供服务,这就形成了比较完整的体系。

 

总结

计算机管理硬件或者软件
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构

进程

首先什么是进程?先说结论 进程就是内核的数据结构+进程对应的磁盘代码

进程是运行起来的程序,那么程序是什么呢?程序的本质就是文件,在磁盘中存放的,那么程序运行起来是要加载到内存的,有那么多的程序都要加载到内存,操作系统要怎么处理呢?

答案是:先描述,再组织。我们可以把程序的特征描述出来,然后再用特定的数据结构去维护他

这里的描述就提到了PCB进程控制块,PCB进程控制块就控制着磁盘加载到内存的数据,那么操作系统对进程的管理就变成了对PCB进程控制块的管理,也就是对数据结构的增删查改,这样就能很方便的实现管理了。

描述进程-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。 课本上称之为PCB process control block ), Linux 操作系统下的 PCB : task_struct

task_struct-PCB的一种

Linux 中描述进程的结构体叫做 task_struct
task_struct Linux 内核的一种数据结构,它会被装载到 RAM( 内存 ) 里并且包含着进程的信息

task_ struct内容分类

标示符 : 描述本进程的唯一标示符,用来区别其他进程。
状态 : 任务状态,退出代码,退出信号等。
优先级 : 相对于其他进程的优先级。( 因为CPU在执行的时候只有一个进程,为了让每个进程都能运行,所以CPU就会在一段时间内执行一个进程,当一个进程执行到一定的时间,CPU就会让该进程保存好当前的数据,也就是上下文数据,等到下一次的时候再加载到CPU,这样就能保证每个进程都能运行,因为CPU的运行速度是极快的,所以我们肉眼没有办法观察到CPU其实是一个进程一个进程运行的。这样就必须要优先级的概念了。每个进程可以在CPU的运行队列中排队,保证他们的优先级,这样就可以保证每一个进程都能运行)
程序计数器 : 程序中即将被执行的下一条指令的地址。
内存指针 : 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据 [ 休学例子,要加图 CPU ,寄存器 ] 。( 所以的上下文数据,不是寄存器硬件,而是寄存器硬件中存储的数据,因为当一个进程结束时,需要保存数据,不然下次进程再加载到内存的时候就不知道执行到哪里了)
I O 状态信息 : 包括显示的 I/O 请求 , 分配给进程的 I O 设备和被进程使用的文件列表。
记账信息 : 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息

组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。

 查看进程

进程的信息可以通过 /proc 系统文件夹查看

 大多数进程信息同样可以使用topps这些用户级工具来获取

下面写一段简单的代码,来看看我们如何查看我们写的程序的进程

#include <stdio.h>
#include <sys.h>
#include <unistd.h>                                                                                                                                                                  
     int main()              
     {                                   
       while(1)                          
       {                                                                                                   
         sleep(1);                                                                                         
       }                                                                                                   
       return 0;                         
     }             

 通过以上指令我们就可以查看到当前我们写的代码的进程了。上述的死循环代码,我们可以通过ctrl +c就可以让其停下来。

通过系统调用获取进程标示符

进程 id PID
父进程 id PPID
首先要知道父进程和子进程两个概念, 首先一般情况下的父进程都是bash,一般你的程序不是由父进程维护的,一般都是子进程进行维护,其中的子进程时由父进程来管理的。
我们可以通过这个方式得到pid以及ppid:

下面我们看看一段有趣的代码:这个可能颠覆你之前所学的语言的知识,明白系统这个学科和语言的不同之处。

 这个结果应该很让你震惊,首先if ,else语句竟然能同时跑起来,并且死循环下两个语句竟然没有一点干扰。这就是进程的知识,这也就是为什么操作系统和语言不同的地方。

为什么出现这个情况呢?

首先我们必须承认的是刚开始的时候这有两个进程,一个父进程,一个子进程,其中的父进程是bash,之后有创建了子进程。而这个子进程就是上个子子进程的子进程,也就是这里包含了爷子孙子的关系。fork创建之后,后面的代码被父子进程共享了。就会导致这个现象的发生,父子进程各自执行各自的,互不影响。那么那个id和返回值又是怎么回事呢?我们可以通过man来查看fork成功之后的返回值,就知道ret的问题,然后创建的子进程是要比父进程的id更大的,这个是普遍的情况。

通过手册就能很好的解释了返回值的问题。

进程状态 

因为每个操作系统的进程状态的叫法都不太相同,我们先看看操作系统这门课程中的3个重要的状态:运行、阻塞、挂起。

前面已经解释过了CPU的速度很快,但他只有一个,所以进程进入CPU的运行队列 中,如果该进程在CPU的运行队列中,那么就说明它正处于R(也就是运行状态)

如果进程需要进行IO过程,那么该进程就不能在CPU的运行队列中,那么他们就会在IO的接口的队列中等待(因为外设都是很慢的,相对CPU来说),那么他们处于这种状态就是处于阻塞状态。

如果我们加载了大量的数据到内存中,内存有可能不够用的时候,处于阻塞状态的数据就会显得很浪费空间,这时操作系统可能就会把这些正在处于阻塞状态的数据先加载到磁盘中,并用指针记录他们的位置,方便下次调用。这种情况就是进程处于挂起状态。

看看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 * 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 */
};

通过查看Linux的内核源代码,我们就会发现好像linux的处理方式和操作系统这本书有点不同,因为操作系统这本书概括的是所有的操作系统,不能把每个操作系统都覆盖,通过看Linux操作系统的设计,我们就能发现:Linux操作系统的进程状态是整数维护的。也就是上面的整数分别代表不同的状态。

R 运行状态( running : 并不意味着进程一定在运行中, 它表明进程要么是在运行中要么在运行队列里。(这个和操作系统的运行状态是一个意思)
S 睡眠状态( sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
interruptible sleep (这里的S和操作系统的阻塞状态很相似)

有IO过程的代码,基本都会处于S状态,那么为什么代码明明在运行,而且刚刚也看到了那个死循环的代码确实是一直在输出的,怎么就处于了S状态了?因为CPU的速度远远高于IO设备,所以那些指令大多都是在IO的队列中排队,等待IO的输出,所以基本上都处于阻塞状态也就是Sleep状态了。至于这里的+是什么意思呢?这里的+是前台运行的意思。如果没有+就是后台运行,后台运行的代码我们就不能使用ctrl + c去终止它,我们只能通过kill -9 加id来杀掉这个进程。 

D 磁盘休眠状态( Disk sleep )有时候也叫不可中断睡眠状态( uninterruptible sleep ),在这个状态的进程通常会等待IO 的结束。 (这个状态就连操作系统都没有办法终止它,只有等到它结束,或者是断电才能终止)
T 停止状态( stopped ): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X 死亡状态( dead ):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

进程状态查看

我们可以通过指令ps axj | grep +文件名 | grep -v grep这样就可以过滤掉除了当前文件进程之外的进程,这样就可以很好的查看到当前的进程了。

Z(zombie)-僵尸进程

除了前面涉及的Linux进程之外还有两个特殊的进程,一个僵尸进程,一个孤儿进程。

首先为什么要有僵尸进程呢?进程在退出的时候不是立刻释放进程的资源,应该保存一段时间由父进程或者操作系统来读取。此时子进程就会变成僵尸进程。

僵死状态( Zombies )是一个比较特殊的状态。当进程退出并且父进程(这里暂时不做讲解)
没有读取到子进程退出的返回代码时就会产生僵死 ( ) 进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以, 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
下面通过代码的方式来见见僵尸进程:
#include <stdio.h>                                                                     
#include <stdlib.h>
#include <unistd.h>
int main()
{
	pid_t ret = fork();
	if (ret < 0)
	{
		printf("fork fail");
		return -1;
	}
	else if (ret == 0)
	{
		//子进程
		while (1)
		{
			printf("pid:%d ,ret = %d\n", getpid(), ret);
			sleep(30);
			exit(-1);
		}
	}
	else
	{
		while (1)
		{
			//父进程
			printf("ppid:%d, ret = %d\n", getppid(), ret);
			sleep(2);
		}
	}
	return 0;
}

我们看看运行的结果:

我们可以看到子进程就处于僵死状态了。 

僵尸进程危害

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。 父进程如果一直不读取,那子进程就一直处于Z状态
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 task_struct(PCB) 中,换句话说,Z 状态一直不退出, PCB 一直都要维护。
如果 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费 ,因为数据结构对象本身就要占用内存,想想C 中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。
这个的话就必然导致一个大问题就是 内存泄露!

孤儿进程

与僵尸进程相对,孤儿进程就是父进程提前结束,然后剩下了子进程,这样的进程就是孤儿进程,名字也十分的形象。那么孤儿进程怎么回收呢?孤儿进程会被1号init进程领养,当然要有init进程回收。

下面看看Linux下的孤儿进程,代码如下:

先看看结果:

 其中要注意的几点:首先如果父进程退出,那么子进程将会在后台运行,看上面的结果我们也知道了,该进程处于S状态,这个时候我们没有办法通过ctrl + c来结果程序,我们应该必须通过kill指令才能终止这个程序。就-9就是杀掉后面的进程的。

进程优先级

基本概念

首先有一个问题:为什么要有优先级?答案是 资源太少了,想想那么多的进程,但是CPU只有一个,就明白优先级对计算机的效率的影响有多大。
cpu 资源分配的先后顺序,就是指进程的优先权( priority )。
优先权高的进程有优先执行权利 。配置进程优先权对多任务环境的 linux 很有用, 可以改善系统性能。
还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU ,可以大大改善系统整体性能。
优先级的本质就是PCB中的一个或者几个的整形的数字,而Linux中是支持调整优先级的。下面会讲到如何通过调整nice值来调整优先级。

查看系统进程

在Linux中我们可以通过ps -l来查看进程

UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级, 其值越小越早被执行
NI :代表这个进程的 nice值

PRI and NI

PRI就是进程的优先级 ,或者通俗点说就是程序被 CPU 执行的先后顺序,此 值越小进程的优先级别越高
NI 就是我们所要说的 nice 值了,其表示进程可被执行的优先级的修正数值
PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为: PRI(new)=PRI(old)(这个老的值是不变的Linux下都是80)+nice
这样,当 nice 值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux 下,就是调整进程 nice
nice其取值范围是-20至19,一共40个级别

用top命令更改已存在进程的nice:

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

刚才我改的nice值是-100,但是最终正如Linux所展示的一样,最小也是-20,最大则是19。 

PRI vs NI

需要强调一点的是, 进程的nice值不是进程的优先级 ,他们不是一个概念,但是 进程nice值会影响到进程的优先级变化。
可以理解 nice 值是进程优先级的修正修正数据,根据上面的公式,我们不难理解, nice的改变是会影响pri值的,从而影响优先级。

其他概念

竞争性 : 系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行 : 多个进程在多个 CPU 下分别,同时进行运行,这称之为并行
并发 : 多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
这里重点讲解并发概念, 因为很多计算机都是只要一个CPU的,但是进程却很多。那么怎么合理使用CPU呢?前面在介绍优先级的时候就提到了进程在CPU运行的时间是有限的,我们把这个时间称为时间片,也就是说在这个时间片里面,CPU只属于这个进程,此时进程的所有信息就会加载到寄存器中,等这个时间片过去了这个进程必须把当前的信息保存(上下文),方便下一次CPU运行该进程的时候知道运行到哪里了。等到下次再次运行的时候再把上下文信息加载到CPU,这样的运行方式就是并发。

环境变量

基本概念

环境变量 (environment variables) 一般是指在 操作系统中用来指定操作系统运行环境的一些参数 ,如:我们在编写C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在 系统当中通常具有全局特性(环境变量可以理解为我们呢学习C语言是的全局变量,那么对应的局部变量就是你在xshell中,以命令行的形式直接定义的变量就是本地的变量(也可以理解为局部变量))

 我们可以通过set来查看到我们写在本地的变量(可以看成局部变量,看上面我们使用env来查看环境变量的时候,我们发现是找不到的,这也就证实了刚刚所说的)

常见环境变量

PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )
SHELL : 当前 Shell, 它的值通常是 /bin/bash

查看环境变量方法

echo $NAME       //NAME: 你的环境变量名称

测试PATH

在Linux中为什么我们写的二进制可执行程序需要./  + 可执行程序才能运行呢?而有些指令确实可以不需要的。在解决这个问题前,我想说明一个结论就是指令就是程序。那么指令是怎么来的呢?其实指令的底层实现就是C语言以及汇编代码写出来的。因为这些指令使用的频率非常高,所以就把他们设置成环境变量,因为这是全局的,所以我们不需要在当前路径下才能找到,而是在全局的任何地方都是能使用的。

在我们还没有将当前文件设置成环境变量的时候就没有办法找到,我们可以通过以下方法进行环境变量的修改

方法一: 通过拷贝我们的指令到系统默认的路径下就可以实现,拷贝的本质就是安装

这是删除方法:

 

 方法二:export加之前的环境变量加上当前文件路径(不能直接加文件路径,这样就会把之前的路径覆盖掉,不过也不需要担心,只要我们重新登录一下就可以解决这个问题)

 测试HOME

  

为什么这里root路径的主工作目录和普通用户的主工作目录不同呢?这里其实和环境变量PWD有关,也就是我们平时使用的pwd。它会记录你当前的路径,下面通过比较USER中的普通用户和root的区别

 通过这段代码我们就可以知道指令是怎么实现的了。下面看看结果;
在当前用户下查看:

但是在root下查看就不是这种情况了:

 

 这就是环境变量的作用,同时这也说明了指令其实就是程序,只不过把这些程序添加到了环境变量中罢了,这样我们就可以随时使用。

和环境变量相关的命令

1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的 shell 变量和环境变量

环境变量的组织方式

通过代码如何获取环境变量

1.通过命令行第三个参数

因为这个是要c99才支持,所以我们要在makefile中增加这样的命令:

 

 

 我们就可以得到这个结果:

 

 这也就解释了指令其实也是通过C语言或者汇编语言写的,上面就是具体的实现方式。

 通过第三方变量environ获取 

其实第三方的environ和上面的实现方式本质是一样的。二级指针和指针数组是等价的。

下面是实现方式:

 结果当然和上面的结果是一样的。我们可以通过增加环境变量看看代码是否正确;

我们就可以找到刚刚我们输入的环境变量。

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

 

 这里的mytest会获取到这个环境变量的根本原因就是环境变量可以被子进程继承。

我们写的程序就是一个bash的子进程,因为环境变量具有全局属性。所以子进程也能获得环境变量,从而显示出来。继承的目的就是更加方便的使用,让bash帮我们找指令路径,身份认证

程序地址空间(不准确)

在讲解之前我们先看看一个用之前的知识不能解决的问题:

我们看看这个结果: 

这个结果应该是相当让人震惊。子进程把全局变量修改之后,父进程的g_val竟然还是0,这个还好理解,因为父子进程共享代码,同时他们不是同一个进程,所以两个值不同很正常。但最让人感到震惊的是他们的地址竟然是相同的。如果用我们以前的知识来回答就没有办法回答了。因为同一地址的值不可能相等,除非他们的地址不相同。那该怎么解释呢?

通过上面的分析可以得到以下结论:

变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
但地址值是一样的,说明,该地址绝对不是物理地址!
在Linux地址下,这种地址叫做 虚拟地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
O S必须负责将 虚拟地址 转化成 物理地址 。

那么我们该怎么理解这些结论呢:

首先我们可以这么认为每一个进程都是独立的。站在进程的角度我们可以认为进程在一个人想用内存的资源。所以每一个进程都有一个虚拟的地址空间。而这个空间和内存大小相同。那么进程的地址空间也是要被操作系统管理的,那怎么管理呢?当然是先描述,后组织。

我们可以用一个数据结构对他进行管理,也就是一个对象mm_struct。所以进程地址空间的本质就是内核的数据结构。

 根据我们之前所学进程就等于内核的数据结构+进程对应的代码和数据。其中数据结构是独立的,代码和数据也是独立的,那不就等于进程是独立的吗

那么上述步骤的完整过程就是以下这样的:

 

进程地址空间

所以之前说 程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?

 刚刚我们解释了进程地址空间是什么,现在我们来谈谈为什么有程序地址空间

第一:如果进程直接访问物理内存,这是不安全的。一旦访问越界那么就可能导致程序改掉了。这样我们用户是访问不到实际的物理地址的。

第二:为了方便进程和进程数据的解耦,这样保证了进程的独立性

第三:让进程以统一的视角来看待进程的代码和数据的区域,方便编译器也以统一的方式来编译代码。因为这具有规则性。所以都要遵守。

前两点比较好理解,那么第三点怎么理解呢:

在我们平时写代码调试的时候相信大家都调试过。尤其在我们看反汇编的时候,我们就会发现在那个阶段就有了地址了。这个也就是虚拟地址。所以在编译器来看,这些都是虚拟地址。这是大家都有遵守的。怎么理解呢?

下面通过画图来理解:

实际上这里是有两套地址的。第一套就标识了物理存在中的代码和数据,另外一套就是程序互相跳转使用的虚拟地址。 

其是cpu很“笨”,cpu只会分析指令,但是他的速度很快。通过我们对进程地址空间的学习,我想大家应该对操作系统有了一定的了解。可以说没有操作系统就等于没有计算机。相信学习到这里都能感受到操作系统的强大。

通过进程地址空间的学习,我们知道了实际的地址并不是我们在C语言中学习到的简单的栈,堆,静态区等等的空间,只是语言这个学科没有办法解释这个问题才使用的不准确的回答。通过学习操作系统我们就很深刻的理解了地址的底层原理以及为什么要这样设计。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值