前からきちっと読んでおきたかった記事をじっくり読んでみた。911030の
comp.unix.internalsのChris TorekのSubject: Re: signal trampoline code
シグナルというのはプロセス間でインデックスだけで通信をするという単純至
極なシステムの割に、そのコードはとてつもなく面倒だ。実際面倒なのはアド
レス空間の異なる関数を呼び出すのが面倒というのに起因する。しかしそれ以
上に混迷して見えるのはそこにトランポリンコード(これは効率化のため)や、
呼び出す関数の受け渡しの手順が規定されていなかったからだろう。それは当
時は富豪的に規定するには許されないハードウェアスペックだったので仕方な
かった。
訳はあやふや。なんとなく理解したような気の範囲で。とても英語が苦手なの で...。訳以外のコメントは「俺」。この投稿の主点はSunOSのハンドラの設定 の実装が気にくわないことに力点がおかれている。
訳はあやふや。なんとなく理解したような気の範囲で。とても英語が苦手なの で...。訳以外のコメントは「俺」。この投稿の主点はSunOSのハンドラの設定 の実装が気にくわないことに力点がおかれている。
俺 これは質問者
> OSF MIPSとMultimaxのシグナル配送のシグナルトランポリンコードはu領域で
> はなく、Cライブラリにあることに気付いた。signal(あるいはsigaction)が呼
> ばれた時、ライブラリの中のトランポリンコードのアドレスが、これ以外のカー
> ネルに保持される引数と共にカーネルに引き渡される。事はBSDにおいても大
> 体そんなとこ(訳XXX) ユーザの(ハンドラの)プログラムカウンタがシグナルを
> 捕捉するためにこのアドレスにセットされる。
>
> 質問: 何故BSD(VAX用)はトランポリンコードをu領域に置くのか?それは機種依
> 存だし明らかに必要ない。
俺 OSF MIPSとMultimaxはシグナルトランポリンとシグナルハンドラを両
俺 方渡す実装ということなのだと思う。
俺 ここから御大。
機種依存で必要ないのは正しいが、それでも(BSD VAXでトランポリンコードを
u領域に置くのには)いい理由がある。
シグナル配送の間にユーザプロセスのコンテキストを退避し、その後で復帰さ
せるのには二つのやり方がある。
a) カーネルがやる。
b) ユーザプロセスがやる。
a)の方法の利点は特別なユーザコードが必要ないことだ。不利な点はカーネル
は特権モードなので、正しく動作させるには難しくなりがちなことだ。その難
しさは機種によるけれど、大体は(b)の方が望ましい。
俺 a)の方法でやるとすれば、まずカーネルはこのプロセス用のアドレス
俺 空間を設定して、シグナルハンドラ用のスタックを設定してユーザモー
俺 ドでシグナルハンドラにジャンプする。シグナルハンドラを実行した
俺 後にもう一度カーネルに戻ってきて、(何故戻ってこないといけないか
俺 というと、カーネルにしかスイッチフレームがないからだ) そのスイッ
俺 チフレームでプロセスを再開する。
((b)の実装にしたので)ユーザコードが自プロセスのコンテキストの退避、復帰
をしなくてはいけなくなった。一般的にこんな感じ
- いくつかのレジスタをしかるべきスタックに退避する。
- いくつかの引数とともにシグナルハンドラを呼ぶ。
- レジスタをスタックから復帰させる。
- "sigreturn"システムコールを呼ぶ。
sigreturnシステムコールもいくつかの引数をとる。それにはどこにリターンす
るかのアドレスと、ユーザコードで退避/復帰できなかったいくつかのレジスタ
sigreturn自体が使うのでいくつかのレジスタ(sigreturn自体で使われるので)
が含まれる
俺 「sigreturnはシグナルが送られたプロセスが以前に中断されたところ
俺 から実行する。アドレス空間はこのシグナルハンドラのために設定さ
俺 れた。あとはきっちりスイッチフレームを設定してやれば、そのプロ
俺 セスにそのまま戻れる。ここでいう操作はスイッチフレームの設定」
例えばVAXではsigreturnシステムコールはその場のスタックを使う。なので元
の(復帰すべき)スタックポインタをsigreturnの引数として渡してやらないとい
けない。これらの引数はカーネルによって用意されないといけない。もしそれ
らがメモリに積まれているなら、カーネルはそれらがどこにあるのかを知る必
要がある。つまりシグナルスタックとプロセスがそのスタックで実行している
かを知らせるフラグ。(通常シグナルは一つの'普通な'スタックで処理される。
しかしこうすることが難しいシステムのために、4BSDでは別のシグナルスタッ
クを用意することにした。VAXのハードウェア割り込み用のスタックのような。
俺 「sigreturnの引数がスタックに積まれているなら、その場所が分から
俺 ないといけない。その位置はsigtrampをカーネルが呼び出す時に設定
俺 したシグナルスタックを設定した場所だ」
これはソフトウェアで実装されるから、スタックがいくつあるかという想定に
関係なく機種非依存だ。
妥当なスタックはこのようにカーネルによって用意される。なので実際の実装
は(a)と(b)の中間になる。カーネルは最低限の用意をして、ユーザのトランポ
リンコードにジャンプする。なのでシグナルハンドラそれ自体は普通のCの関数
とできる。
ユーザのシグナルトランポリンコードを実行するためにカーネルはそれがどこ
にあるかを知らないといけない。もしアドレスがカーネルによって固定されて
いれば、これは簡単だ。VAXはこの方法を採用した。トランポリンコードはユー
ザから読み込み実行ができるu領域に配置し、そのアドレスは固定だ。
俺 「VAXではこのu領域がユーザスタックの一番上にリードオンリーでマッ
俺 プされている」
このように、カーネルがそれぞれのシグナルに対するユーザのシグナルハンド
ラのアドレスを保持していて、シグナルを配送する時は、この'sigtramp'コー
ドにユーザの関数のアドレスと、それに渡す引数を渡してやる。要するに
/* in u. area: */
dead void sigtramp(void (*f)(int, int, struct sigcontext *),
int sig, int code, struct sigcontext *scp) {
(*f)(sig, code, scp);
sigreturn(scp);
/* if we get here, the user broke *scp; kill the process */
もしここに来たとしたら、ユーザがシグナルコンテキストを壊した。プロセスを
終了します。
asm("halt");
/* NOTREACHED */
}
(実際のBSD VAXカーネルでは'callg'命令を使ってスタック操作を低減している)
俺 「sigactionがハンドラを渡して来て、それをカーネルは保持していて、
俺 カーネルはトランポリンコードの位置を知ってるからこれで実行でき
俺 るということ?」
ここでu領域を読みだせないBSD SPARCのような場合を想定しよう。ここでOSF
MIPSやMutimaxカーネルのやり方が使える。signal()を呼ぶ(実際はsigaction)
と、カーネルにユーザのハンドラではなく、sigtramp()のアドレスを渡す。
sigactionはこんな感じで実装する。
/* libc sigaction(): massage the parameters and call the
real kernel sigaction(). */
int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
int ret;
struct sigaction realaction, oldrealaction;
extern void __sigtramp(<some arguments>);
realaction.sa_handler = __sigtramp;
<残りのrealactionの内容を*actを元に用意します>
ret = __kernel_sigaction(sig, &realaction, &oldrealaction);
<エラーチェックします>;
<__sigtrampの結果をユーザ関数の返り値に合わせて変換します>;
return (ret);
}
ここで__sigtramp()はVAXのu領域にあるのと似たようなもの。
俺 「4BSDの場合、シグナルトランポリンコードはユーザエリアの中で固
俺 定だ。VAX,Tahoeではu領域、それはユーザスタックの頂上にリー
俺 ドオンリーでマップされている。HP680x0とSPARCではユーザスタック
俺 の頂上に書き込んである。SunOSの場合はユーザ空間のどこかにあ
俺 るという実装。なのでこれはSunOSについて説明している。4BSDであれ
俺 ばトランポリンコードは固定アドレスなのでカーネルから参照できる」
ここで問題がある。sigtramp()を呼び出してから、なんとかしてユーザのハン
ドラまで行かないといけない。カーネルはもはやこの情報を持っていない。カー
ネルは__sigtramp()のアドレスしか持っていない。
これに対処するのに二つの方法がある。
1. カーネルインターフェースをsigtrampのアドレスとハンドラのアドレス両
方とるようにする。
2. SunOSがやった方法:ユーザプロセスにテーブルを保持しておく。つまり上
のsigaction()で、__kernel_sigactionを呼ぶ前に、ユーザのハンドラのテーブ
ル記録しないといけない。(SunOSのように)こんな感じ
俺 「SunOSがやらない方法だとカーネルに保持しておく」
typedef void (*sig_handler)(int sig, int code, struct sigcontext *scp,
char *addr);
(SunOSは上の4BSDでは3つの引数だったのにさらに引数が追加されている。間違
い。それはsigcontextの中にあるべきでコードは元のままのべきだ。
sigcontext 構造体は両方向(引数/返り値)のインターフェースとして定義され
るべきで(返りだけに使うのではなく)、そしてその受け渡し内容は本質的に機
種依存だ。それは(struct sigcontextという形で)カプセルした方がいいだろう。
不幸にもSunOSがしたようにやるなら、新しい問題がでてくる。テーブルを変更
し、__kernel_sigactionを呼ぶまでの間にもシグナルは配送され得る。例えば
もし前のハンドラが通常のスタックで走って、新しいのがシグナルスタックで
走ったら、新しいハンドラを通常のスタっクで呼ぶことになるだろう。
俺 「ここよくわからない。シグナルハンドラが同時に別のスタックで走
俺 るような実装なの??? カーネルで設定したシグナルフレームだけだと
俺 思うのだけど...」
この問題に対するSunOSの解決は、状況を変更する時はsigblockとsigsetmaskを
使って、シグナルの配送をブロックすることにした。これは、あいにくにも一
回の変更に3回のシステムコールを必要となる。(SunOSは実際には4つのシステ
ムコールを作った。知る限り、良い理由はない)
俺 「これが4BSDだとどうなるか。シグナルトランポリンは固定アドレス
俺 にあるからsigactionから渡す必要はない。そこでトランポリンコード
俺 ではなくシグナルハンドラを渡せばいい。カーネルに渡った時点でロッ
俺 クが効くのでこうする必要がないとういうことだろうか?」
なのでこう書かないといけない。
#ifdef __GNUC__
#define dead volatile
#else
#define dead /*empty*/
#endif
extern dead void sigtramp(int, int, struct sigcontext *, char *);
typedef void (*sig_handler)(int, int, struct sigcontext *, char *);
sig_handler __sigtable[NSIG];
/* libc sigaction(), probably correct, but untested */
int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
int ret, saverr;
sigmask_t omask;
sig_handler ofun;
struct sigaction ra;
/* verify arguments, as much as possible */
if (sig < 1 || sig >= NSIG) {
errno = EINVAL;
return (-1);
}
/*
* If we are going to ignore or default the signal, we
* do not need to change the table. A series of calls
* that set SIG_IGN, followed by nothing further ever,
* is a frequent special case.
*
* If the previous catcher is __sigtramp, the table contents
* must be valid (by definition).
*/
if (act->sa_handler == SIG_DFL || act->sa_handler == SIG_IGN) {
ret = __kernel_sigaction(sig, act, oact);
if (ret >= 0 && oact->sa_handler == __sigtramp)
oact->sa_handler = __sigtable[sig];
return (ret);
}
/*
* We are going to set a real catcher, so we must block
* occurrences of this signal while we change the table.
* Set ra.ra_mask and ra.ra_flags before blocking, in case
* either access faults.
*/
ra.ra_mask = act->sa_mask;
ra.ra_flags = act->sa_flags;
omask = sigblock(sigmask(sig));
ofun = __sigtable[sig];
__sigtable[sig] = act->sa_handler;
ra.ra_handler = __sigtramp;
ret = __kernel_sigaction(sig, &ra, oact);
if (ret >= 0) {
(void) sigsetmask(omask);
if (oact->sa_handler == __sigtramp)
oact->sa_handler = ofun;
return (ret);
}
/*
* The change failed: restore the table, unblock, and return.
*/
saverr = errno;
__sigtable[sig] = ofun;
(void) sigsetmask(omask);
errno = saverr;
return (ret);
}
この3つの中の一つを推薦する。
a) カーネルのインターフェースを関数を含むように変更する。
b) どこに__sigtrampがあるかをカーネルに教えるコールを追加し、それ以外
のカーネルインターフェースはそのままに。(main()を呼ぶ前に、新しいシス
テムコールを呼ぶようにする)
c) 何らかの方法で(カーネルに)わかるアドレスにsigtrampを隠す。
俺 a)はシグナルトランポリンとシグナルハンドラを両方渡す (OSF MIPS)
俺 b)はシグナルトランポリンのコードを教えるシステムコールだけを追加。
俺 c)例えばスタックのてっぺん固定でシグナルトランポリンを配置(4BSD)
俺 d)SunOSはインターフェースをそのままにトランポリンコードを渡すよ
俺 うにして、ハンドラのテーブルをカーネルじゃなくユーザ空間にした
俺 ので余計なロックが必要になってしまった。
BSD SPARCカーネルでは、HP-BSDで使われている(c)の方法を採用した。exec()
で、トランポリンコードをユーザスタックに設定する。その場所はargvと環境
変数のスタックの上だ。あいにくSunOSとの互換性のために、その場しのぎのコー
ドをカーネルの機種非依存の部分に追加しないといけなかった。
そしてこれらの様々な解決法の中からOSFはどれを選んだ?
俺 a)
----------------------------------------------------------------------
以下原文
----------------------------------------------------------------------
From torek@horse.ee.lbl.gov Wed Oct 30 22:52:54 1991
From: torek@horse.ee.lbl.gov (Chris Torek)
Newsgroups: comp.unix.internals
Subject: Re: signal trampoline code
Date: 31 Oct 91 03:28:09 GMT
Reply-To: torek@horse.ee.lbl.gov (Chris Torek)
Organization: Lawrence Berkeley Laboratory, Berkeley
X-Local-Date: Wed, 30 Oct 91 19:28:09 PST
In article <1828@rust.zso.dec.com> schmitz@rust.zso.dec.com
(Michael Schmitz) writes:
>I have noticed that in the OSF MIPS and Multimax signal delivery
>code that the signal trapoline code is not in the u area, but in
>the C library. When signal (or sigaction) is called, the address
>of the library's trampoline code is passed as another system call
>argument which is saved by the kernel. Things are pretty much as
>in BSD -- the user's PC is set to this address to catch the signal.
>
>The Question: Why does BSD (for the VAX) put the trampoline code
>in the u area? That is machine dependent and obviously unnecessary.
It is true that it is machine dependent and unnecessary. There is
still a good reason for it, though.
There are two ways to save the user's process context during signal
delivery, and restore it afterward: a) have the kernel do it; b) have
the user process do it. The advantage of approach (a) is that there
need be no special user code. The disadvantage is that, since the
kernel is privileged, it tends to be difficult to do (a) correctly.
The difficulty level is machine-dependent, but has always been high
enough to favor approach (b).
The code a user process must use to save and restore its own context
generally looks like this:
- save some machine registers on the correct stack;
- call the signal handler with several parameters;
- reload the registers from the stack;
- call the `sigreturn' system call.
The sigreturn system call also takes some parameters, including the
address(es) to which to return and any registers that could not be
saved and restored in user code because they are needed for the
sigreturn call itself. On the VAX, for instance, the sigreturn system
call uses the current stack pointer, so the original stack pointer must
be in the arguments to sigreturn. These parameters must clearly be set
up by the kernel. If they are stored in memory (and they are), the
kernel thus needs to know where to store them, i.e., the address of the
`signal stack' and a flag to tell whether the process is already on
that stack. (Normally, signals are handled on the single `normal'
stack, but this is a burden to some runtime systems, so 4BSD provides
an alternative signal stack, rather like the VAX's hardware interrupt
stack. Since this is done in software, it is not machine-dependent,
beyond the assumption that one or more stacks exist.) The correct
stack is thus normally set up by the kernel. Thus, the approach
actually used is a mix between (a) and (b): the kernel sets up a
minimal amount of state, then `bounces off' the user `trampoline' code,
so that the signal handlers themselves can be ordinary C functions.
In order to reach the user's signal trampoline code, the kernel must
know where it resides. If the address is fixed by the kernel, this is
easy. This is the approach taken on the VAX: the trampoline code is in
the u. area, which is readable and executable by user code, and which
has a fixed address. Thus, the kernel holds the address of the actual
user signal handler for each signal, and when delivering a signal,
calls this `sigtramp' code, passing the address of the user function
and the arguments to hand to it. In effect, we have:
/* in u. area: */
dead void sigtramp(void (*f)(int, int, struct sigcontext *),
int sig, int code, struct sigcontext *scp) {
(*f)(sig, code, scp);
sigreturn(scp);
/* if we get here, the user broke *scp; kill the process */
asm("halt");
/* NOTREACHED */
}
(the BSD VAX kernel actually uses a `callg' instruction to call the
function, saving a few stack operations).
Now suppose that we do not have a readable u. area, as is the case on
the BSD SPARC kernel. Here we can use the approach described for the
OSF MIPS and Multimax kernels. Calls to signal() (actually sigaction)
pass to the kernel the address of the sigtramp() code, rather than the
address of the user's handler. That is, sigaction is implemented
something like:
/* libc sigaction(): massage the parameters and call the
real kernel sigaction(). */
int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
int ret;
struct sigaction realaction, oldrealaction;
extern void __sigtramp(<some arguments>);
realaction.sa_handler = __sigtramp;
<set up the rest of realaction based on *act>;
ret = __kernel_sigaction(sig, &realaction, &oldrealaction);
<check for errors>;
<translate __sigtramp to user functions for returns>;
return (ret);
}
where __sigtramp() is similar to the one found in the u. area on the VAX.
Now we have a problem: somehow, we have to go from the call to sigtramp()
to the actual user's function. The kernel no longer has this information;
the kernel has only the address of __sigtramp().
There are two ways to handle this. We can either change the kernel
interface to provide the `sigtramp address' and the `handler address',
or we can do what SunOS does: keep a table in the user process. That
is, in sigaction() above, before calling __kernel_sigaction, we have
to record the user's handler in a table per signal, something like
(as in SunOS):
typedef void (*sig_handler)(int sig, int code,
struct sigcontext *scp, char *addr);
(Note that SunOS has here added an additional parameter beyond the
three found in 4BSD: a mistake; it should be in the sigcontext, as
should the code have been originally. The sigcontext structure should
have been defined as the interface in both directions, rather than
just in the return direction, as both parts are inherently machine
dependent and it is wise to encapsulate machine dependencies.)
Unfortunately, if we do what SunOS does, we have a new problem. The
signal could be delivered between the time we change the table and the
time we call __kernel_sigaction. If, for instance, the old handler ran
on the regular stack, but the new one runs on the signal stack, we will
then call the new handler on the regular stack. The SunOS solution to
this problem is to use sigblock and sigsetmask to block delivery while
the status is changing. This, unfortunately, requires three system
calls per change. (SunOS actually makes four system calls, for, as far
as I know, no good reason.) That is, we must write:
#ifdef __GNUC__
#define dead volatile
#else
#define dead /*empty*/
#endif
extern dead void sigtramp(int, int, struct sigcontext *, char *);
typedef void (*sig_handler)(int, int, struct sigcontext *, char *);
sig_handler __sigtable[NSIG];
/* libc sigaction(), probably correct, but untested */
int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
int ret, saverr;
sigmask_t omask;
sig_handler ofun;
struct sigaction ra;
/* verify arguments, as much as possible */
if (sig < 1 || sig >= NSIG) {
errno = EINVAL;
return (-1);
}
/*
* If we are going to ignore or default the signal, we
* do not need to change the table. A series of calls
* that set SIG_IGN, followed by nothing further ever,
* is a frequent special case.
*
* If the previous catcher is __sigtramp, the table contents
* must be valid (by definition).
*/
if (act->sa_handler == SIG_DFL || act->sa_handler == SIG_IGN) {
ret = __kernel_sigaction(sig, act, oact);
if (ret >= 0 && oact->sa_handler == __sigtramp)
oact->sa_handler = __sigtable[sig];
return (ret);
}
/*
* We are going to set a real catcher, so we must block
* occurrences of this signal while we change the table.
* Set ra.ra_mask and ra.ra_flags before blocking, in case
* either access faults.
*/
ra.ra_mask = act->sa_mask;
ra.ra_flags = act->sa_flags;
omask = sigblock(sigmask(sig));
ofun = __sigtable[sig];
__sigtable[sig] = act->sa_handler;
ra.ra_handler = __sigtramp;
ret = __kernel_sigaction(sig, &ra, oact);
if (ret >= 0) {
(void) sigsetmask(omask);
if (oact->sa_handler == __sigtramp)
oact->sa_handler = ofun;
return (ret);
}
/*
* The change failed: restore the table, unblock, and return.
*/
saverr = errno;
__sigtable[sig] = ofun;
(void) sigsetmask(omask);
errno = saverr;
return (ret);
}
This argues for one of three things:
a) changing the real kernel interface to include the function;
b) adding a call to tell the kernel where __sigtramp is, and otherwise
leaving the kernel interface alone (the C startup would make the new
system call before calling main());
c) `hiding' sigtramp at a known address, in some way.
In my BSD SPARC kernel, I took approach (c) from the HP-BSD kernel: in
exec(), we build the trampoline code on the user's stack, above the
argv and evironment strings. Unfortunately, for SunOS compatibility I
had to add kludges to otherwise machine-independent parts of the kernel.
Which, if any, of these various solutions have the OSF people chosen?
