プログラミング言語を作る (1)

はじめに

学習目的でスクリプト言語を作っている。
github.com

日々使っている言語も仕組みを知らなければ高いパフォーマンスを出せないし、仕組みを知りたければ自分で作ってみるのが一番だ。

単純な四則演算からはじめて、これまでに以下の機能を実装した。

  • 整数/実数
  • 四則演算 (+, -, * , /, %)
  • 文字列
  • 変数代入
  • 変数参照
  • 比較演算 (==, !=, <, >, <=, >=)
  • 組込関数の呼び出し
  • ユーザー関数の定義
  • ユーザー関数の呼び出し
  • while文
  • if (elsif, else) 文
  • 1行コメント

だいぶ混み入ってきたので、ここらで一旦情報をまとめておく。

環境

言語はC言語を使う。
どの言語を使っても良いのだが、スクリプト言語上にスクリプト言語を作ってもしょうがない。Javaも考えたが、PHPRubyと同じ土俵で仕組みを勉強したかったから。
C++であればオブジェクト指向で構造的に作れるので、今switchで分岐しているような処理はかなり綺麗に書けると思う。
字句解析、構文解析には定番のflex / bisonを使った。
字句解析は正規表現なので自前で作ることもできそうだ。構文解析についても作り方を紹介している書籍があったが、今回はひとまずbisonにお任せした。

仕組み

言語開発の全体の流れは、

  1. 字句解析 ( = language.l )
  2. 構文解析 ( = language.y )
  3. 構文木作成 ( = language.c )
  4. 構文木評価 ( = language.c )

となる。

実装

字句解析

字句解析はflexのルールに則って定義する。前述の通り正規表現なのでわかりやすい。

%{
#include <stdio.h>
#include <stdlib.h>
#include "language.tab.h"
#include "language.h"
%}

%%
"while" { return WHILE; }
"elsif" { return ELSIF; }
"function" { return DECLARE; }
"if" { return IF; }
"else" { return ELSE; }
-?[0-9]+"."[0-9]+ { yylval.str = yytext; return NUMBER; }
-?[0-9]+ { yylval.str = yytext; return NUMBER; }
"println" { yylval.fn = B_print; return FUNC; }
"\n"
[a-zA-Z][a-zA-Z0-9_]* { yylval.sym = lookup(yytext); return IDENT; }
\"(\\\\.|[^\"])*\" { yylval.str = yytext; return STRING; }
"//"[^\n]*\n /* ignore one line comment */
"+" |
"-" |
"*" |
"/" |
"%" |
"=" |
"|" |
"," |
";" |
"{" |
"}" |
"(" |
")" { return yytext[0]; }
"==" { yylval.fn = 1; return CMP; }
"!=" { yylval.fn = 2; return CMP; }
">" { yylval.fn = 3; return CMP; }
"<" { yylval.fn = 4; return CMP; }
">=" { yylval.fn = 5; return CMP; }
"<=" { yylval.fn = 6; return CMP; }
[ \t] /* ignore white space */
. { yyerror("Mystery character %c\n", *yytext); }
%%

yytextは入力文字列、yylavalは構文解析への出力文字列となる。yylval.strやyylval.fnなどの型はlanguage.yで共用体として定義しているので、これにあったものを選択する。
ここではステート文、関数定義修飾子、整数、実数、println(組込関数)、変数、文字列、1行コメント、演算子、スペース文字、改行文字を定義し、それ以外はエラーとしている。

構文解析

構文解析では、まず字句解析から受け取るトークンの型を共用体で定義する。

%union {
	char* str;
	struct ast *a;
	struct symbol *sym;
	struct symlist *sl;
	int fn;
}

整数に関してはもともとint型で定義していたが、実数を実装した段階でchar*型に変更し、構文木作成時に変換する方法に変更した。

次に、これらの型とトークンを紐づける。

%token <str> NUMBER
%token <sym> IDENT
%token <str> STRING
%token <fn> CMP
%token <fn> FUNC
%token WHILE
%token IF
%token ELSE
%token ELSIF
%token DECLARE
%type <a> factor
%type <a> exp explist
%type <sl> symlist
%type <a> let call declare  if else elsif elsiflist elselist ifstmt
%type <a> stmt stmts
%type <a> program

終端記号はtokenで、非終端記号はtypeで定義する。

演算子の結合優先順位も設定できる。

%left '+' '-'
%left '*' '/' '%'
%right '='

また、下にある演算子が優先されるので、乗除演算が加減演算に優先することもここで表現できる。
これにより構文ルールで乗除と加減を分けて定義する必要がなくなる。

構文解析の肝となる定義は、BNFを使って表現する。

%%
program
	: stmts { eval($1); }
	;
stmts
	: stmt
	| stmt stmts { $$ = newast(NODE_STMTS, NULL,  $1, $2); }
	;
stmt
	: let
	| call
	| declare
	| WHILE '(' exp ')' '{' stmts '}' { $$ = newast(NODE_WHILE, NULL, $3, $6); }
	| ifstmt
	;
let
	: IDENT '=' exp ';'{ $$ = newasgn($1, $3); }
	;
call
	: IDENT '(' explist ')' ';' { $$ = newcall($1, $3); }
	| IDENT '(' ')' ';' { $$ = newcall($1,  NULL); }
	| FUNC '(' explist ')' ';' { $$ = newfunc($1, $3); }
	;
declare
	: DECLARE IDENT '(' symlist ')' '{' stmts '}' { dodef($2, $4, $7); }
	| DECLARE IDENT '(' ')' '{' stmts '}' { dodef($2, NULL, $6); }
	;
ifstmt
	: if
	| if elselist { $$ = newast(NODE_SELECT, NULL, $1, $2); }
	;
elselist
	: elsiflist
	| else
	| elsiflist else { $$ = newast(NODE_SELECT, NULL, $1, $2); }
	;
elsiflist
	: elsif
	| elsif elsiflist { $$ = newast(NODE_SELECT, NULL, $1, $2); }
	;
elsif
	: ELSIF '(' exp ')' '{' stmts '}' { $$ = newast(NODE_IF, NULL, $3, $6); }
	;
else
	: ELSE '{' stmts '}' { $$ = newast(NODE_ELSE, NULL, NULL, $3); }
	;
if
	: IF '(' exp ')' '{' stmts '}' { $$ = newast(NODE_IF, NULL, $3, $6); }
	;
symlist
	: IDENT { $$ = newsymlist($1, NULL); }
	| IDENT ',' symlist { $$ = newsymlist($1, $3); }
	;
explist
	: exp ',' explist { $$ = newast(NODE_EXPLIST, NULL,  $1, $3); }
	| exp
	;
exp
	: factor CMP factor { $$ = newcmp($2, $1, $3); }
	| factor '+' factor { $$ = newast(NODE_EXP, "+", $1, $3); }
	| factor '-' factor { $$ = newast(NODE_EXP, "-", $1, $3); }
	| factor '*' factor { $$ = newast(NODE_EXP, "*", $1, $3); }
	| factor '/' factor { $$ = newast(NODE_EXP, "/", $1, $3); }
	| factor '%' factor { $$ = newast(NODE_EXP, "%", $1, $3); }
	| factor
	;
factor
	: '(' exp ')' { $$ = $2; }
	| NUMBER { $$ = newnum($1); }
	| IDENT { $$ = newref($1); }
	| STRING { $$ = newast(NODE_STR, $1, NULL, NULL); }
	;
%%

factorはプライマリとなる数値表現、変数、文字列を表している。

expはfactorそのもの、もしくはfactor同士の演算を表している。
最初は整数の四則演算だけだったので、ここで直接計算結果を返して完結していたが、言語を作る以上ここで構文木を作る必要がある。
newastやnewasgnなどの関数はそのためのものである。

explistはexpそのもの、もしくはexpをカンマ区切りで連ねた物である。
exp (',' exp)*のような書き方ができなかったので、このような再帰表現を用いている。

symlistは変数、または変数をカンマ区切りで連ねたものである。これはexplistを同じ方法で定義している。

ifとelseはそれぞれif文とelse文の定義。
これだけなら簡単なのだが、elsifは0回以上の繰り返しを想定しないといけないため、elsif、elsiflistを定義し、elseと組み合わせたelselistも定義している。
最終的にはifstmtへとreduceされる。

declareはユーザー関数の定義である。

callはユーザー関数とビルトイン関数の定義。
組込関数ではprintfなどCのネイティブ関数を用いているため、ユーザー定義関数とは明確に区別している。
ユーザー関数については引数のあるなし両方を定義している。

letは変数へのexpの代入である。

stmtはletやcall、whileやifなどの文を指す。
stmtsはstmtの繰り返しで、プログラム全体ど同義である。

最後にprogramはstmtsそのものを指すが、ここまでreduceされた段階で全ての構文木が作成されているので、evalを呼び出してプログラムを実行する。

長くなるので次回へ続く。

参考資料

今回は直接参照することはなかったが、Javaを使った言語作成方法を解説していておもしろい。
自前で構文解析を行っているので、bisonを使わない場合にも参考になるだろう。

2週間でできる! スクリプト言語の作り方 (Software Design plus)

2週間でできる! スクリプト言語の作り方 (Software Design plus)

flexとbisonについての書籍。
中はほとんど読んでないが、今回のプログラムは本書のサンプルをベースに始めている。
SQLの解析も行っているので、今後読み進めたい。

Flex & Bison

Flex & Bison

MacでPTHREAD_PROCESS_SHAREDは使えない

preforkサーバのようにプロセス間でロック機構を使いたい場合には、fcntl関数によるPosixファイルロックやディスクリプタパッシングなどの方法が用いられる。
pthreadの機能を使ったスレッドロックもその内の一つで、通常単一プロセスのスレッド間に用いるスレッドロックを、異なるプロセス間で適用する方法である。

この方法はファイルロックに比べて高速で動作させることができるのがメリットだ。

使う方法は、

  1. pthread_mutexattr_t構造体をデフォルトで初期化する
  2. PTHREAD_PROCESS_SHARED属性を設定する
  3. pthread_mutex_initで相互排除変数を初期化する
  4. 相互排除変数をロックする
  5. 相互排除変数をアンロックする

となる。

// 相互排除変数
pthread_mutex_t *mptr;

// 属性構造体
pthread_mutexattr_t mattr;

// 属性初期化
pthread_mutexattr_init(&mattr);

// 複数プロセス間での共有
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);

// 相互排除変数を初期化
pthread_mutex_init(mptr, &mattr);

// 相互排除変数をロック
pthread_mutex_lock(mptr);

/* 任意の処理 */

// 相互排除変数をアンロック
pthread_mutex_unlock(mptr);

ここで表題の件となるが、この方法はMacだと動かない。
pthread_mutex_initを呼び出した時点でプロセスが異常終了してしまう。

調べてみると下記のページが見つかった。
developer.apple.com

ここには以下の通り報告されている。

BUGS
     The PTHREAD_PROCESS_SHARED attribute is not supported.

直してくれよ。

HTTPServerのデーモンプロセス化

HTTPServerはクライアントからのリクエストを待ち受けて処理するタイプのプログラムなので、特定のターミナルに紐付けるのではなく、起動後は独立して動作するデーモンプロセスとする必要がある。

github.com

デーモンプロセスにする場合、以下のような手順で行う。

デーモンプロセス関数の実装

main関数の最初にデーモンプロセス関数を呼び出す。

void
daemon_init(const char *pname, const char *root_dir, int facility)
{
	int i;
	pid_t pid;
	if ( (pid = fork() ) != 0)
		exit(0); /* 親プロセスの終了 */
	/* 最初の子プロセス */
	setsid();
	Signal(SIGHUP, SIG_IGN);
	if ( (pid = fork() ) != 0)
		exit(0); /* 最初の子プロセスの終了 */
	/* 番目の子プロセス */	
        daemon_proc = 1; /* err_XXX() 関数用 */
	chdir(root_dir); /* ワーキングディレクトリの変更 */
	umask(0); /* ファイルモード作成マスクのクリア */
	for ( i = 0; i < MAXFD; i++)
		close(i);
	make_pid_file();
	openlog(pname, LOG_PID, facility);
}

まず、fork関数を呼び出して子プロセスを生成する。
fork関数は子プロセスの場合0を返すため、これを利用して親プロセスを終了する。
親プロセスが終了することで、シェルはコマンドの実行が終了したとみなすため、子プロセスはバックグラウンドでの実行に移行する。

この子プロセスはsetsid関数により、グループリーダー、セッションリーダーとなり、コントロールターミナルを持たない状態となる。
この後再度forkを実行してこの子プロセスは終了するが、この時に2番目の子プロセスにSIGHUPが送られるため、これを無視するようSIgnal(SIGHUP, SIG_IGN)を設定しておく。

2回目のforkを実行して最初の子プロセスを終了する。このforkはセッションリーダーではない2番目の子プロセスに処理を引き継ぐことで、将来ターミナルデバイスがオープンされることを防止するために行う。

daemon_procの設定は、エラー関数内でエラーの出力先を標準出力からsyslogに切り替えるためのもの。

chdirではワーキングディレクトリをroot_dirに変更する。このroot_dirは起動スクリプトから実行時引数として受け取ったものである。特に開発中は環境によって実行パスが異なることから、どこのディレクトリで実行しても動くように設定している。
本来はシステムのルートディレクトリをワーキングディレクトリにすることで、実行ディレクトリのファイルシステムがアンマウントできなくなることを防ぐものらしい。

pidファイルの作成

daemon_init関数の最後でmake_pid_file関数を呼び出し、pidファイルを作成している。
デーモンプロセスでは多重起動を防ぐために、このpidファイルが利用される。pidファイルがある場合は起動スクリプトでエラーが返るようになっている。

また、デーモンプロセスは通常プロセスのように簡単に終了することができない。psコマンドでプロセスIDを調べて、kill -TERMなどで終了する手順が必要だが、コードを変更してコンパイルするたびにプロセスの終了起動まで行う必要があり、これが大変面倒である。
pidファイルにpidを書き込むようにしておけば、停止スクリプトでこのファイルを読み込み、記載されているpidのプロセスを終了させることができるため、非常に便利になる。

void
make_pid_file()
{
	FILE *fp;
	char pid_file[512] = "run/server.pid";
	int openerror;
        openerror = errno;
	if ( (fp = fopen(&pid_file[0], "w") ) == NULL) {
		syslog(LOG_NOTICE, "pid file open error\nerror code: %i\nerror message: %s\nfile path: %s\n", openerror, strerror(openerror), pid_file);
		exit(1);
	}
	fprintf(fp, "%i", getpid());
	fclose(fp);
}

実装内容は単純で、ファイルを書き込み用にオープンし、getpid関数で取得したpidを書き込んでいる。
ファイルオープンに失敗した時、errnoにエラーコードが設定されるため、このコードとstrerrorで取得したエラーメッセージをsyslogdに出力している。

制御用スクリプト

前述の通り、デーモンプロセスはプロセスの制御が面倒なため、制御用スクリプトを実装する。

#!/bin/sh

start() {
	echo "Starting server"
    if [ -e ${PIDFILE} ]; then
        echo "${PIDFILE} already exists"
        exit 1
    fi
	mkdir -p "$RUNDIR"
	../build/server $ROOTDIR
}

stop() {
	echo "Stopping server"
	readonly PID=`cat ${PIDFILE}`
	kill -TERM $PID
	rm -rf "$RUNDIR"
}

readonly USER=`whoami`
if [ "$USER" != "root" ]; then
	echo "Alert: Run as root"
	exit 1
fi

if [ $# -ne 1 ]; then
	echo "Alert: Run with argument as start, stop, or restart"
	exit 1
fi

cd `dirname $0` || exit 1
readonly ROOTDIR=`pwd`"/../"
readonly RUNDIR=${ROOTDIR}"run/"
readonly PIDFILE=${RUNDIR}"server.pid"

case $1 in
	start)
		start
		;;
	stop)
		stop
		;;
	restart)
		stop
		start
		;;
	*)
	echo "Alert: Run with argument as start, stop, or restart"
		;;
esac

実行時引数にstart, stop, restartを取り、これによって処理を分けている。
また、HTTPServerはport80で動作させているため、root権限を要求する。

start処理では、多重起動防止のためにpidファイルの存在チェックを行い、このファイルが存在する場合にはエラーで終了させる。
また、pidファイルの配置場所となるrunディレクトリを作成している。

stop処理では、pidファイルの中身を読み、この番号によって、実行しているデーモンプロセスを終了させている。
またrunディレクトリごとpidファイルを削除する。

restart処理では、stop -> start を逐次実行している。

参考書籍:

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

  • 作者: W.リチャードスティーヴンス,W.Richard Stevens,篠田陽一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 1999/07
  • メディア: 単行本
  • 購入: 8人 クリック: 151回
  • この商品を含むブログ (37件) を見る

Couchbase Server 4.0をCentOS7にインストールする

Couchbase

Couchbaseは分散ドキュメントを扱うNoSQLである。
主な特徴としては、

  • RAMベースでの高速な動作に加え非同期でのストレージ保存 (分離レベルを設定可能)
  • キーバリュー型のシンプルなドキュメントストア構造
  • ノードのクラスタ追加によるリニアなスケールがオンラインで可能
  • コマンドラインREST APIによる操作
  • memcachedモードではスケール可能なmemcachedとしてクライアント側の変更なしに使用が可能

などが挙げられる。

Couchbaseのインストール

まず、Couchbaseの公式ページより、必要なパッケージのリンクを取得する。
f:id:nao_bamboo:20160118155912p:plain
今回はCentOS7にインストールするので、Red Hat 7 のダウンロードリンクをコピーする。
このパッケージをサーバでダウンロードしてインストールする。

[admin@centos7 ~]$ cd /var/src
[admin@centos7 src]$ wget http://packages.couchbase.com/releases/4.0.0/couchbase-server-community-4.0.0-centos7.x86_64.rpm

packages.couchbase.com (packages.couchbase.com) をDNSに問いあわせています... 54.231.49.99
packages.couchbase.com (packages.couchbase.com)|54.231.49.99|:80 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 73719752 (70M) [application/x-redhat-package-manager]
`couchbase-server-community-4.0.0-centos7.x86_64.rpm' に保存中

100%[==========================================================>] 73,719,752 5.89MB/s 時間 39s

2016-01-18 14:52:17 (1.80 MB/s) - `couchbase-server-community-4.0.0-centos7.x86_64.rpm' へ保存完了 [73719752/73719752]

[admin@centos7 src]$ sudo rpm -ivh couchbase-server-community-4.0.0-centos7.x86_64.rpm
準備しています... ################################# [100%]
Warning: Transparent hugepages looks to be active and should not be.
Please look at http://bit.ly/1hTySfg as for how to PERMANENTLY alter this setting.
Warning: Swappiness is not set to 0.
Please look at http://bit.ly/1hTySfg as for how to PERMANENTLY alter this setting.
Minimum RAM required : 4 GB
System RAM configured : 1.80 GB

Minimum number of processors required : 4 cores
Number of processors on the system : 1 cores

更新中 / インストール中...
1:couchbase-server-community-4.0.0-################################# [100%]
Reloading systemd: [ OK ]
Starting couchbase-server (via systemctl): Failed to start couchbase-server.service: Unit couchbase-server.service failed to load: No such file or directory.
[失敗]

You have successfully installed Couchbase Server.
Please browse to http://centos7.osxserver.net:8091/ to configure your server.
Please refer to http://couchbase.com for additional resources.

Please note that you have to update your firewall configuration to
allow connections to the following ports: 11211, 11210, 11209, 4369,
8091, 8092, 8093, 9100 to 9105, 9998, 18091, 18092, 11214, 11215 and
from 21100 to 21299.

By using this software you agree to the End User License Agreement.
See /opt/couchbase/LICENSE.txt.

RAMやプロセッサ数が最低動作要件を満たしていないものの、インストールは可能だ。
だが、インストールは成功したものの、systemctlによるサービスの起動で失敗していることがわかる。
これはCentOS7における既知の問題のようで、フォーラムにも記載があった。
forums.couchbase.com
これによれば、systemctlを使うのではなく、Couchbaseの起動コマンドを使うのが良いようだ。
コマンドを使って起動すると無事起動できた。

[admin@centos7 src]$ sudo /opt/couchbase/etc/couchbase_init.d start
Starting couchbase-server-community

セットアップ

アクセス方法

管理画面へは
http://サーバのIPアドレス:8091/
でアクセスできる。
f:id:nao_bamboo:20160118161719p:plain

ストレージとクラスタの設定

ストレージについてはデータファイルパスとインデックスファイルパスを設定する。
クラスタについては新規クラスタなのか既存のクラスタへの追加なのかを選択できる。
f:id:nao_bamboo:20160118162028p:plain

  • Services: このノードで使用するサービスを選択する
  • Data RAM Quota: データに対するノード単位のメモリ使用量を設定する
  • Index RAM Quota: インデックスに対するノード単位のメモリ使用量を設定する

サンプルバケットの設定

サンプルバケット及びサンプルデータを作成することができる。
バケットはRDBMSでいうところのデータベースに該当する。
f:id:nao_bamboo:20160118162532p:plain

デフォルトバケットの作成

デフォルトのバケットを作成する。
Couchbaseとしての機能を使う場合はCouchbaseを、memcached互換として使う場合はMemcachedを選択する。
f:id:nao_bamboo:20160118162844p:plain

  • Cache Metadata:
    • Value Ejection: RAM上からキャッシュデータを削除する際に、値データのみを削除し、キーやメタデータを削除しない。メモリ使用量が増える代わりにパフォーマンスが向上する。
    • Full Ejection: RAM上からキャッシュデータを削除する際に、キーやメタデータ、値データを含む全てのデータを削除する。パフォーマンスが落ちる代わりに、メモリ使用のオーバーヘッドを減らすことができる。
  • Set the bucket disk I/O priority: 優先度によるディスクI/O最適化を行う場合の詳細設定項目である。
  • Flush: データフラッシュを有効にする。フラッシュ実行時、memcachedバケットのデータは削除可能フラグが立ち、後でデータ削除される。Couchbaseバケットのデータは直ちに削除される。この設定はすぐにフラッシュを実行するという意味ではない。

通知の設定

ソフトウェアアップデートの通知設定とコミュニティへの登録を行う。
f:id:nao_bamboo:20160118164206p:plain

  • Enable software update notifications: 新しいバージョンのCouchbase Serverが利用可能になった時、Webコンソール上で通知が行われる。この設定は匿名で送られ、いかなるキーバーリューで他を含むものでもない。

サーバ管理者の設定

サーバ管理者のアカウントを作成する。
f:id:nao_bamboo:20160118164522p:plain

セットアップ完了

セットアップが完了すると、管理画面のオーバービューが表示される。
f:id:nao_bamboo:20160118164609p:plain

REST APIでの接続

REST APIで接続してみる。

[admin@centos7 src]$ curl 'http://192.168.0.3:8091/pools'
{"pools":[{"name":"default","uri":"/pools/default?uuid=xxxxxxxxxxxxxxxxxxxxxx","streamingUri":"/poolsStreaming/default?uuid=xxxxxxxxxxxxxxxxxxxxxx"}],"isAdminCreds":false,"isROAdminCreds":false,"isEnterprise":false,"settings":{"maxParallelIndexers":"/settings/maxParallelIndexers?uuid=xxxxxxxxxxxxxxxxxxxxxx","viewUpdateDaemon":"/settings/viewUpdateDaemon?uuid=xxxxxxxxxxxxxxxxxxxxxx"},"uuid":"xxxxxxxxxxxxxxxxxxxxxx","implementationVersion":"4.0.0-4051-community","componentsVersion":{"lhttpc":"1.3.0","os_mon":"2.2.14","public_key":"0.21","asn1":"2.0.4","kernel":"2.16.4","ale":"4.0.0-4051-community","inets":"5.9.8","ns_server":"4.0.0-4051-community","crypto":"3.2","ssl":"5.3.3","sasl":"2.3.4","stdlib":"1.19.4"}}

REST APIについては、下記ドキュメントが用意されている。

developer.couchbase.com

developer.couchbase.com

参考書籍

バージョンが古くなっているが、オライリーからCouchbaseに関する入門書 (Ebookのみ) が出ている。
O'Reilly Japan - Couchbase Serverをはじめよう

Macでのsyslogの利用

はじめに

プログラム開発時のログ出力にはsyslog関数を使うと便利である。
特に、特定のターミナルと結びつかないデーモンプログラムの開発では、標準出力へのログ出力を行うことはできないため、syslogは重宝する。
ところが、このsyslogをMacで使う時は少々注意が必要だ。

syslogの出力先

Macでsyslog関数を使った場合、ログはどこに出力されるのか。
syslogの設定ファイルである "/etc/syslog.conf" を見てみる。

$ cat /etc/syslog.conf
# Note that flat file logs are now configured in /etc/asl.conf

install.* @127.0.0.1:32376

どうやら "/etc/asl.conf" で定義されているようだ。
そこで、この "/etc/asl.conf" をみてみる。

$ cat /etc/asl.conf
##
# configuration file for syslogd and aslmanager
##

# authpriv messages are root/admin readable
? [= Facility authpriv] access 0 80

# remoteauth critical, alert, and emergency messages are root/admin readable
? [= Facility remoteauth] [<= Level critical] access 0 80

# broadcast emergency messages
? [= Level emergency] broadcast

# save kernel [PID 0] and launchd [PID 1] messages
? [<= PID 1] store

# ignore "internal" facility
? [= Facility internal] ignore

# save everything from emergency to notice
? [<= Level notice] store

# Rules for /var/log/system.log
> system.log mode=0640 format=bsd rotate=seq compress file_max=5M all_max=50M
? [= Sender kernel] file system.log
? [<= Level notice] file system.log
? [= Facility auth] [<= Level info] file system.log
? [= Facility authpriv] [<= Level info] file system.log

# Facility com.apple.alf.logging gets saved in appfirewall.log
? [= Facility com.apple.alf.logging] file appfirewall.log file_max=5M all_max=50M

これを見る限り、ログの出力先は通常であれば、"/var/log/system.log" になるようだ。
そこで、

syslog(LOG_NOTICE, "%s", "test");<

というコードを書いてログを出力してみるが、一向に system.log に追記されない。

ログレベル

syslog関数のインターフェースは以下のように定義される。

void syslog(int priority, const char `message, ...);

第一引数のpriorityは、levelとfacilityの組み合わせとなっている。
ログレベルは値を持つパラメータとして以下のように定義される。

level 説明
LOG_EMERG 0 システムが使用不能
LOG_ALERT 1 速やかな復旧操作が必要
LOG_CRIT 2 重大
LOG_ERR 3 エラー
LOG_WARNING 4 渓谷
LOG_NOTICE 5 注意が必要 (デフォルト)
LOG_INFO 6 通知
LOG_DEBUG 7 デバッグレベルメッセージ

facilityは送信側のプロセスのタイプを示す。

facility 説明
LOG_AUTH セキュリティ/認証メッセージ
LOG_AUTHPRIV セキュリティ/認証メッセージ(プライベート)
LOG_CRON cronデーモン
LOG_DAEMON システムデーモン
LOG_FTP FTPデーモン
LOG_KERN カーネルメッセージ
LOG_LOCAL0(〜7) ローカルな用途
LOG_LPR ラインプリンタシステム
LOG_MAIL メールシステム
LOG_NEWS ネットワークニュースシステム
LOG_SYSLOG syslog内部で生成されたメッセージ
LOG_USER 雑多なユーザレベルのメッセージ
LOG_UUCP UUCPシステム

改めて "/etc/asl.conf" の設定を見てみると、SenderがKernelの場合はfileがsystem.logに設定されていることがわかる。
levelの設定でも、"<= notice" の場合、つまり LOG_NOTICE よりも値が小さい場合にはfileがsystem.logに設定されている。

ところが、LOG_INFOの場合は、facilityがLOG_AUTHもしくはLOG_AUTHPRIVの場合以外記載がなく、このままではsystem.logには出力されないのだ。
先ほどのコードもlevelをLOG_INFOとしたのが原因でsystem.logに出力されていなかった。

これを解決するには、まずログレベルを変更することが考えられるが、INFOレベルのログをNOTICEとして出すのは良いやり方とは言えない。
また、asl.confに設定を追記する方法が考えられるが、環境依存になってしまうのと、他のソフトウェアへの影響も考えなければならないため、こちらもあまり採用したくはない。

こういった事情を考慮すると、専用のログシステムを設けるのが一番良いのかもしれない。