image-20240420163459407

进程和线程

1. 进程和线程的概念

进程和线程是操作系统中的两个重要概念,用于管理程序的执行。进程是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位。线程是进程中的一个执行单元,是操作系统调度的基本单位。

1.1 进程的虚拟地址空间

表格:

虚拟地址空间 描述
代码段 存放程序的指令代码
数据段 存放程序的全局变量和静态变量
存放动态分配的内存空间,由程序员管理
存放函数的局部变量、函数参数、返回地址等信息
命令行参数 存放命令行参数和环境变量
环境变量 存放程序运行的环境变量
内核空间 用于操作系统内核的代码和数据

2. 进程和线程的区别

  1. 定义:进程是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位。线程是进程中的一个执行单元,是操作系统调度的基本单位。
  2. 资源分配:进程拥有独立的地址空间、文件描述符、信号处理器等资源,线程共享进程的地址空间、文件描述符、信号处理器等资源。

3. 进程和线程的关系

  1. 一个进程可以包含多个线程,线程共享进程的地址空间、文件描述符、信号处理器等资源。
  2. 线程是进程的一部分,线程的创建、销毁、切换等操作都由进程来管理。
  3. 线程之间可以共享全局变量,但是需要注意线程安全问题。
  4. 线程之间可以通过共享内存、消息队列、信号量等方式进行通信。
  5. 线程之间可以通过互斥锁、条件变量等方式进行同步。

4. 操作系统创建进程和线程的过程

  1. 创建进程:操作系统通过调用fork系统调用创建一个新的进程,新的进程拥有独立的地址空间、文件描述符、信号处理器等资源。

    • 写时复制的过程如下:

    • 父子进程共享内存:在fork()创建子进程时,父子进程共享同一份物理内存空间。是共享的,父进程和子进程看到的地址空间是相同的,因为内核将它们映射到了相同的物理页面。

    • 延迟复制:当父进程或子进程尝试修改共享的内存空间中的数据时,内核会启动写时复制机制。具体操作是,内核首先将要修改的页面复制一份,然后将修改后的数据写入这个新的页面。这样,父进程和子进程的内存空间就可以分别拥有各自修改后的数据,互不影响。

    • Unix系统中的信号量是一种重要的进程间通信机制,用于实现进程同步、互斥访问和资源控制,提高了系统的并发性能和稳定性。


  1. 创建线程:操作系统通过调用glibc提供的pthread_create函数创建一个新的线程,新的线程共享进程的地址空间、文件描述符、信号处理器等资源。
    • 在copy_process函数中,调用dup_task_struct函数复制task_struct结构体,用于保存线程的信息。
  2. 分析fork.c源码,了解进程的创建过程。
#include <stdio.h>
#include <unistd.h>

int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
printf("fork failed\n");
} else if (pid == 0) {
printf("child process, pid = %d\n", getpid());
} else {
printf("parent process, pid = %d\n", getpid());
}
return 0;
}
  1. 5.0以后,用户调用fork函数,内核通过kernel_clone函数创建线程,了解线程的创建过程。
#include <stdio.h>
#include <unistd.h>
#include <sched.h>

int thread_func(void *arg) {
printf("thread, pid = %d\n", getpid());
return 0;
}

int main() {
char stack[1024*1024];
int pid = clone(thread_func, stack+1024*1024, CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD, NULL);
if (pid < 0) {
printf("clone failed\n");
} else {
printf("parent process, pid = %d\n", getpid());
}
return 0;
}

两个重要操作:

  1. 创建一个task_struct结构体,用于保存线程的信息。
    • 实际通过传入的参数决定是否共享地址空间、文件描述符、信号处理器等资源。
    • 将当前进程的task_struct结构体复制一份,用于保存线程的信息。
    • 选择是否共享地址空间、文件描述符、信号处理器等资源。
  2. 在wake_up_new_task函数中把新进程状态设置为TASK_RUNNING,加入到调度队列中。

5. 进程调度

  1. 在core.c中实现进程调度的相关函数。
  2. 进程调度的过程:
    • 通过调用schedule函数进行进程调度。
    • 选择一个最优的进程进行执行。
    • 切换到新的进程的上下文,继续执行。
    • 对于切换进程,主要包括切换进程的内核栈、页表、寄存器等信息。包括进程的程序计数器、寄存器状态、内存映射等

6. 线程调度

  1. 在sched.c中实现线程调度的相关函数。

7. 两者切换的区别

  1. 进程切换:切换进程的上下文,包括进程的内核栈、页表、寄存器等信息。
  2. 线程切换:切换线程的上下文,包括线程的程序计数器、寄存器状态、栈信息等。
  3. 在switch_mm函数中实现判断是否切换进程的页表。

问:切换页表的过程是怎样的?

切换页表的过程主要包括以下几个步骤:

  1. 保存当前进程的页表:在切换页表之前,需要保存当前进程的页表信息,以便在切换回来时能够正确恢复。
  2. 切换页表:将新进程的页表加载到MMU(内存管理单元)中,使得CPU能够访问新进程的地址空间。
    • 虚拟内存的实现依赖于硬件和操作系统的支持,主要通过页表和地址转换机制来实现虚拟地址到物理地址的映射关系。
  3. 刷新TLB(Translation Lookaside Buffer):TLB是CPU中的一个高速缓存,用于存储虚拟地址到物理地址的映射关系。在切换页表后,需要刷新TLB中的缓存,以便CPU能够正确访问新进程的地址空间。
  4. 示例代码:

void switch_mm(struct mm_struct *prev, struct mm_struct *next) {
// 保存当前进程的页表信息
prev->pgd = read_cr3();
// 切换页表
write_cr3(next->pgd);
// 刷新TLB
flush_tlb();
}

问:线程的程序计数器、寄存器状态、栈信息保存在哪里?

线程的程序计数器、寄存器状态和栈信息等上下文信息通常保存在线程的线程控制块(Thread Control Block,TCB)中。线程控制块是操作系统管理线程的数据结构,用于存储和管理线程的各种状态信息。

具体来说,线程的上下文信息保存在线程控制块中的不同部分:

程序计数器(Program Counter):程序计数器存储了线程当前执行的指令地址,用于指示下一条要执行的指令。当线程被切换时,程序计数器的值会被保存到线程控制块中,以便下次恢复执行时能够继续执行正确的指令序列。

寄存器状态(Register State):寄存器状态包括通用寄存器(如eax、ebx、ecx等)和特殊寄存器(如栈指针ESP、帧指针EBP等)的值。这些寄存器存储了线程当前的运行环境和状态信息,包括局部变量、函数调用信息等。在线程切换时,这些寄存器的值也会被保存到线程控制块中。

栈信息(Stack Information):栈信息包括线程的栈空间大小、栈指针(Stack Pointer,SP)等。栈空间用于存储函数调用过程中的局部变量、函数参数、返回地址等信息。线程的栈信息也会被保存到线程控制块中,以便在切换时能够正确恢复线程的执行环境。

除了这些信息外,线程控制块还可能包含其他与线程管理相关的信息,例如线程的状态(就绪、运行、阻塞等)、优先级、线程ID等。

线程控制块是在内存中吗?

线程控制块通常被存储在操作系统的内核空间中,因为它包含了与线程管理相关的敏感信息和控制信息。具体来说,线程控制块通常包含以下信息:

线程状态信息:记录线程当前的状态,如就绪、运行、阻塞等。

程序计数器(Program Counter):存储了线程当前的执行指令地址。

寄存器状态(Register State):包括通用寄存器和特殊寄存器的值,用于保存线程的运行环境和状态信息。

栈信息(Stack Information):包括栈空间的大小、栈指针等,用于存储函数调用过程中的局部变量、函数参数等。

优先级信息:记录线程的优先级,用于调度器进行线程调度。

线程ID:唯一标识线程的标识符。