走进Linux——进程(一)相关概念与创建子进程

        进程的概念是操作系统的核心。因此理解进程、学会进程的操作非常重要。为了充分呈现Linux下进程的相关知识,将以多篇博客的形式展现。本篇重点介绍进程的相关概念以及如何利用fork()创建子进程。

目录

进程的概念

进程控制块-PCB

task_struct内容分类

查看进程信息

利用系统调用fork创建子进程


进程的概念

        什么是进程,书本上进程有如下的几个定义:

                一个正在执行的程序。

                一个正在计算机上执行的程序实例。

                能分配给处理器并由处理器执行的实体。

                由一组执行的指令、一个当前状态和一组相关的系统资源表征的活动单元

        也可以把进程视为由一组元素组成的实体,进程的两个基本元素是程序代码和与代码相关联的数据集。假设处理器开始执行这个程序代码,我们把这个执行实体称为进程。

        总的来说,进程等价于程序文件内容+相关数据结构

进程控制块-PCB

        我们知道,曾经我们所有启动程序的过程,本质上都是在系统上创建进程。系统中当然可能存在着大量进程,操作系统需要对这些进程进行管理,所谓的管理手段即先描述,再组织

        描述进程所依靠的正是进程控制块(PCB),PCB究竟是什么呢?在学习C语言的过程中,如果需要让你描述一名学生的信息,你一定会立刻想到结构体。没错,PCB的本质就是C语言上的结构体,现在PCB在我们的眼中就是下面的形式:

struct PCB
{
    //进程的所有属性
};

        在任何进程形成之前,操作系统就要为该进程创建PCB。而在Linux操作系统上,PCB就具体化为task_struct

         有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系,与进程对应的内核创建的该数据结构强相关。

task_struct内容分类

        task_struct存储进程的相关属性,具体内容如下:

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

        下面将针对一些属性做更加具体的说明。

        (1)PID

        PID是Linux中描述进程的唯一表示符。先写下如下测试程序:

#include <iostream>
#include <unistd.h>

int main()
{
   while(true)
   {
     sleep(2);
     std::cout << "I am a proc" << std::endl;
   }
 
   return 0;
}

        运行进程后每隔两秒打印一句话

         此时使用指令ps axj | head -n1 && ps axj | grep myproc

         可以得知13317就是刚才创建进程的PID。

         同时,PPID指的是父进程的PID,我们可以使用ps axj | head -n1 && ps axj | grep 10062查看进程的父进程。

         可以看到,父进程就是我们所熟知的bash。

         在程序中,我们也可以使用getpid()和getppid()来获取本进程以及父进程的pid。

         测试代码如下:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
 
int main()
{
  while(true)
  {
    sleep(2);
     cout << "pid:" << getpid() << " ppid:" << getppid() << endl;                                                                                                       
  }                                                       
                                                           
  return 0;                                               
}

        运行结果:

         (2)退出码

         在学习C语言的过程中,在main函数中,我们一般都会在最后加上return 0,这里所return的0就是退出码,它将被返回给操作系统。一般而言,0表示进程正常结束,否则表示异常。

        在Linux下,我们可以使用指令echo $?查看最近退出进程的退出码。测试代码如下:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>

int main()
{

  return 1;
}

        结果如下:

         如果我们继续使用echo $?指令,新的退出码就会变为0

        这是因为此时最近结束的指令是上一句echo $?,此时显示的是它的退出码。

        (3)上下文数据

        在实际进程中,代码可能不是很短时间就能结束的,如果有多个进程同时存在,且进程数大于CPU数,CPU是绝对不会将一个进程彻底执行完再去执行新的进程的,因为会导致很多进程根本就没有执行的机会。因此规定了每个进程单次运行的时间片,如10ms,时间片一旦到了,就会将该进程切换走,运行其它进程。也就是说进程在运行期间是有切换的!那么这和上下文数据有什么关系呢?

        进程在被切换之前,可能存在大量的临时数据,这些数据被暂时存放在CPU的寄存器中。但是CPU中寄存器只有一套,如果进程在被切换之前什么都不做,下一次轮到这个进程运行的时候,寄存器中的内容已经被其它进程产生的临时数据所覆盖,这个进程又如何接着之前继续运行呢?

        因此,一个进程在被切换之前,会将寄存器中的数据暂存在PCB中(保护上下文),回来时再将上下文数据返回到寄存器(恢复上下文)。

查看进程信息

        查看进程信息可以通过在/proc系统文件夹查看。

利用系统调用fork创建子进程

        fork的功能是创建子进程,使用方式也简洁明了。写下如下测试代码:

#include <iostream>
#include <unistd.h>

int main()
{
  fork();
  std::cout << "pid: " << getpid() << " ppid: " << getppid() << std::endl;

  return 0;
}

         可以看到运行后输出了两边pid和ppid的显示,即一个为子进程输出,一个为父进程输出。同时我们也发现了子进程和父进程做了相同的事情。

         如何理解fork创建子进程?fork的本质是创建进程,即系统会多了一个进程,与进程相关的内核数据结构+进程的代码和数据在系统中也多了一份吗?默认情况下,子进程会继承父进程的代码和数据,内核数据结构也会以父进程为模板,初始化子进程的task_strcut。

        也正是因为子进程的task_strcut是以父进程的task_strcut为模板的,因此它们的程序计数器是相同的,两个进程将沿着创建子进程后的代码执行。

fork()
{
    ……//创建子进程的过程

    return  XXX;//父子进程从此处开始执行
}

        可以简单地认为fork之后,父进程与子进程的代码是共享的,因为代码是不可修改的。默认情况下,数据也是共享的,不过需要考虑修改数据的情况。如果进程要对某一数据进行修改,需要通过写时拷贝来保证进程数据的独立性。

        写下如下测试代码:

#include <iostream>
#include <unistd.h>

int main()
{
  pid_t id = 0;

  id = fork();
  std::cout << "id = " << id << std::endl;

  return 0;
}

        运行结果:

         对同一个值我们获得了两个结果,这就是写时拷贝所导致的。同时我们也用到了fork()的返回值,它会为父进程返回子进程的pid,为子进程返回0。如此设计的原因是实际中只可能出现一个父进程有n个子进程的情况,因此父进程如果想找到指定的子进程,必须要获得该子进程的具体pid;而子进程要找到父进程,只需要getppid()即可(因为一个子进程只可能对应一个父进程)。

        那么我们创建子进程是希望子进程和父进程做相同的事情吗?大多数情况下,这样是没有意义的。此时我们就可以利用fork()的返回值来使子进程和父进程做相同的事情。

        测试代码如下:

#include <iostream>
#include <unistd.h>

int main()
{
  pid_t id = 0;

  id = fork();
  if(id > 0)
  {
    //parent
    std::cout << "parent is run" << std::endl;
  }
  else 
  {
    //child
    std::cout << "child is run" << std::endl;
  }

  return 0;
}

运行结果:

         而fork()之后父子进程谁先运行是不确定的,需要由调度器控制。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

树獭想吃饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值