作業を自動的に復元する機能を持つ独自のLinuxデーモンを作成します

親愛なるハブユーザー、サーバーデーモンを作成した経験を共有したいと思います。 Runetにはこのテーマに関する記事がたくさんありますが、そのほとんどは次のような重要な質問への回答を提供していません。

この部分では、次の点を考慮します。

明確にするために、次の部分のソースコードが表示されます。


悪魔の原理。
悪魔から判断すると、これはバックグラウンドで実行される通常のプログラムです。 ただし、デーモンはinit.dから起動されるため、一定の制限が課されます。

このモデルでは、デーモンは次のアルゴリズムに従って機能します。


プログラムテンプレート。
このコードは、デーモンを正常に起動するために必要なすべてのアクションを実行します。
int main( int argc, char ** argv)
{
int status;
int pid;

// ,
if (argc != 2)
{
printf( "Usage: ./my_daemon filename.cfg\n" );
return -1;
}

//
status = LoadConfig(argv[1]);

if (!status) //
{
printf( "Error: Load config failed\n" );
return -1;
}

//
pid = fork();

if (pid == -1) //
{
//
printf( "Error: Start Daemon failed (%s)\n" , strerror(errno));

return -1;
}
else if (!pid) //
{
//
// ,
//
umask(0);

// ,
setsid();

// , , .
//
chdir( "/" );

// //,
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

//
status = MonitorProc();

return status;
}
else //
{
// , .. ( )
return 0;
}
}


* This source code was highlighted with Source Code Highlighter .

作業の論理は単純であり、理解の問題を引き起こすべきではありません。 明確にする必要がある唯一のもの:

デーモンの状態を監視する開発の基本。
監視の主な目的は、デーモンプロセスのステータスを監視することです。 私たちにとって重要なのは次の2点だけです。
  1. デーモンプロセスの完了の通知。
  2. デーモン終了コードの取得。

デーモンの監視はすべてMonitorProc関数に含まれます。 監視の全体的なポイントは、子プロセスを開始して監視し、その完了コードに応じて、再起動するか、作業を完了することです。
監視機能のソースコード:
int MonitorProc()
{
int pid;
int status;
int need_start = 1;
sigset_t sigset;
siginfo_t siginfo;

//
sigemptyset(&sigset);

//
sigaddset(&sigset, SIGQUIT);

//
sigaddset(&sigset, SIGINT);

//
sigaddset(&sigset, SIGTERM);

//
sigaddset(&sigset, SIGCHLD);

//
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigset, NULL);

// PID'
SetPidFile(PID_FILE);

//
for (;;)
{
//
if (need_start)
{
//
pid = fork();
}

need_start = 1;

if (pid == -1) //
{
//
WriteLog( "[MONITOR] Fork failed (%s)\n" , strerror(errno));
}
else if (!pid) //
{
//

//
status = WorkProc();

//
exit(status);
}
else //
{
//

//
sigwaitinfo(&sigset, &siginfo);

//
if (siginfo.si_signo == SIGCHLD)
{
//
wait(&status);

//
status = WEXITSTATUS(status);

// ,
if (status == CHILD_NEED_TERMINATE)
{
//
WriteLog( "[MONITOR] Child stopped\n" );

//
break ;
}
else if (status == CHILD_NEED_WORK) //
{
//
WriteLog( "[MONITOR] Child restart\n" );
}
}
else if (siginfo.si_signo == SIGUSR1) //
{
kill(pid, SIGUSR1); //
need_start = 0; //
}
else // -
{
//
WriteLog( "[MONITOR] Signal %s\n" , strsignal(siginfo.si_signo));

//
kill(pid, SIGTERM);
status = 0;
break ;
}
}
}

// ,
WriteLog( "[MONITOR] Stop\n" );

// PID'
unlink(PID_FILE);

return status;
}


* This source code was highlighted with Source Code Highlighter .
コードは以下を明確にする必要があります。

PIDファイルを作成するには、補助機能が必要です。
コード:
void SetPidFile( char * Filename)
{
FILE* f;

f = fopen(Filename, "w+" );
if (f)
{
fprintf(f, "%u" , getpid());
fclose(f);
}
}


* This source code was highlighted with Source Code Highlighter .

現時点では、デーモンはすでに起動、その子孫を監視し、基本機能を実行し、必要に応じて再起動するか、設定を変更するためのシグナルを送信することができます。 次に、子孫コードテンプレートを検討します。
int WorkProc()
{
struct sigaction sigact;
sigset_t sigset;
int signo;
int status;

//
//
sigact.sa_flags = SA_SIGINFO;
//
sigact.sa_sigaction = signal_error;

sigemptyset(&sigact.sa_mask);

//

sigaction(SIGFPE, &sigact, 0); // FPU
sigaction(SIGILL, &sigact, 0); //
sigaction(SIGSEGV, &sigact, 0); //
sigaction(SIGBUS, &sigact, 0); // ,

sigemptyset(&sigset);

//
//
sigaddset(&sigset, SIGQUIT);

//
sigaddset(&sigset, SIGINT);

//
sigaddset(&sigset, SIGTERM);

//
sigaddset(&sigset, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigset, NULL);

// -
SetFdLimit(FD_LIMIT);

// ,
WriteLog( "[DAEMON] Started\n" );

//
status = InitWorkThread();
if (!status)
{
//
for (;;)
{
//
sigwait(&sigset, &signo);

//
if (signo == SIGUSR1)
{
//
status = ReloadConfig();
if (status == 0)
{
WriteLog( "[DAEMON] Reload config failed\n" );
}
else
{
WriteLog( "[DAEMON] Reload config OK\n" );
}
}
else // - ,
{
break ;
}
}

//
DestroyWorkThread();
}
else
{
WriteLog( "[DAEMON] Create work thread failed\n" );
}

WriteLog( "[DAEMON] Stopped\n" );

//
return CHILD_NEED_TERMINATE;
}


* This source code was highlighted with Source Code Highlighter .


コードでは次のように言う必要があります。

これらの機能はすでにデーモンの実装に依存しています。

操作の原則は次のとおりです。エラーシグナルにハンドラーを設定し、すべてのワーカースレッドを起動して、シグナルが構成を完了するか更新するのを待ちます。

ログへの詳細なレポートを含む、職場でのエラー処理。

もちろん、悪魔は完全に機能し、いかなる種類のエラーも引き起こさないはずですが、誰もが間違いを犯す可能性があり、時にはテスト段階で検出するのが非常に難しいエラーがあります。 これは、特にワークロードが重いときに発生するエラーの場合に当てはまります。 したがって、デーモンの開発における重要なポイントは、エラーを適切に処理し、エラーから可能な限り多くの情報を絞り出すことです。 この場合、コールスタック(バックトレース)を維持しながらエラー処理を検討します。 これにより、エラーが発生した正確な場所(どの関数で)についての情報が得られます。また、この関数を呼び出した人を見つけることもできます。

エラーハンドラー関数コード:
static void signal_error( int sig, siginfo_t *si, void *ptr)
{
void * ErrorAddr;
void * Trace[16];
int x;
int TraceSize;
char ** Messages;

//
WriteLog( "[DAEMON] Signal: %s, Addr: 0x%0.16X\n" , strsignal(sig), si->si_addr);


#if __WORDSIZE == 64 // 64
//
ErrorAddr = ( void *)((ucontext_t*)ptr)->uc_mcontext.gregs[REG_RIP];
#else
//
ErrorAddr = ( void *)((ucontext_t*)ptr)->uc_mcontext.gregs[REG_EIP];
#endif

// backtrace
TraceSize = backtrace(Trace, 16);
Trace[1] = ErrorAddr;

//
Messages = backtrace_symbols(Trace, TraceSize);
if (Messages)
{
WriteLog( "== Backtrace ==\n" );

//
for (x = 1; x < TraceSize; x++)
{
WriteLog( "%s\n" , Messages[x]);
}

WriteLog( "== End Backtrace ==\n" );
free(Messages);
}

WriteLog( "[DAEMON] Stopped\n" );

//
DestroyWorkThread();

//
exit(CHILD_NEED_WORK);
}


* This source code was highlighted with Source Code Highlighter .

バックトレースを使用すると、次のようなデータを取得できます。
[DAEMON] Signal: Segmentation fault, Addr: 0x0000000000000000
== Backtrace ==
/usr/sbin/my_daemon(GetParamStr+0x34) [0x8049e44]
/usr/sbin/my_daemon(GetParamInt+0x3a) [0x8049efa]
/usr/sbin/my_daemon(main+0x140) [0x804b170]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x126bd6]
/usr/sbin/my_daemon() [0x8049ba1]
== End Backtrace ==

このデータから、メイン関数がGetParamInt関数を呼び出したことは明らかです。 GetParamStr関数は、GetParamStrと呼ばれます。 オフセット0x34のGetParamStr関数で、ゼロアドレスへのメモリアクセスが行われました。

呼び出しスタックに加えて、レジスターの値(uc_mcontext.gregs配列)を保存できます。
-rdynamicオプションを使用するだけでなく、デバッグ情報を切り取らずにコンパイルする場合にのみ、バックトレースから最も多くの情報コンテンツを取得できることに注意する必要があります。

ご覧のとおり、コードは定数CHILD_NEED_WORKとCHILD_NEED_TERMINATEを使用しています。 これらの定数の値は自分で割り当てることができますが、主なことはそれらが同じではないということです。

システムリソースに関連するいくつかの問題。

重要なポイントは、記述子の最大数を設定することです。 開いているファイル、ソケット、パイプなどはすべて記述子を使用します。その後、ファイルを開いたり、ソケットを作成したり、着信接続を受け入れたりすることはできなくなります。 これは、デーモンのパフォーマンスに影響を与える可能性があります。 デフォルトでは、オープン記述子の最大数は1024です。この数は、負荷の高いネットワークデーモンでは非常に小さくなります。 したがって、要件に応じてこの値をより多く設定します。 これを行うには、次の関数を使用します。
int SetFdLimit( int MaxFd)
{
struct rlimit lim;
int status;

// -
lim.rlim_cur = MaxFd;
// -
lim.rlim_max = MaxFd;

// -
status = setrlimit(RLIMIT_NOFILE, &lim);

return status;
}


* This source code was highlighted with Source Code Highlighter .

結論の代わりに。
そこで、悪魔の基礎をどのように作成するかを見ました。 もちろん、コードは完璧なふりをしているわけではありませんが、タスクに完全に対応しています。
次の記事では、デーモンのインストール/アンインストール、デーモンの管理、init.dのスタートアップスクリプトの作成、スタートアップへの直接追加に関連する問題について説明します。

ソースコードリンク: http : //pastebin.com/jdX5wn0E
ソースコードには、1つのファイルで使用されるすべての関数が含まれています。 プロジェクトを開発するときは、機能的な目的に応じて、異なるファイルに分散することをお勧めします。

Source: https://habr.com/ru/post/J129207/


All Articles