【Linux】进程(PCB,fork函数,运行状态)

进程的概念

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

进程是什么?我们正在运行的程序就是进程!我们启动一个软件,本质也是启动了一个进程。在Linux下,当你运行你的程序,输入“./程序名”就代表你运行了一个进程。

描述进程PCB

首先明确PCB的概念。

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

在引入PCB之前,需要先讲一个小故事。

你是刚考入大学的新生,在你拿到通知书之后,你们学校的校长(其他老师也可以,这里统一称为校长)为了保证以后对你进行有效的管理,会对你的各种信息进行管理,比如你的姓名,学号,年龄,性别等等。但是,你们学校不是只有你一个新生,你们学校有几千个新生,校长为了进行统一的管理,对所有的学生都创建统一的模板。(这个模板是什么意思呢?就是你作为一个新生你需要填写学号、姓名、年龄、性别这些信息,张三作为一个新生,也需要填写相同的信息,不是说张三填写的信息为学号,家庭住址)而这个模板里填写的内容就是学生的属性

现在故事讲完了,我们将校长类比为操作系统,将你这个新生类比为进程,将你们填写的信息,也即模板类比为PCB

从上面可以得到一个结论,PCB就是一个模板,里面装的内容是描述进程的属性
下图就是PCB包含的大致信息:
在这里插入图片描述

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

接着上面的小故事。

当校长让你填完表之后,校长就可以对你进行管理了。当你升学的时候,校长将你的表中信息由大一改为大二;当你挂科的时候,校长将你的挂科门数增加一门……

所以本质上,校长对你的管理,是对你的信息的管理(所以校长不需要见你的面,你大学四年也不一定见得到校长……)

根据校长对你的管理是对信息的管理,可以类比到,操作系统对进程的管理,本质是对进程PCB的管理!
所以,操作系统的管理本质是对进程的属性数据进行管理

当然,你也不只有你的信息,你还有你这个人,你是有血有肉的,就像进程不是只有那些属性信息,进程还有自己的代码和数据

你 = 你的信息 + 你自己的肉体
进程 = PCB + 代码和数据
(从这个角度,也相当于给了一个进程的概念)

组织进程

阅读到这里,你可能大概懂了什么是进程,什么是PCB。那么,操作系统是如何组织进程的呢?不能让进程一窝蜂全部涌进来吧?

前面提到,操作系统对进程的管理,本质上是对进程的PCB的管理。所以操作系统对进程的组织,本质上就是对进程的PCB的组织。

那么操作系统如何对PCB进行组织呢?在Linux下,进程的PCB本质是一个结构体对象,这个结构体对象的名字叫做task_struct,而操作系统为了组织这么多结构体对象,将这些task_struct用双链表链接起来,最后在操作系统的队列中(有很多队列,这里举例用运行队列)有struct task_struct *head指向双链表的头部,有struct task_struct * tail指向双链表的尾部。

大致结构如下图:
在这里插入图片描述

查看进程

进程的信息可以通过/proc系统文件夹查看
大多数进程信息同样可以使用top和ps这些用户级工具来获取

在这里插入图片描述
大概是axj和ajx都可以用。

输入:

ps axj | head -1

可以查看到如下信息,大概是一个表的标头的东西。
在这里插入图片描述
接下来,我们随便写一个函数,然后运行,对进程进行查看
输入:

ps axj | head -1 && ps axj | grep proc

这里ps axj | grep proc是将proc从全部的进程信息中过滤出来。

最后打印的结果是:
在这里插入图片描述
这里可以看到打印了两行,按道理应该只有一行,为什么有两行?因为我们使用了grep命令,而grep也是一个进程,所以也会打印出来。

如果想不打印grep该怎么办呢?
输入:

ps axj | head -1 && ps axj | grep proc | grep -v grep

这里的grep -v grep是反向选择的意思,意思是有grep,我们就不要。

最后打印的结果为:
在这里插入图片描述
现在代码运行起来了,该如何停止呢? kill -9 进程pid
在这里插入图片描述

getpid()

查看getpid()的手册
输入

man getpid

得到:
在这里插入图片描述
从上面的图片中可以得知:getpid()返回的值是调用它的进程的pid。
此时我们用vim写一个程序获得pid,也就是调用getpid()

将xshell的窗口复制一份,得到两份,最后右边的窗口输入:

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep; echo "-----------------------------------"; sleep 1; done

在左边的窗口输入./proc运行程序。
得到下面的图,会打印成如下的情况。
在这里插入图片描述

在此过程中,我们多次输入CTRL+ C,使程序中断,并多次继续输入./proc`运行程序,最后得到总结过的下面的表。

在这里插入图片描述
从上面的图可以发现,每次的pid都不同,但是每次的ppid却是相同的。

为什么同一个进程,每次运行的pid却不同?
因为PID是一个由操作系统分配给每个进程的数字标识符。它用于唯一标识每个进程,并且在操作系统的管理下,用于跟踪和控制进程的执行。当一个进程终止后,其PID会被释放,可以被系统再次分配给新创建的进程。每次运行同一个进程时,操作系统将为该进程分配一个新的PID。这是为了确保每个进程都有一个唯一的标识符,方便操作系统管理和控制进程的执行。因此,即使是同一个进程,每次运行时分配的PID也会不同。这是操作系统设计的一种机制,以确保进程的唯一性和管理的灵活性。

ppid就是该进程的父进程的pid,我们查看一下ppid,看谁是父进程。
输入:

ps ajx | head -1 && ps ajx | grep 29265

在这里插入图片描述
从这里可以看出父进程是bash。

fork()函数

通过手册查看fork函数
输入:

man fork

在这里插入图片描述
阅读这个手册,大致的意思是,调用fork函数,会返回两个返回值,分别是子进程的pid和父进程的pid。

读到这里就很迷惑了,一个函数竟然能返回两个值?

写一个程序调用fork函数:
在这里插入图片描述
运行程序,得到:
在这里插入图片描述
为什么会打印两次after?
为了理清楚,我在下面连续提了4个问题,当这4个问题弄懂之后,几乎可以理解为什么fork可以返回两个返回值了。

1.为什么fork()给子进程返回0,给父进程返回子进程pid?

写一个程序来观察:
在这里插入图片描述
(此时有两个窗口)在右边的窗口中输入:

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep; echo "-----------------------------------"; sleep 1; done

在左边的窗口中运行程序,输入:

./proc

最后得到:
在这里插入图片描述
从上图中得到,父进程的pid就是子进程的ppid,证明这个"我是父进程"确实是“我是子进程”的父亲,是有血缘关系的!

现在还是没有说清楚为什么会给子进程返回0,给父进程返回子进程的pid。

我们这样想,操作系统不是傻子,如果父子进程都做同一件事情,那么是不是就没有必要有两个进程了?一个进程就能干的活,为什么要给两个进程?

所以,我们应该知道,父子进程要干不同的活。

在此基础之上,我们理解了为什么要返回两个值,因为,返回不同的返回值,是为了区分,让不同的执行流执行不同的代码块,一般而言,fork之后的代码,父子共享。

那么问题就变成了,为什么要给父进程返回子进程的pid?返回其他的不行吗?

子进程是由父进程创建的,父进程要控制子进程,所以给父进程子进程的pid,是为了让父进程更好的访问控制子进程。

2.fork()函数究竟在干什么?干了什么?

调用fork函数之前

假设只有父进程,因为进程 = PCB + 代码和数据,所以应该是如下图所示的:
在这里插入图片描述
调用fork函数之后:

父进程创建出来了子进程的PCB
在这里插入图片描述
但是此时子进程只有PCB,我们知道进程 = PCB + 代码和数据,所以,现在子进程只有PCB,它的代码和数据去哪里找呢?

答案是,子进程也指向父进程的代码!,如下图这样,这里也反向证明了,父子进程共享代码!
在这里插入图片描述
既然父子进程共享代码,那么怎么控制父子进程执行不同的代码块呢?
答案是:让fork具有不同的返回值
在这里插入图片描述

3.一个函数如何做到返回两次的?

既然上面说到为了让父子进程执行不同的代码块而让fork函数返回不同的返回值,那么,又涉及一个问题,一个函数是如何做到返回两次的?

在解决这个问题之前,需要明确一件事,fork也是函数!!!

这意味着什么呢?
这意味着,当调用fork函数的时候,在fork函数内部,完成了

  1. 创建子进程
  2. 填充PCB对应的内容
  3. 让子进程和父进程指向相同的代码
  4. 父子进程都是独立的task_struct,可以被CPU调度
    此时,还没有走到return语句,那么,
    重点来了!!!
    此时还没有走到return语句,但是!子进程已经被创建了!根据之前所说父子进程共享代码,那么父进程要return ,子进程同样也要return,所以!返回了两个返回值。

画个图让逻辑更清晰一些。
在这里插入图片描述

4.一个变量怎么会有不同的内容?

好的,现在我们也许大概可能明白了为什么fork能返回两次,并且返回两个不同的值了。
但是你也许有点疑惑,那就是,明明只返回一个ret,为什么会有两个值呢?你要是说fork返回两次,但是返回相同的值,那完全可以理解,问题就在,为什么返回的是不同的值?

首先,明确一个概念,进程具有独立性!
可能有人觉得,子进程只有自己的PCB,连代码都是找父进程死皮赖脸要来的,怎么还独立了?

(就好像你已经工作了,你告诉你爸妈你要独立了,让他们不要总是管你,但是你回家上你家的洗手间,你爸妈不能因为你上了公共的洗手间就说你是不独立的。)

继续说回来,代码共享,数据不共享是因为代码不可以修改,数据可以修改,正是因为代码共享,数据独立,从而造成父进程创建子进程之后,父子进程割裂了,因为子进程是独立的!

那么这和一个变量有两个不同的内容有什么关系呢?

别急,先明确这件事~

我们现在知道,父子进程是代码共享,数据不共享。此时,

  1. 子进程会修改父进程的数据(应该是子进程自己的数据)
  2. 子进程大概率不会全部修改父进程的数据

基于以上两点,因为子进程不会完全修改父进程的数据,所以为了节省空间,操作系统不会将父进程的全部数据直接拷贝给子进程,而是当子进程尝试修改父进程的数据时,操作系统先阻止子进程直接修改,操作系统去申请一份空间,让子进程在那里写入,写入少申请多少 – – – – – 这叫数据层面的写时拷贝

在这里插入图片描述
通过图中的内容,现在我们基本能明白,为什么同一个变量,却有不同的返回值了。 – – 因为发生了写时拷贝,一个变量有两个内存(两份空间,父进程的id存在一处,子进程的id存在另一处),所以有两个不同的返回值
但是,同一个变量为什么会有两个内存空间呢?,这个就涉及进程地址空间的知识了,且听下回分解。

其实bash进程创建子进程也是写时拷贝,在bash的内部,通过fork创建子进程,然后bash去做自己的事情,让子进程完成它的使命。

进程状态

一个进程大概有下面这些状态
在这里插入图片描述

运行

运行状态:处于运行状态的进程并不一定在运行中,它表明进程要么正在运行,要么处在运行队列
通过这句话,可以得出,处在运行队列的进程就是在运行状态,但是在运行队列里还需要排队,当排到某个进程在运行,就叫这个进程正在运行。

运行排队的图大致如下。

在这里插入图片描述
此时有一个问题:
如果一个进程进入死循环,那么后面排队的进程是不是要一直排队?
答:不是的,对操作系统而言,进程有很多调度的方法,假设是时间片轮转法,那么每个进程都有一个时间片,当一个进程在运行的时间超过了规定的时间片,那么这个进程就会被强制换下,让后面排队的进程再来运行。

这在计算机中,叫做多个进程并发执行。
所以一定存在大量把进程从CPU放上去和拿下来的动作,这叫做进程切换。

阻塞

计算机中存在各种外设,例如键盘,屏幕,网卡……,我们假设一个外设有一个等待队列。那么在等待队列中的进程,我们就叫做在阻塞状态。

等待特定设备的进程,叫做该进程处于等待队列
如下图。

在这里插入图片描述
当一个设备准备好了,此时就是可以读取了,那么在此时,就把该进程从等待队列中取出,将它放入到运行队列中排队,这就叫进程唤醒。

进程唤醒:将进程放入到运行队列。

一般一个CPU只有一个运行队列,但是有上千个等待队列。

挂起

在这里插入图片描述

总结

本篇文章理清了进程的概念,对PCB进行了详细的描述 – – PCB类似一种模板,是描述进程属性的结构体,进程 = PCB + 代码和数据

讲解了操作系统是如何组织进程的 – – 操作系统对进程的组织,本质就是对PCB的组织,用双链表将进程PCB链接起来。

讲述了查看进程的指令 – ps axj | head -1 && ps axj | grep proc | grep -v grep,并且写了一个程序来查看进程的pid,观察到每次关闭进程再运行进程,进程的pid就会不一样,但是进程的ppid都是一样的,最后查询进程的ppid,发现是bash。

对创建子进程函数fork()进行了深入的讲解,提出了四个问题:

  1. 为什么fork()给子进程返回0,给父进程返回子进程pid?
    答: 给两个进程返回不同的内容,是为了让父进程更好的控制子进程
  2. fork()函数究竟在干什么?干了什么?
    答:fork()函数完成了创建子进程的操作,在fork之前,只有父进程的PCB、代码和数据,
    在fork之后,不仅有了父进程的PCB、代码和数据,还有了子进程的PCB。由于此时子进程没有代码,所以父子进程共享代码。
  3. 一个函数如何做到返回两次的?
    答:在fork之后,有两个进程,分别是父子进程。因为父子进程共享代码,而return语句作为代码也要被共享,所以是父进程执行一次,子进程也执行一次,所以做到了返回两次。
  4. 一个变量怎么会有不同的内容?
    答:因为一个变量存在不同的内存中,而这要解释清楚,必须涉及到进程地址空间。

对进程的状态:运行,阻塞,挂起进行了简单的描述。

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值