Linux进程编程实践1——进程的基本概念、fork创建进程

一、进程的基本概念

在学习进程之前我们需要理解和区分两个概念:程序和进程。

1.1 程序 VS 进程

<1> 什么是程序?

程序是完成特定任务的一系列指令集合。
举个栗子,下面这段代码就是一个程序

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

int main()
{
    
    
  while(1)
  {
    
    
  	printf("hello world\n");
    sleep(1);
  }
  return 0;
}

我们编译好后把它运行起来:
在这里插入图片描述

<2> 什么是进程?

  • 用户的角度来看:进程是程序的一次执行过程
  • 操作系统的核心来看:进程是操作系统分配的内存、CPU时间片等资源的基本单位。

进程是资源分配的最小单位,每一个进程都有自己独立的地址空间执行状态。像Linux这样的多任务操作系统能够让许多程序同时运行,每一个运行着的程序就构成了一个进程
我们另开一个窗口,使用ps命令就可以查看刚才运行的程序以及它所构成的进程
在这里插入图片描述

<3> 进程和程序的区别

  • 进程是动态的,程序是静态
  • 进程的生命周期是相对短暂的,而程序是永久的。
  • 一个进程只能对应一个程序,一个程序可以对应多个进程

1.2 进程数据结构(描述进程)

<1> 操作系统的进程描述——PCB

进程的静态描述由三部分组成:PCB、有关程序段和该程序段对其进行操作的数据结构集。

  • 进程控制块(Process Control Block, PCB):用于描述进程情况及控制进程运行所需的全部信息。
  • 代码段:是进程中能被进程调度程序在CPU上执行的程序代码段。
  • 数据段:一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行后产生的中间或最终数据

<2> Linux下的进程描述——task_struct

  • 在Linux中描述进程的结构体叫做task_struct。它是PCB的一种。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信

task_ struct内容分类

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

二、fork创建进程

2.1 获取进程标识符与查看进程

进程最知名的属性就是进程号(processID,PID)和它父进程号(parent processID,PPID)。

  • PID和PPID都是非零的整数
  • 一个PID唯一标识一个进程。
  • 一个进程创建的另一个新进程称为子进程。相反地,创建子进程的进程称为父进程
  • 所有进程追溯其祖先最终都会落到进程号为1的进程身上,这个进程叫init进程Init进程是linux内核启动后第一个执行的进程。
  • Init引导系统,启动守护进程并且运行必要的程序。

通过调用getpid()获取进程id,调用getppid()获取父进程id,举个栗子:

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

int main()
{
    
    
  printf("子进程pid:%d\n",getpid());
  printf("父进程ppid:%d\n",getppid());

  return 0;
}

运行结果:
在这里插入图片描述
我们可以使用ps,top等命令查看系统进程,也可以通过下述操作查看
在这里插入图片描述

Q1:为什么要知道一个进程的PID以及它父进程的PID呢?

PID常见的用法之一就是创建唯一的文件或目录名。
另一种的用途是把PID写入日志文件做为日志消息的一部分。

2.2 fork创建进程

对于Linux系统,我们运行man fork命令来认识fork
在这里插入图片描述

  • 包含头文件 <unistd.h>
  • 函数功能:创建一个子进程
  • 函数原型:pid_t fork(void);
  • 参数:无参数。
  • 返回值:
    • 如果成功创建一个子进程,对于父进程来说返回子进程ID
    • 如果成功创建一个子进程,对于子进程来说返回值为0
    • 如果为-1表示创建失败
      在这里插入图片描述

掌握了函数的用法后,我们通过实例来掌握并理解fork函数和进程创建

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

int main()
{
    
    
  printf("未调用fork打印一遍\n");
  
  fork();
  printf("调用fork打印两遍,父进程ID:%d , 进程ID:%d \n",getppid(), getpid());
  sleep(1);
  return 0;
}

通过上述代码我们调用fork函数创建进程,可以发现fork()语句之后printf语句被执行了两次
在这里插入图片描述
我们可以更加形象的理解进程间的关系:

bash进程(id:23920)——爷爷

第一个输出进程(id:31987),与bash进程为父子关系——儿子

第二个输出进程(id:31988),与第一个输出进程为父子关系——孙子

Q1:如何理解进程创建?

博客1.2节中介绍了进程的数据结构由PCB+代码段+数据段,而Linux中的PCB是由task_struct代替的,创建一个进程在Linux中,我们目前可以认为需要三部分构成

  • task_struct
  • 数据段
  • 代码段
    但是创建一个子进程确只需要创建task_struct和数据段,代码段是和父进程共享的,这在一定程序上节省了空间。
    在这里插入图片描述

Q2:如何理解父子进程执行顺序?

fork系统调用之后,父子进程将交替执行。

Q3:如何理解fork一次调用,两次返回(两个返回值)?

首先问题的本质是:两次返回,是在各自的进程空间中返回的
在Q1中我们了解到子进程和父进程各有自己的内存空间 (fork函数:代码段、数据段、堆栈段、PCB进程控制块的复制)。父进程调用fork函数,子进程也会调用,且返回值都会存入到自己的数据段中,我们通过一个程序来深刻理解两次返回。

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

int main()
{
    
    
  
  int ret =  fork();
  if(ret < 0)
  {
    
    
    printf("进程创建失败!\n");
  }
  else if(0 == ret)
  {
    
    
    printf("我是儿子(id:%d),fork返回值:%d\n",getpid(),ret);
  }
  else
  {
    
    
    printf("我是父亲(id:%d),fork返回值:%d\n", getpid(),ret);
  }
  sleep(1);
  return 0;
}

运行结果
在这里插入图片描述
由结果可看出,成功创建一个子进程,对于父进程来说返回子进程ID:4451,对于子进程来说返回值为0

猜你喜欢

转载自blog.csdn.net/qq_40076022/article/details/113867381