使用fork()共享堆内存

10

我正在开发一个使用C语言实现的数据库服务器,它将处理来自多个客户端的请求。我使用 fork() 为各个客户端处理连接。

该服务器将数据存储在堆(heap)中,其中包括指向哈希表和动态分配记录的根指针。这些记录是具有指向各种数据类型的指针的结构体。我希望进程能够共享此数据,以便当客户端对堆进行更改时,更改将对其他客户端可见。

我了解到 fork() 使用 COW(写时复制),我的理解是当子进程尝试修改内存中的数据时,它会复制父进程的堆(和栈)内存。

我发现可以使用 shm 库来共享内存。

下面的代码是否是共享堆内存(在 shared_string 中)的有效方法?如果另一个子进程使用类似的代码(即从 //start 开始),那么该子进程在运行时和死亡后,其他子进程是否能够读/写它?

key_t key;
int shmid;

key = ftok("/tmp",'R');
shmid = shmget(key, 1024, 0644 | IPC_CREAT);

//start
char * string;
string = malloc(sizeof(char) * 10);

strcpy(string, "a string");

char * shared_string;

shared_string = shmat(shmid, string, 0);

strcpy(shared_string, string);

以下是我对此的一些想法和关注点:

  • 我在考虑共享数据库的根指针。我不确定这是否可行,或者是否需要将所有分配的内存标记为共享。

  • 我不确定父级/其他子级是否能够访问由子级分配的内存。

  • 我不确定子进程分配的内存在被杀死后是否仍留在堆上,或者是否会被释放。


3
如果你想要在同一个程序的不同部分之间共享内存,使用“线程”是更常见的方式。但是,在使用锁等同步机制时,需要非常小心地管理对共享数据结构的访问。 - A B
1
你需要使用共享内存来共享所有的东西。 - Niklas B.
只有 shm 可以被共享,如果你分配了新的内存,也必须在 shm 上进行,没有捷径可走。 - pizza
1
如果你必须使用 fork(),那么你需要使用共享内存(以某种形式)来处理公共数据,并确保非常小心地控制对该数据的访问。 - Jonathan Leffler
1
我认为你误解了写时复制语义。它对两个进程都发生,并且再现了父进程的完全重复映像的原始UNIX语义。没有发生“逻辑”存储共享,即使在物理上一些(只读)存储是共享的。 - Hot Licks
5个回答

5
首先,使用fork完全不适合你想要实现的目标。即使你可以让它工作,这也是一种可怕的黑客行为。通常情况下,fork只适用于非常简单的程序,并且我会毫不犹豫地说,除了这里之外,fork永远不应该被使用,除非紧接着使用exec。你真正应该使用线程。
话虽如此,在fork之后,唯一有可能在父进程和子进程之间共享内存并且相同指针在两者之间有效的方法就是在fork之前使用mmap(或shmat, 但那样会更加难看)一个文件或匿名映射,使用MAP_SHARED。这样做的原因是在fork之后无法创建新的共享内存,因为不能保证它将在两者之间映射到相同的地址范围内。
所以,不要使用fork。它不是适合这项工作的正确工具。

同意。Fork()创建一个单独的、大部分相同的实例。需要使用线程或asio,后者可能会比较复杂,但Boost在提供它方面做得相当不错。 - std''OrgnlDave

4
我认为你基本上想要做的是Redis(和可能其他一些)所做的事情。他们在http://redis.io/topics/persistence中描述了它(搜索“copy-on-write”)。
  • 线程会破坏目的
  • 经典共享内存(shm,映射内存)也会破坏目的
使用此方法的主要好处是避免锁定,这可能很难正确处理。
据我所知,使用COW的想法是:
  • 想要写入时进行fork,而不是提前
  • 子进程将数据重写到磁盘上,然后立即退出
  • 父进程继续执行其工作,并在子进程退出时检测(SIGCHLD)。如果父进程在执行其工作时对哈希表进行更改,则内核将执行受影响块的副本(正确术语?)。
    使用“脏标志”来跟踪是否需要新的fork来执行新的写入。
需要注意的事项:
  • 确保只有一个未完成的子任务
  • 事务安全性: 先写入临时文件,然后移动它,以便始终拥有完整的副本,如果移动不是原子操作,则可能保留之前的副本。
  • 测试是否会出现其他重复资源的问题(文件描述符,C++中的全局析构函数)

您可能还想看一下redis代码


临时文件并非必需品:您可以使用管道将子进程发起的更改通信回父进程。采用这种方法,每个子进程都可以拥有整个数据的一致、原子视图,而主进程不需要任何锁定来确保一致性。这样,您甚至可以同时拥有多个子进程,每个子进程都有自己不变的、一致的数据视图,以及一个主进程,在分叉新的子进程之间按照子进程的指示更新数据。 - cmaster - reinstate monica
@cmaster 临时文件并不是用作锁定、线程同步或任何类似的东西。它是为了在文件系统中实现原子性。如果在写入过程中崩溃,可能会出现坏文件。如果你完成了写入,则重命名文件(原子操作)。 - nhed
我从没想过这样。我试图指出的是,分叉可以被视为所有内存数据的廉价检查点。一个进程可以自由地分析数据,而主进程已经忙于应用来自另一个子进程的更改。在这样的设置中,您不需要将任何数据写入磁盘以进行通信。而这种设置的目的不是为了持久化数据,而是为了确保对内存数据库进行坚不可摧的并发访问。 - cmaster - reinstate monica
好的,我想我回答了我如何使用COW。我的最终目标是持久性。在您描述的情况下,我认为通常是有意义的,只要一方(子代?)将内存视为只读即可。我回答的主要观点是,如果您决定使用COW,则使用经典共享内存就没有意义。 - nhed

1
我在考虑共享数据库的根指针。我不确定这是否可行,或者我是否必须将所有分配的内存标记为共享。
每个进程都有自己的私有内存范围。写时复制是一个内核空间优化,对用户空间透明。
正如其他人所说,SHM或mmap文件是在独立进程之间共享内存的唯一方法。

0

正如您发现的那样,如果要在独立进程之间共享内存(来自fork或其他方式),则需要使用共享内存,使用SYSV shm库或带有MAP_SHARED的mmap。不幸的是,这些工具是粗粒度的工具,仅适用于处理少量大块,并且不适用于像使用malloc/free一样进行细粒度内存管理。

为了在进程之间拥有有用的共享内存,需要在shm或mmap上构建堆。您可以使用我的小型shm_malloc库来实现这一点,该库允许您像使用malloc/free一样调用shm_mallocshm_free


0

如果你必须使用fork,共享内存似乎是“唯一”的选择。

实际上,在你的场景中,线程更加适合。

如果你不想使用多线程,这里有另一个选择,你可以只使用单进程和单线程模式,比如redis

使用这种模式,你不需要担心像lock这样的问题,如果你想要扩展,只需设计一个路由策略,将路由与key的哈希值相关联即可。


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