进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,只是层次角度不同罢了。一般程序员而言,搞清楚二者的概念,在工作实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以至于到今天,Linux 内核还在持续更新完善 (关于进程和线程的实现模块也是内核完善的任务之一)。
首先,简要了解一下进程和线程。对于操作系统而言,进程是核心之核心,整个现代操作系统的根本,就是以进程为单位在执行任务。系统的管理架构也是基于进程层面的。在按下电源键之后,计算机就开始了复杂的启动过程,此处有一个经典问题:当按下电源键之后,计算机如何把自己由静止启动起来的?同学们自行总结。操作系统启动的过程简直可以描述为从无到有过程,第一个被创造出来的进程是 0 号进程,这个进程在操作系统层面是不可见的,但它存在着。0 号进程完成了操作系统的功能加载与初期设定,然后它创造了 1 号进程 (init),这个 1 号进程就是操作系统的 boss。1 号进程是来管理整个操作系统的,所以在用 pstree 查看进程树可知,1 号进程位于树根。再之后,系统的很多管理程序都以进程身份被 1 号进程创造出来,还创造了与人类沟通的桥梁 ——shell。从那之后,人类可以跟操作系统进行交流,可以编写程序,可以执行任务。
而这一切,都是基于进程的。每一个任务 (进程) 被创建时,系统会为他分配存储空间等必要资源,然后在内核管理区为该进程创建管理节点,以便后来控制和调度该任务的执行。
进程真正进入执行阶段,还需要获得 CPU 的使用权,这一切都是操作系统掌管着,也就是所谓的调度,在各种条件满足 (资源与 CPU 使用权均获得) 的情况下,启动进程的执行过程。
除 CPU 而外,一个很重要的资源就是存储器了,系统会为每个进程分配独有的存储空间,当然包括它特别需要的别的资源,比如写入时外部设备是可使用状态等等。
有了上面的引入,我们可以对进程做一个简要的总结:
进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。它的执行需要系统分配资源创建实体之后,才能进行。
随着技术发展,在执行一些细小任务时,本身无需分配单独资源时 (多个任务共享同一组资源即可,比如所有子进程共享父进程的资源),进程的实现机制依然会繁琐的将资源分割,这样造成浪费,而且还消耗时间。后来就有了专门的多任务技术被创造出来 —— 线程。
线程的特点就是在不需要独立资源的情况下就可以运行。如此一来会极大节省资源开销,以及处理时间。
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。计算机按下电源键启动后,会诞生 0 号进程,它完成操作系统功能加载与初期设定,随后创建 1 号进程(init),1 号进程管理整个操作系统,系统诸多管理程序及与人类交互的 shell 等,都由 1 号进程创建 。每创建一个进程,系统会为其分配存储空间等必要资源,在内核管理区创建管理节点,进程获得 CPU 使用权等资源后,才会进入执行阶段。
随着技术发展,执行细小任务时,进程分割资源会造成浪费和耗时,线程应运而生。线程可在无需独立资源的情况下运行,能极大节省资源开销和处理时间 。
无论是进程还是线程,对于程序员而言,都是实现多任务并发的技术手段。二者均可独立调度,在多任务环境下功能无差异,都有各自实体,是系统独立管理的对象个体,系统层面可通过技术手段控制,且状态相似。在多任务程序中,子进程(子线程)与父进程(父线程)调度一般平等竞争 。早期 Linux 内核 2.4 版以前,线程实现和管理完全按进程方式,2.6 版内核后才有单独线程实现(线程结构体 )。
pid_t fork(void);
),会复制父进程全部资源;线程通过 clone 系统调用实现(int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
),仅复制少部分必要资源,调用 clone 时可通过参数控制复制对象。实际中,多进程程序用 fork 创建子进程,多线程程序常用线程库函数(如 POSIX 线程库的 pthread_create
),线程库内部可能实现 clone 调用 。另外,vfork 系统调用创建的进程共享父进程资源空间,子进程 “结束”(返回时,常接着执行 execv 启动新进程,新进程空间与父进程独立 )后父进程才运行 。exit()
系统调用,此时全部线程同时灭亡 )。多线程程序至少有一个主线程,即含 main
函数的进程,其他线程是其子线程,多线程主进程常称主线程 。多进程程序设计时,因进程间独立,处理资源独立管理有天然优势。如多任务 TCP 服务端,父进程执行 accept()
获客户端连接描述符 DES 后,fork 子进程带 DES 处理连接,父进程继续 accept()
,可用同一变量 val 存 accept()
返回值,子进程复制 val 不影响 。多线程时,父线程不能复用变量 val 多次执行 accept()
,子线程无 val 存储空间,用父线程的,若父线程接受新请求覆盖 val,子线程无法处理上一连接。改进需子线程立马复制 val 到自己栈区,父线程需等子线程复制完再执行新 accept()
,但线程调度独立,父线程难知子线程复制完毕,需线程间通信,子线程复制完通知父线程,这会使父线程处理动作不连贯,效率下降 。另外,资源不独立对线程是缺点,但多进程通信耗时,线程数据共享,不过多个子线程同时写入需互斥,否则数据会 “脏” 。
pid_t
类型(实际是 int
型变量,有限 ),全系统中进程 ID 唯一,通过 PID 管理进程,创建进程时内核创建 task_struct
结构体存进程信息(如 Linux 内核 3.18.1 中 struct task_struct
含 volatile long state
、void *stack
、pid_t pid
等 ) 。线程 ID 是 pthread_t
类型(unsigned long int
型变量 ),范围大,一般在本进程空间内作用,系统管理线程时,在内核创建对应内核态线程,每个用户创建线程有对应内核态线程 。wait()
系统调用回收,未回收的消亡进程成僵尸进程,会虚占 PID 资源 。进程有独立地址空间,一个进程崩溃,保护模式下不影响其他进程;线程是进程中不同执行路径,有自己堆栈和局部变量,但无单独地址空间,一个线程死掉可能导致整个进程死掉,多进程程序比多线程程序健壮,但进程切换时耗费资源大、效率差 。不过,对于一些要求同时进行且共享变量的并发操作,只能用线程,不能用进程 。
(文中未详细展开,可结合进程创建子进程、线程创建子线程等逻辑理解,进程创建子进程是独立新进程,线程创建子线程是进程内新执行路径 )
(文中未详细展开,一般而言,进程池是管理一组进程,复用进程处理任务,减少进程创建销毁开销;线程池是管理一组线程,复用线程处理任务,减少线程创建销毁开销,二者在资源占用、调度、适用场景等方面有差异 )
包括数据传输(一个进程给另一个进程发 1 字节到几 M 字节数据 )、共享数据(多个进程操作共享数据 )、通知事件(一个进程通知其他进程发生某事件,如进程终止通知父进程 )、资源共享(多个进程共享资源,需内核提供锁和同步机制 )、进程控制(如 Debug 进程控制另一个进程执行,拦截陷入和异常,知晓状态改变 ) 。
Linux 进程间通信手段继承自 Unix 平台,AT&T 贝尔实验室和 BSD(加州大学伯克利分校伯克利软件发布中心 )对 Unix 进程间通信贡献大,前者改进扩充形成 “System V IPC”(通信进程限单个计算机内 ),后者形成基于 socket 的进程间通信机制(突破限制 )。Linux 继承二者,有早期 UNIX 进程间通信、基于 System V 进程间通信、基于 Socket 进程间通信、POSIX 进程间通信 。UNIX 进程间通信方式有管道、FIFO、信号;System V 进程间通信方式有 System V 消息队列、System V 信号灯、System V 共享内存 ;POSIX 进程间通信有 posix 消息队列、posix 信号灯、posix 共享内存 。因 Unix 版本多样,IEEE 开发独立 Unix 标准(ANSI Unix 标准,即 POSIX ),大部分 Unix 和流行版本遵循,Linux 一开始就遵循 。很多 Unix 版本单机 IPC 留有 BSD 痕迹(如 4.4BSD 支持匿名内存映射、4.3+BSD 对可靠信号语义的实现等 ) 。
sigal
,也支持符合 Posix.1 标准的 sigaction
函数(基于 BSD,为实现可靠信号机制、统一对外接口,用 sigaction
重新实现 signal
函数 ) 。类型 | 无连接 | 可靠 | 流控制 | 记录消息类型 | 优先级 |
---|---|---|---|---|---|
普通 PIPE | N | Y | Y | N | |
流 PIPE | N | Y | Y | N | |
命名 PIPE (FIFO) | N | Y | Y | N | |
消息队列 | N | Y | Y | Y | |
信号量 | N | Y | Y | Y | |
共享存储 | N | Y | Y | Y | |
UNIX 流 SOCKET | N | Y | Y | N | |
UNIX 数据包 SOCKET | Y | Y | N | N |
若进程间传递信息量少或需通过信号触发行为,软中断信号机制是简捷有效方式;若传递信息量大或有交换数据要求,需考虑其他方式。无名管道简单方便但单向、仅创建它的进程及其子孙进程能用;有名管道虽能给任意关系进程用,但长期存在系统中,使用不当易出错,普通用户一般不建议用;消息缓冲允许任意进程通过共享消息队列通信,由系统调用函数实现消息发送和接收同步,方便但信息复制耗 CPU 时间,不适信息量大或操作频繁场景;共享内存针对消息缓冲缺点,利用内存缓冲区直接交换信息,快捷、信息量大,但进程间读写操作同步问题操作系统无法实现,需进程用其他同步工具解决,且内存实体存在于计算机系统,仅同一计算机系统内进程可共享,不方便网络通信,多个进程使用共享内存块需达成并遵守协议,防止竞争状态,且 Linux 无法严格保证共享内存块独占访问 。
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需用 IPC;数据分开,同步简单 | 因共享进程数据,数据共享简单,但同步复杂 | 各有优势 |
CPU,内存 | 占用内存多,切换复杂,CPU 利用率低 | 占用内存少,切换简单,CPU 利用率高 | 线程占优 |
创建,销毁,切换 | 创建、销毁、切换复杂,速度慢 | 创建、销毁、切换简单,速度快 | 线程占优 |
编程,调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉,导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式,一台机器不够可扩展到多台,进程可跨机器迁移 | 线程适合于在 SMP 机器上运行,适应于多核分布式 | 各有优势 |
内核级线程是操作系统内核直接支持和管理的线程,其创建、撤销以及 I/O 操作等都依赖系统调用进入内核,由内核完成处理 。
用户级线程仅存在于用户空间,其创建、撤销、线程间同步和通信等功能无需系统调用参与,内核完全感知不到用户级线程的存在 。
混合线程是用户级线程和内核级线程的交叉实现,融合了二者的优势,让运行时库和操作系统都能参与线程管理 。
操作系统管理众多进程执行,进程在运行过程中,从内核移出、另一个进程成为活动进程时,会发生进程上下文切换 。进程上下文记录了重启进程和启动新进程所需的全部信息,描述进程当前状态,包括进程 id、指向可执行文件的指针、栈、静态和动态分配变量的内存、处理器寄存器等内容 。
线程也有上下文,当线程被抢占时,会发生线程间上下文切换 。若线程属于同一进程,它们共享进程的地址空间,所以进程上下文恢复的多数信息对线程而言并非必需,但线程有本地且唯一的信息,如线程 id、处理器寄存器状态(程序计数器、栈指针等)、线程状态及优先级、线程特定数据(TSD)等 。
上下文内容 | 进 程 | 线 程 |
---|---|---|
指向可执行文件的指针 | √ | × |
栈 | √ | √ |
内存(数据段和堆) | √ | × |
状态 | √ | √ |
优先级 | √ | √ |
程序 I/O 的状态 | √ | × |
授予权限 | √ | × |
调度信息 | √ | × |
审计信息 | √ | × |
有关资源的信息(文件描述符、读 / 写指针等) | √ | × |
有关事件和信号的信息 | √ | × |
寄存器组(栈指针、指令计数器等) | √ | √ |
从表格可清晰看出,进程上下文涵盖信息更全面,涉及进程整体运行的各类资源、状态等;线程上下文则聚焦于线程本地且唯一的信息,因线程共享进程地址空间等资源,无需重复记录进程已有的通用信息,只需维护自身执行相关的关键状态数据 。比如,进程上下文需记录指向可执行文件指针以明确程序入口,线程上下文无需,因为线程依托进程的可执行文件运行;而线程上下文和进程上下文都需记录栈、状态、优先级、寄存器组等与执行流相关的信息,保障线程和进程能正确恢复执行 。
线程上下文切换发生在线程被抢占时,由于同一进程内线程共享地址空间,相比进程上下文切换,线程上下文切换开销更小 。进程上下文切换需切换地址空间等大量资源,线程上下文切换主要是切换线程本地的寄存器状态、线程特定数据等内容 。
但频繁的线程上下文切换也会影响程序性能 。尽管开销比进程上下文切换小,但若线程数量过多、切换过于频繁,累计的切换时间会消耗大量 CPU 资源,降低程序执行效率 。比如在高并发多线程程序中,若未合理控制线程数量和调度策略,频繁切换可能成为性能瓶颈 。同时,线程上下文切换也需操作系统进行管理和协调,过多切换会增加操作系统内核的负担,影响系统整体的响应速度和稳定性 。
Linux中进程和线程是操作系统中实现多任务并发的重要机制,二者在概念、实现、应用等方面存在诸多区别与联系。理解它们的差异,有助于在实际编程和系统设计中,根据需求合理选择多进程或多线程模型,优化程序性能和资源利用,应对不同场景下的任务处理需求 。