线程与进程的较量:资源划分与内核实现的全景解析

# 线程与进程的较量:资源划分与内核实现的全景解析


在操作系统的世界里,进程和线程是并发执行的两个核心概念。我们常说“进程是资源分配的基本单位,线程是调度的基本单位”,但这句经典表述背后隐藏着怎样的实现细节?进程和线程在内核中如何表示?它们的资源划分有何异同?本文将从内核实现角度,全景式解析线程与进程的较量。


## 一、进程:资源拥有的独立王国


进程是正在运行的程序实例,它拥有独立的资源集合。从内核视角看,每个进程由多个核心数据结构支撑。


### 1.1 进程的核心数据结构


```c

// 内核中进程描述符简化结构

struct task_struct {

    pid_t pid;                      // 进程ID

    volatile long state;             // 进程状态

    struct mm_struct *mm;            // 内存描述符

    struct files_struct *files;      // 打开文件表

    struct fs_struct *fs;            // 文件系统信息

    struct signal_struct *signal;    // 信号处理

    struct cred *cred;               // 凭证信息

    // ... 其他字段

};

```


每个`task_struct`代表一个执行上下文,包含进程的所有信息:

- **内存描述符**:指向页表、代码段、数据段、堆、栈等内存区域

- **文件描述符表**:记录进程打开的所有文件

- **文件系统信息**:当前工作目录、根目录等

- **信号处理表**:信号处理器和信号掩码

- **凭证信息**:用户ID、组ID等权限信息


### 1.2 进程的地址空间


进程拥有独立的地址空间,通过页表实现虚拟地址到物理地址的映射:


```c

struct mm_struct {

    struct vm_area_struct *mmap;     // 虚拟内存区域链表

    pgd_t *pgd;                       // 页全局目录

    unsigned long start_code, end_code; // 代码段范围

    unsigned long start_data, end_data; // 数据段范围

    unsigned long start_brk, brk;      // 堆范围

    unsigned long start_stack;         // 栈起始地址

    // ...

};

```


不同进程的页表独立,因此一个进程无法直接访问另一个进程的内存。


## 二、线程:轻量级的执行单元


线程是进程内的执行流,共享所属进程的大部分资源,只拥有少量私有数据。


### 2.1 线程的内核表示


有趣的是,在内核中,线程同样用`task_struct`表示,并没有独立的“线程描述符”。区别在于线程的某些字段指向共享的资源:


```c

// 创建线程时,共享父进程的资源

struct task_struct *thread = alloc_task_struct();

thread->mm = current->mm;           // 共享内存空间

thread->files = current->files;      // 共享文件表

thread->fs = current->fs;            // 共享文件系统信息

thread->signal = current->signal;    // 共享信号处理

thread->pid = allocate_pid();        // 但有自己的PID

```


### 2.2 线程的私有部分


尽管共享大部分资源,线程仍然拥有一些私有数据:


```c

struct task_struct {

    // 线程私有部分

    void *stack;                      // 内核栈

    struct thread_struct thread;      // 硬件上下文(寄存器、PC、SP)

    pid_t tid;                        // 线程ID

    // ...

};

```

<"c4.s6k3.org.cn"><"y6.s6k3.org.cn"><"e3.s6k3.org.cn">

每个线程拥有:

- **独立的栈**:用户栈和内核栈

- **独立的寄存器上下文**:程序计数器、栈指针等

- **独立的线程ID**:在内核中唯一标识

- **独立的信号掩码**:可以屏蔽不同信号


## 三、资源划分全景对比


### 3.1 共享 vs 私有


| 资源类型 | 进程间 | 线程间 |

|---------|--------|--------|

| 地址空间 | 独立 | 共享 |

| 全局变量 | 独立副本 | 共享同一份 |

| 打开文件 | 独立(可继承)| 共享文件描述符 |

| 信号处理器 | 独立 | 共享 |

| 当前工作目录 | 独立 | 共享 |

| 用户ID/组ID | 独立 | 共享 |

| 栈 | 独立 | 独立 |

| 寄存器 | 独立 | 独立 |

| 线程ID | 唯一 | 唯一 |


### 3.2 代码验证:共享与独立的边界


以下程序直观展示了进程与线程的资源划分:


```c

#include

#include

#include

#include

#include


int global_var = 100;  // 全局变量


void* thread_func(void* arg) {

    int thread_local = 200;  // 线程局部变量

    global_var += 10;

    printf("线程: global_var = %d (地址: %p), thread_local = %d (地址: %p)\n",

           global_var, &global_var, thread_local, &thread_local);

    return NULL;

}


int main() {

    pid_t pid;

    pthread_t thread;

    

    // 创建线程

    pthread_create(&thread, NULL, thread_func, NULL);

    pthread_join(thread, NULL);

    

    // 创建进程

    pid = fork();

    if (pid == 0) {

        // 子进程

        int child_local = 300;

        global_var += 10;

        printf("子进程: global_var = %d (地址: %p), child_local = %d (地址: %p)\n",

               global_var, &global_var, child_local, &child_local);

        exit(0);

    } else {

        wait(NULL);

        printf("父进程: global_var = %d (地址: %p)\n",

               global_var, &global_var);

    }

    

    return 0;

}

```


运行结果分析:

- 线程修改`global_var`后,主线程看到变化(共享)

- 子进程修改`global_var`后,父进程看不到变化(写时复制)

- 线程的局部变量地址各不相同(独立栈)

- 子进程的局部变量地址与父进程相似但独立(独立栈)


## 四、创建与销毁的内核流程


### 4.1 进程创建:fork


`fork`的系统调用流程:


```c

// 简化版fork实现

sys_fork() {

    struct task_struct *child = dup_task_struct(current);  // 复制task_struct

    child->mm = copy_mm(current);  // 复制内存描述符(写时复制)

    child->files = copy_files(current);  // 复制文件表

    child->fs = copy_fs(current);  // 复制文件系统信息

    copy_thread(child, current);  // 复制线程上下文

    wake_up_new_task(child);  // 加入调度队列

    return child->pid;

}

```


关键点:`fork`创建的子进程拥有父进程所有资源的独立副本(通过写时复制优化)。


### 4.2 线程创建:pthread_create


线程创建最终调用`clone`系统调用:


```c

// pthread_create底层调用clone

clone(CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | 

      CLONE_THREAD | CLONE_SYSVSEM, thread_stack);

```

<"u7.s6k3.org.cn"><"t9.s6k3.org.cn"><"i2.s6k3.org.cn">

关键标志含义:

- `CLONE_VM`:共享内存空间

- `CLONE_FILES`:共享文件描述符表

- `CLONE_FS`:共享文件系统信息

- `CLONE_SIGHAND`:共享信号处理器

- `CLONE_THREAD`:放入同一线程组


### 4.3 写时复制技术


无论是`fork`还是线程创建,Linux都采用写时复制优化:


```c

// 页表项初始设置为只读

pte_t pte = pte_wrprotect(old_pte);


// 当进程尝试写入时触发缺页异常

handle_pte_fault() {

    if (pte_write(pte) && !pte_dirty(pte)) {

        // 复制物理页框,更新页表

        new_page = alloc_page();

        copy_page(new_page, old_page);

        set_pte(pte_mkwrite(pte_mkdirty(new_pte)));

    }

}

```


## 五、调度视角:线程与进程的统一


在调度器眼中,线程和进程都是`task_struct`,没有本质区别。调度器关心的是:


```c

struct sched_entity {

    struct load_weight load;    // 调度权重

    struct rb_node run_node;    // 红黑树节点

    u64 sum_exec_runtime;       // 总执行时间

    // ...

};

```


每个`task_struct`包含一个`se`字段,参与调度。无论是进程还是线程,都按照相同的调度策略(CFS、实时等)获得CPU时间。


## 六、性能与开销的量化对比


| 操作 | 进程开销 | 线程开销 |

|------|---------|---------|

| 创建时间 | 较慢(复制页表、文件表等) | 较快(共享大部分资源)|

| 上下文切换 | 较重(TLB刷新) | 较轻(共享地址空间)|

| 通信方式 | IPC(管道、消息队列、共享内存)| 直接读写全局变量 |

| 同步需求 | 较少(地址空间隔离) | 较多(需互斥锁)|

| 内存占用 | 较高(独立页表) | 较低(共享页表)|


实测数据(近似值):

- 进程创建:约10-30微秒

- 线程创建:约5-15微秒  

- 进程切换:约1-3微秒(需刷新TLB)

- 线程切换:约0.5-1微秒(无需刷新TLB)


## 七、选择指南:何时用进程,何时用线程


### 7.1 优先选择进程的场景

- 需要强隔离性(如不同用户的程序)

- 单个进程崩溃不影响其他进程

- 需要独立的安全权限

- 避免复杂的同步逻辑


### 7.2 优先选择线程的场景

- 需要大量数据共享

- 频繁创建销毁执行单元

- 追求更低的开销和延迟

- 并行处理同一任务


## 八、总结


线程与进程的较量,本质是资源共享程度与隔离性的权衡:


- **进程**:资源独立王国,拥有完整的“房产证”,代价是创建和切换开销大,通信复杂

- **线程**:轻量级执行单元,共享“房产证”,代价是需要精细的同步机制


在内核实现层面,两者统一于`task_struct`,通过`clone`标志控制资源共享程度。理解这一设计,不仅能写出更高效的多线程/多进程程序,更能在系统遇到性能瓶颈时,准确判断是资源竞争还是隔离不足导致的问题。


正如Linux内核设计者所言:“进程是资源的容器,线程是执行的实体。”两者的完美配合,构建了现代操作系统强大的并发处理能力。


请使用浏览器的分享功能分享到微信等