跳转至

3 Processes & Threads

3.1 Basic Concepts

进程是什么?

  • 一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。

  • 正在执行中的程序 a program in execution

在内存中进程包含了

  • 代码段 program code
  • Program counter
  • Registers
  • Data section (global data)(数据段)
  • Stack (temporary data)
  • Heap (dynamically allocated memory)

Note

数据段(Data Section / Global Data)的作用是存储程序中的全局变量静态变量

栈是用于管理函数调用过程中的局部变量、函数参数、返回地址等临时数据。

堆(Heap)的作用是用于动态内存分配,程序运行时按需申请和释放内存。

进程的状态包括以下

  • New(新): The process is being created.

  • Running(运行、执行): Instructions are being executed.

  • Ready(就绪): The process is waiting to be assigned to a processor (CPU).

  • Waiting(等待、blocked阻塞): The process is waiting for some event to occur.
  • Terminated(终止): The process has finished execution.

引起进程状态变化的原因有:程序行为如系统调用,操作系统调度或者是外部行为如中断

三个基本状态之间可能转换和转换原因如下:

  • 就绪→运行:当处理机空闲时,进程调度程序必将处理机分配给一个处于就绪状态的进程,该进程便由就绪状态转换为运行状态。
  • 运行→等待:处于运行状态的进程在运行过程中需要等待某一事件发生后(例如因I/O请求等待I/O完成后),才能继续运行,则该进程放弃处理机,从运行状态转换为等待状态。
  • 等待→就绪:处于等待状态的进程,若其等待的事件已经发生,于是进程由等待状态转换为就绪状态。
  • 运行→就绪:处于运行状态的进程在其运行过程中,因分给它的处理机时间片已用完,而不得不让出(被抢占)处理机,于是进程由运行态转换为就绪态。

进程与程序的区别

  • 进程是动态的,程序是静态的:程序是有序代码的集合;进程是程序的执行。

  • 进程是暂时的,程序的永久的:进程是一个状态变化的过程,程序可长久保存。

  • 进程与程序的组成不同:进程的组成包括程序、数据和进程控制块【PCB】(即进程状态信息)。
  • 进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序

Question

例:一个只有一个处理机的系统中,OS的进程有运行、就绪、等待三个基本状态。假如某时刻该系统中有10个进程并发执行,在略去调度程序所占用时间情况下(在user mode下),请问:

  • 这时刻系统中处于运行状态的进程数最多有几个?最少有几个?1 / 0

  • 这时刻系统中处于就绪状态的进程数最多有几个?最少有几个?9 / 1

  • 这时刻系统中处于等待状态的进程数最多有几个?最少有几个?10 / 0

每个进程在操作系统内用进程控制块【PCB】来表示,它包含与特定进程相关的许多信息

3.2 Process Scheduling

将各个进程在这些队列中进行迁移

  • 作业队列(Job Queue):包含系统中所有已提交但尚未完全加载到内存的进程。
  • 就绪队列(Ready Queue):存放已加载到内存、准备好运行但正在等待CPU的进程。
  • 设备队列(Device Queues):保存因等待I/O设备(如磁盘、打印机)而被阻塞的进程

一个对进程调度流程的大致描述

操作系统中有三种类型的调度器

  • Long-term scheduler (or job scheduler) 长程调度(或作业调度):决定哪些进程应该从外存(如磁盘)加载到内存,并加入就绪队列(ready queue)。但是大多数现代操作系统(如 Windows、UNIX、Linux)没有独立的长程调度器。原因是:这些系统通常使用动态内存管理(如虚拟内存),允许进程在需要时才加载部分代码,不需要专门的“作业调度”来控制进入内存。

  • Short-term scheduler(短程调度 / CPU 调度):决定哪个就绪进程应该获得 CPU 的下一个时间片,并执行。是真正决定 CPU 使用顺序的调度器

  • Medium-term Scheduler(中程调度):在内存紧张时,将某些暂时不用的进程从内存中换出到外存(交换空间)

进程可以分为两种类型,一种是 I/O 型进程,花更多时间在做 I/O 操作,CPU 使用的时间短且频繁;还有一种是 CPU 型进程,更多时间花在计算上,CPU 的使用时间长。


当 CPU 从一个进程切换到另一个进程的时候,系统必须要存储旧进程的状态,然后加载新进程的状态,这个过程被称为上下文切换。一个进程的上下文保存在 PCB 当中。这个转换过程是纯开销过程,系统在这个过程中不会做任何工作。

3.3 Operations on Processes

父进程创建子进程,子进程又可以创建其他进程,从而形成一个进程树(process tree)。通常,进程通过一个进程标识符(PID) 来识别和管理。一般父进程和子进程共享一部分资源。

父子进程并发执行,子进程初始时复制父进程的地址空间(采用写时复制机制),父进程需显式调用 wait() 回收子进程资源,而子进程可通过 exec() 系列函数加载并替换为新的程序

Fork & Exec

int pid = fork(); 从系统调用 fork 中返回时,两个进程除了返回值 pid不同外,具有完全一样的用户级上下文。在子进程中,pid的值为0;父进程中, pid的值为子进程的进程号。

exec system call used after a fork to replace the process’ memory space with a new program.

进程可通过正常结束、异常崩溃或外界干预而终止;终止时调用 exit() 请求操作系统回收资源,子进程的退出状态通过 wait() 传递给父进程,最终由操作系统释放所有占用的资源。

父进程使用 wait() 等待子进程结束,来回收它的资源。若未调用 wait(),子进程会变成僵尸进程;若父进程先终止,子进程则成为孤儿进程,由系统进程 init 接管并清理。

3.4 Interprocess Communication

Independent process cannot affect or be affected by the execution of another process.

Cooperating process can affect or be affected by the execution of another process, Cooperating processes need interprocess communication (IPC)

IPC provides a Mechanism for processes to communicate and to synchronize their actions without sharing the same address space

有两种交流的模型

  • Shared memory(共享内存)
  • Message passing(消息传递)

3.4.1 IPC in Shared-Memory Systems

多个进程可以映射到同一个内存区域,这个区域被称为共享内存段(shared memory segment)。所有参与通信的进程都可以读写这块内存,从而交换数据。

操作系统只负责创建和管理共享内存段,但不参与数据的发送与接收。进程之间如何读写、何时读写、使用什么格式,都由程序员自己决定

主要问题是需要提供一种机制,使用户进程在访问共享内存时能够同步它们的行为。需要引入同步操作来确保互斥访问。


下面我们介绍一种经典的进程协作模型:生产者-消费者问题(Producer-Consumer Problem)

主要的过程就是 生产者进程生成信息,由消费者进程消费。 缓冲区也有固定的大小,当缓冲区满时,生产者必须等待,直到消费者取走一些数据;当缓冲区空时,消费者必须等待,直到生产者放入新数据。

对于生产者来说

Producer :
item nextProduced;
while (1) {
    produce an item in nextProduced ;
    while (((in + 1) % BUFFER_SIZE) == out)
          /* do nothing */
    buffer[in] = nextProduced;
    in = (in + 1) % BUFFER_SIZE;
}

对于消费者来说

Consumer :
item nextConsumed;
while (1) {
    while (in == out)
        ; /* do nothing */
    nextConsumed = buffer[out];
    out = (out + 1) % BUFFER_SIZE;
    consume the item in nextConsumed ;
}

上述代码实际上完成的就是一个队列

3.4.2 IPC in Message-Passing Systems

通过消息通信模型进行进程交流,需要两种操作 send 和 receive,然后需要建立一个 communication link。然后通信的方式也可以分为直接和间接,同步和不同步等等

下面阐述一下直接通信,相应的 send 和 receive 函数必须要显示声明相应的对象

  • send (P, message) – send a message to process P

  • receive(Q, message) – receive a message from process Q

链路是自动建立的,同时每个通信对只有一条链路。链路可以是单向的,但通常是双向的。


然后阐述一下间接通信,消息是通过一个共享的邮箱进行发送和接收的,每个邮箱都有一个唯一的标识符,只有共享同一个邮箱的进程才能通信。一条链路可以关联多个进程,每对进程之间可以共享多个通信链路。建立通信的过程就是先创建一个邮箱(端口),然后往邮箱中发送或接受信息

  • send(A, message) – send a message to mailbox A

  • receive(A, message) – receive a message from mailbox A

间接通信和直接通信

特性 间接通信(Indirect) 直接通信(Direct)
消息目标 邮箱(mailbox) 对方进程名
是否需要显式命名对方 否,只需知道邮箱 ID 是,必须指定进程名
通信链路建立条件 共享同一个邮箱 显式调用 send/receive
支持多少进程通信 多对多(多个进程共享邮箱) 一对一(两个进程之间)
是否支持广播 ✅ 支持(多个进程可读同一邮箱) ❌ 不支持
灵活性 更高(可通过邮箱设计复杂通信结构) 较低(仅限于已知进程间通信)

关于进程通信中的同步机制,消息转递有两种基本模式:阻塞(blocking)非阻塞(non-blocking)

阻塞(Blocking)被认为是同步(synchronous)

  • 阻塞发送(Blocking send):发送方调用 send() 后,必须等待接收方收到消息,才能继续执行。
  • 阻塞接收(Blocking receive):接收方调用 receive() 后,必须等待有消息到达,才会继续执行。

非阻塞(Non-blocking)被认为是异步(asynchronous)

  • 非阻塞发送(Non-blocking send):发送方发送消息后立即返回,不关心接收方是否已经准备就绪。

  • 非阻塞接收(Non-blocking receive):接收方尝试接收消息,但不会等待。得到的可能是一个有效的消息也可能是一个空指针

Example

例: 设计一个程序,要求用函数msgget创建消息队列,从键盘输入的字符串添加到消息队列。创建一个进程,使用函数msgrcv读取队列中的消息并在计算机屏幕上输出。

分析 :程序先调用msgget函数创建、打开消息队列,接着调用msgsnd函数,把输入的字符串添加到消息队列中。子进程调用msgrcv函数,读取消息队列中的消息并打印输出,最后调用msgctl函数,删除系统内核中的消息队列

struct msgmbuf { /*结构体,定义消息的结构*
long msg_type; /*消息类型*/
char msg_text[512]; }; /*消息内容*/
int main(){
int qid,len;
key_t key;
struct msgmbuf msg

if((key=ftok(".",'a'))==-1) { /*调用ftok函数,产生标准的key*/ 
    perror("产生标准key出错");
    exit(1);}

/*调用msgget函数,创建、打开消息队列*/
if((qid=msgget(key,IPC_CREAT|0666))==-1) {
    perror("创建消息队列出错");
    exit(1);}
printf("创建、打开的队列号是:%d\n",qid); /*打印输出队列号*/


int pid=fork();
if (pid>0){
    printf(" 我是父进程PID=:%d 发送消息\n",getpid())
    puts("请输入要加入队列的消息:");
    /*键盘输入的消息存入变量msg_text*/
    if((fgets((&msg)->msg_text,512,stdin))==NULL) {
        puts("没有消息");
        exit(1);}
    msg.msg_type=getpid();
    len=strlen(msg.msg_text);
    /*调用msgsnd函数,添加消息到消息队列*/
    if((msgsnd(qid,&msg,len,0))<0) { 
        perror("添加消息出错");
        exit(1);}
else{
    printf("我是子进程PID=:%d 接收消息\n",getpid())
/*调用msgrcv函数,从消息队列读取消息*/
    if((msgrcv(qid,&msg,512,0,0))<0) { 
        perror("读取消息出错");
        exit(1);} 
    /*打印输出消息内容*/
    printf("读取的消息是:%s\n",(&msg)->msg_text); 
    /*调用msgctl函数,删除系统中的消息队列*/
    if((msgctl(qid,IPC_RMID,NULL))<0){
        perror("删除消息队列出错");
        exit(1);}
}

exit(0);

3.5 Examples of IPC Systems

3.5.1 Shared Memory

以 POSIX 为例讲述 Shared Memory

POSIX 共享内存(POSIX Shared Memory),它是 Unix/Linux 系统中一种高效的进程间通信(IPC, Inter-Process Communication)机制。

POSIX

POSIX 是一组操作系统接口标准,旨在提高不同系统之间的兼容性。

  • shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666); 用于创建或打开一个共享内存对象。成功时返回一个文件描述符(file descriptor),记为 shm_fd,后续用来操作这个共享内存对象。
  • ftruncate(shm_fd, 4096);使用 ftruncate() 函数来设定共享内存的大小
  • void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); mmap() 是将文件或设备映射到进程虚拟地址空间的系统调用。它把共享内存对象映射成一个内存指针,这样就可以像操作普通内存一样读写数据

3.5.2 LPC

以 Windows 为例讲述 LPC

还有一种进程间通信(IPC)机制:高级本地过程调用(Advanced Local Procedure Call, LPC)

  • LPC 只能在同一台计算机上的进程之间工作。如果需要跨网络通信需要使用 RPC 等机制
  • 在 LPC 中,端口(port) 是核心概念,类似于前面讲过的“邮箱”(mailbox)或“消息队列”。每个通信端点都有一个对应的 port 对象,用来收发消息

一个典型的 LPC 通信步骤如下

  • 客户端打开服务器子系统的连接端口对象
  • 客户端发送连接请求
  • 服务器创建两个私有通信端口,并返回其中一个给客户端
  • 客户端和服务器使用对应的端口句柄进行消息/回调交互

注意这里的客户端和服务器指的是同一台计算机上的两个本地进程

3.5.3 Pipes

管道通过将一个进程的输出作为另一个进程的输入实现进程间的通信。大多数管道的实现是单向的,如果需要建立双向通信,需要创建两个管道。

Half-duplex & Full-duplex

半双工(Half-duplex):同一时间只能单向传输(比如对讲机)。

全双工(Full-duplex):可以同时双向传输(比如电话)。

普通管道只能由创建它的进程及其子进程访问,通过系统调用如 pipe()(Linux/Unix)或 CreatePipe()(Windows)创建,并随着进程的结束而销毁。使用场景如父进程和子进程之间的通信

// 父进程
int pipefd[2];
pipe(pipefd);           // 创建管道
fork();                 // 创建子进程

if (child) {
    close(pipefd[1]);   // 子进程只读
    read(pipefd[0], buf, size);
} else {
    close(pipefd[0]);   // 父进程只写
    write(pipefd[1], data, size);
}

命名管道不依赖于父子关系,任何进程只要知道名字就可以打开并使用。并且这个管道有文件名,可以在文件系统中被看到。

# 创建命名管道
mkfifo /tmp/my_pipe

# 进程 A 写入
echo "Hello" > /tmp/my_pipe

# 进程 B 读取
cat < /tmp/my_pipe

3.6 Communication in Client–Server Systems

在典型的 客户端-服务器架构(Client-Server Architecture) 中,进程之间进行通信的主要方式有两类:

  • 套接字(Sockets)
  • 远程过程调用(RPC)

3.6.1 Sockets

在计算机网络中,进程之间不能直接通信,必须通过某种机制来建立连接。

Socket

套接字(Socket)就是这个机制中的一个逻辑端点(endpoint),就像电话号码一样,用于标识某个进程在通信中的位置。

一个套接字由两部分组成:

  • IP 地址:表示哪台计算机(主机)
  • 端口号(Port):表示该计算机上的哪个服务或进程

exp: 套接字 161.25.19.8:1625 表示在主机 161.25.19.8 上的端口 1625

所有小于 1024 的端口是“众所周知的”(well-known),用于标准服务

特殊的 IP 地址 127.0.0.1(回环地址)用于指向运行进程的本机系统

通信发生在一对套接字之间

3.6.2 Remote Procedure Calls

RPC 是一种让远程函数调用变得简单透明的技术,它通过 Stub(存根) 实现参数的封装与解封,并利用 端口 区分服务。在 Windows 中,这种机制由 MIDL 语言支持,能够自动生成客户端和服务端的通信代码。

3.7 Basic Concepts of Threads

现代操作系统将进程的两个角色分离:资源拥有执行调度

  • 进程 是资源分配的单位(如内存、文件)。
  • 线程 是调度执行的单位,也称轻型进程(LWP)。

一个进程可包含多个线程,它们共享资源,但独立运行。线程只拥有一点在运行中必不可省的资源(程序计数器、一组寄存器和栈),但它可与同属一个进程的其它线程共享进程拥有的全部资源。

线程定义为进程内一个执行单元或一个可调度实体。

  • 拥有少量的系统资源(资源是分配给进程)
  • 一个进程中的多个线程可并发执行(进程可创建线程执行同一程序的不同部分)
  • 系统开销小、切换快。(进程的多个线程都在进程的地址空间活动)

一个线程与其同属一个进程的其他线程共享以下内容:

  • code section(代码段)
  • data section(数据段)
  • operating-system resources(操作系统资源)

3.8 Multi-threading Models

用户级线程:不依赖于OS内核(内核不了解用户线程的存在),应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程

  • 用户线程的维护由应用进程完成
  • 内核不了解用户线程的存在
  • 用户线程切换不需要内核特权,因为切换发生在用户态,无需进入内核模式。
  • 用户线程调度算法可针对应用优化,如优先处理 I/O 密集型任务,或保证实时响应。
  • 一个线程发起系统调用而阻塞,则整个进程在等待(一对多模型中)

三种主要的线程库:POSIX PthreadsWin32 threadsJava threads


内核级线程:依赖于OS核心,由内核的内部需求进行创建和撤销,用来执行一个指定的函数。一个线程发起系统调用而阻塞,不会影响其他线程。时间片分配给线程,所以多线程的进程获得更多CPU时间。

  • 内核维护进程和线程的上下文信息;

  • 线程切换由内核完成;

  • 时间片分配给线程,所以多线程的进程获得更多CPU时间;

  • 一个线程发起系统调用而阻塞,不会影响其他线程的运行。

三种多线程模型

Many-to-One Model(多对一模型)

  • 多个用户线程 → 映射到 一个内核线程
  • 所有用户线程共享同一个内核线程来与操作系统交互
优点 缺点
✔️ 线程切换在用户态完成,速度快 ❌ 只有一个内核线程,无法利用多核
✔️ 轻量,无需内核支持 ❌ 一旦某个用户线程阻塞(如 I/O),整个进程被挂起
✔️ 应用程序可自定义调度策略 ❌ 不能实现真正的并发

One-to-One Model(一对一模型)

  • 用户线程与内核线程一一对应
优点 缺点
✔️ 每个线程都能独立调度,可并行执行 ❌ 创建大量线程开销大(每个都需要内核资源)
✔️ 支持真正的并发(多核上并行) ❌ 内核线程创建/销毁成本高
✔️ 单个线程阻塞不会影响其他线程 ❌ 不适合轻量级任务密集场景

Many-to-Many Model(多对多模型)

  • 多个用户线程 → 映射到多个内核线程
  • 用户线程和内核线程之间是动态映射关系
优点 缺点
✔️ 结合前两种模型的优点:轻量 + 并发 ❌ 实现复杂,需要复杂的调度机制
✔️ 支持真正的并发(多核) ❌ 需要运行时系统维护映射关系
✔️ 可以优化线程数量(避免过多内核线程) ❌ 有一定开销,但远低于一对一模型
✔️ 单个线程阻塞不影响其他线程,灵活度高,适合现代应用

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!