目录

OSTEP第1期: 进程

前言

本文是 OSTEP 学习笔记 的第0期,主要是介绍一下进程 Process的概念。

本系列文章列表:

什么是进程

进程(Process) 简单来说,就是一个运行中程序的实例。 它是操作系统分配资源的基本单位, 也是程序执行的基本单位。

进程实际上是操作系统对于程序运行的抽象。 它包含了程序的代码、数据、状态信息等, 并且拥有自己的地址空间、文件描述符等资源。

地址空间 Address Space

每个进程都有自己的地址空间(Address Space),他是一片内存地址的集合。 地址空间通常包括以下几个部分:

  • 代码段(Text Segment):存放程序的机器指令。
  • 数据段(Data Segment):存放已初始化的全局变量和静态变量。
  • (Stack Segment):存放函数调用时的局部变量和返回地址。
  • (Heap Segment):用于动态内存分配。

进程状态

在进程的执行生命周期中,进程会经历一系列离散的状态。 这些状态就被称为进程状态(Process State),他是对进程当前时刻行为的描述。

常见的进程状态包括:

  • 新建(New/Initial):这个进程刚刚被创建(在真实系统中,我们往往不会看到这个状态)。
  • 运行(Running):进程正在使用CPU执行指令。
  • 阻塞(Waiting/Blocked):进程等待某个事件(如I/O操作完成)发生。
  • 就绪(Ready):进程已经准备好运行,但还没有被调度器选中。
  • 终止(Terminated/Final/Zombie):进程已经完成但还没有被操作系统清理。

进程状态转换/生命周期

  1. 新建到就绪:当进程被创建后,进入就绪状态。
  2. 就绪到运行:调度器选择一个就绪进程运行。
  3. 运行到阻塞:进程等待某个事件(如I/O操作)时进入阻塞状态。
  4. 阻塞到就绪:等待的事件发生后,进程进入就绪状态。
  5. 运行到就绪:进程被抢占或自愿放弃CPU时进入就绪状态。
  6. 运行到终止:进程完成执行后进入终止状态。

进程控制块 Process Control Block (PCB)

为了管理进程,操作系统为每个进程维护 一个进程控制块(Process Control Block, PCB), 也称为进程描述符(Process Descriptor)。

PCB 通常包含以下信息:

  • 进程标识符(Process ID, PID):唯一标识进程的数字。
  • 进程状态(Process State):如运行、就绪、阻塞等。
  • 程序计数器(Program Counter, PC):指向下一条要执行的指令。
  • 寄存器上下文(Register Context):(在切换离开该进程前)对当前寄存器内容的一个快照(snapshot)。
  • 调度信息(Scheduling Information): 如进程优先级,调度队列的指针。
  • 凭据(Credentials):决定了进程能访问的资源。
  • 内存管理信息(Memory Management Information):分配给进程的内存区域相关信息。
  • 统计信息(Accounting Information):CPU使用时间统计,时间限制。
  • 父进程指针子进程列表资源列表等。

进程表 Process Table

操作系统维护一个进程表(Process Table), 它是一个包含所有PCB的数组或链表。 里面维护了所有当前进程的PID和PCB的映射关系。

同时,操作系统维护了几个列表结构 如就绪列表(Ready List)阻塞列表(Blocked List), 用于存储处于不同状态的进程。

查看进程信息 (ps/pstree)

在类Unix系统中,可以使用 ps 命令查看当前系统中的进程信息。

$ ps [options]
  PID TTY          TIME CMD
20940 pts/4    00:00:00 bash
30812 pts/4    00:00:00 ps
一些实用选项

-e: 显示所有进程,包括其他用户的进程。

-f: 全格式显示,包含更多信息,如PPID(父进程ID)、UID

w: 宽输出格式,显示完整的命令行。

f: 显示进程树结构。

也可以使用 pstree 命令以树状结构显示进程的层级关系。

$ pstree
systemd─┬─2*[agetty]
        └─cron

对进程的操作 Operations on Processes

操作系统为进程管理提供了一些基本的服务,包括:

  • 创建进程
  • 终止进程
  • 挂起/恢复进程
  • 修改进程优先级
  • 等待进程(如父进程等待子进程结束)
  • 获取进程状态
  • 进程间通信(IPC)

创建进程 Create Process

一个进程可以通过系统调用创建另一个进程。 此时,创建新进程的进程称为父进程(Parent Process), 被创建的进程称为子进程(Child Process)(他也可以创建其他子进程,实现进程树)。 当父进程被终止时,操作系统通常会保持子进程继续运行,并允许其独立于父进程存在。

具体流程如下:

  • 操作系统为新进程分配一个唯一PID和相应的内存。
  • 初始化PCB,保存PID PPID, 设置 PC, SP等寄存器。
  • 将新进程添加到进程表和对应队列中。
  • 构造其他必要的资源和状态(如文件,内存等)。
  • 把新进程置于就绪状态,等待调度。

实际操作方法:

在 Unix 系统中,通常使用 fork()exec() 系统调用来创建新进程。

  • fork():通过复制当前进程来创建一个新进程。
  • exec():用一个新程序替换当前进程的内存空间。

在 Windows API 中,通常使用 CreateProcess() 函数来创建新进程。 CreateProcess() 函数允许指定新进程的可执行文件、命令行参数、环境变量等,相当于 fork()exec() 的结合。

fork()代码示例:

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

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) { // fork failed and exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid:%d)\n", (int)getpid());
    } else {
        int wc = wait(NULL); // parent waits for child
        printf("hello, I am parent of %d (pid: %d) \n", rc, (int)getpid());
    }
    return 0;
}

exec()代码示例:

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

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) { // fork failed and exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child process
        printf("hello, I am child (pid:%d)\n", (int)getpid());
        char *args[3];
        args[0] = strdup("wc");         // PROG: WC
        args[1] = strdup("proc3.c");    // PARAM: FILE
        args[2] = NULL;                 // END OF ARRAY
        execvp(args[0], args);
        printf("this shouldn't print out");
    } else { // parent process
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc: %d) (pid: %d) \n", rc, wc, (int)getpid());
    }
    return 0;
}

参考资料