MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

进程的创建、执行与终止过程详解

2021-12-284.5k 阅读

进程的创建

在操作系统中,进程创建是一个关键且复杂的过程。进程是程序在计算机中的一次执行实例,而创建进程则是启动这个执行实例的初始步骤。

1. 创建进程的原因

  • 并发执行需求:现代操作系统通常支持多个程序同时运行,以提高系统资源利用率和用户体验。例如,用户在使用计算机时,可能同时打开浏览器浏览网页、运行音乐播放器播放音乐,这些不同的程序都需要以进程的形式并发执行。
  • 任务分解:对于一些复杂的任务,将其分解为多个进程有助于简化编程和提高系统的可维护性。比如,一个大型的服务器应用程序可能会将数据处理、网络通信等功能分别交给不同的进程来完成。

2. 创建进程的步骤

  • 申请空白PCB:进程控制块(Process Control Block,PCB)是操作系统用于管理进程的核心数据结构,它包含了进程的各种信息,如进程标识符、状态、优先级、程序计数器、内存指针等。在创建进程时,首先需要为新进程在内存中申请一块空白的 PCB 空间,用于存储该进程的相关信息。
  • 为进程分配资源:新进程运行需要占用系统资源,如内存空间、I/O 设备等。操作系统需要为其分配相应的资源。例如,在内存管理方面,要为进程分配一定大小的内存区域,用于存放进程的代码、数据以及运行时的栈空间等。如果进程需要使用 I/O 设备,如打印机、磁盘等,操作系统也要进行相应的设备分配。
  • 初始化 PCB:空白的 PCB 申请完成后,需要对其进行初始化。
    • 进程标识符:为进程分配一个唯一的标识符,通常是一个整数,用于在系统中唯一标识该进程。这个标识符在进程的整个生命周期内保持不变,操作系统通过它来对进程进行各种操作,如调度、终止等。
    • 进程状态:初始状态通常设置为“就绪”状态,表示进程已经准备好运行,只要获得 CPU 资源就可以立即执行。
    • 优先级:根据进程的任务类型和需求,为其设置一个优先级。高优先级的进程在调度时会有更大的机会获得 CPU 资源,优先执行。例如,系统关键进程(如内存管理进程、中断处理进程等)通常具有较高的优先级,而一些用户应用进程可能优先级相对较低。
    • 程序计数器:设置程序计数器(PC)的值,它指向进程要执行的第一条指令的地址。在进程执行过程中,PC 会随着指令的执行不断更新,指向下一条要执行的指令。
    • 内存指针:将 PCB 中的内存指针指向为该进程分配的内存区域,以便进程能够正确访问其代码和数据。
  • 将新进程插入就绪队列:完成上述步骤后,新进程就具备了运行的条件。此时,操作系统会将该进程插入到就绪队列中,等待调度程序的调度,获取 CPU 资源从而开始执行。

3. 创建进程的系统调用

在不同的操作系统中,提供了不同的系统调用来创建进程。以 Unix/Linux 系统为例,常用的创建进程的系统调用是 fork()

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

int main() {
    pid_t pid;
    // 使用 fork() 创建新进程
    pid = fork();

    if (pid < 0) {
        // fork() 失败
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("This is the child process. PID: %d\n", getpid());
    } else {
        // 父进程
        printf("This is the parent process. PID: %d, Child PID: %d\n", getpid(), pid);
    }

    return 0;
}

在上述代码中,fork() 函数会创建一个新的进程(子进程),它与调用它的进程(父进程)几乎完全相同。fork() 函数返回两次,一次在父进程中,返回值是子进程的进程标识符(PID);另一次在子进程中,返回值为 0。通过判断 fork() 的返回值,可以区分父进程和子进程,并执行不同的代码逻辑。

在 Windows 系统中,创建进程的主要函数是 CreateProcess()。它的参数较多,需要指定可执行文件的路径、命令行参数、进程的安全属性等详细信息。以下是一个简单的示例:

#include <windows.h>
#include <stdio.h>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    // 创建新进程
    if (!CreateProcess(
        TEXT("C:\\Windows\\System32\\notepad.exe"),  // 可执行文件路径
        NULL,                                     // 命令行参数
        NULL,                                     // 进程安全属性
        NULL,                                     // 线程安全属性
        FALSE,                                    // 是否继承句柄
        0,                                        // 创建标志
        NULL,                                     // 环境变量
        NULL,                                     // 当前目录
        &si,                                      // 启动信息
        &pi))                                     // 进程信息
    {
        printf("CreateProcess failed (%d).\n", GetLastError());
        return 1;
    }

    // 等待子进程结束
    WaitForSingleObject(pi.hProcess, INFINITE);

    // 关闭进程和线程句柄
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

在这个 Windows 示例中,CreateProcess() 函数用于创建一个新的进程,这里创建的是记事本程序(notepad.exe)。STARTUPINFO 结构体用于指定进程的启动信息,PROCESS_INFORMATION 结构体用于返回新进程的相关信息,如进程句柄、线程句柄等。

进程的执行

进程创建完成并进入就绪队列后,等待调度程序的调度,获得 CPU 资源后便开始执行。

1. 进程调度

进程调度是操作系统核心功能之一,它决定了哪个进程能够获得 CPU 资源并执行。常见的进程调度算法有以下几种:

  • 先来先服务(FCFS):按照进程进入就绪队列的先后顺序进行调度,先进入队列的进程先获得 CPU 资源。这种算法实现简单,但对于短进程不利,可能导致长进程长时间占用 CPU,使短进程等待时间过长。
  • 短作业优先(SJF):优先调度预计执行时间最短的进程。该算法可以提高系统的吞吐量,但需要预先知道每个进程的执行时间,这在实际应用中往往难以做到。
  • 优先级调度:为每个进程分配一个优先级,调度时优先选择优先级最高的进程。优先级可以根据进程的类型(如系统进程优先级高于用户进程)、任务紧急程度等因素来确定。不过,如果高优先级进程持续不断地进入系统,可能会导致低优先级进程长时间得不到执行,出现“饥饿”现象。
  • 时间片轮转调度:将 CPU 的时间划分成一个个固定长度的时间片,每个进程轮流获得一个时间片的 CPU 使用权。当时间片用完后,无论进程是否执行完毕,都会被调度程序暂停,重新回到就绪队列,等待下一次调度。这种算法保证了每个进程都能在一定时间内获得执行机会,适用于分时系统,能提供较好的交互性。

2. 进程上下文切换

当调度程序决定切换到另一个进程执行时,就会发生进程上下文切换。进程上下文包括进程的 PCB 中的所有信息,以及 CPU 寄存器的当前值等。上下文切换的过程如下:

  • 保存当前进程上下文:当调度程序决定切换进程时,首先要保存当前正在执行进程的上下文。这包括将 CPU 寄存器(如程序计数器、通用寄存器等)的值保存到该进程的 PCB 中,以便将来该进程重新获得 CPU 资源时能够恢复到切换前的执行状态。
  • 更新 PCB 信息:将当前进程的状态从“运行”状态更新为其他状态(如“就绪”状态或“阻塞”状态,取决于进程的具体情况),并将其插入到相应的队列中。
  • 选择下一个进程:调度程序根据所采用的调度算法,从就绪队列中选择一个进程。
  • 恢复下一个进程上下文:从选中进程的 PCB 中读取保存的 CPU 寄存器值,恢复到 CPU 寄存器中,同时将程序计数器指向该进程上次暂停时的指令地址,使得该进程能够继续执行。

进程上下文切换是一个开销较大的操作,因为它涉及到内存读写(保存和恢复寄存器值)以及调度程序的运算等。频繁的上下文切换会降低系统的性能,因此操作系统在设计调度算法时,需要尽量减少不必要的上下文切换次数。

3. 进程执行的本质

从硬件层面来看,进程的执行就是 CPU 按照程序计数器(PC)的指示,从内存中读取指令并执行的过程。在进程执行过程中,CPU 不断地进行取指、译码、执行等操作,同时根据指令的要求访问内存中的数据,修改寄存器的值等。

例如,对于一个简单的加法运算指令 ADD R1, R2, R3(将寄存器 R2 和 R3 中的值相加,结果存放到寄存器 R1 中),CPU 首先从内存中读取该指令,然后对指令进行译码,识别出这是一条加法指令,并知道操作数所在的寄存器。接着,CPU 从寄存器 R2 和 R3 中读取数据,在运算器中进行加法运算,最后将结果存放到寄存器 R1 中。

从操作系统层面来看,进程的执行是在操作系统的管理和调度下进行的。操作系统为进程提供运行环境,包括分配资源、调度 CPU 等。同时,操作系统还需要处理进程之间的并发问题,如资源竞争、同步等,以确保系统的稳定性和正确性。

进程的终止

进程在完成其任务或者出现异常情况时,会终止运行。

1. 进程终止的原因

  • 正常完成:当进程执行完其所有的指令,达到程序的结束点时,会正常终止。例如,一个计算两个数之和的程序,在完成计算并输出结果后,就会正常结束进程。
  • 异常退出:进程在执行过程中遇到错误或异常情况,如除零错误、内存访问越界等,会导致进程异常终止。操作系统会检测到这些异常,并终止相应的进程,以防止错误扩散影响系统的其他部分。
  • 被其他进程终止:在某些情况下,一个进程可以请求操作系统终止另一个进程。比如,在一个多进程协作的系统中,如果某个进程出现故障,可能会影响整个系统的运行,此时其他进程可以通过系统调用请求操作系统终止该故障进程。

2. 进程终止的过程

  • 释放资源:当进程终止时,操作系统首先要回收该进程占用的所有资源。这包括内存空间、打开的文件、占用的 I/O 设备等。例如,进程在运行过程中动态分配了内存,终止时需要将这些内存归还给系统的内存管理模块,以便其他进程可以使用。对于打开的文件,操作系统会关闭文件描述符,释放相关的文件资源。
  • 从队列中移除:进程终止后,操作系统会将其从当前所在的队列(如就绪队列、阻塞队列等)中移除。这样,调度程序在进行调度时就不会再选择该已终止的进程。
  • 删除 PCB:最后,操作系统会删除该进程的进程控制块(PCB)。PCB 是操作系统管理进程的核心数据结构,进程终止后,不再需要它来记录进程的信息,因此将其从内存中删除,释放相关的内存空间。

3. 进程终止的系统调用

在 Unix/Linux 系统中,进程可以通过调用 exit() 函数来正常终止自身。exit() 函数接受一个整数参数,该参数作为进程的退出状态码,可以被父进程获取,用于判断子进程的执行情况。

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 执行一些操作
    printf("Process is running.\n");

    // 正常终止进程,退出状态码为 0
    exit(0);

    // 以下代码不会执行
    printf("This line will not be printed.\n");

    return 0;
}

在上述代码中,调用 exit(0) 后,进程会立即终止,并将退出状态码设置为 0。

在 Windows 系统中,进程可以通过调用 ExitProcess() 函数来终止自身。同样,该函数也接受一个参数作为退出状态码。

#include <windows.h>
#include <stdio.h>

int main() {
    // 执行一些操作
    printf("Process is running.\n");

    // 正常终止进程,退出状态码为 0
    ExitProcess(0);

    // 以下代码不会执行
    printf("This line will not be printed.\n");

    return 0;
}

此外,在 Unix/Linux 系统中,父进程可以使用 wait()waitpid() 系统调用来等待子进程的终止,并获取子进程的退出状态码。例如:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int status;

    pid = fork();

    if (pid < 0) {
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process is exiting with status 42.\n");
        exit(42);
    } else {
        // 父进程
        wait(&status);
        if (WIFEXITED(status)) {
            printf("Child exited with status: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

在这个示例中,父进程通过 wait(&status) 等待子进程终止,并通过 WIFEXITED(status) 判断子进程是否正常终止,通过 WEXITSTATUS(status) 获取子进程的退出状态码。

进程的创建、执行与终止是操作系统进程管理的核心内容。深入理解这些过程,对于编写高效、稳定的应用程序以及优化操作系统性能都具有重要意义。无论是在开发多进程应用程序,还是进行操作系统内核开发,都需要对进程的这些基本操作有清晰的认识和掌握。