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


Linux进程编程实践——进程概念、进程描述、fork创建进程

  • 一、进程的基本概念
    • 1.1 程序 VS 进程
      • <1> 什么是程序?
      • <2> 什么是进程?
      • <3> 进程和程序的区别
    • 1.2 进程数据结构(描述进程)
      • <1> 操作系统的进程描述——PCB
      • <2> Linux下的进程描述——task_struct
  • 二、fork创建进程
    • 2.1 获取进程标识符与查看进程
      • Q1:为什么要知道一个进程的PID以及它父进程的PID呢?
    • 2.2 fork创建进程
      • Q1:如何理解进程创建?
      • Q2:如何理解父子进程执行顺序?
      • Q3:如何理解fork一次调用,两次返回(两个返回值)?

一、进程的基本概念 在学习进程之前我们需要理解和区分两个概念:程序和进程。
1.1 程序 VS 进程 <1> 什么是程序?
程序是完成特定任务的一系列指令集合。
举个栗子,下面这段代码就是一个程序
#include #includeint main() { while(1) { printf("hello world\n"); sleep(1); } return 0; }

我们编译好后把它运行起来:
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

<2> 什么是进程?
  • 用户的角度来看:进程是程序的一次执行过程。
  • 操作系统的核心来看:进程是操作系统分配的内存、CPU时间片等资源的基本单位。
进程是资源分配的最小单位,每一个进程都有自己独立的地址空间与执行状态。像Linux这样的多任务操作系统能够让许多程序同时运行,每一个运行着的程序就构成了一个进程。
我们另开一个窗口,使用ps命令就可以查看刚才运行的程序以及它所构成的进程
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

<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 #includeint main() { printf("子进程pid:%d\n",getpid()); printf("父进程ppid:%d\n",getppid()); return 0; }

运行结果:
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

我们可以使用ps,top等命令查看系统进程,也可以通过下述操作查看
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

Q1:为什么要知道一个进程的PID以及它父进程的PID呢?
PID常见的用法之一就是创建唯一的文件或目录名。
另一种的用途是把PID写入日志文件做为日志消息的一部分。
2.2 fork创建进程 【Linux|Linux进程编程实践1——进程的基本概念、fork创建进程】对于Linux系统,我们运行man fork命令来认识fork
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

  • 包含头文件
  • 函数功能:创建一个子进程
  • 函数原型:pid_t fork(void);
  • 参数:无参数。
  • 返回值:
    • 如果成功创建一个子进程,对于父进程来说返回子进程ID
    • 如果成功创建一个子进程,对于子进程来说返回值为0
    • 如果为-1表示创建失败
      Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
      文章图片
掌握了函数的用法后,我们通过实例来掌握并理解fork函数和进程创建
#include #includeint main() { printf("未调用fork打印一遍\n"); fork(); printf("调用fork打印两遍,父进程ID:%d , 进程ID:%d \n",getppid(), getpid()); sleep(1); return 0; }

通过上述代码我们调用fork函数创建进程,可以发现fork()语句之后printf语句被执行了两次
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

我们可以更加形象的理解进程间的关系:
bash进程(id:23920)——爷爷
第一个输出进程(id:31987),与bash进程为父子关系——儿子
第二个输出进程(id:31988),与第一个输出进程为父子关系——孙子
Q1:如何理解进程创建?
博客1.2节中介绍了进程的数据结构由PCB+代码段+数据段,而Linux中的PCB是由task_struct代替的,创建一个进程在Linux中,我们目前可以认为需要三部分构成
  • task_struct
  • 数据段
  • 代码段
    但是创建一个子进程确只需要创建task_struct和数据段,代码段是和父进程共享的,这在一定程序上节省了空间。
    Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
    文章图片
Q2:如何理解父子进程执行顺序?
fork系统调用之后,父子进程将交替执行。
Q3:如何理解fork一次调用,两次返回(两个返回值)?
首先问题的本质是:两次返回,是在各自的进程空间中返回的。
在Q1中我们了解到子进程和父进程各有自己的内存空间 (fork函数:代码段、数据段、堆栈段、PCB进程控制块的复制)。父进程调用fork函数,子进程也会调用,且返回值都会存入到自己的数据段中,我们通过一个程序来深刻理解两次返回。
#include #includeint 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; }

运行结果
Linux|Linux进程编程实践1——进程的基本概念、fork创建进程
文章图片

由结果可看出,成功创建一个子进程,对于父进程来说返回子进程ID:4451,对于子进程来说返回值为0

    推荐阅读