単一インスタンス アプリケーションを作成して、一度に 1 つのプロセスしか実行できないようにするための提案は何ですか? ファイルロック、ミューテックス、または何?
14 に答える
良い方法は次のとおりです。
#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 ファイルを無視できることに注意してください (つまり、それらを削除する必要はありません)。アプリケーションが何らかの理由で終了すると、OS がファイル ロックを解放します。
pid ファイルは古くなる可能性があるため、あまり役に立ちません (ファイルは存在しますが、プロセスは存在しません)。したがって、pid ファイルを作成してロックする代わりに、アプリケーションの実行可能ファイル自体をロックできます。
より高度な方法は、定義済みのソケット名を使用して UNIX ドメイン ソケットを作成およびバインドすることです。アプリケーションの最初のインスタンスでバインドが成功します。ここでも、アプリケーションが何らかの理由で終了すると、OS はソケットのバインドを解除します。bind()
アプリケーションの別のインスタンスが失敗するとconnect()
、このソケットを使用してコマンドライン引数を最初のインスタンスに渡すことができます。
これがC++での解決策です。マキシムの推奨するソケットを使用します。プロセスがクラッシュしてロック ファイルが削除されない場合、ファイル ベースのロック ソリューションは失敗するため、このソリューションはファイル ベースのロック ソリューションよりも優れています。別のユーザーがファイルを削除してロックすることはできません。プロセスが終了すると、ソケットは自動的に削除されます。
使用法:
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;
};
「匿名ネームスペース」AF_UNIX ソケットを作成できます。これは完全に Linux 固有のものですが、ファイルシステムが実際に存在する必要がないという利点があります。
詳細については、unix(7) の man ページを参照してください。
ファイルベースのロックを避ける
アプリケーションのシングルトン インスタンスを実装するために、ファイル ベースのロック メカニズムを使用しないことは常に良いことです。ユーザーはいつでもロック ファイルの名前を別の名前に変更し、次のようにアプリケーションを再度実行できます。
mv lockfile.pid lockfile1.pid
lockfile.pid
アプリケーションを実行する前に存在をチェックするロック ファイルはどこにありますか。
そのため、カーネルのみが直接参照できるオブジェクトに対してロック スキームを使用することが常に推奨されます。そのため、ファイル システムに関係するものはすべて信頼できません。
したがって、最良のオプションは、inet ソケットにバインドすることです。UNIX ドメイン ソケットはファイル システムにあり、信頼できないことに注意してください。
または、DBUS を使用して実行することもできます。
Windows の場合、名前付きカーネル オブジェクト (CreateEvent、CreateMutex など)。UNIX の場合、pid ファイル - ファイルを作成し、プロセス ID を書き込みます。
言及されていないようです-共有メモリにミューテックスを作成することは可能ですが、属性によって共有されるとマークする必要があります(テストされていません):
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);
アプリケーションにインスタンスを 1 つだけ持たせることによって回避したい問題と、インスタンスと見なすスコープによって異なります。
デーモンの場合 — 通常の方法は/var/run/app.pid
ファイルを用意することです。
ユーザー アプリケーションの場合、実行すべきではないアプリケーションを 2 回実行できる問題よりも、アプリケーションを 2 回実行できない問題が多くありました。したがって、「なぜ、どのスコープについて」という答えは非常に重要であり、おそらく、その理由と意図したスコープについて具体的な答えが得られるでしょう。
誰も言及していませんが、sem_open()
最新の POSIX 準拠の OS で実際の名前付きセマフォを作成します。セマフォに初期値 1 を指定すると、ミューテックスになります (ロックが正常に取得された場合にのみ厳密に解放される限り)。
いくつかのsem_open()
に基づくオブジェクトを使用すると、名前付きミューテックス、名前付きセマフォ、名前付きイベントなど、一般的な同等の Windows 名前付きオブジェクトをすべて作成できます。"manual" が true に設定された名前付きイベントは、エミュレートするのが少し難しくなります (適切にエミュレートするには、 、 、および の 4 つのセマフォ オブジェクトが必要CreateEvent()
ですSetEvent()
) ResetEvent()
。とにかく脱線します。
あるいは、名前付き共有メモリがあります。shm_open()
名前付き共有メモリで「共有プロセス」属性を使用して pthread ミューテックスを初期化すると、 /で共有メモリへのハンドルを開いた後、すべてのプロセスがそのミューテックス オブジェクトに安全にアクセスできますmmap()
。 sem_open()
プラットフォームで利用できる場合は簡単です(利用できない場合は、健全性のためです)。
使用する方法に関係なく、アプリケーションの単一インスタンスをテストするにtrylock()
は、wait 関数のバリアント (例: sem_trywait()
) を使用します。実行中のプロセスが 1 つだけの場合、mutex は正常にロックされます。そうでない場合は、すぐに失敗します。
アプリケーションの終了時にミューテックスのロックを解除して閉じることを忘れないでください。
ここでのマキシムの回答のヒントに基づいて、デュアルロールデーモン(つまり、デーモンとして、およびそのデーモンと通信するクライアントとして機能できる単一のアプリケーション)の私のPOSIXソリューションがあります。このスキームには、最初に開始されたインスタンスがデーモンである必要があり、その後のすべての実行がそのデーモンで作業をオフにする必要がある場合に、問題の洗練されたソリューションを提供するという利点があります。これは完全な例ですが、実際のデーモンが行う必要がある多くのことを欠いています (たとえばsyslog
、ログに使用しfork
てバックグラウンドに正しく配置する、権限を削除するなど)。これまでのところ 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;
}