当前位置:   article > 正文

【Linux】进程入门详解_linux进程详解

linux进程详解

今天来通过Linux 来介绍一下 进程

在我们正式进入 进程 之前,我们需要先来了解一下操作系统。

1. 冯诺依曼体系

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系:

下面是冯诺伊曼体系的流程示意图:
冯诺伊曼体系


截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:

  • 输入单元:包括键盘, 鼠标,扫描仪, 写板等
  • 中央处理器(CPU):含有运算器和控制器等
  • 输出单元:显示器,打印机等

关于冯诺依曼,必须强调几点

  • 这里的存储器指的是(物理)内存
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 一句话,所有设备都只能直接和内存打交道

简而言之,冯诺依曼体系 规定了硬件层面上的数据流向:输入单元 ->存储器->CPU->存储器->输出单元

也就是说,我们常说的可执行程序运行必须加载到内存的原因是由于冯诺伊曼的规定。


那么为什么我们的外设要通过内存才能与CPU进行交互(数据层面)?

原因在于 外设离CPU太远,访问的速度慢,会拖慢CPU的效率。而内存在存储分级上(如下图)距离CPU更近,也就是说,访问速度更快。(粗略统计,CPU是纳秒级别,内存是微秒级别,外设是毫米哦级别的)

同时这里可以稍微补充一下,在存储分级中,越靠近CPU的部分容量就越小,速度就越快,成本也越高。这也解释了我们在购买硬盘时相对购买内存更加便宜。

所以,冯诺依曼体系的设计 是平衡 效率 与 成本 的产物。商家在市场可以接受的成本下追求效率,也就是高性价比。
在这里插入图片描述


现在我们假设一个场景:我们要将一份试验报告通过QQ发送给一位好友,那么此时的整个传输流程(不考虑网络)是怎样的?

答:

  1. 将磁盘中的相应数据 写入 存储器
  2. 存储器将数据 载入CPU,经过计算之后返回给存储器,存储器将数据 刷新到 网卡
  3. 好友那端的网卡接受我们的数据 并传入存储器
  4. 存储器将数据 载入CPU,经过计算之后返回给存储器,存储器将数据 刷新到 磁盘

2. 操作系统(Operator System)

2.1 操作系统的概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等等)

简单来说,操作系统是进行 软硬件管理 的 软件

2.2 设计OS的目的

  • 对下,与硬件交互,管理所有的软硬件资源
  • 对上,为用户,开发人员,各类用户,提供一个良好的执行环境

2.3 如何理解管理

管理的本质,总结起来:就是先描述再组织。

为什么呢?我们以管理学校的学生为例子。

学校中可以分为三类人(从管理角度):校长,辅导员,学生。 其中校长是唯一管理者,那么校长是如何在不与学生直接对话的情况下管理学生的?

本质,校长是通过”数据“来管理学生的,那么我们也就需要将学生的信息抽取出来,而这个抽取的过程我们成为“描述学生”。直观来说,如果我们拥有一个学生的姓名,年龄,班级,专业等 信息,那么,我们就可以描述好一个学生了。

对应在编程语言中,其实就是一个结构体:

struct Student
{
  string name;
  int age;
  string class;
  string object;
  ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

但是,很显然,学校有许多个学生。校长如何一个人管理这么多学生?这就是“组织”。

已知,所有的学生已经被 “数据化”为许多个结构体,我们通过其中的成员变量来描述学生。为了便于统一的管理,我们可以使用指针将各个结构体谅解起来,串成一个双向循环链表,这样我们只要有一个头指针,就可以对整个链表进行增删查改,也就达到管理的目的。

由此我们验证了 管理就是先描述再组织。在计算机中,校长对应的是操作系统;辅导员对应的是驱动程序;学生对应的是底层硬件。,也就是说,操作系统不直接管理硬件,而是通过驱动完成。(参照下图)


2.4 操作系统的层状结构

在这里插入图片描述

  • 整个硬件层是 遵守 冯诺依曼体系的

  • 由于操作系统是不信任任何用户的,所有对于软硬件的访问都要经过操作系统。

  • 使用系统调用接口 叫做 系统调用,是操作系统对外给出的接口,我们可以简单理解为一系列C接口。

  • 用户操作接口 是 对 系统调用接口 的再封装,用户层 是对 用户操作接口的再封装,我们平常使用的C,C++语法一般就在这两层。现阶段我们所写的代码例如 一些数据结构,数学库,排序算法 ,是不会贯穿操作系统的,因为这些只是在用户层实现的代码,但如果涉及到打印或者输入函数,就会涉及到访问硬件,也就会贯穿操作系统。


2.5 系统调用和库函数概念

  1. 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  2. 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

3. 进程

3.1 进程的概念

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

3.2 进程到底是什么

这是书本给出的概念,显然,这对我真正理解进程是不够的。

现在我们假设一个情景:

我们写了一个程序 test.c ,然后 ./test.c,也就是运行该程序,此时操作系统是如何运转的?
首先,将存在磁盘上的 程序的代码与数据 加载到内存上,此时一个进程产生。(如下图)

在这里插入图片描述

但是 操作系统 是如何管理 这个进程的呢?先描述后组织

  1. 如何描述?
    联想之前学校的例子,系统会为每一个进程创建一个对应的结构体,用来“描述”该进程。这个结构体佳作PCB(produce control block),中文叫 进程控制块。Linux中定义时是struct task_struct,里面包含了进程相关的所有属性信息,至于具体是什么。之后会讲到。
    在这里插入图片描述

  2. 如何组织?

显然,我们同一时刻内 计算机只运行了一个进程,那么我们如何将许多个进程组织起来,方便管理呢,相信同学们已经猜到了,没错,还是双链表结构。不过我们不是要把进程连起来,而是把他们OS中对应的PCB连起来。

在这里插入图片描述

此时,我们只要拿到这个链表的头,就可以对链表增删查改,同时,由于通过PCB可以找到内存中对应的进程,从而完成OS想要的操作。

由此,我们可以说,OS 对进程的管理,转化成了对进程信息的管理;简而言之,将进程管理转化为对链表的增删查改。

同时,我们也可以反问:为什么要存在PCB?
原因在于操作系统要对进程进行管理,先描述,再组织。

所以,进程到底是什么?
进程 = 你的程序 + 内核(kernel)申请的数据结构(PCB)


3.3 描述进程 - PCB

这里我们来正式介绍一下PCB,同时介绍一下它是如何描述进程的。

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

3.3.1 task_struct (PCB的一种)

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

3.3.2 task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

对于上面一大堆的内容,同学们先不要急,我们先来了解一下标识符

3.3.2.1 进程标识符

所谓标识符,就像我的身份证号一样,各不相同。在Linux中,叫做PID.

在vim编辑器中,我们可以通过下面一段代码验证:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行之后,我们可以直接通过 命令【 ps ajx | head -1 && ps ajx | grep ‘这里填编译后的名称’】 来获取test.c的进程状态。
在这里插入图片描述


3.3.2.2 查看进程

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

如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹

在这里插入图片描述

我们在超级用户状态下,查看1号进程,可以找到我们的操作系统
在这里插入图片描述


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

除此之外,OS还提供了 系统接口 让我们可以直接 访问 PID :

  • getpid() 函数
  • getppid()函数

运行下图中蓝框内的程序 ,比较绿框和红框中 mytest 的PID,验证了getpid()的可行性。
在这里插入图片描述


同是还有 父进程标识符PPID , 获取父进程的系统函数 getppid(),我们也可以验证一下:
在这里插入图片描述


那么这时候有同学会问: 此时的父进程到底是什么呢?
是我们的代码解释器 :bash

也就是说,我们的所有进程,都是命令行解释器的子进程。

在这里插入图片描述



3.3.3.4 多进程下的真实运行状态
  • 时间片
    cpu 中有很多的寄存器,当我们把进程放在CPU里去运行的时候,不是一直运行进程知道进程结束。而是每个进程都有一个运行时间单元,叫做时间片。也就是说,每个进程只运行一个时间片的时间(纳秒,微妙级别),就切换到下一个进程。

  • 进程让出CPU 的两种情况
    一般进程让出CPU:
    a. 来了一个优先级更高的进程b(OS支持抢占)
    b. 时间片到了

  • 并发与并行

  1. 单核CPU,跑多个进程,不是一个跑完再接下一个,而是通过进程快速切换的方式,在一段时间内,使所有的进程代码都得到推进,这个行为叫做 并发

  2. 多核CPU,任何时刻,允许多个进程同时进行,这叫做 并行

所以,多核电脑 理论上是 比单核电脑 要快的。


3.3.3.5 上下文数据

一个进程在运行的时候会产生各种各样的临时数据,其中有些临时数据与当前进程强相关,存储在cpu内的寄存器中,这些数据就是 上下文数据

我们可以取个例子:

#include<stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    printf("%d + %d = %d \n", a, b, a + b);
    return 0;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我随意写一个程序并调试,调到反汇编模式,并查看寄存器。我们可以发现有很多个寄存器,以EIP为例子,我们发现,EIP中存储的总是 当前执行语句的下一条的地址。 所以说,CPU内的寄存器中的数据,也就是上下文数据,对于"描述"该进程是十分重要的。
在这里插入图片描述
同时,我们还可以总结出CPU的行为就是一个 不断取指令,分析指令,执行指令的过程。


3.3.3.6 进程切换

当a进程在运行时因为某些原因要停止执行,让出CPU,需要CPU保存自己的所有的临时数据。保存方式是将a的临时数据从寄存器中拿下来,放进a进程的PCB中。这之后b进程会将自己PCB中的临时数据 载入CPU 寄存器,并开始运行。如果a进程之后想恢复运行,只需要将PCB中的临时数据在载入CPU即可。这个过程称为 进程切换

打个比方,打游戏的时候通常有存档,当我们游戏后睡觉,第二天再打开游戏的时候不用从第一关开始,而是选择昨天的存档,这些存档数据就相当于 上文中 PCB中存储的临时数据。


3.3.3.7 运行队列

我们已知,在OS 中。每一个PCB都用链表连起来了,并且CPU拿任务直接找PCB。
那么,CPU 是直接从这个链表中取PCB吗?
并不是。
实际上,OS中,还存在另一种数据结构:运行队列。(一个CPU一般有一个运行队列)
与此同时,还有阻塞队列,等待队列等,这些暂不讲解

所以进程要运行的实际过程是这样的
将代码与数据载入内存中,将对应的PCB(PCB不存在要先创建PCB)加载到 运行队列里,CPU从队头的PCB中取出临时数据(无临时数据直接运行) 加载到寄存器。此时cpu 开始对PCB 指向的代码和数据进行计算 ,也就是进程的运转。

所以PCB 不仅仅存在于链表中,还与其他数据结构有关联,故我们说PCB中存在大量的指针。
在这里插入图片描述


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

3.4.1 fork()初了解

fork()是OS 对外提供的系统接口,我们可以使用该接口来自主创建子进程。

我们直接通过代码来直观感受一下:
在这里插入图片描述
我们运行这段代码,并查看其进程状态。

我们发现对于mytest这个程序,有两个进程,并且具有父子关系。也就是说这个 while循环部分的代码 是父子两个进程公用的,打印出的结果也证实了这一点。但是,谁先运行是不由fork()决定的,而是由进程优先级决定。

由此,我们验证了fork 确实能创建一个子进程,并且,谁调用了fork(),谁就是父进程。

在这里插入图片描述


3.4.2 理解fork()

父子共享用户代码,而用户数据各私有一份

如何理解上面这句话:

  • 程序员的角度:

原因在于,操作系统中,所有进程都要具有独立性,为了不让进程之间相互干扰,用户数据之间各自拥有,而代码是只读或者不可修改的,所以公享一份就够了。

  • 内核角度

创建子进程,是以父进程为模板,其中子进程默认使用的是父进程的代码与数据(写时拷贝)

3.4.3 fork()的返回值

fork的哈数声明是:

pid_t fork();
  • 1

其中pid_t 是无符号整形,如果返回值是0 ,那么表明是子进程;如果返回子进程的PID,那么表明是父进程

同样,我们使用一段代码来测试一下:

在这里插入图片描述
运行结果:
在这里插入图片描述

可以发先,确实当ret=0的时候,打印的是子进程的标识符。

这里还是要强调一下:父子进程的先后运行顺序不是fork()决定的,而是系统优先级决定。如果我们让子程序先sleep(1),那么1秒内父进程优先级高,一直运行。


但是,有的同学转念一想,为什么这个代码同时执行了ret=0 的情况 和 ret >0的情况?

这是因为fork()函数返回了两个 值。
我们简单分析一下fork()函数的内部,并画一个简单的图(如下),我们暂时忽略其具体如何实现,只关注返回值那一句代码。我们可以肯定的是,在return ret 这个语句之前,子进程已经存在了。我们又知道,父子进程的代码是共享的,那么在父进程执行完return 之后,子进程也会执行return ,那么有两个返回值就不意外了。
在这里插入图片描述


这个疑问解决了,但是为什么子进程返回的是0,而父进程返回的是子进程的PID?

这是因为 子进程 只有一个 父进程 ,但是父进程 可能有很多个 子进程 。父进程为了更好地识别与使用子进程,要给每个子进程 一个标识。


3.5进程状态

3.5.1 进程状态

在这里插入图片描述

  • R状态 运行状态

这里的运行状态不是指进程已经在CPU上运行,而是表示已经在运行队列中,随时可以被调度。

  • S状态 浅度休眠状态

浅度休眠状态下,可以对外部事件作出反应。

  • D状态 深度休眠

Disk Sleep .不可以被杀掉,即使是操作系统也不行。只能等到自动醒来,或者关机重启。

  • T状态 暂停状态
    我们可以通过 【kill -l】来查看所有的 与kill有关的信号:
    在这里插入图片描述
    在这里插入图片描述

  • Z 状态 (僵尸状态) / X状态 (死亡状态)

进程退出的时候,会自动将自己退出时的信息,写入进程的PCB中,供OS 或父进程读取(要判断进程是否完成了任务),读取成功后,该进程才算做是真正死亡。 这种转态叫做X状态。而 在OS或者父进程读取之前的转态,叫做 Z状态。

我们还是使用之前的一段代码来演示一下 僵尸状态:

同时,我们使用
在这里插入图片描述
这里是通过循环建立了五个子进程,但是我们在退出后,父进程没有回收这些子进程,所以观察下图,所有子进程都处在 僵尸状态。

当我们终止程序后,僵尸状态才会转为死亡转态,不过死亡状态是一瞬间的,我们不容易直接捕捉到。
在这里插入图片描述


3.5.2 前台进程 与 后台进程

两者的区别在于 前台进程进行时 bash无法使用 ,后台进程进行时 bash 可以使用。

要是想生成后台程序叶很简单,只要加个& :

./mytest &
  • 1

在这里插入图片描述
可以看到,此时我们运行程序,但结果不会直接显示在命令行上。也就不会干扰我们输入其他指令,即bash是可用的。

但是,我们无法用 【ctrl + c】杀掉后台进程,只能通过 【kill -9 ’进程PID’】

两者还有一个小差别是 后台进程 的运行状态 后会有一个’+’。


3.5.3 孤儿进程

父进程先退出,子进程就称之为“孤儿进程”

此时,孤儿进程被1号init进程领养。同时,该孤儿进程转为后台进程。

在这里插入图片描述
1号进程是 操作系统生成的。同时,在操作系统工作之前,我们还有0号进程,不过之后被1号进程取代了。


3.6 进程优先级

3.6.1 优先级引入

在讲解 优先级之前,我们来辨析一下 优先级 和权限的差别: 优先级是一定能得到某种资源,权限是决定能否得到这种资源。

  • 为什么存在优先级?
    计算机资源有限

3.6.2 查看进程优先级

进程优先级由 pri 和 nice 共同决定(Linux下).

  • 优先级的数据越小,优先级越高。反之越低。
  • 优先级数据不可能一味的高或者低。原因在于OS 的调度器也要适度的考虑公平问题。

我们通过 【ps -l】或者【ps -al】 可以查看当前进程的相关信息:
在这里插入图片描述

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

3.6.3 PRI and NI

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

3.6.4 调整进程优先级

一致我们有一个程序 proc 在运行:
在这里插入图片描述
我们可以用top命令更改已存在进程的nice:

  1. top
  2. 进入top后按“r”–>输入进程PID–>输入nice值

3.6 其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/635197
推荐阅读
相关标签
  

闽ICP备14008679号