# 进程

# 程序

  • 程序(Program)是一组可以被 CPU 执行的计算机指令。
    • 比如在 Windows 系统上双击启动一个软件、在 Linux 系统上执行一条命令,都是启动一个程序。
  • 程序被 CPU 执行时,主要分为以下几个阶段:
    • 启动 :开始执行。
    • 运行 :正在执行。
      • CPU 不一定会一直执行同一个程序,可能暂停执行它,转去执行其它程序,一段时间后再回来继续执行它。
    • 结束 :终止执行(不是暂停执行),并返回一个退出码。也称为程序退出、终止。
  • 程序终止的几种情况:
    • 执行完所有指令,正常退出。此时退出码为 0 。
    • 调用 exit() 函数,提前退出。此时退出码由程序决定。
    • 接收到 SIGTERM 等信号,被杀死。
  • 操作系统一般要求程序结束执行时返回一个整数值,称为退出码(Exit Code)、返回码(Return Code),用于表示程序的执行结果。
    • Shell 中程序退出码的取值范围为 0~255 ,通常取值为 0 时表示正常退出,取值为非 0 时表示异常退出。

# 进程

  • 进程(Process)是程序运行、分配系统资源的最小单位。
    • 启动一个程序时,至少要创建一个进程,来执行程序指令。
    • 当进程执行程序指令时,实际上是由进程中的线程,在 CPU 中执行。
  • 每个进程(称为父进程)可以创建任意个其它进程(称为子进程)。
    • 父进程及其所有子进程属于同一个进程组。
    • 当父进程终止时,Linux 并不会自动杀死其子进程。如果子进程依然运行,则称为孤儿进程(Orphan Process)。
      • 出现孤儿进程时,Linux 会将它的 PPID 改为 1 ,成为 init 进程的子进程,被 init 进程管理。

# 进程组

:Process Group ,包含一个进程,或多个具有父子关系的进程。

  • 每个进程组中有且仅有一个 Leader 进程,是其它进程的父进程。

# 进程会话

:Process Session ,包含一个进程组,或多个具有父子关系的进程组。

  • 会话中的进程组又称为 job ,用于完成某种任务。
  • 一个 Session 中有且仅有一个 Leader 进程,是其它进程、进程组的根父进程。
    • 当 Session Leader 终止时,系统会给该 Session 的所有进程发送 SIGHUP 信号来终止它们。当 Session 中的所有进程都终止时,系统就会删除该 Session 。
  • 例如:用户登录时会创建一个 login shell ,还会创建一个 Session ,以 login shell 作为 Session Leader 。
    • 在该 Session 中,只有一个进程组能工作在前台,其它进程组都只能工作在后台。
    • 当用户登出时,属于该 Session 的所有进程组都会被系统终止。
  • 相关函数:
    #include <unistd.h>
    
    pid_t setsid(void);
        // 创建一个新的进程会话,然后返回其 SID
        // 创建的新会话中,由当前进程担任 Group Leader 和 Session Leader
        // 如果当前进程本来就是当前进程组的 Group Leader ,则不允许创建,避免与当前进程组的其它进程处于不同的会话中
    

# 守护进程

  • 前台进程
    • :绑定到当前终端的 stdin ,因此会阻塞当前终端。
    • 普通方式启动的进程默认会绑定到当前终端的 stdin、stdout、stderr 。
  • 后台进程
    • :没有绑定到终端的 stdin ,但可能绑定了 stdout、stderr 。
    • 前台进程、后台进程都是当前终端的子进程。如果用户关闭当前终端,系统就会给这些进程发送 SIGHUP 信号,终止它们。
  • 守护进程(daemon)
    • :一种特殊的后台进程。运行在一个独立的 Process Session 中。因此当用户关闭当前终端之后,也会继续运行。
    • 系统服务程序通常以守护进程的方式运行。

# 僵尸进程

  • 类 Unix 系统中,进程退出的流程:

    1. 一个进程终止运行(包括主动退出、被杀死的情况)。
    2. 内核回收该进程占用的内存、文件等资源,并向其父进程发送 SIGCHLD 信号,通知它有一个子进程退出了。
    3. 父进程调用 wait() 或 waitpid() 获取该进程的退出状态。
    4. 内核在进程表中删除该进程,删除其 /proc/<PID>/ 目录,使该进程完全消失。
  • 进程在终止运行之后、完全消失之前,状态为 terminated ,称为僵尸进程(Zombie Process)。

    • 如果僵尸进程越来越多,可能占用内核的所有 PID ,导致不能创建新进程。
    • 僵尸进程不能被 kill 命令杀死,因为它已经终止运行了。
  • Windows 系统中,进程退出时会被立即回收、删除,因此不会产生僵尸进程。

  • init 进程会不断调用 wait() 获取其子进程的退出状态,避免产生僵尸进程。

    • 因此,孤儿进程没有危害,会被 init 进程管理,不会变成僵尸进程。
  • 避免产生僵尸进程的几种措施:

    • 子进程终止时通知父进程,让父进程立即回收它。
      • 比如向父进程发送 SIGCHLD 信号,不过一般的进程会忽略该信号。
    • 父进程不断轮询子进程是否终止。
    • 终止父进程,让子进程变为孤儿进程,从而被 init 进程管理。
      • 比如先让当前进程 fork 一个子进程,再让子进程 fork 一个孙进程,然后立即回收子进程。这样孙进程就变为了孤儿进程。
  • 例:

    1. 在 Python 解释器中执行 p = subprocess.Popen('sleep 10', shell=True) 创建子进程。
    2. 执行 ps 命令查看:
      29457 root      20   0  147.2m   8.8m   3.4m S   0.0  0.1   0:00.09          `- python3
      29860 root      20   0  105.5m   0.3m   0.3m S   0.0  0.0   0:00.00              `- sleep 10
      
    3. 当子进程执行完之后,就会变成僵尸进程:
      29457 root      20   0  147.2m   8.8m   3.4m S   0.0  0.1   0:00.09          `- python3
      29860 root      20   0    0.0m   0.0m   0.0m Z   0.0  0.0   0:00.00              `- [sleep] <defunct>
      
    4. 在 Python 解释器中执行 p.wait() ,僵尸进程就会消失。
      • 另外,创建新的子进程时,Python 会自动清理所有僵尸态的子进程。

# 进程属性

# 标识符

Linux 系统会给每个进程、线程分配一个标识符(ID),包括:

  • PID :进程的 ID ,在整个系统中唯一。
  • PPID :父进程的 ID 。
  • TID :线程的 ID ,在其线程组中唯一。
  • PGID :进程组(Process Group)的 ID ,等于其主进程的 ID 。
  • TGID :线程组(Thread Group)的 ID ,等于其主线程的 ID 。
  • SID :Process Session 的 ID ,等于其主进程的 PID 。

# 进程类型

  • s :该进程是 Session Leader 。
  • + :该进程属于前台进程。
  • < :high-priority (not nice to other users)。
  • N :low-priority (nice to other users)。
  • L :已锁定内存中的 page 。
  • l :是多线程的。

# 运行状态

  • R :Running
    • 进程处于 Running 状态时才会占用 CPU 。
  • S :Sleeping
    • 此时进程处于可中断的睡眠状态,被 CPU 挂起,等到某一时刻或满足某些条件时再继续运
    • 例如,HTTP 服务器通常一直处于 Sleeping 状态,收到 HTTP 请求时才有一瞬间切换到 Running 状态。
  • D :Disk Sleep
    • 此时进程处于不可中断的睡眠状态,不会响应异步信号,因此不能被 kill -9 杀死。
    • 例如,进程等待磁盘 IO 时,会进入短暂的 D 状态。
  • I :Idle ,空闲状态。
  • Z :Zombie ,僵尸进程。
  • T :Stopped ,暂停状态。
  • t :Traced ,比如进程被断点调试时处于被跟踪状态。
  • X :进程正在终止,这是一个很短暂的状态。

# 进程间通信

Linux 系统上,进程间通信(Inter Process Communication ,IPC)的主要方式如下:

# 信号

  • 信号(signal)可能来源于硬件(比如键盘信号),也可能来源于软件(比如用 kill 命令)。信号产生后,会被内核发送给进程,相当于软件层的模拟中断。

  • Linux 定义了多种信号,常见的如下:

    编号 宏定义名 默认动作
    1 SIGHUP 终止进程
    2 SIGINT 终止进程
    3 SIGQUIT 终止进程
    9 SIGKILL 终止进程
    10 SIGUSR1 终止进程
    12 SIGUSR2 终止进程
    15 SIGTERM 终止进程
    17 SIGCHLD 表示子进程终止,默认无动作
    19 SIGSTOP 暂停进程
    • 在不同的平台上,信号的编号可能有差异,因此最好通过宏定义名来指定信号。
    • SIGINT 通常由键盘中断(Ctrl+C)引发。
    • 向某个进程发出一个普通的终止信号时,进程可能立即终止,也可能做完清理工作之后再终止(比如释放占用的资源),甚至不终止。
    • SIGKILL、SIGSTOP 两种信号不能被进程忽略或捕捉,因此一定会立即执行。
    • SIGUSR1、SIGUSR2 两种信号常用于被用户绑定自定义的信号处理函数。
      • SIGUSR1 的默认动作是终止进程,不过 Apache、Nginx 等很多程序收到 SIGUSR1 信号之后会进行复位操作,比如刷新缓存、重新加载配置文件、重新打开日志文件,接近于重启进程。
  • 相关 API :

    #include <signal.h>
    
    int kill(pid_t pid, int sig);   // 向进程发送信号,发送成功则返回 0
    
    • pid
      • 如果为 n > 0 ,则选中 PID 等于 n 的进程。
      • 如果为 0 ,则选中当前进程组的所有进程。
      • 如果为 -1 ,则选中当前进程有权限发送信号的所有进程。
      • 如果为 -n < -1 ,则选中 PGID 等于 n 的进程组中的所有进程。
      • 特别地,内核只允许将已注册 handler 的信号发送给 PID 为 1 的进程(通常是 init、systemd 等),会忽略 SIGKILL、SIGSTOP 等信号,避免主机宕机。不过此时调用 kill() 依然会返回 0 。
        • 因此,在主机上执行命令 kill -9 1 不会杀死 1 号进程。
    • sig
      • 可以填 int 值,也可以填 SIGTERM 等宏定义名。
      • 如果为 0 ,则不发送信号,但依然会检测目标进程是否存在、是否有权限发送信号。
  • 当进程收到一个信号时,有三种处理方式:

    • 执行信号的默认动作
    • 忽略信号
    • 捕捉信号:进程将自己的信号处理函数传给内核,与一个信号绑定。当该信号发生时,内核就会执行该函数,从而实现该进程自定义的动作。如下:
      #include <stdio.h>
      #include <signal.h>
      
      static void sig_handle(int sig_no)              // 定义信号处理函数
      {
          if(sig_no == SIGUSR1)
              printf("Received SIGUSR1\n");
          else
              printf("Received signal %d\n", sig_no);
      }
      
      int main(void){
          if(signal(SIGUSR1, sig_handle) == SIG_ERR)  // 绑定信号处理函数
              printf("Can not catch SIGUSR1\n");      // 如果不能绑定即不能捕捉,则报错
          return 0;
      }
      

# 信号量

  • :semophore ,一个非负整数,用于记录某个资源的可用数量。为 0 时表示资源不可用。

# 套接字

# 管道文件

  • 进程可以创建一个管道文件(pipe),和另一个进程同时连接到它,从中读写数据。
  • 采用半双工通信,当一个进程写数据时,另一个进程只能读数据。
  • 匿名管道(PIPE):保存在内存中,没有文件描述符,只能用于父子进程之间的通信。比如管道符 | 。
  • 命名管道(FIFO):保存为文件系统中的一个文件,常用于两个独立进程之间的通信。

# 消息队列

  • :message queue ,一个链表结构,允许多个进程从中读写数据。

# 共享内存

  • :shared memory ,一块内存空间,允许被多个进程同时访问。

# 相关 API

# PCB

  • Linux 内核会在内存中保存一个进程表(process table),记录当前存在的所有进程,包括其 PID、PCB 。

    • 用户可以通过 /proc/<PID>/ 目录获取进程的 PCB 信息。
  • 每个进程用一个 task_struct 结构体记录其信息,称为进程控制块(Process Control Block ,PCB)、进程描述符。如下:

    struct task_struct
    {
        volatile long state;              // 进程的运行状态
        void *stack;                      // 进程的内核栈
        atomic_t usage;                   // 进程描述符的使用计数
        unsigned int flags;               // 进程的状态标志
    
        int prio;                         // 进程的 CPU 调度优先级
        unsigned int policy;              // 进程的 CPU 调度策略
        cpumask_t cpus_allowed;           // 进程可以在 CPU 的哪些核上执行
    
        int exit_state;                   // 进程的退出码
        pid_t pid;                        // 进程的标识符
    
        struct task_struct *parent;       // 一个指针,指向父进程
        struct task_struct *group_leader; // 一个指针,指向当前进程组的主进程
        struct list_head children;        // 一个链表,其中每个元素表示一个子进程
    
        cputime_t utime, stime;           // 进程占用的用户态、内核态 CPU 时长
        struct timespec start_time;       // 进程的启动时刻
    
        struct mm_struct *mm;             // 记录进程的虚拟内存空间,比如 code、data 区域的起始地址、结束地址
        struct mm_struct *active_mm;
    
        ...
    }
    
    • 关于 task->mm :
      • 同一个进程的多个线程,共用一个虚拟内存空间,因此 task->mm 取值相同。
      • 内核线程没有虚拟内存空间,因此 task->mm 取值为 null 。而 task->active_mm 会用于记录最近运行的一个用户态进程的 task->mm ,从而能访问其中的数据。
  • 用户可以给进程设置 Nice 优先级,而内核运行进程时会转换成 Priority 优先级。

    • Nice
      • 取值范围为 -20~19 ,取值越小表示优先级越高。默认为 0 。
      • Nice 也表示友好度。如果一个进程增加其 Nice ,就是降低优先级,对其它进程更友好。
    • Priority
      • 取值范围为 -100~39 ,取值越小表示优先级越高。
      • 对于普通进程,其 Priority = Nice + 20 ,取值范围为 0~39 。
      • 对于 RT 类型的进程,其 Priority = -1 - rt_prior ,取值范围为 -100~-1 。
        • rt_prior 取值范围为 0~99 ,取值越大,优先级越高。
  • 进程的 CPU 调度策略分为多种:

    SCHED_OTHER   # 对于多个进程尽量平均分配时间,因此占用 CPU 时间较短的进程会被优先调度
    SCHED_BATCH   # 批处理
    SCHED_IDLE    # 运行低优先级的后台 job
    SCHED_FIFO    # 按先来后到的顺序执行进程,直到进程主动释放 CPU ,或被更高优先级的进程抢占 CPU
    SCHED_RR      # 与 SCHED_FIFO 类似,但是给每个进程分配时间片,如果耗尽,则轮到相同优先级的其它进程
    
    • Linux v2.6 开始,默认采用 CFS(Completely Fair Scheduler)作为 CPU 调度算法。
    • CPU 调度时,是以线程为单位。
    • SCHED_FIFO、SCHED_RR 采用实时(Real Time ,RT)调度类,对应的进程称为 RT 类型。

# 创建进程

  • 关于创建进程:

    #include <unistd.h>
    
    pid_t fork(void);
        // 拷贝当前进程,创建一个子进程
        // 如果创建成功,则在父进程中返回子进程的 PID ,在子进程中返回 0
        // 如果创建失败,则返回 -1
    
    • 调用 fork() 创建的子进程,与父进程几乎完全相同,比如:
      • 拷贝父进程的虚拟内存空间。
      • 拷贝父进程打开的文件描述符。
    • 调用 fork() 创建的子进程,是一个独立的新进程,与父进程存在少量差异,比如:
      • 拥有不同的 PID 。
      • 将资源使用率、CPU 使用时长重置为零,重新计算。
      • 将待处理的信号集清空。
  • 关于执行程序:

    #include <unistd.h>
    
    int execve(const char *pathname, char *const argv[], char *const envp[]);
        // 执行 pathname 对应的二进制文件,并传入参数 argv 、环境变量 envp
    
    • 调用 execve() 时,是执行另一个程序,替换当前程序。
      • 这会覆盖当前进程的数据段、堆栈。不过进程的 PID 不变,已经打开的文件描述符会保留。
    • 例如:在终端执行 ls -l 命令时,是先调用 fork() 创建子进程,然后子进程再执行 execve("/usr/bin/ls", ["ls", "-l"], 0x7ffc0e3d0910 /* 29 vars */)

# 终止进程

  • 关于终止进程:

    #include <stdlib.h>
    
    void exit(int status);
        // 使当前进程退出,且退出码取值为 status
    
  • 关于等待进程退出:

    #include <sys/wait.h>
    
    pid_t wait(int *status);
      // 阻塞当前线程,直到任意一个子进程退出,然后返回其 PID
      // 如果没有子进程,或执行失败,则返回 -1
    
    pid_t waitpid(pid_t pid, int *status, int options);
      // 阻塞当前线程,直到指定 PID 的子进程的运行状态改变(默认是等待变为 terminated 状态)
    
    • pid
      • 如果为 n > 0 ,则等待 PID 等于 n 的子进程。
      • 如果为 0 ,则等待当前进程的任一子进程。
      • 如果为 -1 ,则等待任一子进程。
      • 如果为 -n < -1 ,则等待 PGID 等于 n 的进程组中的任一子进程。
    • 例:
      wait(NULL);               // 相当于 waitpid(-1, NULL, 0);
      while (wait(NULL) > 0);   // 阻塞当前线程,直到所有子进程都退出
      
    • 例:
      #include <stdio.h>
      #include <unistd.h>
      #include <sys/wait.h>
      
      int main(){
          pid_t pid;
          pid = fork();         // 此时拷贝了一个子进程,往下的代码是主进程、父进程同时各执行一份
          if (pid < 0)
              printf("Failed to fork");
          else if (pid == 0)
              printf("This is child process: [%d] . Its parent process is [%d] \n", getpid(), getppid());
          else{
              printf("This is parent process: [%d] . It just created a child process: [%d] \n", getpid(), pid);
              pid = wait(NULL); // 等待子进程终止
              printf("The child process [%d] has terminated.\n", getpid(), getppid(), pid);
          }
          return 0;
      }
      
      运行结果:
      This is parent process: [9961] . It just created a child process: [9962]
      This is child process: [9962] . Its parent process is [9961]
      The child process [9961] has terminated.