【问题标题】:How to create a single instance application in C or C++如何在 C 或 C++ 中创建单实例应用程序
【发布时间】:2011-07-17 09:18:48
【问题描述】:

您对创建单实例应用程序有什么建议,以便一次只允许运行一个进程?文件锁、互斥锁还是什么?

【问题讨论】:

  • C 语言对此没有概念。您必须指定要在哪个环境中执行此操作,因为解决方案将特定于该环境。
  • 主要关注的是linux环境

标签: c++ c linux single-instance


【解决方案1】:

一个好办法是:

#include <sys/file.h>
#include <errno.h>

int pid_file = open("/var/run/whatever.pid", O_CREAT | O_RDWR, 0666);
int rc = flock(pid_file, LOCK_EX | LOCK_NB);
if(rc) {
    if(EWOULDBLOCK == errno)
        ; // another instance is running
}
else {
    // this is the first instance
}

请注意,锁定允许您忽略陈旧的 pid 文件(即您不必删除它们)。当应用程序因任何原因终止时,操作系统会为您释放文件锁定。

Pid 文件并不是非常有用,因为它们可能是陈旧的(文件存在但进程不存在)。因此,应用程序可执行文件本身可以被锁定,而不是创建和锁定 pid 文件。

更高级的方法是使用预定义的套接字名称创建和绑定 unix 域套接字。您的应用程序的第一个实例绑定成功。同样,当应用程序因任何原因终止时,操作系统会取消绑定套接字。当bind() 失败时,应用程序的另一个实例可以connect() 并使用此套接字将其命令行参数传递给第一个实例。

【讨论】:

  • flock 是非标准的。使用fcntllockf 锁定。
  • @psusi: a) 如果open() 失败flock() 也失败,因此无需检查open() 的结果; b) 文件可能存在但可能是陈旧的,需要锁定它以检查进程是否存在。
  • 如果rc 不为零,但errno != EWOULDBLOCK 怎么办?检查errno 是否重要?
  • /var/run 似乎是放置此类文件的标准位置。但它归 root 所有,所以 open() 抱怨权限!如果我不希望该进程需要由超级用户运行,那么哪里是放置它的好地方?
  • @Gauthier tldp.org/LDP/Linux-Filesystem-Hierarchy/html/…: /tmp: 这个目录主要包含临时需要的文件。许多程序使用它来创建锁定文件和临时存储数据...
【解决方案2】:

这是 C++ 中的解决方案。它使用Maxim 的插座推荐。我比基于文件的锁定解决方案更喜欢这个解决方案,因为如果进程崩溃并且不删除锁定文件,基于文件的解决方案就会失败。其他用户将无法删除该文件并将其锁定。进程退出时会自动删除套接字。

用法:

int main()
{
   SingletonProcess singleton(5555); // pick a port number to use that is specific to this app
   if (!singleton())
   {
     cerr << "process running already. See " << singleton.GetLockFileName() << endl;
     return 1;
   }
   ... rest of the app
}

代码:

#include <netinet/in.h>

class SingletonProcess
{
public:
    SingletonProcess(uint16_t port0)
            : socket_fd(-1)
              , rc(1)
              , port(port0)
    {
    }

    ~SingletonProcess()
    {
        if (socket_fd != -1)
        {
            close(socket_fd);
        }
    }

    bool operator()()
    {
        if (socket_fd == -1 || rc)
        {
            socket_fd = -1;
            rc = 1;

            if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
            {
                throw std::runtime_error(std::string("Could not create socket: ") +  strerror(errno));
            }
            else
            {
                struct sockaddr_in name;
                name.sin_family = AF_INET;
                name.sin_port = htons (port);
                name.sin_addr.s_addr = htonl (INADDR_ANY);
                rc = bind (socket_fd, (struct sockaddr *) &name, sizeof (name));
            }
        }
        return (socket_fd != -1 && rc == 0);
    }

    std::string GetLockFileName()
    {
        return "port " + std::to_string(port);
    }

private:
    int socket_fd = -1;
    int rc;
    uint16_t port;
};

【讨论】:

  • 在 gcc 4.7 之前的编译器上,它给出“SingletonProcess.h:42:29: 错误:ISO C++ 禁止非常量静态成员‘socket_fd’的类内初始化”
  • 所以,我在课程开始时使用:SingletonProcess() : socket_fd(-1) {}
  • 小心。衍生的子进程继承此文件描述符。所以进程完成后netstat显示5555端口被子进程使用
  • 小心 #2:我遇到了一些神秘的问题,即 (linux) 启动脚本没有偶尔启动这些进程之一。我还没有证明,但我怀疑这是因为我忘记在初始化脚本中将network 添加到Required-Start。也许如果网络尚未初始化,绑定(或套接字创建)将失败......
  • 为什么不使用 RAII?在实例化时创建并连接套接字,并在销毁时将其关闭。 (使用状态位(如 std::fstream 那样)或抛出失败以处理错误)
【解决方案3】:

对于 Windows,一个命名的内核对象(例如 CreateEvent、CreateMutex)。对于 unix,一个 pid 文件 - 创建一个文件并将您的进程 ID 写入其中。

【讨论】:

    【解决方案4】:

    您可以创建一个“匿名命名空间”AF_UNIX 套接字。这完全是特定于 Linux 的,但具有实际上不必存在文件系统的优点。

    阅读 unix(7) 的手册页了解更多信息。

    【讨论】:

    • 正确的术语是“abstract 命名空间”(和抽象套接字地址)。
    【解决方案5】:

    避免基于文件的锁定

    避免使用基于文件的锁定机制来实现应用程序的单例实例总是好的。用户可以随时将锁定文件重命名为不同的名称并再次运行应用程序,如下所示:

    mv lockfile.pid lockfile1.pid
    

    其中lockfile.pid 是在运行应用程序之前检查是否存在的锁定文件。

    因此,最好对仅对内核直接可见的对象使用锁定方案。所以,任何与文件系统有关的东西都是不可靠的。

    所以最好的选择是绑定到 inet 套接字。请注意,unix 域套接字驻留在文件系统中并且不可靠。

    或者,您也可以使用 DBUS。

    【讨论】:

    • 另一方面,使用 inet 套接字,用户可以在应用程序之前打开该套接字,以防止它能够运行(DOS 类型的攻击)。所以这也不是万无一失的。使用suitable permissions set up on a filesystem 可以确保基于文件的锁定安全。但是 inet 套接字没有权限控制谁可以打开它们。
    • 可以通过在不同的网络命名空间中运行两个实例来绕过基于“inet socket”的锁(有意或无意地)。
    【解决方案6】:

    似乎没有提到 - 可以在共享内存中创建互斥体,但需要将其标记为由属性共享(未测试):

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_t *mutex = shmat(SHARED_MEMORY_ID, NULL, 0);
    pthread_mutex_init(mutex, &attr);
    

    还有共享内存信号量(但我没找到如何锁定):

    int sem_id = semget(SHARED_MEMORY_KEY, 1, 0);
    

    【讨论】:

    • semop() 用于锁定和解锁 System V 信号量,semget() 将返回。 (POSIX 信号量--man sem_overview--通常更可取。)
    【解决方案7】:

    没有人提到它,但sem_open() 在现代兼容 POSIX 的操作系统下创建了一个真正的命名信号量。如果你给一个信号量一个初始值1,它就变成了一个互斥体(只要在成功获得锁的情况下才严格释放它)。

    使用多个基于sem_open() 的对象,您可以创建所有常见的等效Windows 命名对象- 命名互斥体、命名信号量和命名事件。将“manual”设置为 true 的命名事件有点难以模拟(它需要四个信号量对象才能正确模拟 CreateEvent()SetEvent()ResetEvent())。无论如何,我离题了。

    另外,还有命名共享内存。您可以在命名共享内存中使用“共享进程”属性初始化 pthread 互斥锁,然后在使用shm_open()/mmap() 打开共享内存的句柄后,所有进程都可以安全地访问该互斥锁对象。 sem_open() 如果适用于您的平台会更容易(如果不是,应该是为了理智)。

    无论您使用哪种方法,要测试应用程序的单个实例,请使用等待函数的trylock() 变体(例如sem_trywait())。如果进程是唯一运行的进程,它将成功锁定互斥锁。如果不是,它将立即失败。

    不要忘记在应用程序退出时解锁和关闭互斥锁。

    【讨论】:

    • 不幸的是,如果进程被杀死,POSIX 信号量似乎无法“清理”——这与锁定文件或套接字不同,如果进程被杀死,操作系统将关闭文件或套接字。见The Problem with POSIX Semaphores
    • 这是Linux内核本身的问题。 POSIX 信号量是在用户空间中实现的。 Windows 命名对象在内核中实现,这允许内核在过早终止进程后正确清理。自从写了这个答案后,我切换了我的同步库代码以利用直接共享内存对象 (shm_open()),因为我发现 sem_open() 相当缺乏:github.com/cubiclesoft/cross-platform-cpp
    • 此外,许多 *NIX 将 /dev/shm 映射到文件空间,这允许对这些对象运行常规文件系统调用。这包括使用“rm”删除任何损坏的信号量/共享内存对象。
    • 如何可靠地确定信号量/共享内存对象是否“损坏”?
    • 当人们尝试手动运行应用程序并尝试获取互斥体并失败时,必须注意到应用程序正在退出。由于另一个不再存在的进程正在(部分或全部)持有它,互斥锁将永远不会被释放。这个问题就是内核负责正确实现全局同步对象的原因,这是 Windows 很少做对的地方。
    【解决方案8】:

    这取决于您希望通过强制您的应用程序只有一个实例来避免哪个问题以及您考虑实例的范围。

    对于守护进程——通常的方法是拥有一个/var/run/app.pid 文件。

    对于用户应用程序,我遇到的应用程序问题更多,这些问题阻止我运行它们两次,而不是能够运行两次不应该运行的应用程序。因此,关于“为什么以及在哪个范围”的答案非常重要,并且可能会针对原因和预期范围提供具体的答案。

    【讨论】:

      【解决方案9】:

      根据maxim's answer 中的提示,这里是我的双重角色守护进程的 POSIX 解决方案(即可以充当守护进程并作为与该守护进程通信的客户端的单个应用程序)。该方案的优点是,当首先启动的实例应该是守护进程并且所有后续执行都应该只加载该守护进程的工作时,该方案提供了一个优雅的问题解决方案。这是一个完整的例子,但缺少真正的守护进程应该做的很多事情(例如使用syslog for logging and fork to put itself into background correctly、删除特权等),但它已经很长了,并且完全可以正常工作。到目前为止,我只在 Linux 上对此进行了测试,但 IIRC 它应该是所有 POSIX 兼容的。

      在示例中,客户端可以将整数作为第一个命令行参数传递给它们,并通过套接字由atoi 解析到守护进程,守护进程将其打印到stdout。使用这种套接字,还可以传输数组、结构甚至文件描述符(参见man 7 unix)。

      #include <stdio.h>
      #include <stddef.h>
      #include <stdbool.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <errno.h>
      #include <signal.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      
      #define SOCKET_NAME "/tmp/exampled"
      
      static int socket_fd = -1;
      static bool isdaemon = false;
      static bool run = true;
      
      /* returns
       *   -1 on errors
       *    0 on successful server bindings
       *   1 on successful client connects
       */
      int singleton_connect(const char *name) {
          int len, tmpd;
          struct sockaddr_un addr = {0};
      
          if ((tmpd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
              printf("Could not create socket: '%s'.\n", strerror(errno));
              return -1;
          }
      
          /* fill in socket address structure */
          addr.sun_family = AF_UNIX;
          strcpy(addr.sun_path, name);
          len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
      
          int ret;
          unsigned int retries = 1;
          do {
              /* bind the name to the descriptor */
              ret = bind(tmpd, (struct sockaddr *)&addr, len);
              /* if this succeeds there was no daemon before */
              if (ret == 0) {
                  socket_fd = tmpd;
                  isdaemon = true;
                  return 0;
              } else {
                  if (errno == EADDRINUSE) {
                      ret = connect(tmpd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un));
                      if (ret != 0) {
                          if (errno == ECONNREFUSED) {
                              printf("Could not connect to socket - assuming daemon died.\n");
                              unlink(name);
                              continue;
                          }
                          printf("Could not connect to socket: '%s'.\n", strerror(errno));
                          continue;
                      }
                      printf("Daemon is already running.\n");
                      socket_fd = tmpd;
                      return 1;
                  }
                  printf("Could not bind to socket: '%s'.\n", strerror(errno));
                  continue;
              }
          } while (retries-- > 0);
      
          printf("Could neither connect to an existing daemon nor become one.\n");
          close(tmpd);
          return -1;
      }
      
      static void cleanup(void) {
          if (socket_fd >= 0) {
              if (isdaemon) {
                  if (unlink(SOCKET_NAME) < 0)
                      printf("Could not remove FIFO.\n");
              } else
                  close(socket_fd);
          }
      }
      
      static void handler(int sig) {
          run = false;
      }
      
      int main(int argc, char **argv) {
          switch (singleton_connect(SOCKET_NAME)) {
              case 0: { /* Daemon */
      
                  struct sigaction sa;
                  sa.sa_handler = &handler;
                  sigemptyset(&sa.sa_mask);
                  if (sigaction(SIGINT, &sa, NULL) != 0 || sigaction(SIGQUIT, &sa, NULL) != 0 || sigaction(SIGTERM, &sa, NULL) != 0) {
                      printf("Could not set up signal handlers!\n");
                      cleanup();
                      return EXIT_FAILURE;
                  }
      
                  struct msghdr msg = {0};
                  struct iovec iovec;
                  int client_arg;
                  iovec.iov_base = &client_arg;
                  iovec.iov_len = sizeof(client_arg);
                  msg.msg_iov = &iovec;
                  msg.msg_iovlen = 1;
      
                  while (run) {
                      int ret = recvmsg(socket_fd, &msg, MSG_DONTWAIT);
                      if (ret != sizeof(client_arg)) {
                          if (errno != EAGAIN && errno != EWOULDBLOCK) {
                              printf("Error while accessing socket: %s\n", strerror(errno));
                              exit(1);
                          }
                          printf("No further client_args in socket.\n");
                      } else {
                          printf("received client_arg=%d\n", client_arg);
                      }
      
                      /* do daemon stuff */
                      sleep(1);
                  }
                  printf("Dropped out of daemon loop. Shutting down.\n");
                  cleanup();
                  return EXIT_FAILURE;
              }
              case 1: { /* Client */
                  if (argc < 2) {
                      printf("Usage: %s <int>\n", argv[0]);
                      return EXIT_FAILURE;
                  }
                  struct iovec iovec;
                  struct msghdr msg = {0};
                  int client_arg = atoi(argv[1]);
                  iovec.iov_base = &client_arg;
                  iovec.iov_len = sizeof(client_arg);
                  msg.msg_iov = &iovec;
                  msg.msg_iovlen = 1;
                  int ret = sendmsg(socket_fd, &msg, 0);
                  if (ret != sizeof(client_arg)) {
                      if (ret < 0)
                          printf("Could not send device address to daemon: '%s'!\n", strerror(errno));
                      else
                          printf("Could not send device address to daemon completely!\n");
                      cleanup();
                      return EXIT_FAILURE;
                  }
                  printf("Sent client_arg (%d) to daemon.\n", client_arg);
                  break;
              }
              default:
                  cleanup();
                  return EXIT_FAILURE;
          }
      
          cleanup();
          return EXIT_SUCCESS;
      }
      

      【讨论】:

        【解决方案10】:

        我刚刚写了一个,并进行了测试。

        #define PID_FILE "/tmp/pidfile"
        static void create_pidfile(void) {
            int fd = open(PID_FILE, O_RDWR | O_CREAT | O_EXCL, 0);
        
            close(fd);
        }
        
        int main(void) {
            int fd = open(PID_FILE, O_RDONLY);
            if (fd > 0) {
                close(fd);
                return 0;
            }
        
            // make sure only one instance is running
            create_pidfile();
        }
        

        【讨论】:

        • 这有很大的问题;如果第二个进程在第一个进程的检查之后但在第一个进程创建之前进行检查,会发生什么?
        【解决方案11】:

        只需在单独的线程上运行此代码:

        void lock() {
          while(1) {
            ofstream closer("myapplock.locker", ios::trunc);
            closer << "locked";
            closer.close();
          }
        }
        

        将此作为您的主代码运行:

        int main() {
          ifstream reader("myapplock.locker");
          string s;
          reader >> s;
          if (s != "locked") {
          //your code
          }
          return 0;
        }
        

        【讨论】:

          【解决方案12】:

          这里是基于sem_open的解决方案

          /*
          *compile with :
          *gcc single.c -o single -pthread
          */
          
          /*
           * run multiple instance on 'single', and check the behavior
           */
          #include <stdio.h>
          #include <fcntl.h>    
          #include <sys/stat.h>        
          #include <semaphore.h>
          #include <unistd.h>
          #include <errno.h>
          
          #define SEM_NAME "/mysem_911"
          
          int main()
          {
            sem_t *sem;
            int rc; 
          
            sem = sem_open(SEM_NAME, O_CREAT, S_IRWXU, 1); 
            if(sem==SEM_FAILED){
              printf("sem_open: failed errno:%d\n", errno);
            }
          
            rc=sem_trywait(sem);
          
            if(rc == 0){ 
              printf("Obtained lock !!!\n");
              sleep(10);
              //sem_post(sem);
              sem_unlink(SEM_NAME);
            }else{
              printf("Lock not obtained\n");
            }
          }
          

          一个不同答案的 cmets 说“我发现 sem_open() 相当缺乏”。我不确定缺少的细节

          【讨论】:

            【解决方案13】:

            所有功劳归于 Mark Lakata。我只是做了一些非常小的修饰。

            main.cpp

            #include "singleton.hpp"
            #include <iostream>
            using namespace std;
            int main()
            {
               SingletonProcess singleton(5555); // pick a port number to use that is specific to this app
               if (!singleton())
               {
                 cerr << "process running already. See " << singleton.GetLockFileName() << endl;
                 return 1;
               }
               // ... rest of the app
            }
            

            singleton.hpp

            #include <netinet/in.h>
            #include <unistd.h>
            #include <cerrno>
            #include <string>
            #include <cstring>
            #include <stdexcept>
            
            using namespace std;
            class SingletonProcess
            {
            public:
                SingletonProcess(uint16_t port0)
                        : socket_fd(-1)
                          , rc(1)
                          , port(port0)
                {
                }
            
                ~SingletonProcess()
                {
                    if (socket_fd != -1)
                    {
                        close(socket_fd);
                    }
                }
            
                bool operator()()
                {
                    if (socket_fd == -1 || rc)
                    {
                        socket_fd = -1;
                        rc = 1;
            
                        if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
                        {
                            throw std::runtime_error(std::string("Could not create socket: ") +  strerror(errno));
                        }
                        else
                        {
                            struct sockaddr_in name;
                            name.sin_family = AF_INET;
                            name.sin_port = htons (port);
                            name.sin_addr.s_addr = htonl (INADDR_ANY);
                            rc = bind (socket_fd, (struct sockaddr *) &name, sizeof (name));
                        }
                    }
                    return (socket_fd != -1 && rc == 0);
                }
            
                std::string GetLockFileName()
                {
                    return "port " + std::to_string(port);
                }
            
            private:
                int socket_fd = -1;
                int rc;
                uint16_t port;
            };
            

            【讨论】:

              【解决方案14】:
              #include <windows.h>
              
              int main(int argc, char *argv[])
              {
                  // ensure only one running instance
                  HANDLE hMutexH`enter code here`andle = CreateMutex(NULL, TRUE, L"my.mutex.name");
                  if (GetLastError() == ERROR_ALREADY_EXISTS)
                  {
                      return 0;
                  }
                  
                  // rest of the program
              
                  ReleaseMutex(hMutexHandle);
                  CloseHandle(hMutexHandle);
                  
                  return 0;
              }
              

              发件人:HERE

              【讨论】:

              • 正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-02-09
              • 2012-05-28
              • 1970-01-01
              相关资源
              最近更新 更多