QSAM
QSAM:Queued Sequential Access Method,待機順アクセス法
QSAMの特長
QSAMは順次データセットを、レコード(論理レコード)単位にアクセスするためのプログラミング・インタフェースです。特長はレコード単位であるため、プログラミングが容易であること、自動的なバッファリング制御により、高速なI/O処理が行われることです。
QSAMの他に、BSAM(Basic Sequential Access Method,基本順アクセス法)があります。こちらはレコード単位ではなく、ブロック単位のアクセスとなり、自動バッファリングは行われません。バッファリングの制御はプログラムが自分で行う必要があります。BSAMは途中のブロックを読み飛ばしたり、後ろへ戻ったり、と言った任意の位置からのI/Oを行ったり、I/O処理と他の処理をオーバーラップさせる、非同期I/O制御を行うなど、きめ細かなI/O制御ができますが、それらは一般のデータセットのアクセスではほとんど必要とされません。
QSAMはビギナー向けのEasyなI/O、と言うイメージがありますが、内部では効率のよいバッファリング制御がなされ、高速なI/O処理が可能です。READ処理の場合、データセットのOPENと同時に、5ブロック分が先読みされます。アプリケーションがGETマクロでレコードの読み込みを要求すると、バッファ内のブロックをデブロッキングして、論理レコードをプログラムに渡す(移動モード)か、レコードの先頭アドレスを通知します(位置づけモード)。QSAMは論理レコード単位でアクセスできるため、ブロッキングとデブロッキングと言う、本来のアプリケーションには必要のない処理を作る必要がなく、プログラムはその分、ビジネスロジックに専念できます。そして複雑なバッファリング制御を自ら行わなくとも、高速なI/O処理が行え、しかもI/O処理に使用するバッファの数は、JCLでも指定できるので、データ量に応じてバッファ数を容易に調整できます(省略時は5ブロック分)。現在では特別な理由がない限り、BSAMを選択する必要はないでしょう。COBOLなどのコンパイラー言語でも、順次データセットのアクセスにはQSAMが利用されています。
QSAMの機能
QSAMには次のI/O機能が提供されています。(抜粋)
- OPEN
- CLOSE
- GET
- PUT
- PUTX
- RELSE
- TRUNC
データセットをオープンする。バッファプールが用意されていなければ、I/O用のバッファプールを作成する。読み込みOPENであれば、バッファにブロックを先読みする。
データセットをクローズする。バッファ内に残っている未書き出しのブロックがあれば、それをデータセットに書き出す。
データセットから次の論理レコードを読む。実際にはバッファ内のブロックをデブロックして、次の論理レコードを取り出す。バッファが空になっていれば、続きのブロックを読み込み、次の論理レコードを取り出す。
データセットへ次の論理レコードを書く。実際にはバッファ内に論理レコードを並べてブロックキングを行うだけ。バッファが一杯になればデータセットにブロックとして書き出す。※PUTが完了しても、そこでバッファが一杯にならなければ、書き出したレコードはまだデータセットに書き出されていない。
論理レコードを更新する。更新処理の場合、PUTXに先だってGETを発行しておく。GETで読んだ論理レコードの内容を書き換えて、PUTXで書き戻す形になる。内容の変更はできるがレコードの長さは変えられない。なおPUTXにはこの他に出力モードというのがあり、入力と出力に異なるデータセットを指定することもできる。データセットの複写処理や、新たなレコードを追加するなどができる。
バッファ内の未処理レコードを破棄する。バッファとブロックは対応しているので、ブロック内の残りレコードを捨てると考えてよい。RELSEを発行すると、次のGETでは次のブロックの最初のレコードが通知される。
バッファ内の未出力レコードを、バッファが一杯になるのを待たずに書き出す。もうこれ以上PUTするレコードがなくなった時、TRUNCを出すことで、ブロック長に満たない、ショートブロックとして書き出すことができる。ただしTRUNCを発行しなくてもCLOSEマクロを出せば、CLOSEルーチンの中でTRUNC処理が行われるので、特別な理由がなければ、直接TRUNCを使う必要はない。
バッファプール
QSAMはバッファを使用して、複数ブロックをまとめてI/Oするので、効率がよいアクセス法です。バッファ数はプログラムまたはJCLで変更できます。プログラムで指定する場合は、DCBマクロにBUFNOパラメーターを指定します。JCLで指定する場合は、DD文にDCB=BUFNOパラメーターを指定します。どちらにも指定されない場合は、5が内部で採用されます。なおプログラムでDCBに指定してしまうと、JCLのDD文に指定しても有効にならないので、特別な事情がない限り、DCBマクロでは指定しない方がよいと考えます。
実際どの程度のバッファ数が、パフォーマンスに効果があるかは一概には言えないのですが、昔、テストプログラムで実測したことがあって、その結果では、バッファ数は5?10ぐらいまでが、バッファ数1に比べて、メモリー量も程よく、CPU使用量も減り、ELAPも短縮される、となっていました。それ以上は、例えば100個用意しても、CPU量もELAPもさほどの減少効果はなく、メモリー量だけはバッファ数分しっかり増える、となっていました。つまりメモリー量に見合うほど、CPUとELAPは減らず(メモリーが100倍でもCPUが1/100になるわけではない)、あるところで頭打ちになるわけです。OSのデフォルトが5なのを考えても、妥当な数だと考えます。テストしたデータセットのブロック長は、23476バイトでしたが、ブロック長が比較的小さなデータセット(数千バイト以下)の場合は、もう少し増やしても(20?30ぐらいまで)いいかも知れません。
なお、MSPではQSAMのI/Oバッファは16MBの上の拡張域に作成できないので、ブロック長の大きなデータセットではむやみにバッファ数を増やしても、リージョンが圧迫されるだけです。MVSとVOS3であってもQSAMバッファを31ビット領域に作成するかどうかは、プログラム次第です。
プログラムが使用したバッファプールの解放は、基本的にQSAMルーチンではなく、QSAMを使用したアプリケーション・プログラムの責任でした。従来はCLOSEマクロの後に、同じDCBを再使用して、そのデータセットを再OPENしないのであれば、FREEPOOLマクロによってバッファを明示的に解放する必要がありました。オンライン処理(対話処理)などで、処理するデータセットを端末から入力させるような場合、処理の都度DCBをGETMAINし直し、OPENに使うようなプログラムでは、プログラムでの処理が繰り返されていくうちに、やがてリージョン不足になってしまうことになってしまいます。CLOSEはきちんと出すがFREEPOOLはしていない、と言うプログラマーも結構いるようです。バッチ処理などではあまり問題になりませんが、使った資源は解放するのが基本なので、CLOSEとFREEPOOLはペアーで覚えることを奨めます。
しかしFREEPOOLは不要な場合もあります。それはMVSでDCBEを使いQSAMのバッファを31ビット領域に確保させる場合です。QSAMが使ったバッファはCLOSEルーチンの中で解放されるため、FREEPOOLは必要ありません。また富士通のXSPもFREEPOOLはありません。MSPとXSPの両方をサポートするプログラムの場合は、QSAMそのもののマクロや使い方は似ていますが(DCBがFCBとなるが、基本は同じ)細かな点で相違点があることに注意します。日立のVOS3ではQSAMバッファをMVSと同じく31ビット領域に確保できますが、解放にはMVSと違ってFREEPOOLマクロが必要です。
書き込みデータの保証
QSAMは使いやすく、性能もよいアクセスメソッドですが、x37ABENDのリカバリーが難しい点に注意します。プログラムをRE-RUNすることで、データをリカバリーできるなら問題ありませんが、PUTした後、元のレコードが消えてしまい、RE-RUNで再作成もできない場合は、x37ABENDしないだけの十分な大きさを確保するか、BSAMを使うか、データセットをVSAMに変更できないかの検討も必要です。
QSAMではPUT要求の処理と実際のデータセットへの書き出し処理は連動しません。PUTの完了とはバッファ内にレコードデータが書き込まれるだけです。バッファが一杯になった後、次のPUTの時にI/Oが行われます。それは最初のPUTよりもはるかに後になってからかも知れません。x37ABEND(他のI/Oエラーも含め)は実際のI/Oが行われないと起きませんので、起きたときにはアプリケーションからはレコードデータが消えています。また必ずしもPUTで起きるわけでもありません。最終ブロックの書き込みであれば、CLOSEの延長で発生します。ABEND事象そのものはDCB ABEND出口やESTAEでトラップできますが、リカバリーはかなり面倒です。
具体的にどの程度の量のレコードがロストするかは、ブロッキングファクター×BUFNO数で求められます。例えば、RECFM=FB、BLKSIZE=800、LRECL=80なら、1ブロックに10レコード入るので、BUFNOが5なら50レコードがロストします。BUFNOが1なら10レコード、10なら100レコードのロストです。固定長レコードの場合は、BUFNO分のブロックデータをメモリーなどにSAVEすればリカバることはできるでしょう。BUFNOの実際の値はOPEN後のDCBを見ればわかります。しかし可変長レコードの場合、ブロッキングファクターは個々の論理レコード長に左右されブロック毎に変わります。正確なリカバリーを行うのは無理ではないにしてもかなり面倒です。
私はパフォーマンス上の理由があって、どうしてもQSAMを採用する必要があり、x37ABENDのリカバリー用にPUTしたレコードのコピーをデータ空間に持つ方法を使ったことがあります。その前に、QSAMバッファには書き出せなかったブロックが残っているから、再OPENした後にそれをTRUNCマクロでRE-WRITEできないかなど、いろいろ試したこともありました。メモリーだけ見るとレコードのデータが残っているし何とかできないかって。バッファも予めGETPOOLしたりも試したけど、結局DCB ABEND出口から戻った時には、プール内のバッファは返却されてしまっていた。そのためバッファの先頭に置かれているレコードの頭はバッファチェインのアドレスで壊されてる箇所もあって、上手く行かなかった記録が残ってます。何かやり方がないかあがいたのですが、結局見つけられませんでした。
いずれにしても、x37ABENDをABEND出口でトラップし、無視を要求して、PUTの直後から再開させても、そのPUTで書き込もうとしたレコードより前のレコードは、全部がデータセットに書き出されているわけではありません。復元できない重要なデータを、順次データセットや区分データセットのメンバーとして書き込む場合、このような特性は知っておくべきことです。
サンプルコード
いずれのサンプルも、データセットのコピーを行うQSAMプログラムです。MVS、MSP、VOS3に共通な24ビットモードのQSAMで、マクロの使用に関しては同じです。ただしMVSのHLASM前提のコーディングなので、MSPとVOS3に関しては必要に応じて修正してください。SYSUT1からSYSUT2のDCBにRECFM、BLKSZ、LRECLをコピーするためのMVC命令の箇所です。
なお31ビットモードのQSAMプログラミングについてはこちらのページ「DFP・31ビットモード・プロセッシング」でも解説しています。
UT1 USING IHADCB,SYSUT1 ADDRESS FOR SYSUT1 DCB UT2 USING IHADCB,SYSUT2 ADDRESS FOR SYSUT2 DCB OPEN (SYSUT1,INPUT) OPEN INPUT DATASET(ORIGIN) MVC UT2.DCBRECFM,UT1.DCBRECFM COPY RECFM TO SYSUT2 MVC UT2.DCBBLKSI,UT1.DCBBLKSI COPY BLKSZ TO SYSUT2 MVC UT2.DCBLRECL,UT1.DCBLRECL COPY LRECL TO SYSUT2 OPEN (SYSUT2,OUTPUT) OPEN OUTPUT DATASET(DEST) LOOP DS 0H GET SYSUT1 GET NEXT RECORD LR R0,R1 LOAD LOGOCAL RECORD ADDRESS PUT SYSUT2,(0) PUT NEXT RECORD B LOOP LOOP FOR NEXT RECORD EODAD DS 0H CLOSE (SYSUT1,,SYSUT2) CLOSE USED DATASET FREEPOOL SYSUT1 FREE QSAM I/O BUFFER FREEPOOL SYSUT2 FREE QSAM I/O BUFFER B EXITPROC PROCESSING DONE SPACE , SYSUT1 DCB DDNAME=SYSUT1,MACRF=GL,DSORG=PS,EODAD=EODAD SYSUT2 DCB DDNAME=SYSUT2,MACRF=PM,DSORG=PS
UT1 USING IHADCB,SYSUT1 ADDRESS FOR SYSUT1 DCB UT2 USING IHADCB,SYSUT2 ADDRESS FOR SYSUT2 DCB OPEN (SYSUT1,INPUT) OPEN INPUT DATASET(ORIGIN) MVC UT2.DCBRECFM,UT1.DCBRECFM COPY RECFM TO SYSUT2 MVC UT2.DCBLRECL,UT1.DCBLRECL COPY LRECL TO SYSUT2 BLKSIZEのコピーはしない。RECFMとLRECLは変えないがBLKSIZEはコピー先 データセットの装置タイプに合わせて最適化される。(MVSのみ) OPEN (SYSUT2,OUTPUT) OPEN OUTPUT DATASET(DEST) LOOP DS 0H GET SYSUT1 GET NEXT RECORD LTR R0,R1 EOF ? BZ EODPROC YES, DO CLOSE DATASET PUT SYSUT2,(0) PUT NEXT RECORD B LOOP LOOP FOR NEXT RECORD EODPROC DS 0H CLOSE (SYSUT1,,SYSUT2) CLOSE USED DATASET FREEPOOL SYSUT1 FREE QSAM I/O BUFFER FREEPOOL SYSUT2 FREE QSAM I/O BUFFER B EXITPROC PROCESSING DONE SPACE , EODAD DS 0H SLR R1,R1 INDICATE DATASET IS EOD BR R14 RETURN TO MAIN LINE GLモードならEODADルーチンはこのように汎用化できる。 複数のDCBを使うプログラムなどでは便利な方法。 GR14の示すアドレスは、GETマクロの次のアドレスを指している。 SPACE , SYSUT1 DCB DDNAME=SYSUT1,MACRF=GL,DSORG=PS,EODAD=EODAD SYSUT2 DCB DDNAME=SYSUT2,MACRF=PM,DSORG=PS
UT1 USING IHADCB,SYSUT1 ADDRESS FOR SYSUT1 DCB UT2 USING IHADCB,SYSUT2 ADDRESS FOR SYSUT2 DCB OPEN (SYSUT1,INPUT) OPEN INPUT DATASET(ORIGIN) MVC UT2.DCBRECFM,UT1.DCBRECFM COPY RECFM TO SYSUT2 MVC UT2.DCBBLKSI,UT1.DCBBLKSI COPY BLKSZ TO SYSUT2 MVC UT2.DCBLRECL,UT1.DCBLRECL COPY LRECL TO SYSUT2 OPEN (SYSUT2,OUTPUT) OPEN OUTPUT DATASET(DEST) LOOP DS 0H GET SYSUT1 GET NEXT RECORD PUTX SYSUT2,SYSUT1 PUT NEXT RECORD PUTではなくPUTXの出力モードを使用した例。書き込むレコードアドレスは コピー元のDCBから得られるようになっている。 B LOOP LOOP FOR NEXT RECORD EODAD DS 0H CLOSE (SYSUT1,,SYSUT2) CLOSE USED DATASET FREEPOOL SYSUT1 FREE QSAM I/O BUFFER FREEPOOL SYSUT2 FREE QSAM I/O BUFFER B EXITPROC PROCESSING DONE SPACE , SYSUT1 DCB DDNAME=SYSUT1,MACRF=GL,DSORG=PS,EODAD=EODAD SYSUT2 DCB DDNAME=SYSUT2,MACRF=PM,DSORG=PS