进程的概念
进程(Process)是计算机中的一个具有独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
简单点说,进程就是一个运行起来(加载到内存)的程序 --> 进程在内存中的程序 --> 进程
PCB(process control block)的引入
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为 进程属性的集合。
书上称之为 PCB(process control block),Linux操作系统下的 PCB 是: task_struct
❓ 我们现在思考一个问题,我们写的程序是放在磁盘中的,当我们想要运行它的时候,真的是简简单单的执行起来而已吗?
💡 肯定不是!因为操作系统不只是要运行这个程序,操作系统还得运行其他重要的或者不必要的程序,而我们又知道执行程序是要被加载到内存中的,这个时候操作系统就需要做管理了,必须知道这些程序的属性来进行管理,比如优先级、状态、IO状态情况等等。
我们再仔细想想我们写程序的时候,有没有接触过这些属性呢?答案是没有的!那这些属性是从何而来呢?答案就在 PCB 上!
🔺 当我们执行一个程序的时候,其实没有我们想的那么简单,操作系统会在程序运行的时候申请一个空的 PCB 指向我们要执行的程序,这个 PCB 中其实就包含了我们上面所说的所有属性!接着操作系统不直接管理我们要执行的程序,转而去管理这个指向我们要执行的程序的 PCB !妙不妙~
所以 PCB 的本质其实就是一个结构体!结构体里面包含着各种属性,以及要指向执行文件的指针!
上图中每个 PCB 是用 链表 链接起来的!链表在操作系统是很常见的~
所以可以看出 进程 = 内核数据结构 (task_struct) + 进程对应的磁盘代码
下面是 task_struct 的简介,这些后面我们都会慢慢接触的!
- 在Linux中描述进程的结构体叫做 task_struct。
- task_struct 是Linux内核的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的信息。
- task_struct 内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
查看进程
🔴 值得注意的是,当我们生成可执行文件的时候,这个文件不是进程!因为其还没加载到内存去调度~
1、ps ajx 指令 (ps指令还有多种参数选项,具体的自己查)
这个指令默认向显示器打印出所有目前的进程!
但是由于进程太多,所以我们可以借助 管道 和 grep 指令进行过滤筛选
上图中右边正在执行着一个程序,而左边用来查看进程,可以发现 myproc 这个文件确实是在执行着的!至于它下面这个进程,其实是我们我们调用 grep 指令去查看时候产生的!这个不需要管~
那进程前面这些属性是什么呢?我们一个一个来解释:
为了方便我们观看,我们加上 ps ajx 指令显示的第一行出来,这样子比较直观:
💡 这里有个小技巧,当我们想同时使用多个指令组合搭配的时候,往往可以用逻辑运算来实现比如这里的&&
当然如果我们想要将该进程关掉的话,也就是 windows 下的结束任务,我们可以使用 kill 指令,不过 kill 指令的选项比较多,但是现在只需要知道 -9 这个选项即可,也就是这样子:
kill -9 进程的PID
注意这里的 PID 其实就是上面图片中的 PID 属性,这是用来标识每个进程的!
2、通过访问 /proc 系统文件夹查看
/proc 是 Linux 下专门用来存放进程的文件夹!
其中这些数字文件夹,其实就是以 PID 为名称的进程!
这是可以验证的:先打开一个可执行文件,然后查看其 PID 值,然后在 /proc 文件夹下查找有没有该文件夹,可以发现是有的;接着将该进程 kill 掉后,重新查找该文件夹可以发现已经找不到了!(这里就不演示证明了)
🔺 这里还有一个拓展的知识点:当我们执行一个文件的时候,也就是一个文件加载到内存后变成进程时,我们将其执行文件删除,可以发现程序依然是在执行的,说明执行文件加载到内存后,就与该文件没有关系了,只不过删除了执行文件之后,该进程无法再从执行文件读写信息了,因为被删除了!但是也是有特殊情况的,后面会讲!
通过系统调用获取进程标识符
这里要使用两个函数:getpid() 和 getppid()
通过上面查看进程我们可以了解到 PID 是一个进程的标识符,而 PPID 其实就是这个进程的父进程的标识符!
这两个函数的作用其实就是 返回调用对象的进程标识符或父进程标识符 ~
下面我们来演示一下这两个函数的功能:
myproc.c
--------------------------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{while(1){printf("我是一个进程,我的ID是:%d\n", getpid());sleep(1);} return 0;
}
🐎 下面我们再加上父进程:
// 在printf上加上父进程标识符调用即可:
printf("我是一个进程,我的ID是:%d, 父进程的PID是:%d\n", getpid(), getppid());
这是为啥呢???
还记得我们之前在介绍 shell 的时候吗,我们举了一个例子,就是媒婆、王婆、如花和“我” 的故事,王婆为了不影响自己的工作,就招聘了实习生,让实习生去办理“我”的事情,就算实习生搞砸了,对于王婆来说也没有什么影响。
相同的,这里的父进程和子进程的关系就是王婆和实习生的关系,父进程为了不受许多因素的影响,所以有了子进程,让子进程去解决这些问题,即使子进程出问题,父进程也不会受到影响!子进程崩了,父进程照样运行 (自行验证)~
而这里的**父进程一般情况下其实就是 bash,也就是 Linux 的具体的一种 shell 外壳程序!(特殊情况下可能不是 bash)**
如果我们将 bash 也就是父进程也搞崩了,那么如果是在云服务器上,我们就直接掉线了~
下面我们来看看 bash 进程:
一般来说,当你登录的时候,系统就会帮你设置好你这次登录后的 bash 的进程 PID~
通过系统调用创建进程 - fork
这里我们只是对 fork() 进行初步认识,在我们接下来的学习中会不断的接触它和使用它!
我们来看看 Linux 中的 man 手册是怎么描述它的:
下面我们先不考虑其返回值,来试试看调用该函数会出现什么情况:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> int main()
{fork();printf("我是一个进程,我的ID是:%d, 父进程PID是:%d\n", getpid(),getppid()); sleep(2); return 0;
}
啊???为什么我们执行一个 printf ,但是打印了两次呢?并且这里的 PID 之间还有一些奇妙的关系~
27301 的父进程是 27300,而 27300 的父进程是 18464,接着我们也借助进程信息可以看出 18464 是 bash,而 27300 其实是我们执行找个文件的一个进程,但是我们调用了 fork() 函数,那么其又产生了一个子进程 27301,并且 27301 的父进程是 27300 的子进程!相当于又多了一代!
下面我们再结合其返回值来研究一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> int main()
{pid_t id = fork();printf("我是一个进程,我的ID是:%d, 父进程PID是:%d, ID是:%d\n", getpid(),getppid(), id); sleep(2); return 0;
}
这与上面 man 手册中描述的返回值是一样的!但是很奇怪,一个函数,居然返回了两个值~~
这在语言层面是不能接收的,但是这其实涉及到了系统层面,关于多进程的概念,那么既然有了多个返回值,也就是说其实这是有两个进程在同时执行的,那么来尝试一下下面这个代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> int main()
{pid_t id = fork();if(id == 0){while(1){printf("子进程,我的ID是:%d, 父进程PID是:%d, ID是:%d\n", getpid(),getppid(), id); sleep(1);}}else if(id > 0){while(1){printf("父进程,我的ID是:%d, 父进程PID是:%d, ID是:%d\n", getpid(),getppid(), id); sleep(1);}}else{}sleep(2); return 0;
}
if语句被执行了两次,并且两个进程不停在循环,分别在执行各自的内容!这换做以前是我们想都不敢想的!
这就是多进程~
而且每次重新执行的时候,父进程的 PID 还是不变的,而子进程的 PID 是会改变的!
🔺 结论:
- fork() 是一个函数
- 函数执行之前:只有一个父进程
- 函数执行之后:父进程 + 子进程
- fork() 的后续代码,被父子进程共享,数据各自开辟空间,私有一份(采用写时拷贝)~
- fork() 之后,父子进程会执行后续代码,但是执行的先后次序这个是无法预料的~
- 通过 fork() 的返回值 与 if语句分流,我们可以实现多进程编程!