at cron

前回思わぬ事から、K&R本こと、プログラミング言語C なんてのを参照しちゃったものだから、懐かしくなって再読してる。

表紙の図案が、ペーパーテープを折ってCの文字を作ってる。今時の人は紙テープなんて知らないだろうな。

紙テープに穴をあけて(あいてるかあいてないかをBitのOn/Offに対応させてる)、プログラムやデータを記憶してたんだ。書かれている情報がもじどうり目で見える、優れた記憶媒体!!

あの頃は主流だった。雑に扱うと途中で切れたりして、そういう時はパッチテープで修復したものだ。(修復するのを、横で見ていただけだけど)

こういう職人さんが凄いのは、紙テープの穴の状態を読んで、それを脳内で逆アセンブルしちゃう事。だから、パッチ当てなんていう芸が出来るんだ。ミニコン(PDP-11)顔負けです。

中身をつらつら見て行くと、C語の解説書とUNIX流の考え方がさりげなく披露されてる。副題に、UNIX流プログラム書法と作法って付けた石田先生(この本の翻訳者)の慧眼には恐れいる。

初版は、1981年7月、オイラーの手元にあるやつは、70刷版で、1986年の4月ってなってた。 化け物本だな、ベストセラーだな。みんなこれで大きくなったんだな。

正直言うと、難しくて挫折したんかな。今なら楽しく読めるよ。少しは進歩してんだな。 でも、 余り歴史のあるC語ばかりでも、あれなんでと思っていたら、 「Go 1.11」を公開。WebAssemblyを実験的にサポート なんてのが出てた。着々と進化してますなあ。

プログラミング言語別年収ランク 1位は世界で人気の「Go」にみるように、ググルの手の平で踊る人が多いみたい。

昔は、某社の掌で、Solaris+MySQL+Javaってのが、鉄板だったように思うぞ。でも、某社は、ターミネーター。自分の敵を取り込んでおいて、殺してしまうという悪い事をやってます。

みんなそれに気付いたのかしらないけど、Javaの下落が著しいように思うぞ。これからは、Goだ。Go Go Go.

golang

てな事で、オイラーもGoする。場所は、開発環境が何も無いという、変な所でWindowsに似てる、ルブンツです。LXDE+Ubuntuの環境ね。

取ってきたtar玉を、/usr/localあたりに展開。PATHを通しておく。自Homeに適当なdir(分かり易いようにgoにした)を作り、goの拠点とする。そして、そのDirをGOPATHに登録する。

go試しは、毎度おなじみのやつ。C語に比べて、package宣言が余計に付いているな。

package main

import "fmt"

func main() {
	fmt.Printf("hello, world\n")
}
sakae@lxde:~/go/src/hello$ time go build hello.go

real    0m0.392s
user    0m0.104s
sys     0m0.350s
sakae@lxde:~/go/src/hello$ ./hello
hello, world

以上、試験完了。いやになって撤収する場合は、/usr/local/goの本体、自Dirに作った基地、そして、.cache/go-buildの3点セットを消すだけ。goに染まった恥ずかしい記憶を一切かっさい削除出来る。

本格的にやるなら、makeぐらいは入れておけばいいかな。それ以外のツールは、go tool でアクセス出来る。まあ、使うと言ったら、nmぐらいしかないだろうけど、

sakae@lxde:~/go/src/hello$ go tool nm hello
  47e100 T fmt.Fprintf
  47e210 T fmt.Printf
  54d990 D fmt.boolError
   :

今までやってきた(調べてきた)のをgoしたらどうなる?

Go言語で知るプロセス(2)

ダエモン君の話が出てきたので、

Go by Example

Goでデーモンを実装する

ダエモン君の代表例と言ったら、あれだな。cronとかだ。で、話は繋がっていきます。

at cron

前々回だったか、cronなんてのに頭が行った。覚えているものだねぇ。オイラーが現役の頃は、 散々これのお世話になったよ。いわゆる時の神の名前から頂いてきたのがコマンド名になってる。スケジューラーの事ね。

これを使うには、色々と作法があったな。もう忘れているから外部参照してみるか。

cron と at を使ったジョブ・スケジューリング

ああ、at なんてコマンドも有った。

at コマンドとは これは、一発タイマーか。

一応cron(8)も見ておくかってんで、FreeBSDのやつをみたら、面白いオプションを発見

     -j jitter
             Enable time jitter.  Prior to executing commands, cron will sleep
             a random number of seconds in the range from 0 to jitter.  This
             will not affect superuser jobs (see -J).  A value for jitter must
             be between 0 and 60 inclusive.  Default is 0, which effectively
             disables time jitter.

             This option can help to smooth down system load spikes during
             moments when a lot of jobs are likely to start at once, e.g., at
             the beginning of the first minute of each hour.

みんなが、毎時00分を指定しちゃうと(ありがちな事です)、同時にコマンドが動いてしまって、まずくねぇ。それの救済策。かの昔に作ったシステムで、メールを送ってそれに内包されるデータをrubyで処理するシステムが有った。今風に言うと、IoTなシステム。

毎時00分にメールを投げるものだから、受け取って処理する方はおおわらわ。ロードアベレージが、一気に30ぐらいまで上がってた。それでも、頑丈なSunOSだったから、何とか耐えてくれていたけど。リナだと根を上げてしまっただろうね。

そんでもって、メールを送る時間をばらけさせたよ。FreeBSDを使ってたら、ある程度解消出来ただろうにね。懐かしい記憶だ。

そして、今話題の夏時間、冬時間の対応も組み込まれている。何でも、世界で70ヵ国以上で、これを採用してるとか。そんな国でも、矛盾なく動くように苦労しましたなんて事が書いてあった。何でも、3時間までの時刻飛びは、対応出来るようにしたとか。某爺が提案した、2時間シフトも、余裕で対処出来ますよ。国会で可決した折には、FreeBSDへの乗り換えをお勧めします。

     -s      Enable special handling of situations when the GMT offset of the
             local timezone changes, such as the switches between the standard
             time and daylight saving time.

あっ、Debian様も対応してた。

       Special  considerations  exist when the clock is changed by less than 3
       hours, for example at the beginning and end of daylight  savings  time.
       If  the time has moved forwards, those jobs which would have run in the
       time that was skipped will be run soon after the  change.   Conversely,
       if  the  time has moved backwards by less than 3 hours, those jobs that
       fall into the repeated time will not be re-run.

at

cronってのは、どう実現してるかOpenBSDで見てみる。場所は、/usr/sbinの中。rootさんの管轄。しょっぱなに、atrun.cなんてのが有る。atも決められた時間に処理するんで、広義のcronの仲間か。但し、一回だけの実行だけどね。cron(8)の関係者には、

FILES
     /etc/crontab        system crontab file
     /var/cron/atjobs    directory containing at(1) jobs
     /var/cron/log       cron's log file
     /var/cron/tabs      directory containing individual crontab files
     /var/run/cron.sock  used by crontab(1) to tell cron to check for crontab
                         changes immediately

システム用もスケジュール表と一般ユーザー用のスケジュール表とat用のものが有るとな。

先にat用を見ておくか。なお、atとcronのからみは、cron.cの中から、atrun.cの中の関数、scan_atjobsってのが呼び出されているので、強固に結合してるんだろね。

atのソース群は、/usr/src/usr.bin/atの中。at.1, atq.1, atrm.1なんていうmanの原稿が3本も有るのに、対応するソースは、 at.cとparsetime.cの二本だて。これって、不公平じゃありませんか? atrmとかのソースは何処よ?

ob6$ ls -l | grep crontab
-r-xr-sr-x  4 root  crontab      35776 Mar 25 05:12 at*
-r-xr-sr-x  4 root  crontab      35776 Mar 25 05:12 atq*
-r-xr-sr-x  4 root  crontab      35776 Mar 25 05:12 atrm*
-r-xr-sr-x  4 root  crontab      35776 Mar 25 05:12 batch*
-r-xr-sr-x  1 root  crontab      30664 Mar 25 05:12 crontab*

ちょっと強引に、グループがcrontabのそれを検索。

groupの実行属性がsになってる。スティッキーbitで、crotab族と同じ扱いを受けるのかな? 用心して、sについてlsを調べておく。

                     s     If in the owner permissions, the file is
                            executable and set-user-ID mode is set.  If in the
                            group permissions, the file is executable and set-
                            group-ID mode is set.

atもatrmも同じファイルだな。 どうやって区別してる? 興味津々。

それより先に、時刻のパースを見ておくかな。そしたら、面白いコメントを発見。

static const struct {
    const char *name;   /* token name */
    int value;  /* token id */
    int plural; /* is this plural? */
} Specials[] = {
    { "midnight", MIDNIGHT,0 }, /* 00:00:00 of today or tomorrow */
    { "noon", NOON,0 },         /* 12:00:00 of today or tomorrow */
    { "teatime", TEATIME,0 },   /* 16:00:00 of today or tomorrow */
        :
                /*
                 * evil coding for TEATIME|NOON|MIDNIGHT - we've initialised
                 * hr to zero up above, then fall into this case in such a
                 * way so we add +12 +4 hours to it for teatime, +12 hours
                 * to it for noon, and nothing at all for midnight, then
                 * set our runtime to that hour before leaping into the
                 * month scanner
                 */
        case TEATIME:
                hr += 4;
                /* FALLTHROUGH */
        case NOON:
                hr += 12;
                /* FALLTHROUGH */
         :

teatimeが、午後4時ですって? そんなのオイラー聞いた事ないぞ。お茶の時間は全世界共通で3時と思ってた。(島国根性ですか)

ティータイムの正しい時間はいつ? って、聞いてみたぞ。そうなんですか、知らなかったな。エゲレズは、夕食が割りと遅い時間なんで、お茶の時間も遅めらしい。うちの女房は、そういう事知ってた。知らないのは、ひょっとしてオイラーだけ? 今度、むさ苦しい友人に会ったら、さりげなく聞いてみよう。

カメレオンみたいなプログラムの秘密は、mainの冒頭付近に

        /* find out what this program is supposed to do */
        if (strcmp(__progname, "atq") == 0) {
                program = ATQ;
                options = "cnvq:";
        } else if (strcmp(__progname, "atrm") == 0) {
                program = ATRM;
                options = "afi";
        } else if (strcmp(__progname, "batch") == 0) {
                program = BATCH;
                options = "f:q:mv";
        }

謎の __progname は、プログラムのローディング時にでも、決められるのだろうね。

ob6$ nm atrm | grep __progname
         U __progname

こんな風に、未定義になってるから。そして、Makefileをみたら、atだけを作って後は、atrmとかをハードリンクしてた。挙動を変える方法は、argv[0]を見て名前を得るってのは知ってたけど、新たな知見だな。

それで、その先をざっと見したら、atで指定した実行時間と内容を、cronの都合が良い形式でファイルにするようだ。実験した方が早いな。

ob6$ at -t 10011234
pwd
ls -l
commands will be executed using /bin/ksh
job 1538364840.c at Mon Oct  1 12:34:00 2018

実行時刻を10月1日の12時34分に指定。pwdしてからlsしてねって依頼。 それに対して、ファイルが作られた。1538から始まるのは、Unix時間っぽい。最後に付いてるcは、何だろう?

まず時間の想像が有ってるか?

ob6$ date -r 1538364840
Mon Oct  1 12:34:00 JST 2018

ビンゴですな。次はcの疑問。at(1)

     -q queue
             Uses the specified queue.  A queue designation consists of a
             single letter.  Valid queue designations range from a to z and A
             to Z.  The c queue is the default for at and the E queue for
             batch.  Queues with higher letters run with increased niceness.
             If a job is submitted to a queue designated with an uppercase
             letter, it is treated as if it had been submitted to batch at
             that time.  If the user specified the -l option and at is given a
             specific queue, only jobs pending in that queue will be shown.

実行行列の優先順位か。何も知らない人は、順位が低く抑えられるって、無知は馬鹿って言われてるみたいだな。

で、これがファイルとなって、/var/cron/atjobsって、秘密の領域に格納されてるんだな。

ob6# cd /var/cron/atjobs/
ob6# ls
1538364840.c
ob6# less 1538364840.c
#!/bin/sh
# atrun uid=1000 gid=66
# mail                            sakae 0
umask 22
export LOGNAME=sakae
export HOME=/home/sakae
export PWD=/usr/src/usr.bin/at
export SSH_TTY=/dev/ttyp0
 :
cd /usr/src/usr\.bin/at || {
         echo 'Execution directory inaccessible' >&2
         exit 1
}
/bin/ksh << '_END_OF_AT_JOB'
pwd
ls -l

_END_OF_AT_JOB

たまたま、atコマンドを発行した場所が、/usr/src/usr.bin/at だったんで、その場所を覚えられてる。実行時は、そこへ移動して、指定したコマンドを実行せよとな。 後は、cronさんの出番だぞ。

その前に、細かい事だけど、ファイル名がかぶってしまったら逃げようって手段が組み込まれていた。

newjob(time_t runtimer, int queue)
{
        int fd, i;

        /*
         * If we have a collision, try shifting the time by up to
         * two minutes.  Perhaps it would be better to try different
         * queues instead...
         */
        for (i = 0; i < 120; i++) {
                snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL,
                    (long long)runtimer, queue);
                fd = open(atfile, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR);
                if (fd >= 0)
                        return (fd);
                runtimer++;
        }
        return (-1);
}

コンサートチケットの自動予約をしようとしたら、後からコマンドを投入したら不利になりますって仕掛けだな。微妙に時間を遅くしてる。

cron

cronはダエモン君の一種。作るには作法がある。でも最近はdaemon(3)で、簡単にダエモン君化出来るとか。便利な物は使おうってんで、cronでも使ってるよ。

DESCRIPTION
     The daemon() function is for programs wishing to detach themselves from
     the controlling terminal and run in the background as system daemons.

     If the argument nochdir is zero, daemon() changes the current working
     directory to the root (/).

     If the argument noclose is zero, daemon() redirects standard input,
     standard output and standard error to /dev/null.

昔はこの作法がUNIX USERあたりで、盛んに解説されてたな。隔世の感がありますよ。

cronのmainの先頭付近で、cronを実行して良いか確認。問題なければダエモン君になって活動を始める。最初にジョブデータを取り込み。後は1分毎に、実行すべきジョブを実行って事かな。サマータイムなんてやっかいな事例の時は、一時的に仮想時間に切り替えて、対応してるみたいだ。

cronとatのやり取りは、ユニックスソケットを使ってるぽい。atの所を見るかな。

   scan_atjobs(&at_database, NULL);
   while(TRUE){
	sleep 60s;

                /* Run any jobs in the at queue. */
                atrun(at_database, batch_maxload,
                    timeRunning * SECONDS_PER_MINUTE - GMToff);
                /* Reload jobs as needed. */
                scan_atjobs(&at_database, NULL);
    }

scan_atjobsで次に実行するjobを見つけておいて、atrunで次回実行。実際に実行するのは、run_jobって関数の中。

凄いチェックが仕込まれている。例えばこれ。

        /*
         * Verify the user still exists and their account has not expired.
         */
        pw = getpwuid(job->uid);
        if (pw == NULL) {
                syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
                _exit(EXIT_FAILURE);
        }
        if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
                syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
                    pw->pw_name, atfile);
                _exit(EXIT_FAILURE);
        }

リストラされた人が、腹いせに置き土産のatを仕込んだとする。(rm -rf / とか、機密書類を某所に送るとか)ばれないように、半年後に実行してねって指示。

こういうのは、きちんとアカウントを削除しておくか、失効させておけば防げる。それの防波堤処理だ。そして、その後もファイルが汚染されていないか等のチェックが続く。 atで作られたjobファイルの形式が合ってるか等々。他のOSのコードを見た事が無いから、何とも言えないけど、神経質過ぎる程のチェックだなあ。

本音を言えば、atコマンドなんて、実行して欲しくないぞーーーってのが、ありあり。そして最後に、

                /*
                 * Exec /bin/sh with stdin connected to the at job file
                 * and stdout/stderr hooked up to our parent.
                 * The at file will set the environment up for us.
                 */
                nargv[0] = "sh";
                nargv[1] = NULL;
                nenvp[0] = NULL;
                if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {

やっと実行させてもらえる。jobとのやり取りは、メールで送信しなきゃいけないので、パイプで親側に繋げてある。そして、親側がメール発信っていう塩梅になってる。

最後は、起動したjobの看取りがあります。この段階では、子ではなくて、孫って扱いなんですな。

        /* Wait for grandchild to die.  */
        for (;;) {
                if (waitpid(pid, &waiter, 0) == -1) {
                  :
	}
        _exit(EXIT_SUCCESS);

そして、失敗した記録は、もれなくログされてるぞ。例えば、

bad_file:
        syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name, atfile);
        _exit(EXIT_FAILURE);

シスログの他、cron専用のログも取ってるぞ。用心深いなあ。悪い人には不快でしょうけど。

at FreeBSD

FreeBSD(NetBSD)で、atの実行どうやってる?

sakae@fb:/etc % cat /etc/crontab
# /etc/crontab - root's crontab for FreeBSD
#
# $FreeBSD: releng/11.2/etc/crontab 194170 2009-06-14 06:37:19Z brian $
#
SHELL=/bin/sh
PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin
#
#minute hour    mday    month   wday    who     command
#
*/5     *       *       *       *       root    /usr/libexec/atrun
 :

5分毎に、atrunを走らせてた。その中で、OpenBSDとほぼ同様のセキュリティーチェックを受けた後、実行の運びとなってた。何か、取って付けたような実装だな。

IMPLEMENTATION NOTES
     Note that at is implemented through the cron(8) daemon by calling
     atrun(8) every five minutes.  This implies that the granularity of at
     might not be optimal for every deployment.  If a finer granularity is
     needed, the system crontab at /etc/crontab needs to be changed.

これは最適な実装ではありませんって、自己懺悔してるよ。重要度が高く無いんで、永久放置かな。リナもatを後付けしてるんで、同罪だな。 その点、OpenBSDは隙が無いな。

break time

この所、パソコンに向かって根を詰めている事が多い。気が付くと、2時間ぶっとうしなんてのもザラ。後で、眼にくる。眼をいたわりましょう。

ってな事で、時間が来たら、画面に、休憩って出したいな。

そんなの簡単。先ほどやった at のお世話になればいい。結果はメールで流れてくる。でも、メールの到着を知らせてくれる biff なんて飼っていないぞ。サンダーバードなら、メールが 到着すると画面がわーとなって知らせてくれるけど、今更サンダーバードもあるまい。

ああ、biffってのは、郵便配達人がやって来ると、ワンワン吠えた犬の名前にちなんです。かの昔、これを改造して、花火を上げるようにしてた人が居たな。(スクリーンセーバーに、パイロってのがありましたな)メールが届いたんで、記念に花火をあげましたって、のどかな時代だったな。

あれこれ考えて、wallを使う事にしました。

ob6$ at now + 1 minute
echo break time | wall                        ;; Ctl-D to finish input
commands will be executed using /bin/ksh
job 1536732240.c at Wed Sep 12 15:04:00 2018

Broadcast Message from sakae@ob6.localdomain                                   8
        ((not a tty)) at 15:04 ...

break time

      _

これ、設定して放置しておいた時の図。_ の所にカーソルが有るので、一度Retを叩くと次の行にプロンプトが出て来る。emacsとか使ってた場合は、Ctl-l すれば、煩わしいメッセージは消える。

後は、これをスクリプトにして、.profileにでも仕込んでおけば、loginと同時に計時が始まって万万歳。リナの場合もって思ったら、そもそもatが入っていない。入れようとしたら、メールサーバーも付いてくる。難儀なこっちゃ。もっと汎用的にしよう。

上で使ったwallは画面に割り込んで、メッセージを表示する便利なコマンドだ。rootさんがマシンを落とす前に、ログインしてるユーザーに一斉通知するのに、よく使われていた。さすがこれはリナでも標準で付いていたよ。

NAME
       wall - write a message to all users

SYNOPSIS
       wall [-n] [-t timeout] [message | file]

傍若無人な一斉通知。町でやってる防災無線の画面版。あちらは、振り込み詐欺注意ってのと、徘徊老人の探索依頼とかしか流れてこないけどね。

debian:~$ cat bt
#!/bin/sh
sleep `echo '45 * 60' | bc`
banner -l BREAK | wall

今度は、bannerを使って、迫力満点の広告(花文字)を出す事にした。 画面が乱れてイヤヤって向きには、wallをmorseにでも変更すればよい。時間がきたら、モールス信号で教えてくれるぞ。

そう言えば、かの昔、携帯の着信音をモールス信号に出来ないか吟味した事がある。あの頃は、着メロが流行っていたんで、何かあるだろうと思ってたけど、それらしいのは何も見つからなかった。

再び考えてみるに、音ファイルの仕様さえ分かれば、それに沿ってsoxあたりで音源を作り、後どうする? どうやって携帯に持ち込む? そこらあたりの資料が何処かに転がっていないかな。

所で、Windows10で動いているWSL。wallには無反応、無視される。だめじゃんMS。ちゃんと実装してくれよな。まあ、昔からWindowsの窓の扱いはいい加減と言うか、全くやる気無しが伝統になってますからな。

それはそうと、Windowsだけでオペレーション(Webサーフィンの事ですが)してる時は、休憩時間をどう知らせる?

そんなの簡単、台所からキッチンタイマーを持ってくればOKさ。そんな事したら女房に怒られるぞ。ひょっとして、100近に売ってる? たとえ売ってても、物はもう増やさない主義なんで、そんなの却下。

Cool Timer

SnapTimer - Free Windows Countdown Timer

こんなWindows用のタイマーが有るようですよ。

go on i386

i386なdebianにもGOを入れておくか。入れて試したら、見事なエラー。

debian:hello$ go version
go version go1.11 linux/386
debian:hello$ go build hello.go
# runtime
/usr/local/go/src/runtime/map.go:64:2: bucketCntBits redeclared in this block
        previous declaration at /usr/local/go/src/runtime/hashmap.go:64:18
/usr/local/go/src/runtime/map.go:65:2: bucketCnt redeclared in this block
        previous declaration at /usr/local/go/src/runtime/hashmap.go:65:23
         :
/usr/local/go/src/runtime/map.go:93:2: too many errors

hashmapとmapで、宣言がだぶってるって、どんだけアホはぐぐるや! 信用ならんから、先端バージョンは止めて、古いやつにする。

debian:hello$ go version
go version go1.10.4 linux/386
debian:hello$ go run hello.go
hello, world

やれやれ、やっと儀式が終了したわい。

etc

ob6$ mount | column -t
/dev/wd0a  on  /     type  ffs  (local,         wxallowed)
mfs:82252  on  /tmp  type  mfs  (asynchronous,  local,      nodev,  nosuid,  size=2048000  512-blocks)