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

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

我想说的是:我正在实现一个编译器(用于大学项目),我想添加的额外function之一是支持multithreading代码,以提高编译的程序的性能。 因为一些multithreading的代码会由编译器自动生成,所以这几乎可以保证multithreading代码中会有一小部分。 为了取得胜利,我必须确定使用线程会使这种情况发生。

然而,我的恐惧是,为了使用线程,我必须进行系统调用和必要的中断。 微小的(自动生成的)线程将因此受到进行这些系统调用所花费的时间的高度影响,甚至可能导致性能下降。

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

  • 是否有可能编写可以同时在多个内核上同时运行多个线程的汇编代码, 而不需要系统调用?
  • 如果我有非常小的线程(线程总执行时间很小),性能会有所下降,还是不值得花费精力,我会获得性能上的提升吗?

我的猜测是multithreading汇编代码不可能没有系统调用。 即使是这样的情况,你是否有build议(甚至更好:一些真正的代码)尽可能高效地执行线程?

Solutions Collecting From Web of "在Linux x86 GAS程序集中是否可以创build没有系统调用的线程?"

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

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

编辑: 纳入艾拉·巴克斯特的答案Parlanse 。 如果你确保你的程序有一个在每个逻辑线程中运行的线程开始,那么你可以建立你自己的调度程序,而不依赖于操作系统。 无论哪种方式,您都需要一个调度程序来处理从一个线程跳到另一个线程。 在对调度程序的调用之间,没有特殊的汇编指令来处理多线程。 调度程序本身不能依赖任何特殊的程序集,而是依赖于每个线程中调度程序各部分之间的约定。

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

“医生,医生,当我这样做的时候会感到痛苦”。 医生:“不要那样做”。

简而言之,您可以在不调用昂贵的操作系统任务管理原语的情况下进行多线程编程。 简单地忽略线程调度操作的操作系统。 这意味着你必须编写自己的线程调度程序,而不要将控制权交还给操作系统。 (而且你必须更聪明地处理你的线程开销,而不是相当聪明的操作系统人员)。 我们之所以选择这种方法,是因为windows进程/线程/光纤调用的代价都太高,无法支持几百条指令的计算。

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

PARLANSE在Windows下运行,提供并行的“谷物”作为抽象的并行构造,并且通过高度调整的手写调度器和由PARLANSE编译器生成的调度代码的组合调度这样的谷物,考虑谷物的上下文以最小化调度高架。 例如,编译器确保谷物寄存器在可能需要调度(例如,“等待”)的点处不包含信息,因此调度程序代码仅需要保存PC和SP。 实际上,调度程序代码通常不会得到控制; 分叉的谷物只是存储分叉的PC和SP,切换到编译器预分配的堆栈并跳转到谷物代码。 粮食的完成将重新启动甲叉。

通常有一个同步谷物的互锁,由编译器使用原生的LOCK DEC指令实现,实现了相当于计算信号的数量。 应用程序可以分配数百万颗谷物; 如果工作队列足够长,调度员会限制母公司生成更多的工作,所以更多的工作不会有帮助。 调度程序执行工作窃取以允许工作不足的CPU抓取相邻CPU工作队列中的准备好的粒子。 这已经被实现来处理多达32个CPU; 但是我们有点担心x86供应商在未来几年可能会实际使用这些产品。

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

实现用户模式线程。

历史上,线程化模型被推广为N:M,即在M个内核模型线程上运行的N个用户模式线程。 现代用途是1:1,但并不总是这样,它不一定是这样的。

您可以自由地在单个内核线程中维护任意数量的用户模式线程。 这只是你的责任,经常切换它们,看起来并发。 你的线程当然是合作的,而不是先发制人。 你基本上将yield()调用分散到你自己的代码中,以确保正常的切换发生。

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

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

确保自动生成的多线程能够使线程运行足够长的时间,以获得性能。 不要试图平行化一小段代码,你只是浪费时间产卵和加入线程。 同时也要小心记忆效应(尽管这些测量和预测很难) – 如果多个线程正在访问独立的数据集,那么由于缓存一致性问题,它们的运行速度要比重复访问相同的数据要快得多。

现在有点晚了,但是我自己也对这个话题感兴趣。 事实上,除了并行/性能之外,没有什么特别的线程专门要求内核进行干预。

强制BLUF

问题1:至少需要初始系统调用来在各种CPU核心/超线程上创建多个内核线程。

Q2:这取决于。 如果您创建/销毁执行微小操作的线程,那么您正在浪费资源(线程创建过程将大大超过在退出之前花费的时间)。 如果创建了N个线程(其中N是系统上核心/超线程的数量)并重新执行它们,那么根据您的实现,答案可以为yes。

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

我的2美分的内容价值。

我最近通过管理各种堆栈区域(通过mmap创建)和维护专用区域来存储“线程”的控制/个性化信息,创建了在纯装配中作为线程有效运行的内容。 尽管我没有这样设计,但有可能通过mmap来创建一个单独的大块内存,并将其细分为每个线程的“私有”区域。 因此,只需要一个系统调用(虽然守卫页之间会很聪明,这将需要额外的系统调用)。

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

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

一个概念是什么线程归结为:堆栈操作(有点自我解释,显而易见)一套指令执行(也是显而易见的)一个小块的内存来保存个人注册内容

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

一个线程上下文切换器:一个MACRO被注入代码的不同部分(我通常把它们放在重载函数的末尾),大致等同于'线程良率',它保存线程的状态并加载另一个线程的状态。

因此,确实有可能(完全在汇编中,除了初始化mmap和mprotect之外,不进行系统调用)在非根进程中创建用户模式线程类构造。

我只是添加了这个答案,因为你特别提到了x86汇编,这个答案完全是通过一个完全由x86程序集编写的独立程序来实现的,它实现了最小化系统调用的目标(减去多核功能),并且最小化了系统端线程高架。

首先,你应该学习如何在C中使用Thred。在GNU / Linux上你可能会想要使用Posix线程或者GLib线程。 然后你可以简单地从汇编代码中调用C语言。

这里有一些指针:

  • Posix线程: 链接文本
  • 一个教程,你将学习如何从汇编中调用C函数: 链接文本

现在系统调用并不慢,用syscall或者sysenter代替int 。 不过,只有在创建或销毁线程时才会有开销。 一旦运行,就没有系统调用。 用户模式线程不会真正帮助你,因为它们只能运行在一个内核上。