Linux中的线程与进程

use*_*918 244 linux performance multithreading process

我最近听过一些人说在Linux中,使用进程而不是线程几乎总是更好,因为Linux在处理进程方面非常有效,并且因为线程有很多问题(例如锁定).但是,我很怀疑,因为在某些情况下,线程似乎可以带来相当大的性能提升.

所以我的问题是,当遇到线程和进程都能很好地处理的情况时,我应该使用进程还是线程?例如,如果我正在编写Web服务器,我应该使用进程或线程(或组合)吗?

eph*_*ent 311

Linux使用1-1线程模型,(对内核)没有进程和线程之间的区别 - 一切都只是一个可运行的任务.*

在Linux上,系统调用clone克隆任务,具有可配置的共享级别,其中包括:

  • CLONE_FILES:共享相同的文件描述符表(而不是创建副本)
  • CLONE_PARENT:不要在新任务和旧任务之间建立父子关系(否则,孩子的getppid()=父母的getpid())
  • CLONE_VM:共享相同的内存空间(而不是创建COW副本)

fork()呼叫clone(最少共享)pthread_create()呼叫clone(大多数共享).**

forkpthread_create由于复制表和为内存创建COW映射,成本只是一点点,但Linux内核开发人员已经尝试(并成功)降低了这些成本.

如果任务共享相同的内存空间和各种表,则在任务之间切换比不共享任务要便宜一点,因为数据可能已经加载到缓存中.但是,即使没有共享任何内容,切换任务仍然非常快 - 这是Linux内核开发人员试图确保(并成功确保)的其他内容.

实际上,如果您使用的是多处理器系统,则不共享可能实际上对性能有益:如果每个任务在不同的处理器上运行,则同步共享内存非常昂贵.


*简化. CLONE_THREAD导致信号传递被共享(需要CLONE_SIGHAND,共享信号处理程序表).

**简化.存在两个SYS_forkSYS_clone系统调用,但在内核中,sys_fork并且sys_clone它们都是围绕同一do_fork函数的非常薄的包装器,它本身就是一个薄的包装器copy_process.是的,术语process,thread以及task相当互换使用Linux内核...

  • @Saurabh不是.您可以轻松地`socket`,`bind`,`listen`,`fork`,然后在同一个侦听套接字上有多个进程`accept`连接.如果进程繁忙,进程可以停止接受,并且内核将传入的连接路由到另一个进程(如果没有人正在侦听,内核将排队或丢弃,具体取决于`listen` backlog).你没有比工作分配更多的控制,但通常这已经足够了! (25认同)
  • 我想我们缺少1分.如果为Web服务器进行多个处理,则必须编写另一个进程来打开套接字并将"work"传递给不同的线程.线程提供单线程多线程,简洁的设计.在许多情况下,线程是自然的,在其他情况下,新的过程是很自然的.当问题出现在灰色区域时,其他权衡因素就像ephemient所解释的那样变得很重要. (6认同)
  • @KarthikBalaguru内核支持线程和进程之间的连续行为; 例如,`clone(CLONE_THREAD | CLONE_VM | CLONE_SIGHAND))`会给你一个不共享工作目录,文件或锁的新"线程",而`clone(CLONE_FILES | CLONE_FS | CLONE_IO)`会给你一个"过程"那样做.底层系统通过克隆创建任务; `fork()`和`pthread_create()`只是以不同方式调用`clone()`的库函数(就像我在这个答案中写的那样). (4认同)
  • @KarthikBalaguru在内核本身中,每个任务都有一个`task_struct`.这在整个内核代码中通常称为"进程",但它对应于每个可运行的线程.没有`process_struct`; 如果一堆`task_struct`通过它们的`thread_group`列表链接在一起,那么它们与用户空间是相同的"进程".有一些特殊的"线程"处理,例如所有兄弟线程都在fork和exec上停止,只有"主"线程出现在`ls/proc`中.每个线程都可以通过`/ proc/pid`访问,无论它是否在`/ proc`中列出. (3认同)
  • @Bloodcount Linux 上的所有进程/线程都是通过相同的机制创建的,该机制克隆了现有的进程/线程。传递给 `clone()` 的标志决定了哪些资源是共享的。任务也可以在以后的任何时间点 `unshare()` 资源。 (2认同)

Mar*_*rkR 59

Linux(实际上是Unix)为您提供了第三种选择.

选项1 - 流程

创建一个独立的可执行文件,处理应用程序的某些部分(或所有部分),并为每个进程单独调用它,例如程序运行自身的副本以将任务委派给.

选项2 - 线程

创建一个独立的可执行文件,它以单个线程启动并创建其他线程来执行某些任务

选项3 - 分叉

仅在Linux/Unix下可用,这有点不同.分叉进程实际上是它自己的进程,它有自己的地址空间 - 孩子可以(通常)不会影响其父进程或兄弟地址空间(与线程不同) - 所以你可以增加健壮性.

但是,内存页面不会被复制,它们是写时复制的,因此通常使用的内存比您想象的要少.

考虑一个Web服务器程序,它包含两个步骤:

  1. 读取配置和运行时数据
  2. 提供页面请求

如果您使用了线程,则步骤1将完成一次,而步骤2将在多个线程中完成.如果使用"传统"进程,则需要为每个进程重复步骤1和2,并且存储配置和运行时数据的内存重复.如果您使用了fork(),那么您可以执行第1步,然后执行fork(),将运行时数据和配置保留在内存中,不受影响,不进行复制.

所以真的有三种选择.

  • 如果一个进程与一个打开的mysql连接分叉,那么就会发生坏事,因为套接字是在两个进程之间共享的.即使只有一个进程使用该连接,另一个进程也会阻止它被关闭. (18认同)
  • @Qwertie forking并不那么酷,它以微妙的方式打破了很多库(如果你在父进程中使用它们).它会产生意想不到的行为,甚至会让有经验的程 (7认同)
  • @MarkR你能给出一些例子或关于分叉如何打破库并创建意外行为的链接? (2认同)
  • fork() 系统调用是由 POSIX 指定的(这意味着它在任何 Unix 系统上都可用),如果您使用底层 Linux API,即 clone() 系统调用,那么您在 Linux 中实际上有更多的选择,而不仅仅是这三个。 (2认同)
  • @MarkR 套接字的共享是设计使然。此外,任何一个进程都可以在对套接字调用 close() 之前使用 https://linux.die.net/man/2/shutdown 关闭套接字。 (2认同)

Ada*_*eld 51

这取决于很多因素.进程比线程更重,并且具有更高的启动和关闭成本.进程间通信(IPC)也比线程通信更难,更慢.

相反,进程比线程更安全,更安全,因为每个进程都在自己的虚拟地址空间中运行.如果一个进程崩溃或缓冲区溢出,它根本不会影响任何其他进程,而如果一个线程崩溃,它将关闭进程中的所有其他线程,如果一个线程有缓冲区溢出,它会打开所有线程中的安全漏洞.

因此,如果您的应用程序的模块可以在几乎没有通信的情况下独立运行,那么如果您能负担起启动和关闭成本,则应该使用流程.IPC的性能影响将是最小的,您将对错误和安全漏洞稍微安全一些.如果您需要每一点性能,您可以获得或拥有大量共享数据(例如复杂的数据结构),请使用线程.

  • 亚当的答案很适合作为执行简报.有关更多细节,MarkR和ephemient提供了很好的解释.有关示例的非常详细的解释可以在http://www.cs.cf.ac.uk/Dave/C/node29.html找到,但它看起来确实有点过时. (9认同)
  • Cyber​​Fonic 的适用于 Windows。正如ephemient所说,在Linux下进程并不重。在 Linux 下,所有可用于线程之间通信的机制(futex、共享内存、管道、IPC)也可用于进程并以相同的速度运行。 (2认同)

dmc*_*kee 10

其他人讨论了这些考虑因素

也许重要的区别在于,与线程相比,Windows进程繁重且昂贵,而在Linux中,差异要小得多,因此方程式在不同点进行平衡.


rob*_*ger 8

曾几何时有Unix,在这个老的Unix中有很多进程的开销,所以一些聪明的人做的是创建线程,它将与父进程共享相同的地址空间,他们只需要减少上下文切换,这将使上下文切换更有效.

在当代Linux(2.6.x)中,进程的上下文切换与线程之间的性能差别不大(只有MMU的东西是线程的附加内容).共享地址空间存在问题,这意味着线程中的错误指针可能破坏父进程的内存或同一地址空间内的另一个线程.

进程受MMU保护,因此错误的指针只会导致信号11而不会损坏.

我一般会使用进程(在Linux中没有太多的上下文切换开销,但由于MMU导致的内存保护),但是如果我需要一个实时的调度程序类,那就是pthreads,这是一个不同的茶.

为什么你认为线程在Linux上有如此大的性能提升?你有这方面的数据,还是只是一个神话?

  • @ user17918 - 你有可能分享你用来计算上述时间的代码. (4认同)

Dea*_*n P 7

如果您想尽可能创建一个纯进程,您可以使用clone()并设置所有克隆标志。(或者省去打字的麻烦,直接打电话fork()

如果您想尽可能创建一个纯线程,您可以使用clone()并清除所有克隆标志(或者节省自己的打字工作和调用pthread_create()

有 28 个标志指示资源共享的级别。这意味着您可以创建超过 2.68 亿种任务,具体取决于您想要共享的内容。

这就是我们所说的 Linux 不区分进程和线程,而是将程序内的任何控制流视为任务的意思。不区分两者的理由是,没有唯一地定义超过 2.68 亿种口味!

因此,做出使用进程还是线程的“完美决定”实际上就是决定要克隆 28 个资源中的哪一个。

在此输入图像描述


Rob*_*ert 5

你的任务有多紧密耦合?

如果他们可以彼此独立生活,那么使用流程.如果他们互相依赖,那就使用线程.这样,您可以在不干扰其他任务的操作的情况下终止并重启坏的进程.


gre*_*pit 5

我认为每个人都在回答你的问题时做得很好。我只是在 Linux 中添加更多关于线程与进程的信息,以澄清和总结内核上下文中的一些先前响应。所以,我的回答是关于 Linux 中内核特定的代码。根据 Linux Kernel 文档,除了线程使用共享虚拟地址空间与进程不同之外,线程与进程之间没有明显区别。另请注意,Linux 内核使用术语“任务”来泛指进程和线程。

“没有实现进程或线程的内部结构,而是有一个 struct task_struct 描述了一个称为任务的抽象调度单元”

此外,根据 Linus Torvalds 的说法,您根本不应该考虑进程与线程,因为它的局限性太大,唯一的区别是 COE 或执行上下文在“将地址空间与父级分离”或共享地址空间方面。事实上,他在这里使用了一个 Web 服务器示例来说明他的观点(强烈推荐阅读)。

完全归功于linux 内核文档


edu*_*ffy 3

我必须同意你所听到的。当我们对集群(xhpl等)进行基准测试时,我们总是能通过进程获得比线程更好的性能。</anecdote>