Magnetic Tape Data Storage. Part 1: Tape Drive - IO Commands






4.63/5 (7 votes)
This article describes the simple way to implement Read/Write operation on tape device
Introduction
This article describes the simple way to implement Read/Write operation on tape device. Attached zip file contains TapeOperator
class that exposes Load
, Read
, Write
and Close
methods and BlockSize
property. All these can be used as part of backup utility.
Step 1 - Configuration
Congratulations, you've bought a tape device and connected it to your PC. Now you can implement simple backup utility in C#. Actually there are three ways to work with your tape:
- You can operate it via driver
- You can work with it via some management software (if it's provided by the device manufacturer)
- And the last, simplest way to do it is to operate it via OS handle. This is our case; I'll show to you how you can operate it via OS handle
It is well known that each external device is interpreted by OS as a file, it's true for tape devices as well. The only thing that you have to find out before we start is the appropriate device file name. Open the device manager Window of your PC and you'll see your tape device under the Tape drives node (see picture 1 below).
Perform mouse right click on your tape device node and choose Tape Symbolic Name tab. In this tab you can see tape symbolic name, Tape0 in our example (see Picture 2). This name can be used to create a handle for your device.
So the appropriate file name for your device is @file://./Tape0.
Step 2 - Implementation
TapeOperator
methods description:
Load
: gets as parameter name of your tape device ( "\\.\Tape0"). It creates a handle to the device byCreateFile
and performsPrepareTape
which executes certain procedures to prepare the given tape for I/O, both methods are part of Win32 API and therefore INTEROP is used to invoke it. Now you have the OS handle for your tape device, and it can be interpreted like a regular file handle, I mean you can openSystem.IO.FileStream
. The only difference is that you can't seek openedFileStream
, instead useSetTapePosition
Win32 API (see read and write operations)./// <summary> /// Loads tape with given name. /// </summary> public void Load( string tapeName ) { // Try to open the file. m_handleValue = CreateFile( tapeName, GENERIC_READ | GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE | FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero ); if ( m_handleValue.IsInvalid ) { throw new TapeOperatorWin32Exception( "CreateFile", Marshal.GetLastWin32Error() ); } // Load the tape int result = PrepareTape( m_handleValue, TAPE_LOAD, TRUE ); if ( result != NO_ERROR ) { throw new TapeOperatorWin32Exception( "PrepareTape", Marshal.GetLastWin32Error() ); } m_stream = new FileStream( m_handleValue, FileAccess.ReadWrite, 65536, false ); }
Write
: gets the start position and byte array to write. As it was shown, now you can access your tape device via the openedFileStream
. Just invokeFileStrem.Write
andFileSream.Flush
methods. One thing you have to remember, all I/O operations on tape must be done in multiplies of block size. Each device has min, default and max block sizes (in my example I've used default block size - 65536), these values can be fetched byGetTapeParameters
method (Win32 API), for more information, seeBlockSize
property implementation./// <summary> /// Writes to the tape given stream starting from given position /// </summary> /// <param name="startPos"></param> /// <param name="stream"></param> public void Write( long startPos, byte[] stream ) { // Get number of blocks that will be needed to perform write uint numberOfBlocks = GetBlocksNumber( stream.Length ); // Updates tape's current position SetTapePosition( startPos ); byte[] arrayToWrite = new byte[ numberOfBlocks * BlockSize ]; Array.Copy( stream, arrayToWrite, stream.Length ); // Write data to the device m_stream.Write( stream, 0, stream.Length ); m_stream.Flush(); }
Read
: gets the start positions and number of bytes to read.*By the way, pay attention on
SetTapePosition
(Win32 API) last parameter, it's type isBOOL
in WIN32 definition. Don't pass .NET Boolean type forBOOL
andBOOLEAN
of WIN32. The size of .NETBoolean
is 2 bytes, the size ofBOOL
is 4 bytes and size ofBOOLEAN
is 1 byte, therefore each Win32 API method withBOOL
orBO<code>
OLEAN parameter will return error for .NETBoolean
pass attempt./// <summary> /// Read one logical block from tape /// starting on the given position /// </summary> /// <returns></returns> public byte[] Read( long startPosition ) { byte[] buffer = new byte[ BlockSize ]; SetTapePosition( startPosition ); m_stream.Read( buffer, 0, buffer.Length ); m_stream.Flush(); return buffer; }
Close
: closes the stream and releases unmanaged resources. You can add to this code call toLoadTape
withTAPE_UNLOAD
parameter to eject tape from drive./// <summary> /// Closes handler of the current tape /// </summary> public void Close() { if ( m_handleValue != null && !m_handleValue.IsInvalid && !m_handleValue.IsClosed ) { m_handleValue.Close(); } }
BlockSize
: This property returns the tape's default block size by invocation ofGetTapeParameters
method. One of theGetTapeParameters
is a reference to the structure that is filled by Win32 method. SoMarshal
class is used to allocate unmanaged memory and copy between managed and unmanaged structures. See the source code below:/// <summary> /// Returns default block size for current /// device /// </summary> public uint BlockSize { get { IntPtr ptr = IntPtr.Zero; try { if ( !m_driveInfo.HasValue ) { m_driveInfo = new DriveInfo(); // Allocate unmanaged memory int size = Marshal.SizeOf( m_driveInfo ); ptr = Marshal.AllocHGlobal( size ); Marshal.StructureToPtr( m_driveInfo, ptr, false ); int result = 0; if ( ( result = GetTapeParameters( m_handleValue, DRIVE_PARAMS, ref size, ptr ) ) != NO_ERROR ) { throw new TapeOperatorWin32Exception( "GetTapeParameters", Marshal.GetLastWin32Error() ); } // Get managed media Info m_driveInfo = ( DriveInfo ) Marshal.PtrToStructure ( ptr, typeof( DriveInfo ) ); } return m_driveInfo.Value.DefaultBlockSize; } finally { if ( ptr != IntPtr.Zero ) { Marshal.FreeHGlobal( ptr ); } } }
- See attached files for more information about defined constants,
private
variables,private
methods andPINVOKE
declarations. - Good luck!