最近,在一次采访中有人问我一个问题,流程和线程之间有什么区别。真的,我不知道答案。我想了一会儿,给出了一个很奇怪的答案。
线程共享相同的内存,进程不共享。在回答了这个问题之后,面试官给了我一个邪恶的微笑,并向我提出了以下问题:
问:您知道程序被划分为哪些部分吗?
我的回答:是的(这很简单)堆栈,数据,代码,堆
问:那么,告诉我:线程共享哪些段?
我无法回答这个问题,最后全部说了出来。
请问,有人可以针对进程和线程之间的差异给出正确而令人印象深刻的答案吗?
最近,在一次采访中有人问我一个问题,流程和线程之间有什么区别。真的,我不知道答案。我想了一会儿,给出了一个很奇怪的答案。
线程共享相同的内存,进程不共享。在回答了这个问题之后,面试官给了我一个邪恶的微笑,并向我提出了以下问题:
问:您知道程序被划分为哪些部分吗?
我的回答:是的(这很简单)堆栈,数据,代码,堆
问:那么,告诉我:线程共享哪些段?
我无法回答这个问题,最后全部说了出来。
请问,有人可以针对进程和线程之间的差异给出正确而令人印象深刻的答案吗?
Answers:
您非常正确,但是线程共享除堆栈之外的所有段。线程具有独立的调用堆栈,但是其他线程堆栈中的内存仍然可以访问,并且理论上您可以在其他线程的本地堆栈帧中持有指向内存的指针(尽管您可能应该找到放置该内存的更好位置!)。
真正需要指出的是,这个问题确实有两个方面-理论方面和实现方面。
首先,让我们看一下理论方面。您需要了解流程在概念上的含义,以了解流程与线程之间的区别以及它们之间的共享。
我们从部分以下2.2.2古典线程模型在现代操作系统3E通过的Tanenbaum:
流程模型基于两个独立的概念:资源分组和执行。有时将它们分开很有用;这是线程进入的地方。
他继续:
查看流程的一种方式是将相关资源组合在一起。进程的地址空间包含程序文本和数据以及其他资源。这些资源可能包括打开的文件,子进程,挂起的警报,信号处理程序,记帐信息等。通过以流程的形式将它们组合在一起,可以更轻松地对其进行管理。进程具有的另一个概念是执行线程,通常简称为线程。该线程具有程序计数器,该计数器跟踪接下来要执行的指令。它具有寄存器,用于保存其当前的工作变量。它有一个堆栈,其中包含执行历史记录,每个调用但尚未返回的过程都有一帧。尽管线程必须在某些进程中执行,线程及其过程是不同的概念,可以分别处理。流程用于将资源分组在一起;线程是计划在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 |
以上是线程工作所需的条件。正如其他人指出的那样,诸如段之类的东西是依赖于操作系统的实现细节。
告诉采访者,这完全取决于操作系统的实现。
以Windows x86为例。只有2个段[1]:代码和数据。而且它们都映射到整个2GB(线性,用户)地址空间。基本= 0,限制= 2GB。他们本来可以做到的,但是x86不允许将段同时读/写和执行。因此他们做了两个,将CS设置为指向代码描述符,其余的(DS,ES,SS等)指向另一个[2]。但是,两者都指向同一个东西!
采访您的人隐瞒了一个假设,即他/她没有陈述,这是一个愚蠢的trick俩。
所以关于
问:那么告诉我哪个段线程共享?
这些段与问题无关,至少在Windows上是这样。线程共享整个地址空间。只有1个堆栈段SS,它指向与DS,ES和CS完全相同的内容[2]。即整个流血的用户空间。0-2GB。当然,这并不意味着线程只有1个堆栈。当然每个都有自己的堆栈,但是x86段并不用于此目的。
也许* nix做一些不同的事情。谁知道。问题所基于的前提已被打破。
ntsd notepad
:cs=001b ss=0023 ds=0023 es=0023
通常,线程称为轻量进程。如果我们将内存分为三部分,那么它将是:代码,数据和堆栈。每个进程都有自己的代码,数据和堆栈部分,由于上下文的关系,切换时间有点长。为了减少上下文切换时间,人们想到了线程的概念,该线程与其他线程/进程共享数据和代码段,并且具有自己的STACK段。
进程具有代码,数据,堆和堆栈段。现在,一个或多个线程的指令指针(IP)指向进程的代码段。数据段和堆段由所有线程共享。现在堆栈区域呢?实际的堆栈面积是多少?它是由进程创建的仅用于其线程使用的区域...因为使用堆栈的速度比使用堆栈等快得多。因此,将进程的堆栈区域划分为多个线程,即,如果有3个线程,则进程的堆栈区域分为3个部分,每个部分分配给3个线程。换句话说,当我们说每个线程都有自己的堆栈时,该堆栈实际上是分配给每个线程的进程堆栈区域的一部分。当线程完成其执行时,进程将回收线程的堆栈。事实上,不仅进程的堆栈在线程之间分配,而且线程使用的所有寄存器集(例如SP,PC和状态寄存器)都是进程的寄存器。因此,在共享方面,代码,数据和堆区域是共享的,而堆栈区域只是在线程之间分配。
线程共享代码和数据段以及堆,但不共享堆栈。
线程共享一切 [1]。整个过程只有一个地址空间。
每个线程都有自己的堆栈和寄存器,但是所有线程的堆栈在共享地址空间中都是可见的。
如果一个线程在其堆栈上分配某个对象,然后将地址发送给另一线程,则它们都将对该对象具有相同的访问权限。
实际上,我只是注意到了一个更广泛的问题:我认为您混淆了单词segment的两种用法。
可执行文件(例如ELF)的文件格式具有不同的部分,这些部分可以称为段,其中包含编译的代码(文本),初始化的数据,链接器符号,调试信息等。没有堆或堆栈段在这里,因为它们是仅运行时的结构。
这些二进制文件段可以具有不同的权限(例如,对于代码/文本是只读可执行文件,对于初始化的数据是不可执行的)分别映射到进程地址空间。
按照惯例(由您的语言运行时库强制执行),该地址空间的区域用于不同目的,例如堆分配和线程堆栈。但是,它们全都只是内存,除非您在虚拟8086模式下运行,否则可能不会分段。每个线程的堆栈都是在线程创建时分配的内存块,当前堆栈的最高地址存储在堆栈指针寄存器中,并且每个线程与其他寄存器一起保留自己的堆栈指针。
[1]好,我知道:信号掩码,TSS / TSD等。地址空间(包括其所有映射的程序段)仍然共享。
在x86框架中,可以划分尽可能多的段(最多2 ^ 16-1)。ASM指令SEGMENT / ENDS允许这样做,而运算符SEG和OFFSET允许初始化段寄存器。CS:IP通常由加载程序初始化,但是对于DS,ES,SS,应用程序负责初始化。许多环境都允许所谓的“简化的段定义”,例如.code,.data,.bss,.stack等,并且根据“内存模型”(较小,较大,紧凑等),加载程序还会初始化段寄存器相应地。通常,.data,.bss,.stack和其他常用段(自20年以来我就没有做过,所以我不记得所有)都归为一个组-这就是为什么DS,ES和SS通常指向teh相同的区域,但这只是为了简化事情。
通常,所有段寄存器在运行时可以具有不同的值。因此,采访问题是正确的:线程之间共享CODE,DATA和STACK中的哪一个。堆管理是另一回事-它只是对操作系统的一系列调用。但是,如果根本没有像嵌入式系统那样的OS,该怎么办?您的代码中还能包含new / delete吗?
我对年轻人的建议-阅读一些好的汇编程序设计书。在这方面,大学课程似乎很差。
除了全局内存,线程还共享许多其他属性(即,这些属性对于进程是全局的,而不是特定于线程的)。这些属性包括:
- 进程ID和父进程ID;
- 进程组ID和会话ID;
- 控制终端;
- 处理凭据(用户和组ID);
- 打开文件描述符;
- 记录使用创建的锁
fcntl();
- 信号处置;
- 文件系统相关信息:umask,当前工作目录和根目录;
- 间隔计时器(
setitimer()
)和POSIX计时器(timer_create()
);- 系统V信号量undo(
semadj
)值(第47.8节);- 资源限制;
- 消耗的CPU时间(由返回
times()
);- 消耗的资源(由返回
getrusage()
);和- 不错的值(由
setpriority()
和设置nice()
)。对于每个线程,不同的属性如下:
- 线程ID(第29.5节);
- 信号屏蔽
- 特定于线程的数据(第31.3节);
- 备用信号栈(
sigaltstack()
);- errno变量;
- 浮点环境(请参阅参考资料
fenv(3)
);- 实时调度策略和优先级(第35.2和35.3节);
- CPU亲和力(特定于Linux,在35.4节中介绍);
- 功能(特定于Linux,在第39章中进行了介绍);和
- 堆栈(局部变量和函数调用链接信息)。