计算机技术学习札记

操作系统 1:进程与线程

进程(Process)是由一份代码段,一个当前状态和一组相关系统资源所刻画的活动单元,是程序在操作系统上运行的实体。在操作系统的帮助下,每个进程都产生了「我独占整个硬件,我独享所有内存」的假象,从而能更好地利用系统资源。

htop 工具可以直观地显示 *nix 系统下的进程和资源情况:

说明:Linux 不区分进程和线程,二者视为同一种东西称为 task,由调度器统一调度。进程和线程的区别体现在创建时对内存空间的处理上。

线程(Thread,又译「执行绪」)是一个进程之下的相对独立的可调度的执行单元。一个进程之下的所有线程可以并发执行,共享进程的资源。例如,在 Java 中,我们利用 Runnable 接口可以轻松地创建多个线程,不同线程共享着 Java 虚拟机下的资源,由 Java 虚拟机进行调度。

进程的描述

一个进程由下面几部分「描述」,或者说组成:

进程控制块(PCB)

进程控制块(Process Control Block, PCB)是一个结构体,用来描述进程与其他系统资源的关系。它包含进程的标识符(PID),进程相关用户的标识符(UID),进程的状态,资源(如文件描述符),处理器信息(寄存器的值,称为「栈帧」)等。

在 xv6 中,PCB 就是 kernel/proc.h 中的 proc 结构体;在 Linux 中,PCB 就是 include/linux/sched.h 中的 task_struct 结构体(Linux 将进程称为 task)。

上下文

进程的上下文,包含进程的物理实体(代码段、数据段)和支持它运行的环境。具体地:

  • 进程的代码段、数据段、堆栈等组成用户级上下文。

  • 进程标识、现场、控制信息、内核栈组成系统级上下文。

  • 处理器中寄存器的值构成硬件上下文。

地址空间

几乎所有的现代操作系统都使用虚拟内存技术。在 Linux 平台下,每个用户进程都具有独立的私有地址空间。虚拟内存让每个进程「独享」一大段假想的内存,再由操作系统利用「页表」及硬件上的「内存管理单元」(Memory Management Unit, MMU)将每个进程实际使用的内存「映射」到真实的内存上(下图来自 IBM Linux 开发文档):

进程的特性

  • 动态性——因创建而产生,由调度而执行,因得不到资源而暂停执行,最后因完成或撤销而消亡。

  • 并发性——引入进程的目的就是为了使多个程序并发执行。

  • 独立性——进程是一个能独立运行的基本单位,也是系统进行资源分配和调度的基本单位。

  • 异步性——进程以各自独立的、不可预知的速度向前推进。

  • 结构性——进程 = 程序 + 数据 + PCB

进程的状态、转换与控制

进程的状态

  • 就绪态:已经获取了必要的除 CPU 之外的资源,等待获得 CPU 来执行。

  • 运行态:进程已经获取到 CPU,正在 CPU 上执行。

  • 阻塞态:进程正在等待某一事件而放弃 CPU,暂停运行。

  • 新建:至少已经建立 PCB,但资源还没有调入主存。

  • 终止:进程已经终止,等待回收。

进程的创建和回收

  • 使用系统调用 fork() 创建子进程。子进程拥有父进程的所有资源。

    • 「调用一次返回两次」:在父子进程中各返回一次。

    • 在子进程中返回 0,在父进程中返回子进程的 PID。

  • 使用系统调用 wait() 或者 waitpid() 主动回收子进程。

    • wait() 等待某一个子进程结束。

      函数原型为 pid_t wait(int *status),返回值是此次结束的子进程的 PID,并将子进程的退出状态写到 *status 指针所在处。

      可以使用宏 WIFEXITED(status) 来从 status 中读出进程是否是正常退出的。

      可以使用宏 WEXITSTATUS(status) 来从 status 中读出进程的退出码。

    • waitpid() 等待一个特定的进程结束。

      函数原型为 pid_t waitpid(pid_t pid, int *status, int options)

      options 一般指定为 0。它可以是下面参数的位或:

      • WNOHANG:如果没有可以退出的子进程,就立即返回。

      • WUNTRACED:当一个进程不被 ptrace 追踪但结束时也返回。

      • WCOUNTINUED:当一个停止的进程被信号 SIGCONT 恢复时返回。

进程间通信

  • 共享内存系统

  • 消息传递系统

  • 管道

  • 套接字

  • 远程过程调用

线程的描述

与进程类似,线程可以用「线程控制块」(TCB)+ 堆栈 + 寄存器进行描述。在引入了线程的概念之后,我们有:

  • 进程 = 线程 + 代码段、数据段、内核上下文,由 PCB 控制

  • 线程 = 寄存器 + 栈,由 TCB 控制

线程和进程的关系有:

  • 线程是基本的 CPU 执行单元。

  • 线程不单独拥有系统资源,只拥有其在运行中必不可少的资源。

  • 一个进程可以关联多个线程。

  • 每个线程有自己的控制流。

  • 属于同一个进程的线程共享所属进程的地址空间。

  • 每个线程有自己局部变量的堆栈。

POSIX 线程模型

POSIX 线程模型 POSIX Threads 简称 pthreads,是几乎所有 *nix 平台上都使用的多线程模型。

线程的创建

使用 pthread_create() 函数。原型:

int pthread_create(pthread_t *restrict thread,
                   const pthread_attr_t *restrict attr,
                   void *(*start_routine)(void *),
                   void *restrict arg);

线程的终止

当线程所属进程返回时,线程会隐式地终止。

如果需要显式地终止线程,使用 pthread_exit() 函数。原型:

noreturn void pthread_exit(void *retval);

线程的结合和分离

所谓线程的结合类似进程的 wait(),使用 pthread_join() 函数阻塞当前线程,直到指定的线程终止。

线程的分离指的是,将一个指定的线程标记为 detached。当一个 detached 的线程终止时,它的资源会被系统自动回收,而无须其他进程来结合。使用 pthread_detach() 函数完成这个操作。