signal

lastcommの説明を見てたんだ。そしたら

     The flags are encoded as follows: `F' indicates the command ran after a
     fork, but without a following exec(3), `C' indicates the command was run
     in PDP-11 compatibility mode (VAX only), `D' indicates the command
     terminated with the generation of a core file, `X' indicates the command
     was terminated with a signal, `P' indicates the command was terminated
     due to a pledge(2) violation, and `T' indicates the command did a memory
     access violation detected by a processor trap.

PDP-11やらVAXやら、時代がかったマシン名が出て来た。考古学者は嬉しいだろうね。世はなんか、縄文女子とか言って浮かれているし、オイラーもぷち浮かれてみるかな。

char *
flagbits(int f)
{
        static char flags[20] = "-";
        char *p;

#define BIT(flag, ch)   if (f & flag) *p++ = ch

        p = flags + 1;
        BIT(AFORK, 'F');
        BIT(ACOMPAT, 'C');
        BIT(ACORE, 'D');
        BIT(AXSIG, 'X');
        BIT(APLEDGE, 'P');
        BIT(ATRAP, 'T');
        *p = '\0';
        return (flags);
}

説明書通りの事が書いてあるだけでした。acct.hを見るも

#define AFORK   0x01            /* fork'd but not exec'd */
#define ASU     0x02            /* used super-user permissions */
#define ACOMPAT 0x04            /* used compatibility mode */
#define ACORE   0x08            /* dumped core */
#define AXSIG   0x10            /* killed by a signal */
#define APLEDGE 0x20            /* killed due to pledge violation */
#define ATRAP   0x40            /* memory access violation */
        u_int8_t  ac_flag;      /* accounting flags */

これしか無いんだよな。こういう時は、河岸を変えて、NetBSDの所にでも行ってみればいいのか。恐竜の発掘も川の左岸がいいとか右岸がいいとか言うそうですから。あれ?ASUが漏れてるようだけど、これも歴史の所産?

なお、このlastcommとかsa(print system accounting statistics)なんて、辺境なBSD系とばかり、嘲笑うなかれ。リナちゃんでも、acctパッケージを入れれば、それなりに動くはずですから。

proc

前回やった、プロセスのリソース使用状況をOpenBSDで追ってみた。

318│ preempt(void)
319│ {
320│         struct proc *p = curproc;
321│         int s;
322│
323│         SCHED_LOCK(s);
324│         p->p_priority = p->p_usrpri;
325│         p->p_stat = SRUN;
326│         setrunqueue(p);
327├───────> p->p_ru.ru_nivcsw++;
328│         mi_switch();
329│         SCHED_UNLOCK(s);
330│ }

こんな所で、コンテキスト切り替えの回数をカウントしてた。

Breakpoint 1, preempt () at /usr/src/sys/kern/sched_bsd.c:320
(gdb) bt
#0  preempt () at /usr/src/sys/kern/sched_bsd.c:320
#1  0xd03598fb in mi_ast (p=0xd16f5434, resched=1) at /usr/src/sys/sys/syscall_mi.h:150
#2  0xd0359886 in ast (frame=0xf3706818) at /usr/src/sys/arch/i386/i386/trap.c:520
#3  0xd089c886 in Xdoreti ()
#4  0xf3706818 in ?? ()

実際の値は、下記のようにプロセス構造体の一部。

(gdb) p *p
   :
  p_ru = {      
    ru_utime = {
      tv_sec = 0,
      tv_usec = 0
    },
    :
    ru_nsignals = 0,
    ru_nvcsw = 0,
    ru_nivcsw = 10
  },

そんじゃ、経過時間はどうやってる?

(gdb) bt
#0  tuagg_sub (tup=0xd16f5510, p=0xd16f5434) at /usr/src/sys/kern/kern_resource.c:338
#1  0xd09e7e00 in tuagg_unlocked (pr=0xd1771ad0, p=0xd16f5434) at /usr/src/sys/kern/kern_resource.c:352
#2  0xd077d9a9 in mi_switch () at /usr/src/sys/kern/sched_bsd.c:382
#3  0xd077d7ae in preempt () at /usr/src/sys/kern/sched_bsd.c:328
#4  0xd03598fb in mi_ast (p=0xd16f5434, resched=1) at /usr/src/sys/sys/syscall_mi.h:150
#5  0xd0359886 in ast (frame=0xf37068c8) at /usr/src/sys/arch/i386/i386/trap.c:520
#6  0xd089c886 in Xdoreti ()
#7  0xf37068c8 in ?? ()
(gdb) p *tup
$15 = {
  tu_runtime = {
    tv_sec = 0,
    tv_nsec = 434065950
  },
  tu_uticks = 3,
  tu_sticks = 11,
  tu_iticks = 2
}
tuagg_sub(struct tusage *tup, struct proc *p)
{
        timespecadd(&tup->tu_runtime, &p->p_rtime, &tup->tu_runtime);
        tup->tu_uticks += p->p_uticks;
        tup->tu_sticks += p->p_sticks;
        tup->tu_iticks += p->p_iticks;
}

timespecaddは、関数のように見えたんでBPを置こうとしたら、そんなの無いからって言われた。よくよく調べたらマクロになってた。習慣敗れたり。

#define timeradd(tvp, uvp, vvp)                                         \
        do {                                                            \
                (vvp)->tv_sec = (tvp)->tv_sec + (uvp)->tv_sec;          \
                (vvp)->tv_usec = (tvp)->tv_usec + (uvp)->tv_usec;       \
                if ((vvp)->tv_usec >= 1000000) {                        \
                        (vvp)->tv_sec++;                                \
                        (vvp)->tv_usec -= 1000000;                      \
                }                                                       \
        } while (0)

どうやら深く潜り過ぎた。mi_switchあたりに、

363│         /*
364│          * Compute the amount of time during which the current
365│          * process was running, and add that to its total so far.
366│          */
367├───────> nanouptime(&ts);

このnanouptimeってのが、高精度のカウンターの値を読んで、時刻に変換してるっぽい。分解能がnanoセコンドですって。こんな高度な事をWSLに実装しても意味ないだろう。誰も気にしない。だったら、さぼっちゃえってのがMSの人達の態度なんだな。ましてや、プロセスの切り替え理由なんか知った所で意味ないじゃん、はたから眺めるしか手はないもの。もっとマクロな見直しをして、スピードアップに貢献してくださいな。

unix v6

プロセス回りがどうなってるか? いきなりソースを見て行くと迷子になっちゃう。そこで、 家にある唯一の本、Unixカーネルの設計なんて本をパラパラして予習。

ATTのUnixの講義用に書かれた本とか。ATTと言われて、ベル研と条件反射。そこから連想記憶が動き出した。

The Unix Tree から辿って、 v6 source に行きつく。

ここの、 v6root.tar.gz (usr/sys{ken/,dmr/}が、いわゆるカーネルソースが入ってる。 そして、 v6src.tar.gz source into /usr/srcには、ユーザーランド側のソースが入ってる。

参考に Unix V6コードリーディング(fork) , UNIX V6コードリーディング備忘録 その1を、参照。

初めてのOS source code reading(UNIX 6th source code readingのススメ)

pdp11の遊び方

参考と言えば、v6doc.tar.gz も置いてあった。取り寄せて展開してみるとmanの原稿ぽい。manにしたやつもあったけど、どうかなあ。試しに

ob6$ mandoc as/as
                 UNIX Assembler Reference Manual


                        Dennis M. Ritchie

                        Bell Laboratories
                     Murray Hill, New Jersey

なんてやったら、textに整形されたものが出て来た。古文書を読む趣だなあ。

saなんてのがこの時期のunixに装備されてた。詳しくは、usr/source/s2/sa.c

じゃ、独立独歩のリナはどうよ? Debian Sourcesから検索してください。

signal

Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する

シグナルとコールバック

暴走したプログラムを端末から殺すには、通常 Ctl-C を入力する。これで、暴走アプリが殺さる経路を追ってみるかな。暴走プログラムの代わりに、今回は昼寝をするプログラムを 用意した。

#include <stdio.h>

main(){
   printf("pid= %d\n", getpid());
   sleep(1200);
}

寝すぎると、夜眠れなくなるので、程ほどにね。起動するとpidを報告してから、20分のシエスタ時間になる。殺すためのキーバインドは、以下で確認出来る。

ob6$ stty -a
 :
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
        eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;
        min = 1; quit = ^\; reprint = ^R; start = ^Q; status = <undef>;
        stop = ^S; susp = ^Z; time = 0; werase = ^W;

intrっていうシグナルがCtl-Cの押し下げで送られる。このintrは、

ob6$ kill -l
 1    HUP Hangup                        17   STOP Suspended (signal)
 2    INT Interrupt                     18   TSTP Suspended
 3   QUIT Quit                          19   CONT Continued
 :

で確認出来るけど、より詳しくは、sigactionに説明が有るとの事。

     Name          Default Action       Description
     SIGHUP        terminate process    terminal line hangup
     SIGINT        terminate process    interrupt program
     SIGQUIT       create core image    quit program
      :

プロセスがターミネーターされるとな。映画になったターミネーターと一緒。殺人鬼である。 その他、ゲロを吐かせるシグナルもあるよ。ああ、ゲロって上品に言うとcoreの事ね。 このゲロを後でgdbにでもかけて分析すると、死亡原因が分かる。

で、流れを追いたいんだけど、カーネル側はプログラムが物語になっていない。何かのシステムコールで、流れが作られる。だから、発生するであろうシステムコールにBPを置くのが常道。

でも、今回はちょいと違いそう。そんな訳で、何処にBPを置いたら良いか、下調べをしておく。 多分、シグナル名が、ソースに散りばめられていると思うんで、探してみる。

ob6$ grep SIGINT *.c
kern_sig.c:             case SIGINT:
tty.c:                      pgsignal(tp->t_pgrp, SIGINT, 1);
tty.c:                              CCEQ(cc[VINTR], c) ? SIGINT : SIGQUIT, 1);

シグナルを扱ってますよって主張してるファイルと、ttyからの入力を扱ってますよってのが出てきた。3か所なんで、全てに網を張ってもいいけど、オイラーの勘でtty.cに網を張る。

(gdb) b tty.c:277
Breakpoint 1 at 0xd088a544: file /usr/src/sys/kern/tty.c, line 277.
(gdb) b tty.c:360
Breakpoint 2 at 0xd088a8bb: file /usr/src/sys/kern/tty.c, line 360.
(gdb) c
Continuing.

Breakpoint 2, ttyinput (c=3, tp=0xd17be400) at /usr/src/sys/kern/tty.c:361

Ctl-Cしたら、見事に当たったね。

 359│                                 pgsignal(tp->t_pgrp,
 360│                                     CCEQ(cc[VINTR], c) ? SIGINT : SIGQUIT,1
 361├───────────────────────────────> goto endcase;

Ctl-Cだから、ASCIIコードにしたら、0X03 になってたから、間違いなさそう。pgsignalってどんな働きするの?

     The pgsignal() function posts signal number signum to each member of the
     process group described by pgrp.  If checkctty is non-zero, the signal
     will be posted only to processes that have a controlling terminal.  If
     pgrp is NULL no action is taken.
(gdb) p *tp->t_pgrp
$2 = {
  pg_hash = {
    le_next = 0x0,
    le_prev = 0xd17905ac
  },
  pg_members = {
    lh_first = 0xd17711d4
  },
  pg_session = 0xd1768af0,
  pg_id = 11115,
  pg_jobc = 1
}
(gdb) p cc[VINTR]
$3 = 3 '\003'

眠りに付く前に言ってきたpidとpg_idは一致してる。そして、3項演算子のtest句がtrueになってるんで、確かにSIGINTが、寝てるプロセスに送られた事が判明した。

後は、このシグナルを受けて、プロセスがどう振る舞うかだ。スケジューラの所に、プロセスを殺す部分が有るに違いない。

ob6$ ls *sch*
kern_sched.c    sched_bsd.c
ob6$ grep exit *sch*
kern_sched.c: * be NULL if the process is exiting. NULL for the old proc simply
kern_sched.c:                           exit2(dead);
kern_sched.c:sched_exit(struct proc *p)

ファイル名がスケジューラっぽい名前を列挙。その中にプロセスを終了させそうなやつがないか確認。

(gdb) b exit2
Breakpoint 3 at 0xd03a1d75: file /usr/src/sys/kern/kern_exit.c, line 357.
(gdb) c
Continuing.

Breakpoint 3, exit2 (p=0xd16f516c) at /usr/src/sys/kern/kern_exit.c:357
(gdb) bt
#0  exit2 (p=0xd16f516c) at /usr/src/sys/kern/kern_exit.c:357
#1  0xd08c8234 in sched_idle (v=0xd0d38ae0 <cpu_info_primary>) at /usr/src/sys/kern/kern_sched.c:162
#2  0xd089aec9 in proc_trampoline ()

a.outが実行してたnanosleepが起こされた。ここからまた、スケジューラーの中を駆け巡り、 exitが実行されたよ。と、お茶を濁すのもあれなんで、exit1に網を張ってみた。このexit1は、システムコールsys_exitの実質的な部分だ。ユーザーランド側からは、sys_exitを呼び出すけど、カーネル内からだとexit1って事だ。

 82│ sys_exit(struct proc *p, void *v, register_t *retval)
 83│ {
 84│         struct sys_exit_args /* {
 85│                 syscallarg(int) rval;
 86│         } */ *uap = v;
 87│
 88│         exit1(p, W_EXITCODE(SCARG(uap, rval), 0), EXIT_NORMAL);
 89│         /* NOTREACHED */
 90│         return (0);
 91│ }
  :
110│ /*
111│  * Exit: deallocate address space and other resources, change proc state
112│  * to zombie, and unlink proc from allproc and parent's lists.  Save exit
113│  * status and rusage for wait().  Check for child processes and orphan them
114│  */
115│ void
116│ exit1(struct proc *p, int rv, int flags)
117│ {
118│         struct process *pr, *qr, *nqr;
119│         struct rusage *rup;
120│
121├───────> atomic_setbits_int(&p->p_flag, P_WEXIT);

例によって、バックトレースした。

(gdb) bt
#0  exit1 (p=0xd16f516c, rv=2, flags=1) at /usr/src/sys/kern/kern_exit.c:121
#1  0xd0982fbe in sigexit (p=0xd16f516c, signum=2) at /usr/src/sys/kern/kern_sig.c:1459
#2  0xd0982db9 in postsig (p=0xd16f516c, signum=2) at /usr/src/sys/kern/kern_sig.c:1390
#3  0xd0983e56 in userret (p=0xd16f516c) at /usr/src/sys/kern/kern_sig.c:1839
#4  0xd0359d38 in mi_syscall_return (p=0xd16f516c, code=91, error=4, retval=0xf371a390) at /usr/src/sys/sys/syscall_mi.h:98
#5  0xd0359b85 in syscall (frame=0xf371a3d8) at /usr/src/sys/arch/i386/i386/trap.c:632

exit code

上で使った昼寝プログラム、そのまま終了するとecho $? の結果はZERO。Ctl-Cで止めると130だった。どゆ事? kill(1)に、こんな説明が。

     -l [exit_status]
             Display the name of the signal corresponding to exit_status.
             exit_status may be the exit status of a command killed by a
             signal (see the special sh(1) parameter `?') or a signal number.

そして、exitの説明では、

     Following this, exit() calls _exit(2).  Note that typically _exit(2) only
     passes the lower 8 bits of status on to the parent, thus negative values
     have less meaning.

128げたを履いたコードになるのか? だとすれば、130 - 128 = 2 (== SIGINT) だな。 他のSIGで確かめる。sttyのキーコンビネーションでは、Ctl-\ で、QUITを送れるとな。

このSIGQUITは、コアを吐いてお亡くなりになるのがデフォ。実際やってみたら、コアしたよ。 そして終了コードは、131だった。128を引くと、3でQuitに一致してる。

もう一つ例題。SIGTERMを自分自身に送る。簡単にmainの中に一行入れるだけ。

   kill(getpid(), 15);

これでコンパイルして実行すると、Terminatedって表示されて終了。コードは143だった。128のげたを外せば、15って事で、シグナル番号に一致してるね。よかったよかった。

(gdb) bt 4
#0  exit1 (p=0xd16f516c, rv=15, flags=1) at /usr/src/sys/kern/kern_exit.c:121
#1  0xd0982fbe in sigexit (p=0xd16f516c, signum=15) at /usr/src/sys/kern/kern_sig.c:1459
#2  0xd0982db9 in postsig (p=0xd16f516c, signum=15) at /usr/src/sys/kern/kern_sig.c:1390
#3  0xd0983e56 in userret (p=0xd16f516c) at /usr/src/sys/kern/kern_sig.c:1839
(More stack frames follow...)

sys_killに入ってから、exit1で止めた時。ものの本の通り、システムコールの出口で、シグナルが来ていないか確認してるのね。

キーコンビネーションで発生するSignalは、プロセスに取っては暗殺者、刺客だな。Ctl-cは、死体を残さない完全犯罪。Ctl-\は、堂々と死体を晒して行く自信過剰なやつ。また、プログラムの中に埋め込んで、自分自身にSiganalを送るのは、自殺って事になるか。ああ、物騒だわい。

それから、一つ訂正。上で、exit2が来てからexit1が呼ばれる風に書いちゃったけど、呼ばれる順番が逆だった。2つの関数にBPを置いて走らせたら、先にexit1が網に引っかかってきた。 流れがスケジューラーでちょん切られいるんで、理屈が分からないと誤解する。ソースをつまみ見して、分かった積りになるのは、大いなる誤解の元だ。自戒せよ。

etc

1000 night 1000 book