IPC共享内存和POSIX共享内存的区别

3
我目前正在实现一个使用 posix 共享内存(shm_open()ftruncate()mmap()shm_unlink())的小C函数。
我面临的问题是,通常情况下我的应用会优雅地退出,我的清理例程会调用shm_unlink()。但如果我的进程被强制终止,我面临的问题是 shm 段仍然存在。我的应用程序使用fork(),甚至可以在多个实例中启动。因此,在启动过程中,如果我检测到有共享内存段存在,我该如何决定它是否是崩溃后的残留物,以便我可以重置它,或者某些其他进程可能仍在使用它?
在 SystemV IPC 共享内存函数集中,一旦我执行了 shmget() 和后续的 shmat(),我就可以访问shm_nattch。那么,在 posix 共享内存中是否有类似的东西呢?

也许你可以使用处理程序拦截信号并优雅地退出? - webuster
我能想到的唯一解决方案是某种心跳系统。也许在shm段中存储活动PID列表,并轮询它们以确保它们仍然存活? - Drew McGowen
@Webuster,你无法拦截SIGKILL信号,这是由kill -9引发的。 - Drew McGowen
嗯...看起来System V IPC共享内存设计得更好? - daparic
1个回答

3

POSIX共享内存实际上是映射内存的一种变体。主要区别在于使用shm_open()打开共享内存对象(而不是调用open()),并使用shm_unlink()关闭和删除该对象(而不是调用不删除对象的close())。shm_open()中的选项远少于open()提供的选项。

IPC共享内存是在程序之间传递数据的有效方法。一个程序将创建一个内存部分,其他进程(如果允许)可以访问它。

在我看来,它们之间的区别并不是很大或很大,因为POSIX共享内存只是后来和更标准的共享内存概念实现。kill -9问题确实是个问题。您无法设置任何处理程序,甚至atexit也不会被调用。但是您可以为SIGTERM设置处理程序并使用命令终止您的进程:

kill -SIGTERM <pid>

atexit和信号处理程序的示例代码:

#include <signal.h>
#include <unistd.h>
#include <cerrno>
#include <system_error>
#include <iostream>

static sigset_t theMask;

static void
signalWrapper(
    int         theSignalNumber,
    siginfo_t*  theSignalDescription,
    void*       theUserContext)
{
  if (theSignalNumber == SIGTERM)
  {
    std::cerr << "Clear shared memory and exit" << std::endl;
    exit(1);
  }

  // Reinstall handler (usefull for other signals)
  struct ::sigaction sa;
  sa.sa_sigaction = &signalWrapper;
  sa.sa_mask = theMask;
  sa.sa_flags = SA_SIGINFO;

  try
  {
    if (::sigaction(theSignalNumber, &sa, NULL) == -1)
      throw std::error_code(errno, std::system_category());
  }
  catch (const std::error_code& ec)
  {
    std::cerr << ec << std::endl;
  }
}

void
setupSignalHandlers()
{
  struct ::sigaction sa;

  // Prepare mask
  sigemptyset(&theMask);
  sigaddset(&theMask, SIGTERM);
  // Add some more if you need it to process

  sa.sa_mask      = theMask;
  sa.sa_flags     = SA_SIGINFO;
  sa.sa_sigaction = &signalWrapper;

  // Perform setup
  try
  {
    if (::sigaction(SIGTERM, &sa, NULL) == -1)
      throw std::error_code(errno, std::system_category());
  }
  catch (const std::error_code& ec)
  {
    std::cerr << ec << std::endl;
  }
}

void
bye()
{
  std::cout << "Bye!" << std::endl;
}

int
main()
{
  std::cout << "Set handler!" << std::endl;
  setupSignalHandlers();
  if (std::atexit(bye))
  {
    std::cerr << "Failed to register atexit" << std::endl;
    return 2;
  }

  std::cout << "Emit SIGTERM signals" << std::endl;
  kill(getpid(), SIGTERM);
  sleep(100);

  return 0;
}

另一种有趣的方法是在 shm_open() 之后直接使用 shm_unlink(),并使用 O_CLOEXEC。我编写了一个小代码示例来说明如何使用unlink和POSIX共享内存重新打开:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <cerrno>
#include <cstring>

#include <system_error>
#include <iostream>

// Simple ShMem with RAII
struct ShMem
{
  ShMem(
    const std::string& theName)
  : _handle(-1),
    _mode(S_IRUSR | S_IWUSR),
    _name(theName)
  {
    // Here we try to create the object with exclusive right to it
    // If the object exists - it must fail.
    // File opened as 0600. The open mode might be set with
    // class enum and additional paramter to constructor
    if ((_handle = shm_open(_name.c_str(), O_CREAT | O_RDWR | O_EXCL, _mode)) < 0)
    {
      std::cerr << strerror(errno) << std::endl;

      std::cout << "File " << _name << "exists. Try to reopen it!" << std::endl;
      if ((_handle = shm_open(_name.c_str(), O_RDWR | O_TRUNC, _mode)) < 0)
      {
        std::cerr << strerror(errno) << std::endl;
        throw std::error_code(errno, std::system_category());
      }
      else
        std::cout << _name << " reopened OK";
    }
    else
      std::cout << _name << " created OK";

    // Unlink but keep descriptor
    shm_unlink(_name.c_str());
  }

 ~ShMem()
  {
  }

  const int
  handle()
  {
    return _handle;
  }

private:

  int         _handle;
  mode_t      _mode;
  std::string _name;
};

使用 O_CLOEXEC,您可以完全删除 shm_unlink() 并查看在 kill -9 发生时会发生什么。


谢谢,我会尝试这个。除此之外,我还使用 POSIX 信号量(sem_open())来保护我的共享内存段。因此,我想对于信号量,我应该采用相同的方法。 - Karsten Brinkmann
我发现,一旦你使用shm_unlink()函数,一个并行进程调用带有O_CREAT | O_EXCL标志的shm_open()函数将不会失败,而是创建一个全新的shm段,因此我写入shm的信息不会在进程之间传输。此外,在执行shm_unlink()函数后,所有非O_CREAT标志的shm_open()函数调用都将失败。 - Karsten Brinkmann
嗯,好的。你可以尝试将O_CLOEXEC与创建标志组合使用,看看会发生什么。shm_unlink()必须放在析构函数中。 - Tanuki
@Tanuki,我不明白你在O_CLOEXEC上的意思。我的代码中使用的是shm_open()、ftruncate()和mmap()。在mmap()之后,我关闭了文件描述符。所以你的意思是我应该使用带有O_CLOEXEC的fcntl()吗?顺便说一下,我将shm_unlink()放到了析构函数中,在SIGINT和SIGTERM时析构函数被调用,现在唯一剩下的问题就是崩溃或kill -9。 - Karsten Brinkmann
我尝试了shm_open(_name.c_str(), O_CREAT | O_RDWR | O_EXCL | O_CLOEXEC, _mode)并且在我的系统上它可以正常工作。这意味着您可以在创建时启用O_CLOEXEC。 - Tanuki
我已经尝试了O_CLOEEXEC,但是我没有看到任何好处。你能再次在这里发布你的代码吗? - Karsten Brinkmann

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