Linux中对进程的理解

文章目录

  • 1 再次谈谈进程的概念
  • 2 进程的PCB(task_struck)中到底有什么东西,如何理解这些属性
  • 3 查看进程的方式
  • 4 创建进程的方式
  • 5 理解fork创建子进程
  • 6 fork的返回值意义
  • 7 fork的基本使用
  • 8 Linux的进程状态理解
  • 9 僵尸进程
  • 10 孤儿进程

1 再次谈谈进程的概念

我们都知道,进程只不过是一个被加载到内存的一个可执行程序罢了,但是仅仅这么理解是不够的,进程,绝对不是就是一个被加载到内存的可执行程序;
再次期间,我们需要理解,程序是谁把它加载到内存的?答案是操作系统;
么程序被操作系统加载到内存时候,是否需要对进程进行管理呢
?答案也是肯定的,当然需要管理。
那么进程一旦被操作系统管理,那么操作系统又是如何管理进程的呢?答案是:通过描述进程的各种属性,然后对该属性进行管理,从而达到管理该进程的目的;

怎么理解操作系统管理进程这个说法呢:
操作系统为了管理进程,在进程被加载到了程序的之前,就已经对进程做了手脚。那么是做了什么呢?就是把进程的相关属性信息,记录了下来,这个属性信息就是一个进程控制块,PCB;也就是说每个进程都有一个单独的PCB,并且这个PCB是进程被加载进来之前,操作系统就会给进程分配好的,但是每个进程都需要一个PCB,这就需要操作系统管理每一个进程,如何管理进程呢?那当然就是管理每个进程的PCB就达到管理进程的目的;
而每个进程的管理方法可以是有很多方式,比如我们可以通过双向链表的方法管理进程,通过队列啊,或者其他手段管理进程,也就是所谓的数据结构罢了。我们都知道数据结构的目的主要是增删查改,也就是为了管理,而PCB就是被数据结构所管理的一个东西,类似于在双向链表中的结点,这个结点就是PCB,PCB在Linux中,本质就是一个结构体,task_struct{ 里面有各种进程的属性信息}
其实准确的说:进程应该可以等于被加载到内存的代码+进程的PCB,进程的PCB里面有一个属性,用来指向进程代码部分的指针,这样只要我们OS管理到了PCB自然也就可以管理到了进程的代码和数据;


通过代码来看看进程的样子:
Linux中对进程的理解_第1张图片


2 进程的PCB(task_struck)中到底有什么东西,如何理解这些属性

task_ struct内容分类

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

  • 首先进程的属性,标识符是什么意思,每个进程都有一个唯一标识符,很明显标识符类似一种身份的东西,它就代表进程的身份;我们可以通过代码来获得这个进程的表示符;
#include
#include
int main()
{
  while(1)
  {
    
    printf("本进程的标识符getpid = %d\n本进程的父进程的getppid = %d\n",getpid(),getppid());
    sleep(1);
  }

  return 0;
}

Linux中对进程的理解_第2张图片


查看进程的方式:通过命令:ps -ajx | grep myproc,其中myproc为进程的名字;
通过man 2 getpid查看 getpid的相关属性和getppid的相关属性
Linux中对进程的理解_第3张图片


  • 如何理解tast_struct的属性:任务状态,退出代码,退出信号等。
    这里说一说这个退出代码:比如我们写的main函数,进程返回0时候, 这个0就是退出代码,可以通过echo $?查看; echo $? 会输出离该命令最近运行程序的退出状态码,当然你的命令也是属于程序,当执行命令后,也可以退出状态码;
    再说这个,任务状态又是什么,呃呃,其实一两句话很难说清楚,就是一个进程在内存中是有多个状态的,比如就绪状态,,等待状态呀,运行状态;这个就好比我们人,在去面试的时候,也有等待状态呀,面试时候的状态呀,什么准备面试的状态呀这样;反转我们得知道,这个状态可以表示当时程序处于什么样的状态,那么该进程就得做什么样的事就可以了;

  • 优先级也很好理解:优先级也就是每个进程被执行的优先程度,看看哪个进程被cpu调度而已,我们可以通过一些代码策略改变进程的优先级,但是一般每个进程都会有一个时间片,时间片的意思是:该进程被cpu执行的时间,一旦该进程能够在时间片执行结束,那么该进程就结束,不可以在一个时间片内结束,那么该进程就被操作系统调度,离开cpu,让另一个进程来获得cpu的使用权,也就是操作系统调度了另一进程给cpu执行;按道理来说每个进程的时间片都是相等的,也就是说cpu执行每个进程的时间都是公平的,但是我们可以通过一些策略修改进程的执行顺序,也就是说,优先级;

  • 程序计数器: 程序中即将被执行的下一条指令的地址。理解这个程序计数器,我们知道程序是有一种执行状态得,顺序执行,跳转执行,本质执行指令,都是cpu调度得结果,cpu怎么知道如何执行你的代码指令呢?就是通过这个程序计数器来判断,这个程序计数器就是一个寄存器,存放该进程下一条要执行的指令;

  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
    要知道,我们的进程是包括,进程的代码和相关数据+该进程的PCB信息,那么进程的PCB信息是如何和进程的代码联系起来的呢?那就是通过这个内存指针,这个内存指针会执行进程的代码和相关数据;然后OS通过管理PCB,进而就会管理到PCB中的内存指针,由于有内存指针指向了代码和相关数据,那么也就管理到了进程的代码和相关数据;

  • [ ]上下文数据: 进程执行时处理器的寄存器中的数据。
    上下文信息,说到这个,我都非常激动,因为这个是理解进程的核心概念属性;上下文数据,上下文信息都可以这么叫,那到底什么是上下文信息呢?
    我们知道程序在运行时候,是需要把一些临时数据存放在寄存器中的,这些临时数据是运行程序的关键数据,我们也知道,每个进程都会有一个时间片,当该时间片被用完了,OS就会让该进程离开CPU,进入让另一个进程来使用CPU,但是我们知道,如果一个进程离开CPU前,什么都没有留下来的话(也就是相关的数据:比如该进程在执行过程中,突然到一半时候,时间片用完,但是数据还没有被处理完全,这个时候这个数据没有被留下来,没有被保存),此时等下次再次调度该进程,由于什么之前离开的相关数据都没有被保存下来,CPU执行该进程就不可以恢复到上次没有执行完的哪个步骤了;类似于在一个学习,你说你要去当兵,但是你离开学校前,并没有和学习说明情况,没有把你的学籍保留,没有告知学校你的课程没有上完,此时你冒然离开;学校不知道你离开的事实,就把你当作旷课处理,等你当兵回来时候,发现自己被开除了;就和进程的道理一样;
    所以说:上下文信息:就是一个进程被调度时候,需要保存的临时信息;
    并且这个信息有保存信息,和恢复信息的操作;保存信息的意思是:进程离开CPU 时候,该进程的寄存器相关属性信息被保存在了PCB中,恢复信息是:进程获得CPU时候前,需要恢复原来离开CPU时候保存下来的信息,这样才可以继续使用CPU,被调度执行;

为什么上下文信息需要被保存切换呢?

这个主要是因为CPU调度调度时候,只能调度一个进程,而进程有保存上下文信息,进程如此之多,也就是上下文信息如此多,而CPU里的寄存器只有一份:CPU为了能够调度每个进程使其运行起来,只能对上下文进行保存切换


其他的信息就不怎么谈了,我们要知道PCB就是一个进程的控制块,被OS所管理,OS管理进程控制块,也就是PCB,通过一些数据结构的方式管理,比如链表啥的,管理了PCB也就管理了进程的代码,因为PCB属性里有一个内存指针,指向了进程代码和相关数据;
OS不直接管理进程的代码,而是通过进程的PCB间接管理进程的相关代码和数据;


3 查看进程的方式

  1. ps ajx | grep 进程 :查看进程相关信息;
    在这里插入图片描述

如果需要配合相关的详细信息:
2. ps ajx | head -1 && ps ajx | grep 进程,这个命令会多了一些进程相关信息的解释;
在这里插入图片描述
3.通过文件查看文件 /porc查看进程,这个目录是Linux带给我们的一个查看进程的一个目录
Linux中对进程的理解_第4张图片
其中数字都是一些进程的ID号;
可以通过ls -l /proc/进程id号查看你的进程的详细信息属性;


4 创建进程的方式

  1. 第一种创建进程的方式:就是指向我们的命令,这就是创建进程了,只不过指向命令时候进程一下子就执行完了;比如:执行ls命令,这就是创建进程了,执行结果就是显示文件名倍;
  2. 第二种就是我们写好的程序通过./a.out方式调用,也是在创建进程;
  3. 通过fork函数来创建子进程

5 理解fork创建子进程

fork函数创建子进程,它可以达到一个效果:创建成功后,fork之后的代码会执行两次:
Linux中对进程的理解_第5张图片


其实fork之后就是创建出一个子进程,它的父进程就是main函数;而main函数的父进程就是bash进程,bash也就是命令行解释器;
在这里插入图片描述


该如何理解我们fork创建子进程这个说法?

Linux中对进程的理解_第6张图片

  1. 首先,我们必须有一个认知,我们创建进程的方式虽然又好几种,但是在操作系统的角度上来理解进程,本质是没有任何差别,不管你是./a.out运行进程还是编写代码fork创建子进程,都是一样的;
  2. 其次我们要知道for本质是创建子进程,在操作系统的角度上,系统就会多出一个进程,那么也就是说:操作系统要多管理一个进程,那么一个进程的出现,自然而然就会有自己相关的数据结构和代码和数据;
  3. 而这个数据结构,在Linux操作系统上就是task_struct,代码和数据是:父进程的代码和数据被拷贝到了子进程中,也就是说,子进程的代码和数据,是和父进程一模一样滴;
  4. 但是fork之后,我们子进程的代码和父进程的代码共享的,共享的意思是,父进程和子进程一起使用同一份代码,但是数据却又是独立的,独立的意思是子进程和父进程的数据虽然是一样,但是却独立于每个进程,父子进程的数据互不干扰;

fork之前的代码会被共享嘛?

h
当然会被共享,只不过,当程序执行到了fork之后,就会形成子进程,也就是说,main函数不再单独执行自己的代码,也会执行子进程的代码,相当于多出来一个执行流(子进程的执行流);
执行到了fork之后,就不会在跳转到fork之前的代码执行了,这本身就是程序按顺序执行的方式;


fork之后的子进程数据也是共享的嘛?

我们知道fork之后的代码时共享的,但是数据呢?数据默认情况共享的,但是这个必须有个前提:是父子进程的数据在没有被修改时候,才算是共享的,一旦修改了就不是共享的了;
这也就是说:父子进程本身就是独立的,相互不干扰的;


fork之后的子进程是和父进程相互独立?

进程本来就是相互独立的,进入fork之后创建的是子进程,但是进程独立性这个是不可以被撼动的;
在操作系统上,是通过一种技术:“写时拷贝”来达到进程之间独立性的目的;
写时拷贝:大概意思就是:子进程和父进程在读数据时候,读的都是共享的数据,但是一旦数据被修改,也就是说在子进程修改该,或者在父进程修改了,这个共享的数据就会被拷贝一份出来,让那个修改的进程去修拷贝的数据,而不修改共享的数据;


6 fork的返回值意义

我们说,假如在父进程创建了一个子进程,让父子进程都做一样的事情有意义嘛?其实是没意义的,既然我父进程本来不fork都可以完成的是,为什么还要fork一个新的子进程来做和父进程一样的事情;
所以说我们fork是有返回值的,这个返回值的意义就是:
可以让父子进程做不一样的是:

fork一次调用,两次返回:
在父进程返回子进程的pid,在子进程返回0;


Linux中对进程的理解_第7张图片


代码验证以下:一次调用,两次返回
Linux中对进程的理解_第8张图片


如何理解fork一次调用两次返回?
Linux中对进程的理解_第9张图片


我们需要知道一个函数开始执行return语句了那么久代表该函数已经执行完了;
那么对于fork函数来说:fork内部有个处理逻辑就是创建子进程,那么一旦创建子进程成功后,父子进程就会同时执行之后的代码,而fork内部的return语句在创建子进程之后,也就是说,这个语句是会被父进程和子进程都执行到的;
这就可以带表,为什么说,fork一次调用,会有两次返回;


如何理解fork返回是两个不同的pid,也就是返回值不一样呢?

这就是写时拷贝的技术,也就是说,当fork内部开始调用return语句返回时候,假如是父进程开始返回,那么就直接返回子进程的pid给外面的变量接收了,再到子进程开始返回,此时子进程返回的就不是自己的pid,而是修改该返回值为0,返回给子进程外面的变量接收,外面的变量都是同一个变量,却又不同的值,也就是写时拷贝的技术;


如何理解给父进程返回子进程的id,给子进程返回0呢?

我们知道,在我们现实生活中,父:子 = 1:n;也就是父亲只有一个,孩子却可以有多个,父亲为了控制管理多个孩子,需要记住每个孩子的名字,而孩子却不需要记住父亲的名字也可以找到父亲,因为父亲只有一个,他就是唯一一个;
所以说:给父进程返回子进程id,是为了达到父进程控制子进程的目的,子进程返回0没关系,假如他要知道父进程的id,直接调用getppid()就可以;


7 fork的基本使用

#include    
#include                                                 
    
int main()    
    
{    
  pid_t pid = fork();    
    
  if(pid == 0){    
    //child process    
    while(true){    
    
      std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "
      <<getppid()<<"fork ret = "<< pid <<std::endl;        
      sleep(1); //子进程睡一秒,为了让cpu调度执行父进程    
    }    
  }    
  else if(pid >0){    
    //parent process            
    while(true){    
      std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "
      <<getppid()<<"fork ret = "<< pid <<std::endl;        
      sleep(1);//父进程睡1秒,为了让cpu调度执行子进程    
      
    }    
  }    
  else{  
     //error
  }
  sleep(1);
  return 0;
  


执行的结果如下;
在这里插入图片描述


以后我们就可以通过fork创建子进程,通过if else 分流实现在不同的进程执行不同逻辑的代码


8 Linux的进程状态理解

为什么我们要强调Linux系统下的进程状态呢?
我们平时在操作系统书本上看的进程状态,它们都不是具体的状态,也就是说:操作系统书上的进程状态更加一般化,它适用于任何操作系统上的进程状态解释;
往往我们理解操作系统书本上的进程状态都比较吃力,是因为没有一个具体的操作系统去理解进程转台到底是什么意思:


这是操作系统书上一般化的进程状态的解释:
Linux中对进程的理解_第10张图片


我们可以看看我们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 */
};

首先我们可以问问问自己进程状态的信息存在哪里呢?

在进程的PCB里,在Linux种也就是task_struct种;

进程的状态的意义是什么?

方便操作系统快速判断进程的状态,完成某种特定的功能,比如调度;
本质上进程的状态就是一种分类;

我们一个一个来理解具体的进程状态:
首先是进程状态R

进程状态R:表示该进程处于运行状态,它表示的是PCB数据结构存放在运行队列中,这样就表示该进程处于运行态;
Linux中对进程的理解_第11张图片


处于运行态的进程一定会被CPU调度嘛?

不一定,因为处于运行住状态的进程是在运行队列中,看它是否被执行,被调度,是看它在队列的哪个位置;


代码验证一下R状态:

#include
#include

int main()
{
	while(true);
	return 0;
}

在这里插入图片描述


对于S和D状态:

当我们进程为了完成某种任务时候,任务条件不具备时候,那么该进程就需要等待该条件合适才可以被调度,此时该进程就会被放入等待队列,我们把这种状态的进程成为S和D状态;


比如:在我们进程为了读取磁盘信息时候,发现磁盘没有信息,在访问网卡资源时候,发现网卡还没有资源,在等待键盘输入字符时候,我们还没有按下键盘字符,此时的进程都会被放入一个等待队列中,等待该条件触发,才可以被调度;
换句话说:不是所有的进程都是为了等待CPU资源的,而我们的等待队列的进程就不是为了等待CPU资源,而是等待其他外设的资源,因为外设的资源太慢了,不可以让该进程在运行态的队列等待cpu资源,只能让它到放到另一个队列:即等待队列中等待外设资源;
当我们外设资源等到,也就是比如说磁盘又内容可以读时候,我们操作系统就会把该等待磁盘资源的进程,从等待队列插入到运行队列中,等待CPU调度;

也就是说:进程的状态不是一成不变的,而是会更具它需要完成某种任务功能而发生改变的;


Linux中对进程的理解_第12张图片


S状态和D状态的理解

这两种状态都是等待的状态;
S称为可中断的等待状态;也就是说,当该进程处于S状态,我们是可以通过其他信号发出终止的,不让它继续处于S状态;
D称为不可中断的等待状态;对于这种D状态的进程,终止方式只有两种,直接关机,和等待该进程完成它的功能


R状态和S状态之间的联系

一个进程的task_struct从运行队列中放入到等待队列中,我们就把这种叫做挂起(阻塞)进程; 一个进程的task_struct从等待队列放入到运行队列中,我们就把这种叫做唤醒进程;


T状态:

T状态也就是暂停状态,处于暂停状态的进程,数据是不会再被更新的了,也就是说,该进程被暂停运行了;
而它和S状态区别在于,S状态的进程,数据可会被更新,比如S状态的进程再sleep,等sleep时间到了,也就是时间的数据被更新了,它又会被唤醒了;


t状态:

这个状态是追踪状态,也就是说进程在处于调试状态时候,就会是t状态;


X状态

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
处于死亡状态的进程,也就是回收了该进程的资源,也就是释放了该进程的PCB信息和代码和数据;


9 僵尸进程

Z状态:

处于Z状态的进程,我们成为僵尸进程
如何理解僵尸进程呢?也就是说该进程本运行完了,但是它的进程控制块信息,也就是task_struct还没又被释放;为什么会产生僵尸进程呢?我们知道,在fork子进程时候,假如我们子进程先提前结束了,也就是子进程运行完了,父进程还没有运行完,此时父进程没有发现读取到子进程执行完的信息,也就是父进程不知道子进程已经结束了,那么此时子进程就会变成僵尸进程;


僵尸进程的状态演示:
我们控制让子进程先退出,父进程不退出,观察子进程的状态:

#include    
    
int main()    
    
{    
  pid_t pid = fork();    
    
  if(pid == 0){    
  //child process    
    
    std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;        
    return 1;    
}    
  else if(pid >0){    
  //parent process    
    
  while(true){    
    std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;        
    sleep(1);//父进程睡1秒,为了让cpu调度执行子进程    
  }    
 }                                                                                      
 else{    
  //error    
 }    
  sleep(1);    
  return 0; 

在这里插入图片描述


僵尸进程的危害:

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的! 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 内存泄漏?是的!


对于如何避免这种状态的产生之后会分析。


10 孤儿进程

孤儿进程:也就是父进程先退出了,子进程还没退出,此时这个子进程就会成为孤儿进程;
父进程退出了,没有回收子进程,那么子进程就会被1号进程领养,也就是说:1号进程成为孤儿进程的父进程;
1号进程就是操作系统,孤儿进程自然而然会给1号进程回收;


我们也来代码演示一下:
让父进程先退出,子进程继续运行;


#include                                                                                                   
#include

int main()

{
  pid_t pid = fork();

  if(pid == 0){
  //child process

  sleep(30); //子进程睡30秒,让父进程执行它的代码,让他退出,为了让子进程形成孤儿进程
    std::cout<<"i am a child pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
  }
  else if(pid >0){
  //parent process

    std::cout<<"i am parent pid = "<<getpid()<<"parent pid = "<<getppid()<<"fork ret = "<< pid <<std::endl;
  }
 else{
  //error
 }
  sleep(1);
  return 0;
}

在这里插入图片描述


你可能感兴趣的:(Linux,linux)