Linux进程与线程的区别

一、引言

进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,只是层次角度不同罢了。一般程序员而言,搞清楚二者的概念,在工作实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以至于到今天,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 版内核后才有单独线程实现(线程结构体 )。

(二)不同点

1. 实现方式的差异
  • 资源与调度角色:进程是资源分配基本单位,线程是调度基本单位。进程和线程都可被调度,线程是更小调度单位,进程强调分配资源对象是进程,不会给线程单独分配系统管理资源。运行任务时,需先有进程,子任务以线程身份运行共享资源 。
  • 系统调用与资源复制:进程通过 fork 系统调用实现(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 函数的进程,其他线程是其子线程,多线程主进程常称主线程 。
2. 多任务程序设计模式的区别

多进程程序设计时,因进程间独立,处理资源独立管理有天然优势。如多任务 TCP 服务端,父进程执行 accept() 获客户端连接描述符 DES 后,fork 子进程带 DES 处理连接,父进程继续 accept() ,可用同一变量 val 存 accept() 返回值,子进程复制 val 不影响 。多线程时,父线程不能复用变量 val 多次执行 accept() ,子线程无 val 存储空间,用父线程的,若父线程接受新请求覆盖 val,子线程无法处理上一连接。改进需子线程立马复制 val 到自己栈区,父线程需等子线程复制完再执行新 accept() ,但线程调度独立,父线程难知子线程复制完毕,需线程间通信,子线程复制完通知父线程,这会使父线程处理动作不连贯,效率下降 。另外,资源不独立对线程是缺点,但多进程通信耗时,线程数据共享,不过多个子线程同时写入需互斥,否则数据会 “脏” 。

3. 实体间(进程间,线程间,进线程间)通信方式的不同
  • 进程间通信方式:有共享内存、消息队列、信号量、有名管道、无名管道、信号、文件、socket ,共 8 种 。这些通信方式要么需切换内核上下文(运行环境切换 ),要么涉及外设访问(有名管道、文件 ),速度较慢 。
  • 线程间通信方式:可沿用进程间部分方式,还有互斥量、自旋锁、条件变量、读写锁、线程信号、全局变量等独特方式,共 13 种 。线程间通信用的信号不能用进程间信号(信号基于进程单位,线程共属同一进程空间 ),需用线程信号。线程用特有通信方式,基本在进程空间内完成,无需切换,通信速度较快 。进程与线程间穿插通信,除信号外,其他进程间通信方式也可采用 。
4. 控制方式的异同
  • 身份标识 ID 管理:进程 ID 为 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 资源 。
5. 资源管理方式的异同

进程有独立地址空间,一个进程崩溃,保护模式下不影响其他进程;线程是进程中不同执行路径,有自己堆栈和局部变量,但无单独地址空间,一个线程死掉可能导致整个进程死掉,多进程程序比多线程程序健壮,但进程切换时耗费资源大、效率差 。不过,对于一些要求同时进行且共享变量的并发操作,只能用线程,不能用进程 。

6. 个体间父子关系的迥异

(文中未详细展开,可结合进程创建子进程、线程创建子线程等逻辑理解,进程创建子进程是独立新进程,线程创建子线程是进程内新执行路径 )

7. 进程池与线程池的技术实现差别

(文中未详细展开,一般而言,进程池是管理一组进程,复用进程处理任务,减少进程创建销毁开销;线程池是管理一组线程,复用线程处理任务,减少线程创建销毁开销,二者在资源占用、调度、适用场景等方面有差异 )

四、进程间通信方式(IPC)

(一)通信目的

包括数据传输(一个进程给另一个进程发 1 字节到几 M 字节数据 )、共享数据(多个进程操作共享数据 )、通知事件(一个进程通知其他进程发生某事件,如进程终止通知父进程 )、资源共享(多个进程共享资源,需内核提供锁和同步机制 )、进程控制(如 Debug 进程控制另一个进程执行,拦截陷入和异常,知晓状态改变 ) 。

(二)Linux 进程间通信的发展

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 对可靠信号语义的实现等 ) 。

(三)Linux 使用的进程间通信方式及特点

  1. 管道(pipe)、流管道(s_pipe)和有名管道(FIFO)
    • 管道:半双工通信,数据单向流动,只能在有亲缘关系(通常父子进程 )进程间使用 。
    • 流管道 s_pipe:去除管道单向限制,可双向传输 。
    • 有名管道 name_pipe:克服管道无名字限制,允许无亲缘关系进程间通信,除管道功能外,可让无亲缘关系进程通信 。
  2. 信号量(semaphore):是计数器,控制多个进程对共享资源访问,常作锁机制,防止进程访问共享资源时其他进程同时访问,主要作为进程间及同一进程内不同线程间同步手段 。信号较复杂,用于通知接收进程有事件发生,Linux 支持 Unix 早期信号语义函数 sigal ,也支持符合 Posix.1 标准的 sigaction 函数(基于 BSD,为实现可靠信号机制、统一对外接口,用 sigaction 重新实现 signal 函数 ) 。
  3. 消息队列(message queue):由消息链表组成,存于内核并由消息队列标识符标识,克服信号传递信息少、管道承载无格式字节流及缓冲区大小受限等缺点 。包括 Posix 消息队列和 system V 消息队列,有足够权限进程可添加消息,被赋予读权限进程可读取消息 。
  4. 信号(signal):复杂通信方式,通知接收进程某事件发生,作为进程间及同一进程不同线程间同步手段 。
  5. 共享内存(shared memory):映射一段能被其他进程访问的内存,由一个进程创建,多个进程可访问,是最快的 IPC 方式,针对其他进程间通信方式效率低设计,常与信号量等通信机制配合,实现进程间同步和通信 。
  6. 套接字(socket):进程间通信机制,可用于不同机器间进程通信,是更一般的进程间通信机制,起初由 Unix 系统 BSD 分支开发,现可移植到类 Unix 系统(如 Linux 和 System V 变种支持套接字 ) 。

(四)进程间通信方式的效率比较与优缺点

类型 无连接 可靠 流控制 记录消息类型 优先级
普通 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
  • 管道:速度慢,容量有限,只有父子进程能通讯 。
  • FIFO:任何进程间都能通讯,但速度慢 。
  • 消息队列:容量受系统限制,第一次读要考虑上一次未读完数据问题 。
  • 信号量:不能传递复杂消息,只能用来同步 。
  • 共享内存区:易控制容量,速度快,但要保持同步(如一个进程写时,另一个进程要注意读写问题,相当于线程安全 ),也可用作线程间通讯(不过线程本就共享同一进程内一块内存,没必要 ) 。

若进程间传递信息量少或需通过信号触发行为,软中断信号机制是简捷有效方式;若传递信息量大或有交换数据要求,需考虑其他方式。无名管道简单方便但单向、仅创建它的进程及其子孙进程能用;有名管道虽能给任意关系进程用,但长期存在系统中,使用不当易出错,普通用户一般不建议用;消息缓冲允许任意进程通过共享消息队列通信,由系统调用函数实现消息发送和接收同步,方便但信息复制耗 CPU 时间,不适信息量大或操作频繁场景;共享内存针对消息缓冲缺点,利用内存缓冲区直接交换信息,快捷、信息量大,但进程间读写操作同步问题操作系统无法实现,需进程用其他同步工具解决,且内存实体存在于计算机系统,仅同一计算机系统内进程可共享,不方便网络通信,多个进程使用共享内存块需达成并遵守协议,防止竞争状态,且 Linux 无法严格保证共享内存块独占访问 。

五、多进程与多线程的对比总结

对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需用 IPC;数据分开,同步简单 因共享进程数据,数据共享简单,但同步复杂 各有优势
CPU,内存 占用内存多,切换复杂,CPU 利用率低 占用内存少,切换简单,CPU 利用率高 线程占优
创建,销毁,切换 创建、销毁、切换复杂,速度慢 创建、销毁、切换简单,速度快 线程占优
编程,调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉,导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式,一台机器不够可扩展到多台,进程可跨机器迁移 线程适合于在 SMP 机器上运行,适应于多核分布式 各有优势

六、线程的分类与实现方式

(一)内核级线程(Kernel - Supported Threads,KST)

内核级线程是操作系统内核直接支持和管理的线程,其创建、撤销以及 I/O 操作等都依赖系统调用进入内核,由内核完成处理 。

  • 优势
    • 在多处理器环境下,内核能够调度同一进程内的多个内核级线程同时工作,充分利用硬件资源提升程序执行效率 。比如在多 CPU 服务器上,一个进程的多个内核级线程可分别在不同 CPU 上并行执行任务。
    • 当进程中的某一个内核级线程因等待 I/O 等操作阻塞时,其他内核级线程依然可以正常运行,保证进程整体的执行流畅性 。例如,进程中一个线程在读取磁盘文件时阻塞,其他线程可继续处理网络请求等任务。
  • 不足
    • 用户线程切换到内核级线程时,需要从用户态切换到内核态,由内核完成切换操作,切换代价较大 。每次切换都涉及到不同特权级的转换,会消耗较多 CPU 资源和时间,影响程序整体性能。
    • 内核级线程驻留在内核空间,是内核对象,每个用户线程需映射或绑定到一个内核级线程,用户线程生命周期内会一直绑定该内核级线程,一旦用户线程终止,对应的两个线程(用户线程和绑定的内核级线程)都会离开系统,这种 “一对一” 线程映射方式在一定程度上限制了线程管理的灵活性 。

(二)用户级线程(User - Level Threads,ULT)

用户级线程仅存在于用户空间,其创建、撤销、线程间同步和通信等功能无需系统调用参与,内核完全感知不到用户级线程的存在 。

  • 长处
    • 线程切换无需转换到内核空间,节省了宝贵的内核空间资源,也避免了用户态与内核态切换的开销 。在一些对性能要求较高、线程切换频繁的应用场景(如高并发网络服务器处理大量短连接请求),能有效提升程序运行效率。
    • 调度算法可以是进程专用的,由用户程序自行指定,用户可根据应用需求灵活定制线程调度策略 。比如,针对特定业务逻辑,设计专门的优先级调度或轮转调度算法,优化线程执行顺序。
    • 用户级线程实现与操作系统无关,具有良好的可移植性 。同一套用户级线程代码,可在不同操作系统平台(如 Linux、Windows 等)上运行,无需针对不同系统做大量修改。
  • 短板
    • 系统调用阻塞问题突出,同一进程中若一个用户级线程执行系统调用(如 I/O 操作)阻塞,整个进程都会被阻塞 。因为内核以进程为单位进行调度,无法区分进程内的用户级线程,会将整个进程置为阻塞状态,影响进程内其他线程执行。
    • 一个进程通常只能在一个 CPU 上获得执行机会,无法充分利用多处理器优势 。内核调度进程时,会把整个进程分配到一个 CPU 上,进程内的用户级线程只能在该 CPU 上分时执行,限制了程序的并行处理能力。

(三)混合线程(Hybrid Threads)

混合线程是用户级线程和内核级线程的交叉实现,融合了二者的优势,让运行时库和操作系统都能参与线程管理 。

  • 实现机制:进程拥有自己的内核线程池,可运行的用户线程由运行时库分派并标记为准备好执行的可用线程,操作系统选择用户线程并映射到线程池中的可用内核线程,多个用户线程可分配给相同内核线程,采用 “多对多” 或尽量 “多对一” 的线程映射方式 。例如,进程 A 有两个内核线程,进程 B 有三个内核线程,创建新用户线程时,只需映射到现有内核线程池中的内核线程即可。
  • 独特优势
    • 内核线程池不会被销毁和重建,始终存在于系统中,必要时分配给不同用户级线程,避免了频繁创建销毁内核线程的开销 。相比每次创建用户级线程都新建内核线程,这种方式能有效节省系统资源,提升线程管理效率。
    • 结合了用户级线程切换开销小和内核级线程能利用多处理器优势的特点 。用户级线程在用户空间灵活管理和切换,内核级线程负责与硬件资源适配调度,让程序在性能和资源利用上达到较好平衡。

七、线程上下文解析

(一)进程上下文与线程上下文的概念

操作系统管理众多进程执行,进程在运行过程中,从内核移出、另一个进程成为活动进程时,会发生进程上下文切换 。进程上下文记录了重启进程和启动新进程所需的全部信息,描述进程当前状态,包括进程 id、指向可执行文件的指针、栈、静态和动态分配变量的内存、处理器寄存器等内容 。
线程也有上下文,当线程被抢占时,会发生线程间上下文切换 。若线程属于同一进程,它们共享进程的地址空间,所以进程上下文恢复的多数信息对线程而言并非必需,但线程有本地且唯一的信息,如线程 id、处理器寄存器状态(程序计数器、栈指针等)、线程状态及优先级、线程特定数据(TSD)等 。

(二)进程上下文与线程上下文的对比

上下文内容 进 程 线 程
指向可执行文件的指针 ×
内存(数据段和堆) ×
状态
优先级
程序 I/O 的状态 ×
授予权限 ×
调度信息 ×
审计信息 ×
有关资源的信息(文件描述符、读 / 写指针等) ×
有关事件和信号的信息 ×
寄存器组(栈指针、指令计数器等)

从表格可清晰看出,进程上下文涵盖信息更全面,涉及进程整体运行的各类资源、状态等;线程上下文则聚焦于线程本地且唯一的信息,因线程共享进程地址空间等资源,无需重复记录进程已有的通用信息,只需维护自身执行相关的关键状态数据 。比如,进程上下文需记录指向可执行文件指针以明确程序入口,线程上下文无需,因为线程依托进程的可执行文件运行;而线程上下文和进程上下文都需记录栈、状态、优先级、寄存器组等与执行流相关的信息,保障线程和进程能正确恢复执行 。

(三)线程上下文切换的特点与影响

线程上下文切换发生在线程被抢占时,由于同一进程内线程共享地址空间,相比进程上下文切换,线程上下文切换开销更小 。进程上下文切换需切换地址空间等大量资源,线程上下文切换主要是切换线程本地的寄存器状态、线程特定数据等内容 。
但频繁的线程上下文切换也会影响程序性能 。尽管开销比进程上下文切换小,但若线程数量过多、切换过于频繁,累计的切换时间会消耗大量 CPU 资源,降低程序执行效率 。比如在高并发多线程程序中,若未合理控制线程数量和调度策略,频繁切换可能成为性能瓶颈 。同时,线程上下文切换也需操作系统进行管理和协调,过多切换会增加操作系统内核的负担,影响系统整体的响应速度和稳定性 。

Linux中进程和线程是操作系统中实现多任务并发的重要机制,二者在概念、实现、应用等方面存在诸多区别与联系。理解它们的差异,有助于在实际编程和系统设计中,根据需求合理选择多进程或多线程模型,优化程序性能和资源利用,应对不同场景下的任务处理需求 。

你可能感兴趣的:(Linux,linux,运维,服务器,unix)