转发文章 原文地址:http://reeze.cn/2014/08/23/multi-thread-service-and-fork/?f=http://blogread.cn/
名词解释
- PHP vs HHVM: PHP指的是php.net(Zend)实现的PHP,而HHVM指的是Facebook开源的PHP实现。
- PHP-FPM: (PHP Fastcgi Process Manager) 一个PHP Sapi实现,目前的主流的Web应用使用的方式。基于多进程的模型
- HHVM: AdminServer 这是HHVM中为了更好的运维和定位问题实现的一个HTTP操作接口,可以实时的获取和操作HHVM内部状态,
这对于我们是一个非常便利的接口,比如可以打印出内部的队列长度(fpm中也有类似接口,不过灵活性差很多)
多线程下fork()/exec()出现的性能问题
贴吧目前使用的HHVM来运行PHP程序,HHVM采用的是多线程模型, 以前我们使用的是PHP-FPM,PHP-FPM采用的是多进程的模型。 我们通过一个我们上线遇到的问题来看看Linux的写时复制和多线程相关的问题。
上周我们迁移一个服务HHVM运行环境时发现上线后CPU占用飚的非常高,经过分析发现程序时间消耗的最多时间的地方是:fork()
, 这个有点奇怪,fork的时候怎么会花那么长时间呢?经过分析发现我们有个基础库的实现中使用了PHP的exec()
函数来启动shell程序做字符处理,exec的实现和bash类似,先fork()一个进程然后通过exec系统调用执行相应的命令, 这没有什么不对,对不对。但是为什么在HHVM下会导致CPU利用率过高,而PHP中没事呢? 先看一些基本的背景:
关于写时复制
在Linux中要启动一个新进程的方式通常是:先调用fork()
函数fork出一个新的进程,然后在 新的进程中调用exec()函数来启动新的程序从而达到启动新程序的目的,比如采用下面的代码实现。
int start_prog(char *prog, char* args[]) { pid_t pid = fork(); // 创建子进程 if (pid < 0) return -1; if (pid == 0) { // 子进程 if (execv(prog, args) < 0) return -1; // 加载新的程序 } else { return 1; } } int main() { char* args[] = {NULL}; return start_prog("/bin/ls", args); }
我们知道进程间的内存地址空间是隔离的,fork()系统调用的结果是生成一个新的子进程,为了保证隔离性, 早期的UNIX采用在fork()
将父进程的地址空间完整的复制一份。这个操作非常的耗时。 为了提高效率现代的Unix及Linux采用了一种称为写时复制
的技术,其实也就是一种延迟操作的做法, 子进程和父进程在fork()
时并不马上复制,而是暂时共享内存空间,随后只要父进程或者子进程试图写共享的内存就会产生一个异常, 这时内核才把内存空间进程复制,比如我们在Shell中启动一个程序时随后就会启动新的程序,启动后的程序将会覆盖旧的内存空间, 如果提前就复制了,那么这个复制操作其实是白做了,为此系统将这个操作优化为写时复制。
写时复制,发生写时才复制内存地址空间
如果马上进行exec加载新的程序,那么复制地址就没必要了
解决方案
从前面的介绍我们知道,启动新程序的时候利用了写实复制的技术避免不必要的消耗。对于HHVM来说, 我们使用的是多线程,每个线下都在并行的执行,也就是说,在某个线程在执行fork()的是时候 还会有其他线程在处理任务,由于线程间是共享进程空间的,那时不可避免的可能会写内存。这就触发了 操作系统的写实复制,导致大量的内存复制操作(其实也是没必要的),这会导致资源占用急剧上涨。
说到这里你可能会说:你看,多线程模式不太好吧。HHVM的实现是不是有问题?其实对于PHP也会有类似的问题的, 如果你使用的是PHP的多线程模型(现在应该很少的人使用)。
这个问题,HHVM使用了一个比较巧妙的方式来解决。
HHVM的思路是这样的,既然多线程下写实复制容易出俩捣乱,那么就让fork发生在非多线程的进程中,让他不可能发生空间复制。
- HHVM启动时先预先启动N个(可配置)代理进程,在父进程和这些代理进程之前预先开启管道方便通信
- 有需要启动子进程的时候,通过管道选择一个没有正在fork()进程的代理进程,将执行信息通过管道发给代理进程
- 代理进程根据要执行的程序fork()一个新的子进程并执行相应的命令,然后将执行完成的信息通过管道写回主进程。
从上面可以知道,因为代理进程每个进程都只有一个线程不会存在多线程写的问题。 HHVM中将它称为轻进程。
解决方案
使用了HHVM的轻进程后,CPU直接就降了下来,我们虽然可是使用这个方案解决这个问题,不过我们还是将 exec()
调用改成了PHP原生文件读取操作,一来exec()函数的成本相对较高,二来使用shell不利于可移植性, 虽然我们不太可能使用Windows,不过这样的耦合是没必要的。
总结
- 多线程vs多进程。其实这两个模式没有绝对的好坏,就要需要什么,多进程的好处是进程隔离,程序出现问题也能保证服务不整体crash掉,但是多进程带来的问题是进程间通信的成本,多线程也有多好处,比如HHVM中的AdminServer,队列的管理等等,如果不是多线程,JIT,实例管理,Debug都会非常的复杂。
- 对于高并发的程序建议少用不用
exec()/system()
等函数,除了内存占用,进程数也可能会变成资源瓶颈,其实和其他思路类似,尽量在用户态把事情做了。 - 多线程下
fork()
还有其他的问题,我们的场景是fork() 然后马上执行新的程序,如果你是真的要fork,这可能会遇到更多的问题,比如锁等等,请参考http://rachelbythebay.com/w/2014/08/16/forkenv/
参考资料
- 《Understanding Linux Kernel 3rd》
- HHVM AdminServer: https://github.com/facebook/hhvm/blob/master/hphp/doc/command.admin_server
- PHP-FPM status page: https://www.centos.bz/2013/09/enable-php-fpm-status-page/
- HHVM: http://hhvm.com/
相关推荐
在计算机科学中,多进程和多线程是两种并发执行的方式,它们允许程序在同一时间处理多个任务,从而提高系统的效率和响应性。C语言作为一门底层且强大的编程语言,提供了丰富的系统调用接口来实现多进程和多线程编程...
例如,当程序需要频繁使用fork()时,单线程是唯一选择,因为多线程会导致复杂性和潜在的死锁问题。而在限制CPU占用的场景下,单线程也可能更合适,因为它可以更好地控制资源消耗。 多线程的优势在于能够在一个进程...
- **适用场景**:适用于读多写少的场景,但在写操作频繁时可能导致性能下降。 **17. 通信工具类** - **CountDownLatch**:允许一个或多个线程等待其他线程完成操作。 - **CyclicBarrier**:让一组线程等待至某个...
在IT领域,多进程(Multithreading)和多线程(Multiprocessing)是并发执行任务的两种主要方式,尤其在Linux系统中,这两种技术被广泛应用于优化系统性能和提高资源利用率。本文将深入探讨C语言在Linux环境下实现多...
Python3的Socket编程结合Threading和Fork技术可以创建高效的并发服务器,允许同时处理多个客户端请求。...注意,虽然示例没有使用多进程,但在实际应用中,根据负载情况,结合多线程和多进程可以进一步优化服务器性能。
2. 性能优化:通过合理使用多进程和多线程,可以提高程序并行计算的能力,但过度使用可能导致上下文切换开销过大。 3. 资源管理:有效管理进程和线程的生命周期,避免资源泄露。 4. 并发调试:并发程序的调试往往...
创建子进程`fork()`虽然采用了写时复制(Copy-on-Write)技术,减少了内存拷贝,但依然需要复制内存页表和文件描述符等资源。相比之下,创建线程(如POSIX线程)时则无需这些复制,因此速度更快。`clone()`函数用于...
非线程安全的函数在多线程程序中使用时需谨慎,可能需要加锁保护。 7. **线程可移植性**:POSIX线程标准保证了代码的跨平台兼容性。在不同的UNIX-like系统(如Solaris、FreeBSD、Linux)上,可以使用相同的API编写...
- 创建进程(fork)的开销大,即便使用了写时复制(Copy on Write)技术,依然存在资源消耗。 - 父子进程间的通信复杂,通常需要借助IPC(进程间通信)。 - 并发连接数有限,过多的进程会导致系统压力增大。 2. ...
当一个线程正在执行,并持有资源锁(如互斥量)时,如果在这个时刻调用了`fork()`系统调用,将会在子进程中复制父进程的状态,包括占用的锁。这将使得子进程在执行时也尝试获取相同的锁,从而造成死锁,因为两个进程...
因此,编写多线程代码时,程序员必须对线程安全编程有深入理解,并谨慎使用同步机制。 总之,POSIX线程是多线程编程的重要工具,适用于那些需要高效并发处理和跨平台兼容性的应用。通过理解和熟练掌握POSIX线程,...
在线程的创建过程中,使用`pthread_create`函数,这与创建一个全新的进程时使用的`fork`函数形成对比,后者会复制整个进程的上下文。 #### 知识点四:POSIX线程的编程示例 以下是一个简单的POSIX线程编程示例,...
首先,传统的并发方式包括使用fork或者spawn threads,但PHP并不支持多线程编程,而fork通常用于Unix/Linux系统中进程的复制。因此,寻求在PHP中实现多线程的并发网络请求,实际上是在寻找异步或非阻塞的方式来提升...
Linux作为一个多用户多任务的现代操作系统,其进程和线程的管理是系统设计的重要组成部分。在Linux中,进程管理涉及进程的创建、执行、状态转换和退出等,而线程作为一种轻量级的进程,为程序并发执行提供了条件。...
在多线程环境下,线程共享进程的地址空间和其他资源,这让它们可以高效地进行通信。多线程并发编程的一个核心概念是同步。在并发实验中,使用了共享内存(共享变量)和信号量来协调不同线程间的执行顺序和数据访问。...
学会使用`errno`、`perror()`、`strerror()`等进行错误处理,以及利用`gdb`等调试工具进行多线程/进程调试是必不可少的技能。 9. **死锁与饥饿**: 并发编程中常见的问题包括死锁(多个进程互相等待对方释放资源导致...