程序与进程

  • 简单地说,程序就是为了完成某种任务而设计的软件,进程就是运行中的程序。

  • 程序只是一些列指令的集合,是一个静止的实体,而进程则有以下特性:

    • 动态性:进程的实质是一次程序执行的过程,有创建、撤销等状态的变化。而程序是一个静态的实体。
    • 并发性:进程可以做到在一个时间段内,有多个程序在运行中。程序只是静态的实体,所以不存在并发性。
    • 独立性:进程可以独立分配资源,独立接受调度,独立地运行。
    • 异步性:进程以不可预知的速度向前推进。
    • 结构性:进程拥有代码段、数据段、PCB(进程控制块,进程存在的唯一标志)。也正是因为有结构性,进程才可以做到独立地运行。
  • 而随着程序的发展越做越大,又会继续细分,从而引入了线程的概念,当代多数操作系统、Linux 2.6及更新的版本中,进程本身不是基本运行单位,而是线程的容器。

    线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。因为线程中几乎不包含系统资源,所以执行更快、更有效率。

  • 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

进程的属性

进程的分类

按功能和服务对象

  • 从这个角度看,可以将进程分为用户进程与系统进程
    • 用户进程:通过执行用户程序、应用程序或称之为内核之外的系统程序而产生的进程,此类进程可以在用户的控制下运行或关闭。
    • 系统进程:通过执行系统内核程序而产生的进程,比如可以执行内存资源分配和进程切换等相对底层的工作;而且该进程的运行不受用户的干预,即使是 root 用户也不能干预系统进程的运行。

按应用程序的服务类型

  • 从这个角度看,可以将进程分为交互进程、批处理进程、守护进程
    • 交互进程:由一个 shell 终端启动的进程,在执行过程中,需要与用户进行交互操作,可以运行于前台,也可以运行在后台。
    • 批处理进程:该进程是一个进程集合,负责按顺序启动其他的进程。
    • 守护进程:守护进程是一直运行的一种进程,在 Linux 系统启动时启动,在系统关闭时终止。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。

进程的衍生

父进程与子进程

  • 子进程是由父进程开启的,比如在终端中输入bash再启动一个终端,那么新打开的终端则称为子进程,前者则为父进程。关于父进程与子进程便会涉及这两个系统调用 fork()exec()

    • fork():主要作用是为当前的进程创建一个新的进程,这个新的进程就是它的子进程,这个子进程除了父进程的返回值和 PID 以外其他的都一模一样,如进程的执行代码段,内存信息,文件描述,寄存器状态等等
    • exec():作用是切换子进程中的执行程序,也就是替换其从父进程复制过来的代码段与数据段
  • 子进程在退出前主函数main()会执行exit(n)或者return n返回一个信号值,系统会把这个 SIGCHLD 信号传给其父进程,当然若是异常终止也往往是因为这个信号。

僵尸进程

  • 正常情况下,父进程会收到两个返回值:exit code(SIGCHLD 信号)与 reason for termination 。之后,父进程会使用 wait(&status) 系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的 PCB;而如若父进程没有这么做的话,子进程的 PCB 就会一直驻留在内存中,一直留在系统中成为僵尸进程(Zombie)。
  • 虽然僵尸进程是已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,在进程列表中保留一个位置,记载该进程的退出状态等信息供其父进程收集,从而释放它。但是 Linux 系统中能使用的 PID 是有限的,如果系统中存在有大量的僵尸进程,系统将会因为没有可用的 PID 从而导致不能产生新的进程。

孤儿进程

  • 另外如果父进程结束(非正常的结束),未能及时收回子进程,子进程仍在运行,这样的子进程称之为孤儿进程。在 Linux 系统中,孤儿进程一般会被 init 进程所“收养”,成为 init 的子进程。由 init 来做善后处理,所以它并不至于像僵尸进程那样无人问津,不管不顾,大量存在会有危害。

进程0(内核初始化进程)

  • 进程 0 是系统引导时创建的一个特殊进程,也称之为内核初始化,其最后一个动作就是调用 fork() 创建出一个子进程运行 /sbin/init 可执行文件,而该进程就是 PID=1 的进程 1,而进程 0 就转为交换进程(也被称为空闲进程),进程 1 (init 进程)是第一个用户态的进程,再由它不断调用 fork() 来创建系统里其他的进程,所以它是所有进程的父进程或者祖先进程。同时它是一个守护程序,直到计算机关机才会停止。

进程组与Session

  • 每一个进程都会是一个进程组的成员,而且这个进程组是唯一存在的,他们是依靠 PGID来区别的,而每当一个进程被创建的时候,它便会成为其父进程所在组中的一员。
  • 一般情况,进程组的 PGID 等同于进程组的第一个成员的 PID,并且这样的进程称为该进程组的领导者,也就是领导进程,进程一般通过使用 getpgrp() 系统调用来寻找其所在组的 PGID,领导进程可以先终结,此时进程组依然存在,并持有相同的PGID,直到进程组中最后一个进程终结。
  • 与进程组类似,每当一个进程被创建的时候,它便会成为其父进程所在 Session 中的一员,每一个进程组都会在一个 Session 中,并且这个 Session 是唯一存在的,Session 中的每个进程都称为一个工作(job)。

工作管理

  • 并且每个终端或者说 bash 只能管理当前终端中的 job,不能管理其他终端中的 job。

  • 结束前台进程可以使用Ctrl+C,但无法结束后台进程。

  • 如果使用&符号,进程则会在后台中运行,查看运行中的进程可以使用ls &,得到序号如[1] 204分别是该 job 的 job number 与该进程的 PID,而最后一行的 Done 表示该命令已经在后台执行完毕。

    image-20231201224354846

  • 如果希望暂停当前工作并存放到后台可以使用Ctrl+Z

    image-20231201224613524

  • 被放置到后台的工作可以使用jobs查看。

    image-20231201224819667

    其中第一列显示的为被放置后台 job 的编号,而第二列的 + 表示最近(刚刚、最后)被放置后台的 job,同时也表示预设的工作,也就是若是有什么针对后台 job 的操作,首先对预设的 job,- 表示倒数第二(也就是在预设之前的一个)被放置后台的工作,倒数第三个(再之前的)以后都不会有这样的符号修饰,第三列表示它们的状态,而最后一列表示该进程执行的命令。

  • 如果希望将后台的工作拿到前台继续进行,可以使用fg命令:

    1
    2
    3
    #后面不加参数提取预设工作,加参数提取指定工作的编号
    #ubuntu 在 zsh 中需要 %,在 bash 中不需要 %
    fg [%jobnumber]

    image-20231201225144514

  • 而如果希望其在后台继续而不拿到前台,可以使用bg命令:

    1
    2
    #与fg类似,加参则指定,不加参则取预设
    bg [%jobnumber]

    image-20231201225553266

  • 如果希望删除后台的工作,可以使用kill命令结束进程:

    1
    2
    3
    4
    5
    #kill的使用格式如下
    kill -signal %jobnumber

    #signal从1-64个信号值可以选择,可以这样查看
    kill -l

    其中常用的有这些信号值

信号值 作用
-1 重新读取参数运行,类似与restart
-2 如同 ctrl+c 的操作退出
-9 强制终止该任务
-15 正常的方式终止该任务