线程之间共享哪些资源?

336

最近在面试中被问到一个问题,进程和线程的区别是什么。实际上我并不知道答案,思考了一会儿后给出了一个非常奇怪的回答:

线程共享内存,进程不共享。回答完这个问题后,面试官冷笑着向我提出了下面的问题:

问:你知道程序被分成哪些段吗?

我的回答:当然(认为这是一个简单的问题)栈、数据、代码、堆。

问:那么,告诉我,线程共享哪些段?

我无法回答这个问题,最终只好说共享所有部分。

请问有谁能够正确而且令人印象深刻地解释进程和线程之间的区别呢?


17
线程共享相同的虚拟地址空间,进程不共享。 - Benny
2
可能是进程和线程的区别是什么?的重复问题。 - sashoalm
1
可能有点晚,但这非常有用:https://www.cs.rutgers.edu/~pxk/416/notes/05-threads.html - Mahi
如果代码是动态链接库,甚至可以在进程之间共享,对吗? - caramel1995
这个答案提供了对你问题的回答。请看一下表格。 - undefined
13个回答

255

你的理解基本正确,但线程共享所有段 除了 栈。线程有独立的调用栈,然而其他线程栈中的内存仍然是可访问的,并且理论上你可以保存一个指向另一个线程本地栈帧中的内存的指针(虽然你可能应该找一个更好的位置来放置该内存!)。


54
有趣的是,尽管线程有独立的调用堆栈,但其他堆栈中的内存仍然是可访问的。 - Karthik Balaguru
1
是的 - 我在想是否可以在线程之间访问其他堆栈中的内存?只要您确定不尝试引用已被释放的堆栈,我不确定是否存在问题? - bph
3
“@bph: 可以访问另一个线程的堆栈内存,但出于良好的软件工程实践考虑,我不认为这样做是可接受的。” - Greg Hewgill
1
访问其他线程的堆栈,特别是写入其中,会干扰几个垃圾收集器的实现。尽管如此,这可能可以被视为垃圾收集器实现的错误。 - yyny
@MegaLegend:如果你对教授所讲的内容有疑问,请直接向教授提问,而不是在一个13年前的问题下发表评论。 - Greg Hewgill
显示剩余3条评论

70
需要指出的是,这个问题实际上有两个方面——理论方面和实现方面。
首先,让我们看看理论方面。要理解进程和线程之间的区别以及它们之间共享了什么,您需要从概念上理解进程是什么。
我们从现代操作系统第三版(作者:Tanenbaum)的第2.2.2节“经典线程模型”中得到以下内容:
“进程模型基于两个独立的概念:资源分组和执行。有时将它们分开是很有用的,这就是线程的作用...”
他接着说:
一个查看进程的方式是将相关资源分组在一起。进程具有地址空间,其中包含程序文本和数据,以及其他资源。这些资源可能包括打开的文件、子进程、待处理的警报、信号处理程序、记帐信息等。通过将它们组合成进程的形式,可以更轻松地管理它们。进程的另一个概念是执行线程,通常缩写为线程。线程具有程序计数器,用于跟踪下一条指令的执行。它有寄存器,保存其当前工作变量。它有一个堆栈,其中包含执行历史记录,每个已调用但尚未返回的过程都有一个框架。虽然线程必须在某个进程中执行,但线程和其进程是不同的概念,可以分别处理。进程用于将资源分组在一起;线程是在CPU上执行的实体。
接着,他提供了以下表格:
Per process items             | Per thread items
------------------------------|-----------------
Address space                 | Program counter
Global variables              | Registers
Open files                    | Stack
Child processes               | State
Pending alarms                |
Signals and signal handlers   |
Accounting information        |

上述内容是实现线程所需的必要条件。正如其他人指出的那样,像segments这样的东西是依赖于操作系统的实现细节。

5
这是一份很好的解释。但为了被视为“答案”,它可能需要与问题有所联系。 - catalyst294
关于这个表格,程序计数器不是一个寄存器吗?线程的“状态”是否也被捕获在寄存器的值中?我还缺少指向它们运行的代码(进程文本指针)。 - onlycparra
每个线程的线程本地存储也不会在线程之间共享,存储在其中的变量对于特定线程而言就像全局变量一样。 - Pradeep Anchan

61

来自维基百科(我认为这将是面试官的很好答案:P)

线程与传统多任务操作系统进程的不同之处在于:

  • 进程通常是独立的,而线程作为进程的子集存在。
  • 进程携带大量状态信息,而进程内的多个线程共享状态、内存和其他资源。
  • 进程有单独的地址空间,而线程共享它们的地址空间。
  • 进程仅通过系统提供的进程间通信机制进行交互。
  • 同一进程中的线程之间进行上下文切换通常比进程之间更快速。

3
关于上面的第二点:对于线程,CPU 也会维护一个上下文。 - Jack

33
告诉面试官,这完全取决于操作系统的实现。以Windows x86为例,只有2个段[1],代码和数据。它们都映射到整个2GB(线性,用户)地址空间。Base = 0,Limit = 2GB。它们本来可以只使用一个段,但是x86不允许一个段既可读/写又可执行。所以他们制作了两个,并将CS设置为指向代码描述符,其余部分(DS,ES,SS等)指向另一个[2]。但是这两个都指向相同的东西! 面试你的人有一个未明确说明的隐藏假设,这是一种愚蠢的把戏。所以关于 "Q.告诉我哪个段线程共享?" 这个问题与段无关,至少在Windows上如此。线程共享整个地址空间。只有一个堆栈段SS,它指向与DS、ES和CS完全相同的内容[2]。即整个用户空间。0-2GB。当然,这并不意味着线程只有一个堆栈。自然每个线程都有自己的堆栈,但是x86段不用于此目的。也许*nix做些不同的事情。谁知道。这个问题的前提是错误的。从ntsd notepad cs = 001b ss = 0023 ds = 0023 es = 0023

1
是的... 段取决于操作系统和编译器/链接器。有时会有一个单独的BSS段,与DATA段分开。有时会有RODATA(类似常量字符串的数据,可以在标记为只读的页面中)。一些系统甚至将DATA分成SMALL DATA(可以从基址+16位偏移量访问)和(FAR)DATA(需要32位偏移量才能访问)。还可能存在额外的TLS DATA(线程本地存储)段,这是根据每个线程生成的。 - Adisak
7
啊,不对!你把段落和节混淆了!如你所描述的,节是连接器将模块分成的部分(数据、只读数据、代码、未初始化数据等..)。但我所说的是在英特尔/ AMD x86 硬件中指定的段落。这与编译器/连接器完全无关。希望这样讲清楚了。 - Alex Budovski
然而,Adisak关于Thread Local存储的说法是正确的。它是线程私有的,不会被共享。我了解Windows操作系统,但不确定其他操作系统。 - Jack

32
一个进程有代码、数据、堆和栈段。现在,一个或多个线程的指令指针(IP)指向进程的代码段。数据和堆段由所有线程共享。那么栈区域呢?实际上,栈区域是进程专门为其线程创建的区域,因为与堆等相比,栈可以更快地使用。进程的栈区域被分配给各个线程,即如果有3个线程,则进程的栈区域将被分成3部分并分别分配给3个线程。换句话说,当我们说每个线程都有自己的栈时,这个栈实际上是进程栈区域分配给每个线程的一部分。当线程完成执行时,该线程的栈被进程回收。事实上,不仅一个进程的栈被分配给各个线程,而且线程使用的所有寄存器集合如SP、PC和状态寄存器都是进程的寄存器。因此,当涉及到共享时,代码、数据和堆区域是共享的,而栈区域只是在各个线程之间进行了划分。

21

一般来说,线程被称为轻量级进程。如果我们将内存分成三个部分,那么它们是:代码,数据和堆栈。 每个进程都有自己的代码、数据和堆栈部分,因此上下文切换时间稍长。为了减少上下文切换时间,人们提出了线程的概念,它与其他线程/进程共享数据和代码段,并拥有自己的堆栈段。


你忘了堆(Heap)。如果我没记错的话,堆应该在线程之间共享。 - Phate

20

线程共享代码段、数据段和堆,但是它们不共享栈。


11
“能够访问栈中的数据”和共享栈之间是有区别的。每个线程都有自己的栈,在调用方法时会被推入和弹出。 - Kevin Peterson
2
它们都是同样有效的观点。是的,每个线程在某种意义上都有自己的堆栈,这意味着线程和堆栈之间存在一对一的对应关系,每个线程都有一个用于自己正常堆栈使用的空间。但它们也是完全共享的进程资源,如果需要,任何线程都可以像访问自己的堆栈一样轻松地访问任何其他线程的堆栈。 - David Schwartz
@DavidSchwartz,我可以总结你的观点如下:每个线程都有自己的堆栈,堆栈由两部分组成——在进程变为多线程之前在线程之间共享的第一部分和在拥有线程运行时填充的第二部分。同意吗? - FaceBro
3
@nextTide,这里没有两个部分。堆栈是共享的,就是这样。每个线程都有自己的堆栈,但它们也是共享的。或许一个好比喻是,如果你和你的妻子各自有一辆车,但你们随时可以彼此使用对方的车。 - David Schwartz

6
线程共享数据和代码,而进程不共享。堆栈对于两者都不是共享的。
进程也可以共享内存,更准确地说是代码,例如在Fork()之后,但这是一种实现细节和(操作系统)优化。被多个进程共享的代码将在第一次写入代码时复制 - 这称为写时复制。我不确定线程代码的确切语义,但我认为是共享的代码。
进程 线程
堆栈 私有 私有 数据 私有 共享 代码 私有1 共享2 1代码在逻辑上是私有的,但出于性能原因可能会共享。 2我不确定是否100%正确。

我会说代码段(文本段),与数据不同,在大多数架构上几乎总是只读的。 - Jorge Córdoba

5

除了全局内存之外,线程还共享许多其他属性(即这些属性是进程级别的,而不是特定于线程的)。这些属性包括以下内容:
- 进程 ID 和父进程 ID; - 进程组 ID 和会话 ID; - 控制终端; - 进程凭证(用户和组 ID); - 打开的文件描述符; - 使用 fcntl() 创建的记录锁; - 信号处理方式; - 文件系统相关信息:umask、当前工作目录和根目录; - 时间间隔计时器(setitimer())和 POSIX 计时器(timer_create()); - System V 信号量撤销(semadj)值(第 47.8 节); - 资源限制; - CPU 时间消耗(由 times() 返回); - 消耗的资源(由 getrusage() 返回); - nice 值(由 setpriority() 和 nice() 设置)。
对于每个线程独有的属性,包括以下内容:
- 线程 ID(第 29.5 节); - 信号掩码; - 线程特定数据(第 31.3 节); - 替代信号栈(sigaltstack()); - errno 变量; - 浮点环境(参见 fenv(3)); - 实时调度策略和优先级(第 35.2 和 35.3 节); - CPU 亲和力(Linux 特有,描述见第 35.4 节); - 权限(Linux 特有,描述见第 39 章); - 栈(本地变量和函数调用链接信息)。

选自:《Linux编程接口》(第二版), Michael Kerrisk,第619页


4

线程共享所有[1]。整个进程只有一个地址空间。

每个线程都有自己的堆栈和寄存器,但是所有线程的堆栈在共享的地址空间中可见。

如果一个线程在其堆栈上分配了某个对象,并将地址发送给另一个线程,则它们两者都可以平等地访问该对象。


实际上,我刚注意到一个更广泛的问题:我认为你混淆了“段”这个词的两种用法。

可执行文件的文件格式(例如ELF)其中包含不同的部分,可能被称为段,包含已编译的代码(文本),初始化数据,链接器符号,调试信息等。这里没有堆或栈段,因为那些只是运行时构造。

这些二进制文件段可以单独映射到进程地址空间中,具有不同的权限(例如,只读可执行代码/文本,以及复制写入非可执行初始化数据)。

按约定(由语言运行时库强制执行),此地址空间的区域用于不同的目的,例如堆分配和线程堆栈。这仅仅是内存,除非您正在虚拟8086模式下运行,否则可能不会分段。每个线程的堆栈是在线程创建时分配的一块内存,当前堆栈顶部地址存储在堆栈指针寄存器中,每个线程都保持其自己的堆栈指针以及其他寄存器。


[1]好吧,我知道:信号掩码、TSS/TSD等。包括所有映射的程序段在内的地址空间仍然是共享的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接