如何在Unix中使用fork()?为什么不使用类似fork(pointerToFunctionToRun)的形式?

8

我有些困惑如何使用Unix的fork()。在需要并行化时,我习惯于在我的应用程序中生成线程。通常是这样的形式:

CreateNewThread(MyFunctionToRun());

void myFunctionToRun() { ... }

现在,当学习Unix的fork()时,我被给予了以下形式的示例:
fork();
printf("%d\n", 123);

在fork()之后的代码中,“分裂”了。我不明白fork()如何有用。为什么fork()没有与上面的CreateNewThread()类似的语法,你可以将要运行的函数的地址传递给它?

要实现类似于CreateNewThread()的功能,我需要创意地做一些事情,比如

//pseudo code
id = fork();

if (id == 0) { //im the child
    FunctionToRun();
} else { //im the parent
    wait();
}

也许问题在于我太习惯使用.NET的方式来生成线程,以至于我无法清晰地思考这个问题。我错过了什么?使用fork()相比CreateNewThread()有哪些优势?
PS:我知道fork()会生成一个新的进程,而CreateNewThread()会生成一个新的线程
谢谢

1
好的...那就是优势。 - Ignacio Vazquez-Abrams
1
有什么优点?我只看到了复杂性。 - devoured elysium
fork 在你想要执行同一个函数多次时似乎更有用,如果要使用不同的函数,则使用 if。 - mpapis
1
如果想要多次调用同一个函数,为什么不直接多次调用同一个函数呢?这样可以减少复杂性和开销。 - devoured elysium
8个回答

9

fork()函数的作用是“将当前进程状态复制到一个新进程,并从此处开始运行它”。由于代码现在在两个进程中运行,因此实际上会返回两次:一次在父进程中(其中它返回子进程的进程标识符),一次在子进程中(其中它返回零)。

fork()之后,在子进程中调用哪些函数是安全的有很多限制(请参见下文)。期望的是,fork()调用是生成运行具有自己状态的新可执行文件的新进程的第一部分。这个过程的第二部分是调用execve()或其变体之一,指定要加载到当前正在运行的进程中的可执行文件的路径、要提供给该进程的参数以及要包围该进程的环境变量。(如果您真的想这样做,没有任何阻止您重新执行当前正在运行的可执行文件并提供一个标志,使其从父进程离开的地方继续执行。)

UNIX中的fork()-exec()操作大致相当于Windows的CreateProcess()操作。更像它的是一个较新的函数:posix_spawn()

使用fork()的一个实际例子是shell,例如bash。命令shell一直在使用fork()。当您告诉shell运行一个程序(例如echo "hello world")时,它会fork自己,然后exec该程序。管道是由父进程在fork()exec()之间适当地设置stdoutstdin的分叉进程集合。

如果要创建新线程,应使用Posix线程库。使用pthread_create()创建新的Posix线程(pthread)。您的CreateNewThread()示例将如下所示:

#include <pthread.h>

/* Pthread functions are expected to accept and return void *. */ 
void *MyFunctionToRun(void *dummy __unused);

pthread_t thread;
int error = pthread_create(&thread,
        NULL/*use default thread attributes*/,
        MyFunctionToRun,
        (void *)NULL/*argument*/);

在线程问世之前,fork()是UNIX提供的最接近多线程的东西。现在有了线程,使用fork()几乎完全限制于生成一个新进程以执行不同的可执行文件。
下面是由于fork()先于多线程而出现的限制,因此只有调用fork()的线程在子进程中继续执行。根据POSIX

将创建带有单个线程的进程。如果多线程进程调用fork(),则新进程将包含一个调用线程及其整个地址空间的副本,可能包括互斥锁和其他资源的状态。因此,为了避免错误,在执行任何exec函数之前,子进程只能执行异步信号安全操作。[THR] [Option Start] 可以通过pthread_atfork()函数建立fork处理程序,以维护应用程序不变式跨fork()调用[Option End]

当应用程序从信号处理程序调用fork()并且pthread_atfork()注册的任何fork处理程序调用了一个不是异步信号安全的函数时,行为未定义。

因为您调用的任何库函数都可能代表您生成线程,所以偏执的假设是,在调用fork()exec()之间,您总是被限制执行异步信号安全操作。

7

除了历史背景之外,进程和线程在资源所有权和生命周期方面存在一些根本差异。

当您进行fork操作时,新进程将占用完全分离的内存空间。这与创建新线程非常重要的区别。在多线程应用程序中,您必须考虑如何访问和操作共享资源。已经被fork的进程必须使用进程间通信手段(例如共享内存、管道、远程过程调用、信号量等)显式地共享资源。

另一个区别是,fork()出的子进程可以比父进程存在更长的时间,而所有线程都会在进程终止时死亡。

在预计需要非常长的运行时间的客户端-服务器架构中,使用fork()而不是创建线程可能是一种有效的策略来对抗内存泄漏。您只需fork出一个新的子进程来处理每个客户端请求,然后在完成后杀死该子进程,而无需担心清理线程中的内存泄漏。唯一可能导致内存泄漏的来源就是调度事件的父进程。

一个类比:您可以将生成线程视为在单个浏览器窗口中打开选项卡,而将fork视为打开单独的浏览器窗口。


1
把线程分离看作是在单个浏览器窗口中打开选项卡,而分叉则像是打开独立的浏览器窗口。你没听说过Chrome吗?;-) 对于孩子能够比父母活得更久这一点提出了一个坚实的观点,加1。 - Tony Delroy
关于通过让进程死亡来进行资源清理:您不需要为每个客户端请求执行此操作,我记得Apache 1.3有一个参数,即每个进程处理的请求数量,然后再死亡。 - ninjalj

6

我认为更有效的问题是为什么CreateNewThread不像fork()一样返回线程 ID ... 毕竟fork()设置了一个先例。您的观点只是因为您先看到了其中一个而导致偏见。退一步讲,考虑到fork()会复制进程并继续执行...有什么比在下一条指令处更好的地方呢?为什么要增加一个函数调用以及一个只接受void*的函数(然后再添加其他东西)来使事情变得更加复杂呢?

您对迈克的评论说"我无法理解何时何地需要使用它。"基本上,当您想要时,您可以使用它:

  • 使用exec函数族来运行另一个进程
  • 独立进行一些并行处理(在内存使用、信号处理、资源、安全性和健壮性方面),例如:
    • 每个进程可能有管理文件描述符数量的限制,或者在32位系统上可能会有内存量的限制:第二个进程可以共享工作,同时获得自己的资源
    • 网络浏览器倾向于分叉出不同的进程,因为它们可以进行一些初始化,然后调用操作系统函数以永久降低其权限(例如,更改为不太可信任的用户ID、更改可以访问文件的“根”目录,或将某些内存页面设置为只读);大多数操作系统不允许在每个线程基础上进行同样程度的细粒度权限设置;另一个好处是如果子进程发生段错误或类似问题,则父进程可以处理该问题并继续运行,而多线程代码中的类似故障则会引起问题,例如崩溃的线程是否已损坏了内存,或者锁定是否仍由剩余线程保持,从而导致剩余线程受到影响

顺便提一下,使用UNIX/Linux并不意味着您必须放弃使用fork()来创建进程的线程...如果您更喜欢线程编程范式,可以使用pthread_create()和相关函数。


我的观点是,我调用CreateThread的原因非常明确。我之所以调用它,是因为我想并行运行一个函数。那么调用fork的常见用途是什么? - devoured elysium
1
@devoured elysium 运行新进程(例如运行新/不同程序)。在*nix中,fork()是创建新进程的唯一方法。如果您正在进行完全独立的并行处理,那么您也可以这样做-这样一个进程中的问题/错误/崩溃不会影响其他进程,而对于常规线程,其中一个崩溃会影响所有其他线程。 - nos
4
当然不会:Unix 讨厌驼峰式命名。 - tchrist
2
@devoured elysium,不完全正确。它提供了exec(programPath..)等功能,但这并不会创建一个新的进程,而是用programPath的映像替换现有进程。在win32中,CreateProcess()会创建一个新的进程并加载程序映像。在*nix中,您需要分两步进行操作:使用fork()创建一个新进程,然后使用exec()加载程序。 - nos
1
@devoured elysum:关于“调用fork的常规用途是什么?”- 用于创建进程的第二个副本,可以执行不同的进程或与第一个进程合作,可能接管某些I/O流(例如TCP客户端),或运行一些耗时的处理并通过共享内存协调结果。请记住,线程旨在使共享数据的处理更加容易/快速,因此在某些比较中,fork()进程似乎确实有点倒退。 - Tony Delroy
显示剩余2条评论

2

暂时不考虑生成进程和线程之间的区别:基本上,fork()是更基础的原语。虽然SpawnNewThread需要做一些后台工作来将程序计数器放在正确的位置,但fork不需要这样的工作,它只是复制(或虚拟复制)您的程序内存并继续计数器。


1
我理解fork的作用,但我不明白在哪些情况下你会想要使用它。 - devoured elysium

2

很久很久以前,就有了Fork。在“启动运行特定函数的线程”的想法出现之前,人们就想到了Fork。

人们使用fork并不是因为它“更好”,而是因为它是唯一一个跨越所有Linux变体的非特权用户模式进程创建函数。如果你想创建一个进程,你必须调用fork。对于某些目的来说,进程是你需要的,而不是线程。

你可能需要研究一下这个主题的早期论文。


所以,也许我不明白的是为什么要创建一个新进程。我的Windows使用经验告诉我,创建进程的唯一目的就是运行另一个可执行文件。我有什么遗漏吗? - devoured elysium
1
尝试实现一个Shell或以任何其他方式启动守护进程。 - bmargulies
3
这个 Leenooks 的东西和标准的 Unix fork(2) 有什么关系呢? - tchrist

1

当你想要同时做多件事情时,你可以使用fork。这被称为多任务处理,非常有用。

例如,这里有一个类似于telnet的程序:

#!/usr/bin/perl
use strict;
use IO::Socket;
my ($host, $port, $kidpid, $handle, $line);

unless (@ARGV == 2) { die "usage: $0 host port" }
($host, $port) = @ARGV;

# create a tcp connection to the specified host and port
$handle = IO::Socket::INET->new(Proto     => "tcp",
                                PeerAddr  => $host,
                                PeerPort  => $port)
       or die "can't connect to port $port on $host: $!";

$handle->autoflush(1);              # so output gets there right away
print STDERR "[Connected to $host:$port]\n";

# split the program into two processes, identical twins
die "can't fork: $!" unless defined($kidpid = fork());

if ($kidpid) {                      
    # parent copies the socket to standard output
    while (defined ($line = <$handle>)) {
        print STDOUT $line;
    }
    kill("TERM" => $kidpid);        # send SIGTERM to child
}
else {                              
    # child copies standard input to the socket
    while (defined ($line = <STDIN>)) {
        print $handle $line;
    }
}
exit;

看,这有多容易?


1
告诉那些习惯于使用线程进行并发的人,分叉提供并发并不是一个很大的优势。 - dmckee --- ex-moderator kitten

1
值得注意的是,多进程并不完全等同于多线程。由fork创建的新进程与旧进程共享非常少的上下文,这与线程的情况非常不同。
因此,让我们看看类Unix的线程系统:pthread_create的语义类似于CreateNewThread。
或者,反过来看,让我们看看Windows(或Java或其他以线程为生的系统)如何生成一个与当前正在运行的进程相同的进程(这就是Unix上的fork所做的)...好吧,我们可以期望有一个,但实际上没有:这不是所有时间都使用线程模型的一部分。(这并不是坏事,只是不同而已)。

0

Fork() 的最普遍用途是作为一种克隆服务器以便为每个新的 connect() 的客户端提供服务(因为新的进程会继承所有文件描述符的状态)。 但我也曾将其用于按需启动一个新的(本地运行的)服务端。 这种方案最好使用两次 fork() 调用 - 其中一个保留在父进程会话中,直到服务器启动并能够连接,另一个(我从子进程中派生它)成为服务器并离开父会话,因此无法再被(例如)SIGQUIT 所达到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接