进程、线程和协程是现代计算机系统中实现任务并发执行的三种基本单元。
进程
进程是一个正在执行的程序实例,它是操作系统进行资源分配和调度的基本单位。每个进程都有自己的独立地址空间、内存管理结构、打开文件列表、环境变量等。进程之间的地址空间通常是隔离的,这意味着一个进程无法直接访问另一个进程的内存。
通过系统调用如 fork()
、exec()
、exit()
来创建和终止进程。
操作系统根据一定的调度算法(如轮转调度、优先级调度等)来决定哪个进程获得CPU时间。
进程是程序在数据集上的执行过程,是动态产生、消亡的。
每个进程都拥有独立的地址空间,一个进程崩溃不会影响到其他进程。
进程控制块
进程是资源分配和调度的基本单位。为了管理和跟踪进程的状态及其相关信息,操作系统通常会为每个进程维护一个数据结构,通常称为“进程控制块”(Process Control Block,PCB)。PCB 包含了大量的信息,用以描述进程的状态和行为,使操作系统能够有效地管理进程。
进程控制块的内容
进程控制块(PCB)包含了进程运行所需的全部信息,具体包括但不限于以下几个方面:
进程标识符
- 进程ID (PID):唯一的进程标识符,用于区分不同的进程。
- 父进程ID (PPID):创建当前进程的父进程的PID。
- 进程组ID (PGID):用于标识进程所属的进程组,通常用于信号处理。
CPU 相关信息
- 程序计数器(Program Counter,PC):指示下一条指令的地址。
- 寄存器集合:
- 通用寄存器:存储函数参数和返回值,以及局部变量。
- 程序状态寄存器(PSW):包含条件码、模式标志等信息。
- 指令寄存器(IR):当前正在执行的指令。
- 栈指针(SP):指向当前进程的栈顶。
- 程序状态字 (PSW):包含了当前进程的状态信息,如中断屏蔽位等。
- 调度信息:优先级、调度策略、进程运行时间等信息,用于调度算法。
进程状态
- 状态标志:表示进程当前的状态,如就绪(Ready)、运行(Running)、等待(Waiting/Sleeping)等。
- 阻塞原因:如果进程处于等待状态,记录导致阻塞的原因,如等待I/O完成、等待信号量等。
内存管理信息
- 页表:如果操作系统使用了分页内存管理,那么PCB中会包含页表的指针,用于映射虚拟地址到物理地址。
- 段表:对于分段内存管理的系统,PCB中会包含段表的指针。
- 内存分配信息:记录进程占用的内存区域,如堆、栈等。
文件系统信息
- 打开文件表:记录进程打开的所有文件描述符及其对应的文件对象。
- 当前工作目录:进程当前的工作目录路径。
- 文件权限掩码:用于控制新建文件的权限。
进程控制信息
- 信号队列:记录发送给进程但尚未处理的信号。
- 等待队列:如果进程因某种原因而被阻塞,它会被加入到相应的等待队列中。
- 事件队列:记录进程感兴趣的事件,如定时器到期、I/O 完成等。
进程属性
- 用户/组信息:记录进程运行时的身份信息,如实际用户ID(UID)、有效用户ID(EUID)、实际组ID(GID)、有效组ID(EGID)等。
- 环境变量:进程的环境变量集合。
- 优先级:进程的调度优先级。
- 资源限制:进程的最大资源使用限制,如最大内存使用量、最大文件大小等。
子进程信息
- 子进程链表:如果进程创建了子进程,那么PCB中会包含指向子进程PCB的链接。
进程间通信
进程间的通信(Inter-Process Communication,IPC)是指在一个操作系统中运行的不同进程之间相互发送信息和数据的过程。这种机制使得多个进程能够协同工作,共同完成任务。
在现代操作系统中,每个进程通常都有独立的虚拟地址空间,它们不能直接访问其他进程的内存。因此,为了实现进程间的数据交换和信息共享,需要特定的系统调用和功能来实现这一目标。
管道
管道 (Pipes)是用于连接两个进程之间的一个单向数据通道,分为匿名管道和命名管道。
管道实质上是一个内核缓冲区,这个缓冲区的大小是有限的。
匿名管道
只能用于具有亲缘关系的进程(如父子进程)之间的通信。它是半双工的,数据只能在一个方向上流动,且传输的是无格式字节流。
使用 pipe()
系统调用来创建一个管道,父进程或子进程可以通过 write()
向管道中写入数据,另一端通过 read()
读取数据。
命名管道
命名管道,也称为FIFO,是一种特殊的文件,位于文件系统中,可以被任何进程访问,因此可以用于无关进程之间的通信。
使用 mkfifo()
创建命名管道,进程可以通过 open()
打开这个文件,然后使用标准的文件读写函数进行通信。
消息队列
消息队列(Message Queues)是一种高级的 IPC 方法,它提供了在进程之间发送消息的能力。消息队列中的消息具有一定的结构,可以包含消息类型等信息。
消息队列是一种链表结构,存放在内核中并由标识符标识。它可以传输格式化的数据(消息),并且支持消息优先级。
消息队列由内核管理,可以保证消息的传递顺序。
使用系统调用 msgget()
创建消息队列, msgsnd()
发送消息,msgrcv()
接收消息。
优点在于其能够实现异步通信,并且可以高效地处理大量数据,其缺点包括在内核和用户空间之间需要进行四次数据拷贝,这会影响性能。
共享内存
共享内存 (Shared Memory)允许两个或多个进程共享同一块内存区域。这是一种高效的通信方式,因为数据不需要在内核空间和用户空间之间复制。但是还是需要配合信号量等同步机制来避免竞态条件。
使用 shmget()
创建共享内存段,shmat()
将其映射到进程的地址空间,进程可以通过指针直接读写这块内存。
信号量
信号量(Semaphores)主要用于控制多个进程对共享资源的访问,常作为一种锁机制使用。它可以用来实现进程间的同步和互斥,主要用于同步,不是直接的通信手段。
系统提供了两种信号量:一种是基于内存的信号量,另一种是System V信号量,可以通过 semget()
, semop()
, semctl()
系统调用进行操作。
这通常用于分布式系统中,但也可以在同一台机器的不同进程之间使用。
信号
信号 (Signal)是一种软件中断机制,用于通知接收进程某个事件发生了。它常用于传递少量信息或实现简单的错误处理。它是异步的,即发送信号可以在任何时候发送,而接收信号的过程可能正在做其他事情。
常见的信号包括SIGINT(中断)、SIGTERM(终止)和SIGPIPE(管道破裂)等,用来处理一些异常情况,比如终止进程、暂停进程等。
套接字
除了本地进程间通信方法之外,还可以使用网络协议来进行进程间通信。
套接字 (Sockets)是网络通信的基础,它提供了一种在不同主机或同一主机上的不同进程之间建立连接并进行数据交换的方式。
使用 socket()
创建套接字,bind()
绑定地址,listen()
开始监听连接,accept()
接受连接,connect()
连接到远程地址,send()
和 recv()
发送和接收数据。
线程
线程是进程中的一个执行单元,它是CPU调度和分派的基本单位。同一进程内的所有线程共享该进程的资源,包括地址空间、文件描述符、环境变量等。这意味着线程之间的通信更加直接和高效,但是这也意味着线程之间必须小心地管理共享资源以防止数据竞争条件。
线程控制块
线程是进程中的一个执行单元,是操作系统调度的基本单位之一。为了管理和跟踪线程的状态及其相关信息,操作系统通常会为每个线程维护一个数据结构,通常称为“线程控制块”(Thread Control Block, TCB)。TCB 包含了大量的信息,用以描述线程的状态和行为,使操作系统能够有效地管理线程。
线程控制块的主要内容
线程控制块(TCB)包含了线程运行所需的全部信息,具体包括但不限于以下几个方面:
线程标识符
- 线程ID (TID):唯一的线程标识符,用于区分不同的线程。
- 进程ID (PID):线程所属进程的标识符。
线程状态
- 状态标志:表示线程当前的状态,如就绪(Ready)、运行(Running)、等待(Waiting/Sleeping)等。
- 阻塞原因:如果线程处于等待状态,记录导致阻塞的原因,如等待I/O完成、等待信号量等。
CPU 相关信息
- 寄存器值:保存线程执行时的寄存器状态,包括程序计数器(PC)、指令寄存器(IR)、状态寄存器(SR)、通用寄存器等。
- 程序状态字 (PSW):包含了当前线程的状态信息,如中断屏蔽位等。
- 调度信息:优先级、调度策略、线程运行时间等信息,用于调度算法。
栈信息
- 栈指针:指向线程的栈底或栈顶的位置。
- 栈大小:线程栈的大小。
局部变量
- 局部变量:线程的局部变量,这些变量只在线程的生命周期内有效。
- 线程专用数据 (TLS):线程局部存储,每个线程有一份独立的副本,用于存储线程特有的数据。
锁和同步信息
- 锁信息:线程持有的锁或其他同步对象的状态信息。
- 条件变量:线程可能等待的条件变量信息。
线程控制信息
- 信号队列:记录发送给线程但尚未处理的信号。
- 等待队列:如果线程因某种原因而被阻塞,它会被加入到相应的等待队列中。
- 事件队列:记录线程感兴趣的事件,如定时器到期、I/O 完成等。
线程属性
- 优先级:线程的调度优先级。
- 资源限制:线程的最大资源使用限制,如最大内存使用量、最大文件大小等。
- 用户/组信息:记录线程运行时的身份信息,如实际用户ID(UID)、有效用户ID(EUID)、实际组ID(GID)、有效组ID(EGID)等。
线程间的协作
- 线程组:线程所属的线程组信息,用于同步操作。
- 线程间通信:线程间可能存在的通信机制信息。
不同的操作系统可能会有不同的TCB实现。例如,在Linux系统中,线程本质上是作为轻量级进程(LWP)来实现的,而在Windows系统中,线程则是作为内核对象来管理的。
在Linux系统中,线程实际上是作为进程的一个特例来实现的。Linux内核中并没有专门的线程数据结构,而是将线程视为一个特殊的进程。每个线程都有一个 struct task_struct
结构体,这是Linux内核中表示进程和线程的数据结构。task_struct
包含了上述提到的各种信息以及其他用于内核管理和调度的信息。
查看代码
struct task_struct {
...
pid_t pid; /* 进程ID */
pid_t tgid; /* 进程组ID (主线程的pid) */
struct list_head thread_group; /* 线程组链表 */
...
unsigned long state; /* 状态标志 */
...
struct mm_struct *mm; /* 内存管理信息 */
...
struct files_struct *files; /* 打开文件信息 */
...
struct signal_struct *signal; /* 信号信息 */
...
struct task_struct *parent; /* 父进程指针 */
...
struct thread_struct thread; /* 寄存器等信息 */
...
};
这里的 struct thread_struct
包含了寄存器信息等与CPU相关的数据,struct mm_struct
则包含了内存管理信息,struct files_struct
包含了文件描述符表等信息。这些结构体共同构成了一个完整的线程控制块。
线程间通信
线程间的通信(Inter-Thread Communication,ITC)是指在一个进程内的多个线程之间进行信息交换的过程。与进程间的通信(IPC)相比,线程间的通信通常更为简单和高效,因为它们共享相同的地址空间,可以直接访问相同的内存区域。然而,线程间通信同样需要考虑同步和互斥的问题,以避免数据竞争和死锁等情况。
共享内存
由于线程共享同一个进程的地址空间,它们可以直接访问进程中的全局变量、静态变量以及堆分配的内存。这是线程间通信最简单的方式,但需要同步机制来避免竞态条件。
同步机制:
- 互斥锁(Mutexes):用于保护共享资源,确保同一时间只有一个线程可以访问该资源。
- 读写锁(Read-Write Locks):允许多个读操作同时进行,但写操作需要独占访问。
- 条件变量(Condition Variables):允许线程在某些条件下挂起或被唤醒。
线程局部存储
线程局部存储(Thread Local Storage,TLS) 提供每个线程私有的存储空间,因此可以用来存储线程特定的数据,而不需要担心与其他线程的冲突。
信号量
信号量(Semaphores)是一种常用的同步机制,可以用来控制对共享资源的访问。它是一个整数变量,可以用来计数或表示资源的状态。
- 二进制信号量:通常用于互斥。
- 计数信号量:可以用来表示可用资源的数量。
管道和消息队列
虽然这些机制通常用于进程间通信,但在某些线程库中,它们也可以用于线程间的通信。
协程
协程(Coroutine)是一种用户空间的轻量级线程,它不像线程那样由操作系统内核调度,而是由用户程序自己控制调度逻辑。协程拥有自己的堆栈,可以保存局部变量状态,并且可以在某个点暂停执行并恢复到之前的状态。协程之间的切换开销非常小,因为它们不需要涉及内核态和用户态之间的切换。
协程的特点
轻量级
协程是比线程更轻量级的执行单元。每个协程在创建时不需要像线程那样复制整个线程栈,因此创建和销毁协程的开销远小于线程。
协作式多任务
协程通过显式的暂停(yield)和恢复(resume)操作来切换执行,而非抢占式。这意味着协程会在明确的切换点交出控制权,而不是被操作系统强制中断。
非阻塞式编程
协程通常用于实现非阻塞式的代码,可以提高程序的响应性和效率。当协程遇到 I/O 操作时,它会主动让出 CPU,等待 I/O 操作完成后再恢复执行。
共享资源
协程通常运行在同一个线程中,共享同一地址空间。这使得协程之间可以轻松地共享数据,但同时也需要特别注意数据的一致性。
上下文切换开销小
协程的上下文切换开销非常小,通常只涉及寄存器的保存和恢复。这使得协程非常适合用于 I/O 密集型应用,因为大部分时间都花在等待 I/O 操作完成上。
易于使用
协程的使用通常比线程更简单,因为它们不需要复杂的同步机制(如锁、信号量等)。协程库或语言内置的机制通常提供了易于使用的 API,使得开发者可以更专注于业务逻辑。
上下文
上下文(Context)在计算机科学中指的是一个执行单元的状态,它包含了该执行单元的当前状态信息,以便在执行单元暂停或恢复执行时能够恢复到正确的执行环境。上下文可以包括程序计数器、寄存器状态、栈指针、程序变量等。
进程上下文
进程上下文是指一个进程在执行过程中所需要的所有状态信息,包括但不限于:
- 寄存器状态:CPU寄存器的值,如程序计数器(PC)、指令寄存器(IR)、状态寄存器(SR)、通用寄存器等。
- 栈信息:栈的基址和栈顶指针。
- 内存映射:进程的虚拟地址空间及其映射情况。
- 打开的文件描述符:进程打开的所有文件的描述符。
- 环境变量:进程的环境变量集合。
- 信号队列:发送给进程但尚未处理的信号。
- 资源限制:进程的最大资源使用限制,如最大内存使用量、最大文件大小等。
- 用户/组信息:进程的实际用户ID(UID)、有效用户ID(EUID)、实际组ID(GID)、有效组ID(EGID)等。
- 优先级:进程的调度优先级。
- 调度策略:进程采用的调度策略。
线程上下文
线程上下文是指一个线程在执行过程中所需要的所有状态信息,通常包括:
- 寄存器状态:线程使用的CPU寄存器的值。
- 栈信息:线程的栈基址和栈顶指针。
- 内存映射:线程使用的内存区域。
- 锁信息:线程持有的锁或其他同步对象的状态信息。
- 条件变量:线程可能等待的条件变量信息。
- 优先级:线程的调度优先级。
- 资源限制:线程的最大资源使用限制。
协程上下文
协程上下文是指一个协程在执行过程中所需要的所有状态信息,通常包括:
- 寄存器状态:协程使用的CPU寄存器的值。
- 栈信息:协程的栈基址和栈顶指针。
- 内存映射:协程使用的内存区域。
- 局部变量:协程的局部变量。
- 函数指针:协程执行的函数指针。
- 参数:协程函数的参数。
- 返回值:协程的返回值。
- 状态标志:协程当前的状态(如就绪、运行、挂起等)。
上下文切换
上下文切换是指操作系统在切换执行的进程或线程时所进行的操作,主要包括保存当前执行单元的上下文信息,并加载下一个执行单元的上下文信息。上下文切换是操作系统调度中的一个重要环节。
上下文切换的过程
- 保存当前上下文:操作系统保存当前执行单元(进程或线程)的所有寄存器值、栈指针等信息。
- 选择新的执行单元:操作系统根据调度策略选择下一个要执行的进程或线程。
- 加载新上下文:操作系统将选中的新执行单元的上下文信息加载到CPU中,恢复其寄存器值、栈指针等。
上下文切换的影响
上下文切换会对系统的性能产生影响,具体表现为:
- 开销:上下文切换涉及到大量的寄存器保存和恢复操作,这会消耗CPU时间和内存带宽。
- 延迟:频繁的上下文切换会导致程序执行的延迟,影响系统的响应速度。
- 吞吐量:过多的上下文切换会降低系统的整体吞吐量,因为CPU的时间被频繁的切换消耗掉了。