システムリソースの逐次化
1 2 3 4 5 6 7 8 9 10 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- SETLOCK OBTAIN, OBTAIN LOCAL LOCK + TYPE=LOCAL,MODE=UNCOND,REGS=USE : : ロックが必要な処理をこの間に行う… : : SETLOCK RELEASE, RELEASE LOCAL LOCK + TYPE=LOCAL,REGS=USE |
MVSには、制御表のキューの操作や数ビットあるいは数ワードに対して連続的な変更をしている間、それらのメモリー領域が逐次化されることを必要とする処理が随所にあります。これらの排他制御を行うのがロック機能です。SETLOCKマクロによって、ロック機能による資源の獲得と解放を行うことができます。ロック機能では、ENQ/DEQと異なり資源に自由な名前を付けることはできず、OSによってロックの対象となる資源が決められています。
サンプルで示したのは、ローカル・ロックと呼ばれるアドレス空間に固有の資源を逐次化する際に用いられるロックです。一般のプログラムがロックを使うことはありませんが、MVS自身は逆にENQ/DEQではなくロック機能を使ってコントロール・ブロックの変更やチェインの追加・切り離しなどの制御に必要な逐次化を行っています。OS以外のサブシステムやミドルウェア製品では、直接の排他制御目的とは別に、GETMAIN(BRANCH=YES)、POST(BRANCH=YES)、CHANGKEYなどの1部のAPIで、呼び出し時の環境要件としてローカル・ロックの保持を求められる場合がありそのために使うことがあります。これらは、SRBルーチン等のSVC命令の発行が許されないような状況でOSのサービスを受けるために使用する特別なものです。
ロックを使えば複数の不連続なメモリー領域(例えば2000番地からの1ワード、3000番地からの2ワード、4000番地の1ビット等)を続けて変更するような時、全てのフィールドの変更が終了するまでそれらのメモリー領域の変更を逐次化することができます。なお、1ワードあるいは連続した2ワードの領域の内容を変更するだけならCPU命令によって逐次化することができます。1バイトの領域であっても前後のバイトを含めて1ワードの領域として逐次化することができます。そのような場合はあえてロック機能を使う必要はありません。
フルワード・カウンターの更新
1 2 3 4 5 6 7 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- L R1,CTRWORD LOAD CURRENT COUNTER FIELD TRYAGAIN DS 0H LA R0,1(,R1) INCREMENT COUNTER CS R1,R0,CTRWORD TRY TO UPDATE COUNTER FIELD BNE TRYAGAIN IF CONTENTION, TRY AGAIN : |
CS命令は、第1オペランドで指定したレジスターの内容と第2オペランドで指定したフルワード領域の内容を比較して、一致していれば第3オペランド(r3レジスター)で指定したレジスターの内容を第2オペランドで指定したフルワード領域に書き込みます。一致していなければ主記憶の内容を第1オペランドで指定したレジスターに再ロードします。主記憶の内容は変更されません。実行結果は条件コードで判定できます。
CS命令は、参照する主記憶域に特別な操作を行います。それが逐次化(Serialization)あるいはインターロック(Interlock)と呼ばれる動作です。CS命令は、主記憶からデータを読み出して、比較し、書き込むという一連の動作を行います。読み出しから格納までの間、他のCPUから同じ主記憶アドレスへの参照は禁止されます。この機能が無いと、マルチCPU環境では読み出してから格納するまでの一瞬の間に同じ領域が書き換えられる可能性があります。1つの同じ命令ですら読み出しから格納までには他のCPU動作が入り込む隙があるのですから、L命令とST命令の2つで同じ事をやったら壊れてしまうタイミングはもっと増えてしまいます。
システム系プログラムでは、複数の実行単位(マルチタスクはもちろん、シングルタスクであってもAPIの非同期出口ルーチン機能を使う場合は複数の実行単位になる)で構成されるプログラムで同じ領域を更新する場合は、プログラムを動かすプロセッサーがマルチCPUかどうかに関係なくCS命令を使用してフィールドを更新することをまず考えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- 【最初の例】 L R1,CTRWORD LOAD CURRENT COUNTER FIELD LA R1,1(,R1) INCREMENT COUNTER ST R1,CTRWORD UPDATE COUNTER FIELD : : 【2番目の例】 TRYAGAIN DS 0H L R1,CTRWORD LOAD CURRENT COUNTER FIELD LA R0,1(,R1) INCREMENT COUNTER C R1,CTRWORD UPDATED BY OTHER TASK ? BNE TRYAGAIN YES, TRY AGAIN ST R0,CTRWORD UPDATE COUNTER FIELD : : |
最初の例は、CS命令を使わない方法です。CTRWORDフィールドを変更するプログラムが、1つのタスクでのみ動くなら問題ありません。しかし、同じプログラムが複数のタスクで動く場合はCTRWORDフィールドは競合します。命令のリファレンスには、逐次化機能は2つ以上のCPU間で主記憶領域を排他制御するための仕組みと解説されていますから、CPUが1つなら大丈夫なのか?と思えますが、例示のロジックの場合、CPUの数は関係ありません。
シングルCPUのプロセッサーでも、タスクAがLA命令で値を更新してからST命令を実行しようとしたタイミングでI/O割込みなどが発生すると、ST命令の実行直前でプログラムは中断されます。割込み処理が終わればプログラムの実行は再開されますが、必ずしも次にまたタスクAがディスパッチされる保証はありません。同じプログラムを動かす別のタスクBにディスパッチが移るかも知れません。そうなるとCTRWORDはタスクBによって更新されてしまいます。その後にタスクAが再開されるとST命令によって上書きされますが、その内容にはタスクBで更新した値が反映されていません。これは、CPU間の主記憶の競合の問題ではなく、排他を掛けずに主記憶フィールドを読み出して、別の命令で書き込むという分割した動作でやっているからです。
少し工夫して2番目の例のように改良できますが、BNE命令とST命令の間に割込みが入れば同じことです。CS命令は、このような主記憶域の更新動作を排他制御付きでやってくれる命令です。最初はわかりにくいかも知れませんが、カウンター更新のサンプルを見てその動きをぜひ覚えて下さい。SETLOCKなどを使うことはまずないと思いますが、競合するフルワード領域のCS命令による更新は制御プログラムでは多用されます。
1 2 3 4 5 6 7 8 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- 【3番目の例】 L R1,CTRWORD LOAD CURRENT COUNTER FIELD TRYAGAIN DS 0H LA R0,1(,R1) INCREMENT COUNTER CS R1,R0,CTRWORD TRY TO UPDATE COUNTER FIELD BNE TRYAGAIN IF CONTENTION, TRY AGAIN : |
CS命令では、更新に失敗すると(他のタスクで更新された)、第1オペランドレジスターに最新の主記憶内容が読み込まれます。これはCS命令の大切な特性です。2番目の例のように自ら記憶域内容を読み直す必要はありません。読み直しても間違いではありませんが余計なことです。失敗したCS命令によって読まれた最新の値に+1することで正しくカウンターは更新できます。
もう1つのCDS命令は、CS命令と同じ動作をしますが対象領域がダブルワードに拡張されたものです。なお、LoadやSTore命令と異なり第2オペランドで指定する主記憶アドレスは、CS命令ならワード境界、CDS命令ならダブルワード境界になっていなけれ指定例外でABENDします。
トランザクションのキューイング
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- USING TRXBLOCK,R7 ADDRESS TO OUR TRANSACTION BLK LR R7,R1 GR7 --> LASTEST TRANSACTION BLK : L R1,QUEUEPTR LOAD LAST TRANSACTION CHAIN TRYAGAIN DS 0H ST R1,NEXTTRX CHAIN IT TO LATEST TRANSACTION CS R1,R7,QUEUEPTR TRY TO ADD CHAIN BNE TRYAGAIN IF CONTENTION, TRY AGAIN : : QUEUEPTR DC A(0) POINTER TO TRANSACTION QUEUE : : TRXBLOCK DSECT , TRANSACTION STRUCTURE NEXTTRX DC A(0) NEXT TRXBLOCK POINTER TRXNAME DC CL8' ' TRANSACTION NAME TRXDATA DC XL16'00' TRANSACTION PARMS : LRXBLOCK EQU *-TRXBLOCK (LENGTH OF CONTROL BLOCK) |
1 2 3 4 5 6 7 8 9 10 11 12 |
QUEUEPTR TRX2 TRX1 +-------------+ +-------------+ +-------------+ | | ----------------> | NEXTTRX | --> | NEXTTRX | +-------------+ * +-------------+ +-------------+ | | | | | | +-------------+ +-------------+ | R7 +-------------+ | NEXTTRX | +-------------+ | | +-------------+ |
トランザクション・キューイング(チェイニング)のサンプルです。QUEUEPTRフィールドは、最後に発生したトランザクションを示す制御表のアドレスを持っています。そこからポイントされる制御表(TRXBLOCK)のNEXTTRXフィールドは、さらにその1つ前のトランザクションをポイントします。最初に発生したトランザクションのNEXTTRXは、それより古いのがないので0になっています。
上の図に示すように、最新のトランザクションをQUEUEPTRに繋げて、そのNEXTTRXフィールドにそれまでの最新であるTRX2のアドレスが入るようにします。このようなデータ構造は「連結リスト」などと呼ばれます(ここではアルゴリズムのことは解説しません)。MVSでは、TCB~RBチェインなどOS自身もこのような構造でコントロール・ブロックを連結しているものが沢山あります。
サンプルでは、QUEUEPTRはプログラム内に持っていますが、OSのプログラムではこのようなフィールドはプログラムの外に置かれて、複数のタスクで参照されます。トランザクションをキューイングする各々のタスクがQUEUEPTRフィールドにTRXBLOCKを繋げようとしますから競合します。CS命令を使えば、このような簡単なロジックでロストすることなく確実にチェインさせることができます。取り出すときはFIFOであればQUEUEPTRからリンクをたどり、NEXTTRXが0になっているチェインの最後のTRXBLOCK(TRX1)を取り出します。この時、1つ前のTRXBLOCK(TRX2)にTRX1のNEXTTRXの内容(実際は0)を格納します。取り出し時も、1つしかTRXBLOCKが繋がっていなければQUEUEPTRが、2つ以上繋がっていればTRXBLOCKが競合しますから、やはり更新にはCS命令を使う必要があります。
ビットフラグの排他制御付き更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- OI CNTLFLG2,FLAG1 INDICATE FLAG1 NI CNTLFLG2,255-FLAG1 RESET FLAG1 : : OIL CNTLFLG2,FLAG1, INDICATE FLAG1 WITH INTERLOCK + REF=CNTLFLD,WREGS=(,,14) NIL CNTLFLG2,255-FLAG1, RESET FLAG1 WITH INTERLOCK + REF=CNTLFLD,WREGS=(,,14) : : CNTLFLD DS 0F CONTROL FIELD CNTLFLG1 DC AL1(0) CONTROL FLAGS-1 CNTLFLG2 DC AL1(0) CONTROL FLAGS-2 FLAG1 EQU X'80' FLAG2 EQU X'40' CNTLFLG3 DC AL1(0) CONTROL FLAGS-3 CNTLFLG4 DC AL1(0) CONTROL FLAGS-4 |
フラグ・ビットのON/OFFにはOIおよびNI命令を使いますが、マルチタスクで同じフラグ・バイト内のビットを扱うとやはり競合が発生します。ビットを1にするタスクとビットを0にするタスクが異なる場合、何も考えずにOI命令とNI命令を使うと手痛い目に遭うことがあります。そういうプログラムを作ってしまい、市場に出してしまってから泣きそうな苦労をしていたエンジニアを見たことがあります。ほとんどのケースでは上手く行くが、タイミングによって問題が浮かび上がるという典型的な例でもあります。一番いいのは競合するようなフラグを使わずに済むデザイン、使ったとしてもマルチタスクで突き合うようなことをしないで済むデザインを考えることですが、どうしてもフラグで制御したい場合はOI/NI命令の代わりにOIL/NILマクロを利用できます。
上記のサンプルは、いずれもCNTLFLG2領域にFLAG2ビットをON/OFFする例です。OIL/NILマクロを使うと、対象ビットのON/OFFに加え、前後のバイトを含めたワード単位でCS命令によって更新が掛かります。作業用に3つのレジスターを必要としますが、それらはWREGSパラメーターで指定します。省略時は、GR0~2の3つのレジスターが使われます。この例では、レジスター2に代えてレジスター14を使うようにしています。WREGS=(,,14)は、WREGS=(0,1,14)と同じです。OILとNILマクロは、APF許可プログラム用のアセンブラー・サービス(API)マニュアルに記載されていますが、実際のAPF許可は不要です。MVS3.8のSYS1.MACLIBにも入っており、30年以上も使われている伝統的なものでもあります。CS命令の応用的な使い方の実例でもありますので、マクロの展開形を見ると勉強になります。
MVSで動くプログラムであれば、逐次化の方法には3種類あることを知って下さい。最も汎用的なのがENQ/DEQマクロによる方法で、どんなものに対しても排他制御を掛けることができます。ただし、オーバーヘッドは最も大きく、頻繁に掛け合う排他制御に使うとCPU消費量は大きくなります。オーバーヘッドを避けるにはSETLOCKマクロによるロックを使う方法もありますが、本来はMVSがOSとしての制御に使うものでアプリケーションが自分の排他制御に使うものではありません。知識として知っておけばいいでしょう。SETLOCKを必要とするようなプログラムをきちんと作れるようになったら、システム系の制御プログラマーとしては上級レベルです。(SETLOCKマクロが使えればと言う意味ではありません…)
CS(CDS)命令によるメモリー領域の逐次化の方法は、制御プログラミングの基本でもあってメインフレームの仕組みでは最もオーバーヘッドの少ない排他制御です。メモリー領域の更新であれば、ENQ/DEQサービスを使う前にCS/CDS命令を組み合わせたロジックで排他制御が実現できないかをまず考えましょう。連続しない複数のフィールドを一連の処理として更新する場合は、単純に機械命令による逐次化はできませんが、逆に、命令による逐次化が可能なデータ構造にできないかを考えることも重要です。
IBM社のz/Architecture解説書の付録「数の表現と命令の使用例」に「マルチプログラミングとマルチプロセッシングの例」が記載されています。ここに書かれているCS/CDS命令の解説やそれを使った各種のサンプルは、そのまま実戦のプログラミングに流用できる非常に優れたものです。命令の使い方を覚えたらぜひ一度は読むことを勧めます。