线程和进程的深入解读

简介:  自己理解:  进程是操作系统分配资源的最小单位,线程是cpu计算的一种方式,是一种最优方式,可以很好的实现cpu最大化的运行和程序的正常话。 关于进程和线程,大家总是说的一句话是“进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元”。

 

自己理解:  进程是操作系统分配资源的最小单位,线程是cpu计算的一种方式,是一种最优方式,可以很好的实现cpu最大化的运行和程序的正常话。

 

关于进程和线程,大家总是说的一句话是“进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元”。这句话理论上没问题,我们来看看什么是所谓的“资源”呢。

 

什么是计算机资源

 

经典的冯诺依曼结构把计算机系统抽象成 CPU + 存储器 + IO,那么计算机资源无非就两种:

1. 计算资源

2. 存储资源

 

CPU是计算单元,单纯从CPU的角度来说它是一个黑盒,它只对输入的指令和数据进行计算,然后输出结果,它不负责管理计算哪些”指令和数据“。 换句话说CPU只提供了计算能力,但是不负责分配计算资源。

 

计算资源是操作系统来分配的,也就是常说的操作系统的调度模块,由操作系统按照一定的规则来分配什么时候由谁来获得CPU的计算资源,比如分时间片

 

存储资源就是内存,磁盘这些存储设备的资源。在这篇计算机底层知识拾遗(一)理解虚拟内存机制 我们说了操作系统使用了虚拟内存机制来管理存储器,从缓存原理的角度来说,把内存作为磁盘的缓存。进程是面向磁盘的,为什么这么说呢,进程表示一个运行的程序,程序的代码段,数据段这些都是存放在磁盘中的,在运行时加载到内存中。所以虚拟内存面向的是磁盘,虚拟页是对磁盘文件的分配,然后被缓存到物理内存的物理页中。

 

所以存储资源是操作系统由虚拟内存机制来管理和分配的。进程应该是操作系统分配存储资源的最小单元。

 

再来看看线程,理论上说Linux内核是没有线程这个概念的,只有内核调度实体(Kernal Scheduling Entry, KSE)这个概念。Linux的线程本质上是一种轻量级的进程,是通过clone系统调用来创建的。何谓“轻量级”会在后面细说。进程是一种KSE,线程也是一种KSE。所以“线程是操作系统调度的最小单元”这句话没问题。

 

什么是进程

进程是对计算机的一种抽象,

1. 进程表示一个逻辑控制流,就是一种计算过程,它造成一个假象,好像这个进程一直在独占CPU资源

2. 进程拥有一个独立的虚拟内存地址空间,它造成一个假象,好像这个进程一致在独占存储器资源

 

这张图是进程的虚拟内存地址空间的分配模型图,可以看到进程的虚拟内存地址空间分为用户空间和内核空间。用户空间从低端地址往高端地址发展,内核空间从高端地址往低端地址发展。用户空间存放着这个进程的代码段和数据段,以及运行时的堆和用户栈。堆是从低端地址往高端地址发展,栈是从高端地址往低端地址发展。

 

内核空间存放着内核的代码和数据,以及内核为这个进程创建的相关数据结构,比如页表数据结构,task数据结构,area区域数据结构等等。

 

 

从文件IO的角度来说,Linux把一切IO都抽象成了文件,比如普通文件IO,网络IO,统统都是文件,利用open系统调用返回一个整数作为文件描述符file descriptor,进程可以利用file descriptor作为参数在任何系统调用中表示那个打开的文件。内核为进程维护了一个文件描述符表来保持进程所有获得的file descriptor。

每调用一次open系统调用内核会创建一个打开文件open file的数据结构来表示这个打开的文件,记录了该文件目前读取的位置等信息。打开文件又唯一了一个指针指向文件系统中该文件的inode结构。inode记录了该文件的文件名,路径,访问权限等元数据。

 

操作操作系统用了3个数据结构来为每个进程管理它打开的文件资源

 

fork系统调用

操作系统利用fork系统调用来创建一个子进程。fork所创建的子进程会复制父进程的虚拟地址空间。

要理解“复制”和“共享”的区别,复制的意思是会真正在物理内存复制一份内容,会真正消耗新的物理内存。共享的意思是使用指针指向同一个地址,不会真正的消耗物理内存。理解这两个概念的区别很重要,这是进程和线程的根本区别之一。

 

那么有人问了如果我父进程占了1G的物理内存,那么fork会再使用1G的物理内存来复制吗,相当于一下用了2G的物理内存? 

答案是早期的操作系统的确是这么干的,但是这样性能也太差了,所以现代操作系统使用了 写时复制Copy on write的方式来优化fork的性能,fork刚创建的子进程采用了共享的方式,只用指针指向了父进程的物理资源。当子进程真正要对某些物理资源写操作时,才会真正的复制一块物理资源来供子进程使用。这样就极大的优化了fork的性能,并且从逻辑来说子进程的确是拥有了独立的虚拟内存空间。

fork不只是复制了页表结构,还复制了父进程的文件描述符表,信号控制表,进程信息,寄存器资源等等。它是一个较为深入的复制。

 

从逻辑控制流的角度来说,fork创建的子进程开始执行的位置是fork函数返回的位置。这点和线程是不一样的,我们知道Java中的Thread需要写run方法,线程开始后会从run方法开始执行。

 

既然我们知道了内核为进程维护了这么多资源,那么当内存进行进程调度时进行的进程上下文切换就容易理解了,一个进程运行要依赖这么些资源,那么进程上下文切换就要把这些资源都保存起来写回到内存中,等下次这个进程被调度时再把这些资源再加载到寄存器和高速缓存硬件。

进程上下文切换保存的内容有:

页表 -- 对应虚拟内存资源

文件描述符表/打开文件表 -- 对应打开的文件资源

寄存器 -- 对应运行时数据

信号控制信息/进程运行信息

 

进程间通信

虚拟内存机制为进程管理存储资源带来了种种好处,但是它也给进程带来了一些小麻烦,我们知道每个进程拥有独立的虚拟内存地址空间,看到一样的虚拟内地址空间视图,所以对不同的进程来说,一个相同的虚拟地址意味着不同的物理地址。伤感的句子我们知道CPU执行指令时采用了虚拟地址,对应一个特定的变量来说,它对应着一个特定的虚拟地址。这样带来的问题就是两个进程不能通过简单的共享变量的方式来进行进程间通信,也就是说进程不能通过直接共享内存的方式来进行进程间通信,只能采用信号,管道等方式来进行进程间通信。这样的效率肯定比直接共享内存的方式差

 

什么是线程

上面说了一堆内核为进程分配了哪些资源,我们知道进程管理了一堆资源,并且每个进程还拥有独立的虚拟内存地址空间,会真正地拥有独立与父进程之外的物理内存。并且由于进程拥有独立的内存地址空间,导致了进程之间无法利用直接的内存映射进行进程间通信。

 

并发的本质是在时间上重叠的多个逻辑流,也就是说同时运行的多个逻辑流。并发编程要解决的一个很重要的问题就是对资源的并发访问的问题,也就是共享资源的问题。而两个进程恰恰很难在逻辑上表示共享资源。

线程解决的最大问题就是它可以很简单地表示共享资源的问题,这里说的资源指的是存储器资源,资源最后都会加载到物理内存,一个进程的所有线程都是共享这个进程的同一个虚拟地址空间的,也就是说从线程的角度来说,它们看到的物理资源都是一样的,这样就可以通过共享变量的方式来表示共享资源,也就是直接共享内存的方式解决了线程通信的问题。而线程也表示一个独立的逻辑流,这样就完美解决了进程的一个大难题。

 

从存储资源的角度理解了线程之后,就不难理解计算资源的分配了。从计算资源的角度来说,对内核而言,进程和线程没有什么区别,所以内核用内核调度实体(KSE)来表示一个调度的单元。

 

clone系统调用

在Linux系统中,线程是使用clone系统调用,clone是一个轻量级的fork,淘宝开店教程它提供了一系列的参数来表示线程可以共享父类的哪些资源,比如页表,打开文件表等等。我们上面说过了共享和复制的区别,共享只是简单地用指针指向同一个物理地址,不会在父进程之外开辟新的物理内存。

clone系统调用可以指定创建的线程开始执行代码位置,也就是Java中的Thread类的run方法。

 

Linux内核只提供了clone这个系统调用来创建类似线程的轻量级进程的概念。C语言利用了Pthreads库来真正创建了线程这个数据结构。Linux采用了1:1的模型,即C语言的Pthreads库创建的线程实体1:1对应着内核创建的一个KSE。Pthreads运行在用户空间,KSE运行在内核空间。

 

既然线程共享了进程的资源,那么线程的上下文切换就好理解了。对操作系统来说,它看到要被调度进来的线程和刚运行的线程是同一个进程的,那么线程的上下文切换只需要保存线程的一些运行时的数据,比如

线程的id

寄存器中的值

栈数据

 

而不需要像进程上下文切换那样要保存页表,文件描述符表,信号控制数据和进程信息等数据。页表是一个很重的资源,我们之前说过,如果采用一级页表的结构,那么32位机器的页表要达到4MB的物理空间。 有一种沉默是感怀所以线程上下文切换是很轻量级的。

 

进程采用父子结构,init进程是最顶端的父进程,其他进程都是从init进程派生出来的。这样就很容易理解进程是如何共享内核的代码和数据的了。

而线程采用对等结构,即线程没有父子的概念,所有线程都属于同一个线程组,线程组的组号等于第一个线程的线程号。

 

我们来看看Java的线程到底是如何实现的。Java语言层面提供了java.lang.Thread这个类来表示Java语言层面的线程,并提供了run方法表示线程运行的逻辑控制流。

我们知道JVM是C++/C写的,JVM本身利用了Pthreads库来创建操作系统的线程。JVM还要支持Java语言创建的线程的概念。

聊聊JVM(五)从JVM角度理解线程 这篇已经说了从JVM的角度如何理解线程。 JVM提供了JavaThread类来对应Java语言的Thread,即Java语言中创建一个java.lang.Thread对象,JVM会相应的在JVM中创建一个JavaThread对象。同时JVM还创建了一个OSThread类来对应用Pthreads创建的底层操作系统的线程对象。

 

构建并发程序可以基于进程也可以线程,

比如Nginx就是基于进程构建并发程序的。而Java天生只支持基于线程的方式来构建并发程序。

 

最后再总结一下  进程VS 线程

 

1. 进程采用fork创建,线程采用clone创建
2. 进程fork创建的子进程的逻辑流位置在fork返回的位置,线程clone创建的KSE的逻辑流位置在clone调用传入的方法位置,比如Java的Thread的run方法位置
3. 进程拥有独立的虚拟内存地址空间和内核数据结构(页表,打开文件表等),当子进程修改了虚拟页之后,会通过写时拷贝创建真正的物理页。线程共享进程的虚拟地址空间和内核数据结构,共享同样的物理页
4. 多个进程通信只能采用进程间通信的方式,比如信号,管道,而不能直接采用简单的共享内存方式,原因是每个进程维护独立的虚拟内存空间,所以每个进程的变量采用的虚拟地址是不同的。多个线程通信就很简单,直接采用共享内存的方式,因为不同线程共享一个虚拟内存地址空间,变量寻址采用同一个虚拟内存
5. 进程上下文切换需要切换页表等重量级资源,线程上下文切换只需要切换寄存器等轻量级数据
6. 进程的用户栈独享栈空间,线程的用户栈共享虚拟内存中的栈空间,没有进程高效
7. 一个应用程序可以有多个进程,执行多个程序代码,多个线程只能执行一个程序代码,共享进程的代码段
8. 进程采用父子结构,线程采用对等结构

目录
相关文章
|
18天前
|
消息中间件 并行计算 安全
进程、线程、协程
【10月更文挑战第16天】进程、线程和协程是计算机程序执行的三种基本形式。进程是操作系统资源分配和调度的基本单位,具有独立的内存空间,稳定性高但资源消耗大。线程是进程内的执行单元,共享内存,轻量级且并发性好,但同步复杂。协程是用户态的轻量级调度单位,适用于高并发和IO密集型任务,资源消耗最小,但不支持多核并行。
37 1
|
1天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
4天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
13 2
|
5天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
30天前
|
存储 消息中间件 人工智能
进程,线程,协程 - 你了解多少?
本故事采用简洁明了的对话方式,尽洪荒之力让你在轻松无负担的氛围中,稍微深入地理解进程、线程和协程的相关原理知识
40 2
进程,线程,协程 - 你了解多少?
|
16天前
|
Python
Python中的多线程与多进程
本文将探讨Python中多线程和多进程的基本概念、使用场景以及实现方式。通过对比分析,我们将了解何时使用多线程或多进程更为合适,并提供一些实用的代码示例来帮助读者更好地理解这两种并发编程技术。
|
18天前
|
消息中间件 并行计算 安全
进程、线程、协程
【10月更文挑战第15天】进程、线程和协程是操作系统中三种不同的执行单元。进程是资源分配和调度的基本单位,每个进程有独立的内存空间;线程是进程内的执行路径,共享进程资源,切换成本较低;协程则更轻量,由用户态调度,适合处理高并发和IO密集型任务。进程提供高隔离性和安全性,线程支持高并发,协程则在资源消耗和调度灵活性方面表现优异。
43 2
|
24天前
|
算法 安全 调度
深入理解操作系统:进程与线程的管理
【10月更文挑战第9天】在数字世界的心脏跳动着的,不是别的,正是操作系统。它如同一位无形的指挥家,协调着硬件与软件的和谐合作。本文将揭开操作系统中进程与线程管理的神秘面纱,通过浅显易懂的语言和生动的比喻,带你走进这一复杂而又精妙的世界。我们将从进程的诞生讲起,探索线程的微妙关系,直至深入内核,理解调度算法的智慧。让我们一起跟随代码的脚步,解锁操作系统的更多秘密。
34 1
|
30天前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
29 3
|
10天前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。