Skip to content

进程

一、进程的抽象

1.1 进程的定义

进程是操作系统为运行中的程序提供的核心抽象,简单来说,进程就是运行中的程序。程序本身是静态的,存储在磁盘上的指令和数据,而操作系统通过执行这些指令,使程序具备生命周期,变成动态的进程。

1.2 关键问题:虚拟化CPU

操作系统通过虚拟化CPU,提供多个虚拟CPU的假象,让用户感觉可以同时运行多个程序。核心技术是时分共享(time sharing),即通过时间片轮换运行不同进程,实现并发。

  • 优势:允许多个进程看似同时运行,简化用户操作。
  • 代价:性能开销,因CPU共享,每个进程运行速度可能略慢。

1.3 进程的组成

进程的机器状态(machine state)定义了其运行时的核心组成部分:

  • 内存(地址空间):包括指令、数据、堆、栈等。
  • 寄存器:包括程序计数器(PC)、栈指针、帧指针等,用于指令执行和函数调用管理。
  • I/O信息:如打开的文件描述符,用于与持久存储设备交互。

1.4 机制与策略

操作系统实现进程抽象需要:

  • 机制(Mechanism):低级方法,如上下文切换,用于暂停一个进程并运行另一个。
  • 策略(Policy):高级决策算法,如调度策略,决定哪个进程优先运行,依赖历史信息、工作负载和性能目标。

提示时分共享空分共享是资源共享的两种方式。时分共享在时间上分配资源(如CPU),空分共享在空间上分配资源(如磁盘块)。

设计原则:分离机制与策略(Lampson定律),便于模块化设计,策略可变而机制稳定。

二、进程API

2.1 核心API功能

现代操作系统提供的进程API包括:

  • 创建(create):创建新进程,如通过命令行或点击图标启动程序。
  • 销毁(destroy):终止进程,支持用户强制停止失控进程。
  • 等待(wait):暂停父进程,直到子进程完成。
  • 其他控制:暂停、恢复进程等操作。
  • 状态查询(status):获取进程运行时间、状态等信息。

2.2 UNIX进程API

UNIX系统通过以下系统调用实现进程管理:

  • fork():创建子进程,子进程是父进程的副本。
  • exec():将当前进程替换为新程序,保留进程ID但加载新代码。
  • wait():父进程等待子进程完成。

三、进程创建的细节

3.1 创建流程

操作系统将程序转变为进程的步骤:

  1. 加载代码和静态数据
    • 从磁盘(或SSD)读取可执行文件到内存,形成进程的地址空间。
    • 早期系统采用尽早加载(eagerly),现代系统使用惰性加载(lazily),按需加载代码和数据。
  2. 分配运行时栈
    • 为局部变量、函数参数、返回地址分配内存。
    • 初始化栈,填入main()函数参数(如argcargv)。
  3. 分配堆
    • 为动态分配数据(如链表、树)预留内存,初始较小,调用malloc()时扩展。
  4. 初始化I/O
    • 设置标准输入、输出、错误文件描述符(如UNIX中的0、1、2)。
  5. 启动程序
    • 跳转到main()函数,CPU控制权移交新进程,程序开始执行。

3.2 进程状态

进程在运行中可能处于以下状态:

  • 运行(running):正在执行指令。
  • 就绪(ready):准备运行,但等待调度。
  • 阻塞(blocked):等待事件(如I/O完成)。
  • 其他状态:
    • 初始(initial):进程创建时的过渡状态。
    • 僵尸(zombie):进程已退出但未清理,保留返回码供父进程检查。

状态转换

  • 就绪 ↔ 运行:通过调度/取消调度。
  • 运行 → 阻塞:发起I/O请求。
  • 阻塞 → 就绪:I/O完成。

3.3 状态转换示例

  • 纯CPU任务:两个进程交替运行,状态在运行和就绪间切换。
  • 带I/O任务:进程发起I/O后阻塞,OS调度其他进程,I/O完成后回到就绪状态。

四、UNIX进程API详解

4.1 fork()系统调用

  • 功能:创建子进程,子进程是父进程的副本,拥有独立地址空间、寄存器等。
  • 返回值
    • 父进程:子进程的PID。
    • 子进程:0。
  • 特点:子进程从fork()调用返回处开始执行,非从main()开始。
  • 非确定性:父子进程的执行顺序由调度器决定,可能导致输出顺序不同。

示例代码(p1.c):

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

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork失败
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // 子进程
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // 父进程
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

输出示例

hello world (pid:29146)
hello, I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)

或:

hello world (pid:29146)
hello, I am child (pid:29147)
hello, I am parent of 29147 (pid:29146)

4.2 wait()系统调用

  • 功能:父进程等待子进程完成,同步执行顺序。
  • 效果:确保子进程输出先于父进程,消除执行顺序的非确定性。
  • 示例代码(p2.c):
#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) {
        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);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}

输出

hello world (pid:29266)
hello, I am child (pid:29267)
hello, I am parent of 29267 (wc:29267) (pid:29266)

4.3 exec()系统调用

  • 功能:将当前进程替换为新程序,加载新代码和数据,保留进程ID。
  • 变体execvp()execl()等。
  • 特点:成功调用不返回,失败则继续执行后续代码。
  • 示例代码(p3.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid:%d)\n", (int) getpid());
        char *myargs[3];
        myargs[0] = strdup("wc"); // 程序:wc
        myargs[1] = strdup("p3.c"); // 参数:文件
        myargs[2] = NULL; // 数组结束
        execvp(myargs[0], myargs); // 执行wc
        printf("this shouldn't print out");
    } else {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}

输出

hello world (pid:29383)
hello, I am child (pid:29384)
29 107 1030 p3.c
hello, I am parent of 29384 (wc:29384) (pid:29383)

4.4 输出重定向

  • 功能:将进程输出重定向到文件而非屏幕。
  • 实现:在fork()后、exec()前关闭标准输出(STDOUT_FILENO),打开目标文件。
  • 示例代码(p4.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        close(STDOUT_FILENO);
        open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
        char *myargs[3];
        myargs[0] = strdup("wc");
        myargs[1] = strdup("p4.c");
        myargs[2] = NULL;
        execvp(myargs[0], myargs);
    } else {
        int wc = wait(NULL);
    }
    return 0;
}

输出

prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c

4.5 为什么分离fork()和exec()

  • 优势fork()exec()分离为shell实现功能(如重定向、管道)提供了灵活性。
  • 示例:shell在fork()后、exec()前修改环境(如重定向输出到文件)。
  • Lampson定律:做对事比抽象或简化更重要,fork()exec()的组合简单且强大。

五、数据结构

5.1 进程列表

操作系统维护进程列表(process list),跟踪所有进程的状态和信息。每个进程的信息存储在进程控制块(PCB)中。

5.2 xv6的进程结构

xv6内核的proc结构示例:

struct context {
    int eip; // 指令指针
    int esp; // 栈指针
    int ebx; // 通用寄存器
    // ...
};

enum proc_state { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

struct proc {
    char *mem; // 进程内存起始地址
    uint sz; // 内存大小
    char *kstack; // 内核栈底
    enum proc_state state; // 进程状态
    int pid; // 进程ID
    struct proc *parent; // 父进程
    void *chan; // 阻塞等待的通道
    int killed; // 是否被终止
    struct file *ofile[NOFILE]; // 打开的文件
    struct inode *cwd; // 当前目录
    struct context context; // 寄存器上下文
    struct trapframe *tf; // 中断帧
};
  • 作用:保存进程的寄存器上下文、状态、文件描述符等,供上下文切换和状态管理使用。

六、其他API与工具

6.1 信号机制

  • kill():向进程发送信号,如终止、暂停等。
  • 信号子系统:处理外部事件,丰富进程交互。

6.2 实用工具

  • ps:列出当前运行的进程。
  • top:实时显示进程资源占用。
  • MenuMeters:监控CPU利用率。

七、扩展与总结

7.1 进程管理的意义

进程抽象和API是操作系统多任务处理的核心。通过虚拟化CPU和提供灵活的API(如fork()exec()wait()),操作系统实现了并发、资源共享和用户交互的简化。

7.2 UNIX设计的哲学

UNIX的进程API设计体现了模块化和灵活性:

  • 分离机制与策略:便于扩展和优化。
  • 简单而强大fork()exec()的组合支持复杂功能(如管道、重定向)。
  • 用户态shell:将复杂功能交给用户程序,保持内核简洁。