前言

      本章要谈论进程的概念为后续进程状态做铺垫

 一、进程的基础概念

(1)是什么

        进程是内核的数据结构+程序的代码和数据。

(2)为什么

       当我们的所有程序执行时都会变成进程,被操作系统去运行,操作系统需要一种方式去“记住”每一个程序的状态,合理的分配资源。没有进程的话操作系统不知道谁在跑,谁在消耗资源。进程是操作系统资源分配的基本单位。

  (3)怎么样

       操作系统中会存在多个进程,所以必然要对这些进程去做管理,通过上一篇文章所得出的结论也就是“先描述再管理”通过数据结构的方式将进程管理起来。进程描述(PCB)是在内核之中的,存在一个结构体(task_struct)去描述这个进程。我们知道内存分为用户空间和内核空间,我们的进程描述(PCB)是存在于系统内核之中的,我们平时写的代码数据等之类的加载到内存就是加载到用户空间之中,两者井水不犯河水。此时呢,首先将位于磁盘的代码和数据加载到用户空间之中,此时应该注意就是此时还不算进程,然后再在操作系统中准确来说是在系统内核中形成一个描述这个的结构体(task_struct),当然这个结构体以后再说,此时就可以说是一个进程了。

       所以总结一下,无非我们要讨论与学习的就是只有两个,一个就是这个结构体的内部是什么样的也就是属性,还有一个就是操作系统是这么管理的,也就是数据结构。所以我们先来学习一下这个属性是什么,中途会穿插少量数据结构的内容。

二、进程描述---PCB

   (1)概念

   进程信息被放在一个叫做进程控制块的数据结构之中,可以理解为进程属性的集合。

   课本上称之为PCB,Linux操作系统下的PCB是:task_struct,也就是Linux中描述进程的结构体。

   task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息

(2)task_struct

   这个是Linux操作系统下的PCB,它的内容包括:

• 标⽰符: 描述本进程的唯⼀标⽰符,⽤来区别其他进程。

• 状态: 任务状态,退出代码,退出信号等。

• 优先级: 相对于其他进程的优先级。

• 程序计数器: 程序中即将被执⾏的下⼀条指令的地址。

• 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

• 上下⽂数据: 进程执⾏时处理器的寄存器中的数据[休学例⼦,要加图CPU,寄存器]。

• I∕O状态信息: 包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。

• 记账信息: 可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。

• 其他信息

三、指令或者代码

1、查看进程

     (1)指令:ls /proc/。

  

通过这个指令就可以得到进程的进程信息。

(2)指令:top。指令就是top这个是动态的(停下来按ctrl+c)

(3)指令ps。这个指令需要vim一个文档。

2、父进程和子进程
(1)基本概念

子进程是由父进程创建出来,父子之间是以树形关系,也就是说一个子进程只有一个父进程,而一个父进程可以有多个子进程。

PID(Process ID):进程的ID。进程的身份证号,可以标识一个进程。

PPID(Parent Process ID)父进程的ID,表明“我是谁生的”。

      (2)fork();

       是一个函数,可以用来创建子进程。

#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();  // 这里分裂成两个进程
    
   if(pid==0)
   {
       //子进程会进入这里
       printf("子进程的PID:%d,  PPID:%d\n",getpid(),getppid());
   }
   else
   {
       //父进程会进入这里
      i sleep(3);
       printf("父进程的PID:%d,  PPID:%d\n",getpid(),getppid());
   }
    return 0;
}
 

fork是一个函数就会有返回值

返回值                          说明了什么

-1                           子进程创建失败,当前处于父进程

0                            子进程创建成功,当前处于子进程

正数                       子进程创建成功,当前处于父进程 

当创建子进程成功后,子进程会创建一个新的PCB内容拷贝自父进程的PCB,但不是完全拷贝,此时做为两个独立的进程会有各自不同的PID,并且此时的拷贝为浅拷贝也就说明先共享,等写时才会真正的复制。但是当子进程创建成功之后子进程的PPID会变成父进程的PID,这也是父子两个进程之间的关联。

上边提到了代码共享,数据写时复制,那这里实现一下看看效果

#include <unistd.h>
#include <stdio.h>

int main() {
    int data=200;
    printf("创建子进程之前的data:%d    data地址:%p\n",data,&data);

    pid_t pid = fork();  // 这里分裂成两个进程
    
    if(pid==0)
    {
        //子进程进入这里
        data=100;
        printf("创建子进程之后的子进程data:%d    data地址:%p\n",data,&data);
    }
    else
    {
        //父进程进入这里
        data=0;
        printf("创建子进程之后的父进程data:%d    data地址:%p\n",data,&data);
    }
    return 0;
}

注意到,data的值不同但是地址都是完全相同的,所以大胆猜测一下就是这里的data地址不是真实的物理地址,如果是真实的物理地址那么对应的data值应该完全相等才对。这就是写时拷贝(COW)。

(3)写时拷贝(COW)

        谈这个不得不谈虚拟地址空间,这个虚拟地址空间是伴随着进程的创建而创建(PCB和虚拟地址空间同时产生),随着进程销毁而销毁,当我们的父进程创立之后就会通过页表(真实存在的数据结构,并且不同的进程页表是独立的)建立虚拟地址到物理内存的映射关系,然后当有了这个虚拟地址空间我们就可以找到对应的物理内存并把磁盘的代码和数据加载到内存之中。下面看一段代码还有现象。

int main() {
    int data=200;
    printf("创建子进程之前的data:%d    data地址:%p\n",data,&data);

    pid_t pid = fork();  // 这里分裂成两个进程
    
    if(pid==0)
    {
        //子进程进入这里
        printf("创建子进程之后的子进程data:%d    data地址:%p\n",data,&data);
    }
    else
    {
        //父进程进入这里
        data=0;
        printf("创建子进程之后的父进程data:%d    data地址:%p\n",data,&data);
    }
    return 0;
}
 

在这里会发现父进程修改的数据不会影响子进程,但是data地址还是一样的,这也就更加证明了这里的地址是虚拟内存地址。由于父进程先于子进程创建,此时操作系统会产生一个父进程的虚拟内存地址,并且通过页表将虚拟内存地址于物理内存相对应,此时子进程创建会复制父进程的虚拟内存地址(是完全独立的),此时这个虚拟内存地址会指向同一片物理空间,所以这就是为什么说父子进程之间资源共享。但是当我们的父进程修改之后不会影响子进程,是因为修改是将物理空间的数据复制然后修改父进程对应的页表,通过新的映射找到新的物理空间然后将数据加载到内存中后修改,原来旧的数据还在原来的物理空间,所以我们可以看到修改父进程不影响子进程,同时可以猜想到子进程不影响父进程。所以可以得出一个结论就是发生写入操作之后就会分配新的物理页面,页表的映射关系也会单独更新。

(4)fork的返回值

       上文提到了fork是一个函数,那么就会有返回值,我们可以注意到fork返回值有0,-1,正整数,并且在代码中会注意到返回了两个值,当fork返回0的话子进程就会进入这里。

当fork大于0的时候就会进入else那里

并且会执行子进程的代码和父进程的代码,所以不妨猜测一下这里的PID会返回两个值,一个是0还有一个大于0的值分别进入不同的判断条件。为什么会返回两个值呢,我们说fork是一个函数是函数就会有返回值(除了void之类的),当我们执行return的时候其实fork的任务已经完成了,也就是说子进程已经创建好了,父子进程共享代码,此时在执行return之前的我们已经有了父子两个进程同时在执行到return语句的时候是两个进程共享的,所以会返回两个值。看一段代码,看现象。

#include <unistd.h>
#include <stdio.h>

int main() {

    pid_t pid = fork();  // 这里分裂成两个进程
    
    if(pid==0)
    {
        //子进程会进入这里
        printf("子进程的fork返回值:%d,PID:%d,PPID:%d\n",pid,getpid(),getppid());
    }
    else
    {
        //父进程进入这里
        printf("父进程的fork返回值:%d,PID:%d,PPID:%d\n",pid,getpid(),getppid());
    }
    return 0;
}
 

子进程的fork返回值是0没有问题,父进程的fork返回值是子进程的PID,这里的作用是每一个进程都有唯一的PID,同时一个父进程可以有多个子进程,为了标识不同的子进程才用了子进程的PID去标识,同时这也是为后文介绍僵尸进程,孤儿进程做一个准备。至于为什么同一个id既可以==0又可以大于0就是后边说的了。

后记

      后续将会更新Linux:操作系统的进程状态。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐