09. 进程与线程

深入理解进程与线程的区别、PCB 结构、线程模型与并发编程基础,掌握 Linux 进程状态与进程间通信机制。

1. 进程(Process)

1.1 定义

进程是资源分配的基本单位,拥有独立的地址空间、文件描述符、代码段、数据段和堆栈。

进程地址空间布局:

高地址 → ┌─────────────┐
         │   栈(Stack) │  ← 局部变量、函数调用,向下增长
         │     ↓       │
         │   堆(Heap)  │  ← 动态分配,向上增长
         │     ↑       │
         │ BSS(未初始化数据)│
         │ 数据段(全局/静态)│
         │ 代码段(Text)   │
低地址 → └─────────────┘

1.2 进程控制块(PCB)

PCB 是操作系统管理进程的核心数据结构:

字段说明
进程 ID(PID)唯一标识
状态就绪、运行、阻塞、创建、终止
程序计数器(PC)下一条执行指令地址
寄存器CPU 寄存器状态快照
内存信息页表/段表基址、代码/数据/堆栈地址
调度信息优先级、时间片、等待时间
文件描述符表打开的文件
信号处理待处理的信号与处理函数

1.3 进程状态转换

        创建
          ↓
       [就绪] ←────── 时间片到
          ↓          ↑
    被调度选择   [运行]
          ↓          ↓
       [运行] ───→ [阻塞](等待 I/O、信号量)
                     ↓
                I/O 完成
                     ↓
                  [就绪]

1.4 进程创建(Linux)

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

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child PID: %d, Parent PID: %d\n", getpid(), getppid());
        execlp("ls", "ls", "-l", NULL);  // 替换为 ls -l
    } else {
        // 父进程
        int status;
        waitpid(pid, &status, 0);  // 等待子进程结束
        printf("Child exited with status: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

fork() 使用写时复制(COW):子进程复制父进程页表,仅当写入时才复制物理页。


2. 线程(Thread)

2.1 定义

线程是 CPU 调度的基本单位,同一进程的线程共享代码段、数据段、堆和文件描述符表,仅独享线程栈、程序计数器和寄存器。

进程 vs 线程资源对比:

            进程            同一进程内的线程
地址空间     独立            共享
堆           独立            共享
全局变量     独立            共享
栈           独立            独立(线程栈)
文件描述符   独立            共享
信号处理     独立            共享

2.2 Linux 线程实现

Linux 中线程通过 clone() 实现,是轻量级进程(LWP)

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>

int thread_func(void *arg) {
    printf("Thread running, arg: %s\n", (char*)arg);
    return 0;
}

int main() {
    char stack[8192];
    // CLONE_VM: 共享地址空间
    // CLONE_FS: 共享文件系统信息
    // CLONE_FILES: 共享文件描述符表
    clone(thread_func, stack + 8192, 
          CLONE_VM | CLONE_FS | CLONE_FILES, 
          "hello");
    return 0;
}

2.3 POSIX 线程(pthread)

#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int num = *(int*)arg;
    printf("Thread %d working\n", num);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;
    int arg = 42;
    
    pthread_create(&tid, NULL, worker, &arg);
    pthread_join(tid, NULL);  // 等待线程结束
    
    printf("Main thread done\n");
    return 0;
}

3. 线程模型

模型特点代表
一对一(1:1)每个用户线程对应一个内核线程Linux pthread、Windows
多对一(M:1)多个用户线程映射到一个内核线程早期 Green Thread
多对多(M:N)多用户线程映射到多内核线程Go goroutine、Erlang
1:1 模型:
  用户线程 T1 ──→ 内核线程 K1
  用户线程 T2 ──→ 内核线程 K2
  (线程切换由内核调度,线程阻塞不影响其他线程)

M:1 模型:
  用户线程 T1 ──┐
  用户线程 T2 ──┼──→ 内核线程 K1
  用户线程 T3 ──┘
  (线程切换在用户态,但一个线程阻塞全阻塞)

M:N 模型:
  用户线程 T1 ──┐     ┌──→ 内核线程 K1
  用户线程 T2 ──┼─────┼──→ 内核线程 K2
  用户线程 T3 ──┘     └──→ 内核线程 K3
  (灵活调度,结合了两种模型的优点)

4. 进程间通信(IPC)

4.1 管道(Pipe)

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

int main() {
    int pipefd[2];
    pipe(pipefd);
    
    pid_t pid = fork();
    if (pid == 0) {
        close(pipefd[1]);  // 子进程关闭写端
        char buf[64];
        read(pipefd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
        close(pipefd[0]);
    } else {
        close(pipefd[0]);  // 父进程关闭读端
        write(pipefd[1], "Hello from parent", 17);
        close(pipefd[1]);
        wait(NULL);
    }
    return 0;
}

4.2 共享内存 + 信号量

#include <sys/shm.h>
#include <sys/sem.h>

// 共享内存:最快的 IPC(无需内核拷贝)
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *addr = shmat(shmid, NULL, 0);

// 信号量同步
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// P 操作(等待)、V 操作(信号)
struct sembuf p = {0, -1, SEM_UNDO};
struct sembuf v = {0, 1, SEM_UNDO};
semop(semid, &p, 1);  // P
// 访问共享内存...
semop(semid, &v, 1);  // V

4.3 消息队列

#include <sys/msg.h>

struct msg_buffer {
    long mtype;
    char mtext[256];
};

int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
struct msg_buffer msg = {1, "Hello"};
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);

4.4 IPC 方式对比

方式速度数据格式同步方式适用场景
管道字节流读写阻塞父子进程通信
命名管道(FIFO)字节流读写阻塞无亲缘关系进程
共享内存最快(无拷贝)原始内存需额外同步大量数据、高性能
消息队列结构化消息自带同步异步消息、解耦
信号量同步原语互斥/同步
Socket较慢字节流/报文自带跨网络、跨机器
信号整数异步中断事件通知

5. 并发 vs 并行

单核 CPU:
  ┌───┬───┬───┬───┬───┬───┐
  │ P1│ P2│ P1│ P3│ P2│ P1│  → 并发(Concurrency)
  └───┴───┴───┴───┴───┴───┘     任务交替执行,逻辑上同时

多核 CPU:
  Core 1: ┌───┬───┬───┐
          │ P1│ P1│ P1│        → 并行(Parallelism)
  Core 2: ├───┼───┼───┤         任务真正同时执行
          │ P2│ P2│ P2│
          └───┴───┴───┘

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 16. 数据链路层
  2. 15. 网络层与路由
  3. 14. 网络模型与协议