BSD process accountingを調べることになったきっかけ
本を読んだらたまたま出てきた。実はよく知らなかったので、ソースを読むことにした。ソースはLinux4.7です。
ちなみに、説明はLinux manが詳しいです。
ソース検索
Linuxのソースを"BSD"をキーワードに検索。
init/Kconfig
391:config BSD\_PROCESS_ACCT
392: bool "BSD Process Accounting"
ということで、BSD_PROCESS_ACCTで検索かけます。
obj-$(CONFIG\_BSD_PROCESS_ACCT) += acct.o
acct()システムコール
* This file implements BSD-style process accounting. Whenever any
* process exits, an accounting record of type "struct acct" is
* written to the file specified with the acct() system call. It is
* up to user-level programs to do useful things with the accounting
* log. The kernel just provides the raw accounting information.
process accounting機能は、acct()というlibc関数で有効/無効を切り替えているようです。acct()はLinux manに詳しいです。
acct()システムコールは以下の通り。
SYSCALL_DEFINE1(acct, const char __user *, name)
{
// 略
if (name) {
struct filename *tmp = getname(name);
// 略
error = acct_on(tmp);
// 略
acct_on()は以下の通り。一番大事な箇所は、指定されたファイルをopen()している箇所で、あとは諸々の初期化です。
static int acct_on(struct filename *pathname)
{
// 略
/* Difference from BSD - they don't do O_APPEND */
file = file_open_name(pathname, O_WRONLY|O_APPEND|O_LARGEFILE, 0);
// 略
acct->file = file;
acct->needcheck = jiffies;
// 略
return 0;
}
exit()でのacct処理
process accountingは、プロセスのexit()時にログを取る機能なので、当然exit()でacct.cの関数を呼んでいます。
void do_exit(long code)
{
struct task_struct *tsk = current;
// 略
group_dead = atomic_dec_and_test(&tsk->signal->live);
// 略
acct_collect(code, group_dead);
// 略
if (group_dead)
acct_process();
// 略
}
ここで、acct_process()はgroup_deadというフラグで実行可否を決めています。
元になっているのは、task構造体のsignal->liveというカウンタ変数で、これがデクリメントした結果0になったかどうかを見ています。
このカウンタは、fork()やclone()経由で呼ばれるcopy_process()でインクリメントされています。
static struct task_struct *copy_process(unsigned long clone_flags,
// 略
{
// 略
if (thread_group_leader(p)) {
// 略
} else {
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
// 略
}
ところで、Linuxは、以下の引用のように、プロセスをスレッドグループで表現しています。そして、プロセスの最初のスレッドをスレッドグループリーダとしています。
POSIXの指すプロセスとは、同一メモリ空間など処理に必要なリソースを共有する、1つ以上のスレッドからなります。Linuxではスレッドグループに対応します。
つまり、exit()でのgroup_deadの役割は、以下のコメントで補足したようになります。
// プロセスの最後のスレッドがexit()するとき
if (group_dead)
acct_process();
acct_collect()とacct_process()
acct_collect()ですが、スレッドの累積実行時間などログに録りたいデータを回収するだけですので、省略します。
回収したデータは、current->signal->pacctに格納されます。ソースを読む際はこれを頭に入れてください。
また、acct_process()は処理の中でdo_acct_process()を呼び出しています。この処理がログへの書き込み処理です。
骨となる部分を抜き出した引用は以下のとおりです。
static void do_acct_process(struct bsd\_acct_struct *acct)
{
// 略
/*
* First check to see if there is enough free_space to continue
* the process accounting system.
*/
if (!check_free_space(acct))
goto out;
// fill_ac()はログに書き込むためのエントリを作成する。
// fill_ac()は、先にacct_process()で格納したcurrent->signal->pacct内のデータを使っています。
fill_ac(&ac);
// 略
if (file_start_write_trylock(file)) {
/* it's been opened O_APPEND, so position is irrelevant */
loff_t pos = 0;
__kernel_write(file, (char *)&ac, sizeof(acct_t), &pos);
file_end_write(file);
}
// 略
}
最後に、上に引用したdo_acct_process()から呼び出しているcheck_free_space()はファイルシステムの残りブロック数をチェックする関数です。実装をみましょう。
頻繁なファイルシステムへのアクセスを避けつつ、vfs_statfs()による空きブロック数のチェックを行う工夫がなされています。
static int check_free_space(struct bsd_acct_struct *acct)
{
struct kstatfs sbuf;
// ブロックする可能性のあるvfs_statfs()を毎回呼び出すのは辛い。
// よって、一定の時間が経過していないのであれば、ファイルシステムの
// 利用可能ブロック数は大きく変わらないと思うので、チェックしない
if (time_is_before_jiffies(acct->needcheck))
goto out;
/* May block */
if (vfs_statfs(&acct->file->f_path, &sbuf))
goto out;
if (acct->active) {
u64 suspend = sbuf.f_blocks * SUSPEND;
do_div(suspend, 100);
// ファイルが含まれているファイルシステムの残りブロック数に余裕がなければ
// 記録を一時停止。
// 将来余裕が出るまで待つ。
if (sbuf.f_bavail <= suspend) {
acct->active = 0;
pr_info("Process accounting paused\n");
}
} else {
u64 resume = sbuf.f_blocks * RESUME;
do_div(resume, 100);
// ファイルが含まれているファイルシステムの残りブロック数に余裕が出てきたので
// 記録を再開。
if (sbuf.f_bavail >= resume) {
acct->active = 1;
pr_info("Process accounting resumed\n");
}
}
acct->needcheck = jiffies + ACCT_TIMEOUT*HZ;
out:
return acct->active;
}
これでprocess accoutingが何をしているか、イメージがつかめました。