Linux系统下进程的概念《一》

发布于:2022-08-07 ⋅ 阅读:(437) ⋅ 点赞:(0)

  个人主页:欢迎大家光临——>沙漠下的胡杨

  各位大帅哥,大漂亮

 如果觉得文章对自己有帮助

 可以一键三连支持博主

 你的每一分关心都是我坚持的动力

 

 ☄: 本期重点:Linux下的进程的状态

  希望大家每天都心情愉悦的学习工作。 

我们在了解进程之前,我们先聊聊我们平时用的计算机吧。这样我们才能更深刻的认识我们后续要学习的内容。

冯诺依曼体系结构:

我们简单认识下上面的东西是什么:

存储器:就是内存。

输入设备:就是键盘,网卡,磁盘,摄像头等等。

输出设备:就是显示器,网卡,磁盘,音响等等。

运算器存储器:就是CPU

其中运算器主要作用:算术运算,逻辑运算(简称算逻运算)

控制器:CPU是可以响应外部事件的,比如拷贝数据到内存。因为内存比较慢,然后我们CPU在拷贝时不可能等着内存,只能是内存准备好了,我们对此做出反应,把它拷贝过来。

那么为什么冯诺依曼这样设计呢?

       我们为什么不可以直接把数据直接用CPU处理,然后我们再显示或者保留数据呢?

 那我们还要简单了解下我们的存储设备了。常见的存储设备如下:

  

我们知道,速度越快的,价格越高,相反速度慢的价格稍微便宜点。那么我们如果直接不使用所谓的内存条存储,我们直接CPU连接外设备,也就是磁盘等,这速度肯定是要慢上不少,可能面对的是别人电脑打开再写文件了,我们还在开机。

那么我们如果都用寄存器存储呢?在技术角度应该可以的,但是我们还要考虑下我们的成本问题,如果我们使用寄存器存储,那么价格上可能会高出普通电脑价格的几百倍都很正常,那么这样,这个计算机也就没人用了,价格上太高了,并且在速度上可能没有提高太多。

所以我们选择一个中间的,我们使用内存存储,让CPU之和内存交互,来提高交互速度,然后再让内存和外部设备进行交互,这样形成的落差比较小。就比如CPU就是亿万富翁,而我们的内存只是百万富翁,而外部设备就是穷人一个,让CPU和外部设备交互,就像是亿万富翁和穷人一样,差距太大了,所以我们就出现了内存,也就是百万富翁。

根本上这样设计原因就是:如果想向全世界人民都用的起这个计算机,价格上不能太高,然后在性能上不能太差,要平衡两边的差距。 

冯诺依曼体系结构的特点:

       冯诺依曼体系决定了CPU读取数据时都是要从内存中读取的,站在数据的角度,我们认为CPU不和我们的外部设配直接交互,但是我们要处理数据,需要先将数据加载到内存中,站在数据的角度,外设只和内存进行交互。总结·就是,所有的设备都只能直接和内存交互

操作系统:

操作系统是什么呢?它是什么呢?我们真的“使用”过它吗?

我们每个计算机中都有一个操作系统的东西,但是我们好像没有像QQ,微信一样,我们直接使用过它,它也很神秘。

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

设计操作系统的目的:

1.给用户提供一个安全,稳定,简单的执行环境。、

2.是与软硬件交互,管理所以软硬件资源

操作系统的定位:

一款只负责 “ 搞管理 ”的软件。

如何理解 “管理” 呢?

就像学校的校长一样,我们进入学校,就会有人给我们一个表格,然后让我们要填写表格的一些内容,比如我们的身高体重年龄等等,这张表格中就有我们的一些数据了。然后再让每个班长收齐这个表格,校长在让班长把这些表格按照一定规律排好序,最后交给校长,此时校长就可以根据这个总的表格来进行管理了,其实上校长也没有去直接管理学生,而是通过管理学生的一些数据信息,来管理学生。

这个校长管理时就先让每个学生填写表格,这也就是把一个学生抽象为一个 结构体 或者 类。然后把这些学生信息安照规律排好序,这也就是把每个结构体 对象,每个类对象,通过一些数据结构进行存储的。然后我们校长管理学生了吗?根本没有,校长是指对学生的信息进行管理来达到对学生的管理。

换句话说,我们管理不一样非要直接进行管理,更多的是通过数据进行管理,比如你学习太差了,直接就体现在成绩不好,再比如说睡眠不好,比如天天早上迟到。这些都是通过数据来管理的。我们操作系统也是类似的,我们也要先进行所谓的抽象为对象,然后在对这些对象进行一些数据结构的存储,最后再来管理这些数据的。称之为:先表述,在组织。

对于操作系统而言,即需要考虑用户方面的一些需求,又要对硬件进行相对应的管理,起到一个承上启下的作用,我们的系统调用接口用来和用户进行交互,我们的相对应的硬件驱动程序来负责和硬件进行交互,所以操作系统起到了承上启下的作用。

进程的概念:

我们对于进程了解多少呢?我们打开自己的任务管理器,会发现有好多进程是被打开的,而我们启动的程序好像也都变成了一个个的进程存在。我们在Linux下启动一个可执行程序,也好像变成了一个个的进程。

那么我们怎么管理这些进程呢?叫做先“描述”,再“组织”。

我们怎么描述进程呢?

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。 称之为PCB(process control block),Linux操作系统下的PCB是: task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

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

那我们怎么组织进程呢?

可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
在Linux下我们怎么查看进程呢?使用proc,top,ps(常用)。

ps (process status 进程状态)命令用于显示当前进程的状态,类似于 windows 的任务管理器。参数说明如下:
-A:显示所有的进程,和 -e 的效果一样;

-a:显示所有进程,包括其他用户的进程;

-u:选择有效的用户id或者是用户名;

-x:显示没有控制终端的进程,同时显示各个命令的具体路径

-e:显示所有的进程,和 -A 的效果一样;

-f:显示更完成;通常与 -e 一起用;

-j或j:采用工作控制的格式显示程序状况。

-l或l:采用详细的格式来显示程序状况。

使用grep进行行过滤,然后我们在使用head显示出头一行的名字。 

我们可以使用proc这个系统级别的文件来查看

其中cwd就表示的是当前的工作目录,exe就是这个文件在磁盘的位置,也就是我们可执行文件所在的地方。其中在每个进程中,都会保存自己的工作目录。

所以什么是进程呢?

我们一般把 加载到内存的可执行文件和我们内存中的PCB结构体合在一起叫做一个进程。在这其中,我们的 操作系统想管理采用:先表述,在组织。先把文件加载进入内存,然后形成了一个PCB的结构体,最后把这些结构体用链表的形式存储起来方便管理。

获取PID和PPID:

我们每一个进程都会有唯一 一个PID,和唯一的PPID,其中PID被叫做子进程标识符,PPID叫做父进程标识符。我们可以通过系统调用,来获取进程标示符。我们使用man来查看获取PID和PPID的头文件和使用方法。

我们可以尝试使用下:

下面我们的进程就会打印自己的PID值。

 然后我们可以根据这个PID值来查看这个进程的状态。

 同理,我们还可以获取这个进程的PPID也就是它的父进程的id值。

然后我们多次启动下这个程序,发现我们的PID一直在变,但是我们的PPID是不会变的,这个是为什么呢?

首先,这和PID一直在变是因为我们的程序每次都终止在重新启动,相当于每次都给我们分配一个新的PID,这很正常,但是为什么PPID不变呢?我们先来看下父进程是什么吧!

我们会发现,其中这个程序的父进程就是我们所谓的shell外壳程序,我们也就得到一个结论,就是我们所有的进程的父进程,几乎都是这个进程。因为是它帮我们创建所谓的子进程然后加载进内存中的。

杀死进程:

我们可以使用kill命令发送9号命令加上这个进程的PID,可以把这个进程结束。

同样,这个进程的父进程是shell外壳,我们可以杀死shell外壳下的子进程,那么我们的shell外壳可以被杀死吗?是可以的,下面我们进程演示下:

这时,我们的shell已经成停止工作了,不能够在进行命令行解释了。 

fork创建子进程:

我们可以通过fork命令,使用代码来创建一个子进程。同样我们先使用man 来查看下使用。

 这个函数接口是系统调用的接口,然后我们如果创建成功,给子进程返回 0,给父进程返回子进程的PID,如果失败给父进程返回-1。那么这个系统接口就十分奇怪,我们先用用试试,看看有哪些奇怪的地方。

这里我们可以看到,其中fork之后的语句被执行了两次,是因为在fork之后,我们的进程就变为两个了,我们在稍微修改下我们的代码,我们创建子进程后,我们直接在进行获取PID和PPID:

 

 我们发现,其中先执行的是父进程,PID为7724,它的父进程就是shell外壳bash,然后它的子进程就是7725,相对应子进程的父进程就是7724,但是我们的代码上,只有一行代码,结果,他执行了两次。这是因为我们有两个输入流,然后我们分别都进行了执行这行代码,一次是在父进程执行的,一次是在子进程执行的,所以我们对应的 gitpid()和 gitppid()不相同。

那么下面的情况就有点不太对了,我们使用变量保存下fork的返回值,然后我们进行打印:

我们发现,我们fork中的返回值,在不同的进程下,有不同的值。也就是又两个返回值,我们在学习C语言和C++时,我们从来没有一个函数能够return两次的情况,而我们的fork这个系统接口,好像能够返回两个返回值。我们稍后解释为什么会有两个返回值。

fork的使用:

我们fork之后是创建了一个进程,但是我们不可能让两个进程做同一件事情呀,所以我们要让这两个进程做不同的事情,我们可以使用if else if   else 这种多分支结构,然后利用fork函数的不同返回值,来做不同的事情

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <stdlib.h>
  5 
  6 int main ()
  7 {
  8     pid_t ret = fork();
  9     if(ret<0)
 10     {
 11         perror("fork");
 12         exit(-1);
 13     }
 14     else if(ret == 0)
 15     {
 16         while(1)
 17         {
 18             printf("child process PID:[%d],PPID:[%d]\n",getpid(),getppid());
 19             sleep(1);
 20         }
 21     }
 22     else
 23     {
 24         while(1)
 25         {
 26             printf("perent process PID:[%d],PPID:[%d]\n",getpid(),getppid());                     
 27             sleep(1);
 28         }
 29     }
 30     return 0;
 31 }

然后我们编译运行一下,然后我们在使用脚本来进行监视它:

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

 我们会从监视窗口看出这是两个进程,然后这两个进程之间是由父子关系的,并且这两个进程分别执行的是不同的代码,这也中可以说明,这个循环的代码是父子进程共享的,其中,父子进程执行属于它自己的部分,互不影响我们在这其中,也看到了两个死循环交替执行,直接原因是因为有两个执行流;其中我们也看到了if elseif else这种多选一的选择中执行多个的结果,直接原因是因为有两个返回值

为什么会有两个返回值呢?

我们在C语言或者C++中,我们的函数只会有一个返回值,也就是我们return只能返回一个值,那么有两个返回值就意味着有两个return 被执行,那么是怎么被执行的呢?

我们创建子进程时,我们的操作系统都做什么呢?

首先是执行fork的逻辑,此时在return id时,我们的子进程已经创建完成了,然后我们把它加载到内存你,生成对应的PCB,其中子进程的PCB是以父进程为模板来进行创建的,此时我们return id之前,我们就是看两个进程了,此时父进程会执行一次return,而子进程也会执行一次他return。所以就会有两个返回值,本质上就是,我们在return之前就是两个进程了。

当然我们虽然返回了两次,但是我们的返回都是id呀,我们判断的逻辑还是根据id值来判断呀,我们怎么做到返回了两次,但是每次的值不相同呢?我们后续揭晓。

进程的状态:

我们在操作系统笼统概念上理解的话,有以下几种:

新建状态:一个进程刚开始被创建完成。

运行状态:我们创建的PCB(task_struct)结构体再运行队列中排队,就叫做运行状态(在这个状态下,我们的进程可能在运行或者在排队)

阻塞状态:等待非CPU资源就绪的状态,就叫做阻塞状态。

  

挂起状态:当内存不足时,我们的操作系统通过适当的置换代码的数据到磁盘中,这个状态就叫做挂起状态。 

退出状态:我们的进程可以被回收。 

但是我们在Linux系统下,我们有更为具体的进程状态:我们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 */
};

R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。(对应上述的运行状态)


S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(nterruptible sleep))(对应的是上述的阻塞状态)

其中我们的S状态,我们一般情况下如果这个进程和硬件有交互的话,那么就会是S状态,比如我们打印一些字符,此时虽然在运行,但是我们的在和硬件交互,我们有很长一段时间都是在等待显示器的队列中,所以我们的状态为阻塞状态。

如果我们不进行打印数据,那么我们的进程就会处于运行状态。

此时我们可以看到,我们的状态变为了R+。

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(对应的是上述的阻塞状态)

对于这个状态来说,我们一般是一个进程在对磁盘数据进行写入时,防止我们CPU因为内存紧张从而让我们的进程杀死,然后导致我们进程的数据丢失,我们把它设置为不可中断的睡眠状态,我们没办法终止它,只能让他自动醒来,即使是kill 的9号信号也不行。


T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(常用于调试状态)


X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态(瞬时性非常强,不能够看到)

僵尸进程(Z):我们通常进程是都是创建的父子进程,那我们如果子进程突然挂了,但是父进程没事,此时我们的子进程还不能够直接被系统回收,那么此时的子进程我们称为僵尸进程。


 刚开始我们的子进程时一个正常的进程,然后我们子进程突然挂了,状态上也变为了Z状态,后面还有一个 <defunct>表示失效的,也就是此时这个子进程已经成变成了一个僵尸进程,而我们的父进程还在正常运行。(关于僵尸进程的内容我们后续在进程控制时进行详细讲解)

孤儿进程:

那我们如果在父子进程中,我们的父进程突然挂了,那么我们子进程突然没有人能够管理了,这怎么办呢?这种情况被称为孤儿进程,我们还要有进程进行收留呀,我们的操作系统进行管理,也就是1号进程进行管理。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到