是否可以在Linux x86 GAS程序集中创建没有系统调用的线程?

时间:2009-04-03 17:31:44

标签: linux multithreading assembly gas

在学习“汇编语言”(在使用GNU作为汇编程序的x86架构上的linux中)时,其中一个时刻是使用system calls的可能性。这些系统调用非常方便,有时甚至是您的程序runs in user-space所必需的 然而,系统调用在性能方面相当昂贵,因为它们需要中断(当然还有系统调用),这意味着必须从用户空间中的当前活动程序到内核空间中运行的系统进行上下文切换。

我想说的是:我目前正在实现一个编译器(用于大学项目),我想要添加的一个额外功能是支持多线程代码以提高性能编译的程序。因为一些多线程代码将由编译器本身自动生成,所以这几乎可以保证其中也会有很少的多线程代码。为了获得性能,我必须确保使用线程可以实现这一点。

但我担心的是,为了使用线程,我必须进行系统调用和必要的中断。因此,微小的(自动生成的)线程会受到进行这些系统调用所需的时间的影响,这甚至可能导致性能损失......

因此我的问题是双重的(在其下方有一个额外的奖励问题):

  • 是否可以编写汇编程序 可以运行多个线程的代码 同时在多个核心上 曾经,没有需要系统 呼叫?
  • 如果我有非常小的线程(线程的总执行时间很小),性能损失,或者根本不值得努力,我会获得性能提升吗?

我的猜测是,如果没有系统调用,多线程汇编程序代码不可能。即使是这种情况,你是否有一个建议(甚至更好:一些真正的代码)来尽可能高效地实现线程?

7 个答案:

答案 0 :(得分:23)

简短的回答是,你不能。当您编写汇编代码时,它会在一个且只有一个逻辑(即硬件)线程上顺序运行(或使用分支)。如果您希望某些代码在另一个逻辑线程上执行(无论是在同一个内核上,在同一CPU上的不同内核上,还是在不同的CPU上),您需要让操作系统设置另一个线程的指令指针( CS:EIP)指向您要运行的代码。这意味着使用系统调用来让操作系统做你想做的事。

用户线程不会为您提供所需的线程支持,因为它们都在相同的硬件线程上运行。

编辑:将Ira Baxter的答案与 Parlanse 结合起来。如果您确保程序在每个逻辑线程中都有一个运行的线程,那么您可以构建自己的调度程序而不依赖于操作系统。无论哪种方式,您都需要一个调度程序来处理从一个线程到另一个线程的跳转。在对调度程序的调用之间,没有用于处理多线程的特殊汇编指令。调度程序本身不能依赖于任何特殊程序集,而是依赖于每个线程中调度程序各部分之间的约定。

无论哪种方式,无论您是否使用操作系统,您仍然必须依赖某些调度程序来处理跨线程执行。

答案 1 :(得分:13)

“医生,医生,我这样做会很疼”。医生:“不要这样做。”

简短的回答是你可以不用多线程编程 调用昂贵的OS任务管理原语。简单地忽略OS的线程 调度操作。这意味着你必须编写自己的线程 调度程序,并且永远不会将控制权传递回操作系统。 (而且你必须更聪明地了解你的线程开销 比漂亮的智能操作系统家伙)。 我们选择这种方法正是因为windows process / thread / 光纤调用太昂贵了,无法支持计算 几百个指令的颗粒。

我们的PARLANSE编程语言是一种并行编程语言: 见http://www.semdesigns.com/Products/Parlanse/index.html

PARLANSE在Windows下运行,提供并行“粒度”作为抽象并行性 通过高度组合来构建和安排这些谷物 调整手写的调度程序和生成的调度代码 PARLANSE编译器,它考虑了grain的上下文 最小化调度开销。例如,编译器 确保谷物的寄存器在该点不包含任何信息 可能需要调度(例如,“等待”),因此 调度程序代码只需保存PC和SP。事实上, 通常,调度程序代码根本无法获得控制权; 叉形谷物只存储分叉PC和SP, 切换到编译器预先分配的堆栈并跳转到粒度 码。粮食的完成将重新启动粮仓。

通常有一个互锁来同步谷物,实施 由编译器使用本机LOCK DEC指令实现 什么等于计算信号量。应用 可以逻辑地分叉数以百万计的谷物;调度程序限制 如果工作排队,父母的粮食会产生更多的工作 足够长,所以更多的工作将无济于事。调度程序 实现工作窃取,允许工作匮乏的CPU抓住 准备好的谷物形成相邻的CPU工作队列。这有 已实施处理多达32个CPU;但我们有点担心 x86供应商实际上可能会使用超过 在接下来的几年里!

PARLANSE是一个成熟的语言;我们自1997年以来一直在使用它, 并在其中实施了数百万行并行应用程序。

答案 2 :(得分:7)

实施用户模式线程。

历史上,线程模型被概括为N:M,也就是说在M个内核模型线程上运行的N个用户模式线程。现代用法是1:1,但它并不总是那样,它不一定是那样。

您可以在单个内核线程中维护任意数量的用户模式线程。只是你有责任在它们之间经常切换它们看起来并发。你的线索当然是合作而不是先发制人;你基本上在你自己的代码中散布了yield()调用,以确保定期切换。

答案 3 :(得分:5)

如果您想获得性能,则必须利用内核线程。只有内核可以帮助您在多个CPU核心上同时运行代码。除非您的程序受I / O限制(或执行其他阻塞操作),否则执行用户模式协作多线程(也称为fibers)不会获得任何性能。您将只执行额外的上下文切换,但是您的实际线程正在运行的一个CPU仍将以100%的速度运行。

系统调用变得更快。现代CPU支持sysenter指令,这比旧int指令快得多。另请参阅this article,了解Linux如何以最快的方式进行系统调用。

确保自动生成的多线程的线程运行时间足以获得性能。不要试图并行化短代码,你只会浪费时间产生和加入线程。还要警惕记忆效应(虽然这些很难测量和预测) - 如果多个线程正在访问独立的数据集,它们的运行速度会比由于cache coherency问题而重复访问相同数据的速度快得多

答案 4 :(得分:3)

首先,您应该学习如何在C中使用线程(pthreads,POSIX theads)。在GNU / Linux上,您可能希望使用POSIX线程或GLib线程。 然后,您只需从汇编代码中调用C即可。

以下是一些提示:

答案 5 :(得分:3)

系统调用现在不是那么慢,syscallsysenter而不是int。但是,创建或销毁线程时只会产生开销。一旦它们运行,就没有系统调用。用户模式线程不会真正帮助你,因为它们只在一个核心上运行。

答案 6 :(得分:3)

现在有点晚了,但我自己对这种话题很感兴趣。 事实上,对于特别需要内核干预EXCEPT以实现并行化/性能的线程,没有什么特别之处。

强制性BLUF

Q1:否。至少需要初始系统调用才能在各种CPU内核/超线程上创建多个内核线程。

Q2:这取决于。如果你创建/销毁执行微小操作的线程,那么你就是在浪费资源(线程创建过程将大大超过在它退出之前使用的时间)。如果您创建了N个线程(其中N是系统上的核心/超线程的#)并重新执行它们,那么答案可能会是肯定的,具体取决于您的实现。

问题3:如果您提前知道订购操作的精确方法,您可以优化操作。具体来说,您可以创建相当于ROP链(或前向调用链)的内容,但实际上最终实现起来可能更复杂。该ROP链(由线程执行)将连续执行“ret”指令(到其自己的堆栈),其中该堆栈被连续地预先添加(或者在其翻转到开头的情况下附加)。在这种(奇怪的!)模型中,调度程序保持指向每个线程的“ROP链末端”的指针并向其写入新值,由此代码通过内存执行功能代码循环,最终产生ret指令。同样,这是一个奇怪的模型,但仍然很有趣。

我的2美分内容。

我最近通过管理各种堆栈区域(通过mmap创建)并维护专用区域来存储“线程”的控件/个性化信息,从而创建了有效操作纯组件中的线程的内容。虽然我没有这样设计,但是有可能通过mmap创建一个大的内存块,我将其细分为每个线程的“私有”区域。因此,只需要一个系统调用(虽然它们之间的保护页面很聪明,但这需要额外的系统调用)。

此实现仅使用在进程生成时创建的基本内核线程,并且在整个程序执行过程中只有一个用户模式线程。程序更新自己的状态,并通过内部控制结构自行安排。 I / O等在可能的情况下通过阻塞选项进行处理(以降低复杂性),但这不是严格要求的。当然我使用了互斥锁和信号量。

要实现此系统(完全在用户空间中,如果需要,还可以通过非root访问),需要以下内容:

关于线程归结为什么的概念: 堆栈操作的堆栈(有点自我解释和明显) 一组执行指令(也很明显) 用于保存单个寄存器内容的小块内存

调度程序归结为: 在调度程序指定的有序列表(通常是优先级)中,一系列线程的管理器(注意进程从未实际执行,只是它们的线程所做)。

线程上下文切换器: 一个MACRO注入到代码的各个部分(我通常将它们放在重载函数的末尾),大致相当于“线程产量”,它保存线程的状态并加载另一个线程的状态。

因此,在非根进程中创建用户模式类似线程的结构确实可以(完全在汇编中,而不是初始mmap和mprotect之外的系统调用)。

我只是添加了这个答案,因为你特别提到了x86程序集,这个答案完全是通过一个完全由x86程序集编写的自包含程序得出的,它实现了最小化系统调用的目标(减去多核功能)并最小化系统 - 线程开销。