赞
踩
今天来通过Linux 来介绍一下 进程
在我们正式进入 进程 之前,我们需要先来了解一下操作系统。
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系:
下面是冯诺伊曼体系的流程示意图:
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:
关于冯诺依曼,必须强调几点:
简而言之,冯诺依曼体系 规定了硬件层面上的数据流向:输入单元 ->存储器->CPU->存储器->输出单元
也就是说,我们常说的可执行程序运行必须加载到内存的原因是由于冯诺伊曼的规定。
那么为什么我们的外设要通过内存才能与CPU进行交互(数据层面)?
原因在于 外设离CPU太远,访问的速度慢,会拖慢CPU的效率。而内存在存储分级上(如下图)距离CPU更近,也就是说,访问速度更快。(粗略统计,CPU是纳秒级别,内存是微秒级别,外设是毫米哦级别的)
同时这里可以稍微补充一下,在存储分级中,越靠近CPU的部分容量就越小,速度就越快,成本也越高。这也解释了我们在购买硬盘时相对购买内存更加便宜。
所以,冯诺依曼体系的设计 是平衡 效率 与 成本 的产物。商家在市场可以接受的成本下追求效率,也就是高性价比。
现在我们假设一个场景:我们要将一份试验报告通过QQ发送给一位好友,那么此时的整个传输流程(不考虑网络)是怎样的?
答:
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
简单来说,操作系统是进行 软硬件管理 的 软件。
管理的本质,总结起来:就是先描述再组织。
为什么呢?我们以管理学校的学生为例子。
学校中可以分为三类人(从管理角度):校长,辅导员,学生。 其中校长是唯一管理者,那么校长是如何在不与学生直接对话的情况下管理学生的?
本质,校长是通过”数据“来管理学生的,那么我们也就需要将学生的信息抽取出来,而这个抽取的过程我们成为“描述学生”。直观来说,如果我们拥有一个学生的姓名,年龄,班级,专业等 信息,那么,我们就可以描述好一个学生了。
对应在编程语言中,其实就是一个结构体:
struct Student
{
string name;
int age;
string class;
string object;
...
};
但是,很显然,学校有许多个学生。校长如何一个人管理这么多学生?这就是“组织”。
已知,所有的学生已经被 “数据化”为许多个结构体,我们通过其中的成员变量来描述学生。为了便于统一的管理,我们可以使用指针将各个结构体谅解起来,串成一个双向循环链表,这样我们只要有一个头指针,就可以对整个链表进行增删查改,也就达到管理的目的。
由此我们验证了 管理就是先描述再组织。在计算机中,校长对应的是操作系统;辅导员对应的是驱动程序;学生对应的是底层硬件。,也就是说,操作系统不直接管理硬件,而是通过驱动完成。(参照下图)
整个硬件层是 遵守 冯诺依曼体系的
由于操作系统是不信任任何用户的,所有对于软硬件的访问都要经过操作系统。
使用系统调用接口 叫做 系统调用,是操作系统对外给出的接口,我们可以简单理解为一系列C接口。
用户操作接口 是 对 系统调用接口 的再封装,用户层 是对 用户操作接口的再封装,我们平常使用的C,C++语法一般就在这两层。现阶段我们所写的代码例如 一些数据结构,数学库,排序算法 ,是不会贯穿操作系统的,因为这些只是在用户层实现的代码,但如果涉及到打印或者输入函数,就会涉及到访问硬件,也就会贯穿操作系统。
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
这是书本给出的概念,显然,这对我真正理解进程是不够的。
现在我们假设一个情景:
我们写了一个程序 test.c ,然后 ./test.c,也就是运行该程序,此时操作系统是如何运转的?
首先,将存在磁盘上的 程序的代码与数据 加载到内存上,此时一个进程产生。(如下图)
但是 操作系统 是如何管理 这个进程的呢?先描述后组织。
如何描述?
联想之前学校的例子,系统会为每一个进程创建一个对应的结构体,用来“描述”该进程。这个结构体佳作PCB(produce control block),中文叫 进程控制块。Linux中定义时是struct task_struct,里面包含了进程相关的所有属性信息,至于具体是什么。之后会讲到。
如何组织?
显然,我们同一时刻内 计算机只运行了一个进程,那么我们如何将许多个进程组织起来,方便管理呢,相信同学们已经猜到了,没错,还是双链表结构。不过我们不是要把进程连起来,而是把他们OS中对应的PCB连起来。
此时,我们只要拿到这个链表的头,就可以对链表增删查改,同时,由于通过PCB可以找到内存中对应的进程,从而完成OS想要的操作。
由此,我们可以说,OS 对进程的管理,转化成了对进程信息的管理;简而言之,将进程管理转化为对链表的增删查改。
同时,我们也可以反问:为什么要存在PCB?
原因在于操作系统要对进程进行管理,先描述,再组织。
所以,进程到底是什么?
进程 = 你的程序 + 内核(kernel)申请的数据结构(PCB)
这里我们来正式介绍一下PCB,同时介绍一下它是如何描述进程的。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
对于上面一大堆的内容,同学们先不要急,我们先来了解一下标识符
所谓标识符,就像我的身份证号一样,各不相同。在Linux中,叫做PID.
在vim编辑器中,我们可以通过下面一段代码验证:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
运行之后,我们可以直接通过 命令【 ps ajx | head -1 && ps ajx | grep ‘这里填编译后的名称’】 来获取test.c的进程状态。
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹
我们在超级用户状态下,查看1号进程,可以找到我们的操作系统
除此之外,OS还提供了 系统接口 让我们可以直接 访问 PID :
运行下图中蓝框内的程序 ,比较绿框和红框中 mytest 的PID,验证了getpid()的可行性。
同是还有 父进程标识符PPID , 获取父进程的系统函数 getppid(),我们也可以验证一下:
那么这时候有同学会问: 此时的父进程到底是什么呢?
是我们的代码解释器 :bash
也就是说,我们的所有进程,都是命令行解释器的子进程。
时间片
cpu 中有很多的寄存器,当我们把进程放在CPU里去运行的时候,不是一直运行进程知道进程结束。而是每个进程都有一个运行时间单元,叫做时间片。也就是说,每个进程只运行一个时间片的时间(纳秒,微妙级别),就切换到下一个进程。
进程让出CPU 的两种情况
一般进程让出CPU:
a. 来了一个优先级更高的进程b(OS支持抢占)
b. 时间片到了
并发与并行
单核CPU,跑多个进程,不是一个跑完再接下一个,而是通过进程快速切换的方式,在一段时间内,使所有的进程代码都得到推进,这个行为叫做 并发。
多核CPU,任何时刻,允许多个进程同时进行,这叫做 并行
所以,多核电脑 理论上是 比单核电脑 要快的。
一个进程在运行的时候会产生各种各样的临时数据,其中有些临时数据与当前进程强相关,存储在cpu内的寄存器中,这些数据就是 上下文数据
我们可以取个例子:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d + %d = %d \n", a, b, a + b);
return 0;
}
我随意写一个程序并调试,调到反汇编模式,并查看寄存器。我们可以发现有很多个寄存器,以EIP为例子,我们发现,EIP中存储的总是 当前执行语句的下一条的地址。 所以说,CPU内的寄存器中的数据,也就是上下文数据,对于"描述"该进程是十分重要的。
同时,我们还可以总结出CPU的行为就是一个 不断取指令,分析指令,执行指令的过程。
当a进程在运行时因为某些原因要停止执行,让出CPU,需要CPU保存自己的所有的临时数据。保存方式是将a的临时数据从寄存器中拿下来,放进a进程的PCB中。这之后b进程会将自己PCB中的临时数据 载入CPU 寄存器,并开始运行。如果a进程之后想恢复运行,只需要将PCB中的临时数据在载入CPU即可。这个过程称为 进程切换。
打个比方,打游戏的时候通常有存档,当我们游戏后睡觉,第二天再打开游戏的时候不用从第一关开始,而是选择昨天的存档,这些存档数据就相当于 上文中 PCB中存储的临时数据。
我们已知,在OS 中。每一个PCB都用链表连起来了,并且CPU拿任务直接找PCB。
那么,CPU 是直接从这个链表中取PCB吗?
并不是。
实际上,OS中,还存在另一种数据结构:运行队列。(一个CPU一般有一个运行队列)
与此同时,还有阻塞队列,等待队列等,这些暂不讲解
所以进程要运行的实际过程是这样的:
将代码与数据载入内存中,将对应的PCB(PCB不存在要先创建PCB)加载到 运行队列里,CPU从队头的PCB中取出临时数据(无临时数据直接运行) 加载到寄存器。此时cpu 开始对PCB 指向的代码和数据进行计算 ,也就是进程的运转。
所以PCB 不仅仅存在于链表中,还与其他数据结构有关联,故我们说PCB中存在大量的指针。
fork()是OS 对外提供的系统接口,我们可以使用该接口来自主创建子进程。
我们直接通过代码来直观感受一下:
我们运行这段代码,并查看其进程状态。
我们发现对于mytest这个程序,有两个进程,并且具有父子关系。也就是说这个 while循环部分的代码 是父子两个进程公用的,打印出的结果也证实了这一点。但是,谁先运行是不由fork()决定的,而是由进程优先级决定。
由此,我们验证了fork 确实能创建一个子进程,并且,谁调用了fork(),谁就是父进程。
父子共享用户代码,而用户数据各私有一份。
如何理解上面这句话:
原因在于,操作系统中,所有进程都要具有独立性,为了不让进程之间相互干扰,用户数据之间各自拥有,而代码是只读或者不可修改的,所以公享一份就够了。
创建子进程,是以父进程为模板,其中子进程默认使用的是父进程的代码与数据(写时拷贝)
fork的哈数声明是:
pid_t fork();
其中pid_t 是无符号整形,如果返回值是0 ,那么表明是子进程;如果返回子进程的PID,那么表明是父进程
同样,我们使用一段代码来测试一下:
运行结果:
可以发先,确实当ret=0的时候,打印的是子进程的标识符。
这里还是要强调一下:父子进程的先后运行顺序不是fork()决定的,而是系统优先级决定。如果我们让子程序先sleep(1),那么1秒内父进程优先级高,一直运行。
但是,有的同学转念一想,为什么这个代码同时执行了ret=0 的情况 和 ret >0的情况?
这是因为fork()函数返回了两个 值。
我们简单分析一下fork()函数的内部,并画一个简单的图(如下),我们暂时忽略其具体如何实现,只关注返回值那一句代码。我们可以肯定的是,在return ret 这个语句之前,子进程已经存在了。我们又知道,父子进程的代码是共享的,那么在父进程执行完return 之后,子进程也会执行return ,那么有两个返回值就不意外了。
这个疑问解决了,但是为什么子进程返回的是0,而父进程返回的是子进程的PID?
这是因为 子进程 只有一个 父进程 ,但是父进程 可能有很多个 子进程 。父进程为了更好地识别与使用子进程,要给每个子进程 一个标识。
这里的运行状态不是指进程已经在CPU上运行,而是表示已经在运行队列中,随时可以被调度。
浅度休眠状态下,可以对外部事件作出反应。
Disk Sleep .不可以被杀掉,即使是操作系统也不行。只能等到自动醒来,或者关机重启。
T状态 暂停状态
我们可以通过 【kill -l】来查看所有的 与kill有关的信号:
Z 状态 (僵尸状态) / X状态 (死亡状态)
进程退出的时候,会自动将自己退出时的信息,写入进程的PCB中,供OS 或父进程读取(要判断进程是否完成了任务),读取成功后,该进程才算做是真正死亡。 这种转态叫做X状态。而 在OS或者父进程读取之前的转态,叫做 Z状态。
我们还是使用之前的一段代码来演示一下 僵尸状态:
同时,我们使用
这里是通过循环建立了五个子进程,但是我们在退出后,父进程没有回收这些子进程,所以观察下图,所有子进程都处在 僵尸状态。
当我们终止程序后,僵尸状态才会转为死亡转态,不过死亡状态是一瞬间的,我们不容易直接捕捉到。
两者的区别在于 前台进程进行时 bash无法使用 ,后台进程进行时 bash 可以使用。
要是想生成后台程序叶很简单,只要加个& :
./mytest &
可以看到,此时我们运行程序,但结果不会直接显示在命令行上。也就不会干扰我们输入其他指令,即bash是可用的。
但是,我们无法用 【ctrl + c】杀掉后台进程,只能通过 【kill -9 ’进程PID’】。
两者还有一个小差别是 后台进程 的运行状态 后会有一个’+’。
父进程先退出,子进程就称之为“孤儿进程”
此时,孤儿进程被1号init进程领养。同时,该孤儿进程转为后台进程。
1号进程是 操作系统生成的。同时,在操作系统工作之前,我们还有0号进程,不过之后被1号进程取代了。
在讲解 优先级之前,我们来辨析一下 优先级 和权限的差别: 优先级是一定能得到某种资源,权限是决定能否得到这种资源。
进程优先级由 pri 和 nice 共同决定(Linux下).
我们通过 【ps -l】或者【ps -al】 可以查看当前进程的相关信息:
一致我们有一个程序 proc 在运行:
我们可以用top命令更改已存在进程的nice:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。