One Close is Always Enough If a file is closed twice, it is possible to corrupt the file system on a disk. Without a clear understanding of how the file system allocates access paths to files that are currently open, it is possible to adopt a rather cavalier attitude about opening and closing files. This Note explains why it is necessary to be very careful about opening and closing files. When the File Manager receives an Open call, it will look at the parameters passed in the parameter block and create a new access path for the file that is being opened. The access path is how the File Manager keeps track of where to send data that is written, and where to get data that is read from that file. An access path is nothing more than: - a buffer that the file system uses to read and write data, and
- a File Control Block that describes how the file is stored on a disk.
A call such as: ErrStuff = FSpOpenDF (fsspec, permission, firstRefNum); will create the access path as a buffer and a File Control Block (FCB) in the FCB buffer. The term "FCB buffer" is used in most documentation, although it actually behaves more like an array than a buffer. However, to avoid confusion, this Technote will continue to use the term "FCB buffer," although "FCB array" would be a better description. Note: The following example is here for illustrative purposes only; dependence on it may cause compatibility problems with future system software. | The FCBSPtr is a low-memory global (at 0x034E ) that holds the address of a nonrelocatable block. That block is the File Control Block buffer, and is composed of the two byte header which gives the length of the block, followed by the FCB records themselves. The records are of fixed length, and give detailed information about an open file. The structure of the queue can be visualized as: As depicted, any given record can be found by adding the length of the previous FCB records to the start of the block, adding 2 for the two byte header; giving an offset to the record itself. The size of the block, and hence the number of files that can be open at any given time, is determined at startup time and expanded on demand later. The call to open the file referenced by fsspec above, will produce the file reference number (which refers to the access path to the file) in firstRefNum . This is the number that will be used to access that file from that point on. The File Manager passes back an offset into the FCB buffer as the reference number (RefNum ). This offset is the number of bytes past the beginning of the queue to that FCB record in the buffer. That FCB record will describe the file that was opened. An example of a number that might get passed back as a RefNum is $1D8 . That also means that the FCB record is $1D8 bytes into the FCB block. A visual example of a record in use, and how the RefNum relates is: Base is merely the address of the nonrelocatable block that is the FCB buffer. FCBSPtr points to it. The RefNum (a number like $1D8 ) is added to Base , to give an address in the block. That address is what the file system will use to read and write to an open file, which is why you are required to pass the RefNum to the PBRead and PBWrite calls. So RefNum is merely an offset into the buffer. Let's step through a dangerous imaginary sequence and see what happens to a given record in the FCB buffer. Here's the sequence we will step through:
ErrStuff = FSpOpenDF (fsspec, permission, firstRefNum);
ErrStuff = FSClose ( firstRefNum );
ErrStuff = FSpOpenDF (secondFileSpec, permission, secondRefNum);
ErrStuff = FSClose ( firstRefNum ); {the wrong file gets closed!!!}
{the above line will close 'SecondFile', not 'FirstFile', which is
already closed}
| Before any operations, the record at $1D8 is not used. After the call: ErrStuff = FSpOpenDF (firstFileSpec, permission, firstRefNum); firstRefNum = $1D8 and the record is in use. After the call: ErrStuff = FSClose (firstRefNum); firstRefNum is still equal to $1D8, but the FCB record is unused. After the call: ErrStuff = FSpOpenDF (secondFileSpec, permission, secondRefNum); SecondRefNum = $1D8, FirstRefNum = $1D8, and the record is reused. After the call: ErrStuff = FSClose (firstRefNum); The firstRefNum = $1D8, secondRefNum = $1D8, and the FCB buffer element is cleared. This happens even though firstFile was already closed. Actually, secondFile was closed: Note: The second close is using the old RefNum . The second close will still close a file, and in fact will return noErr as its result. Any subsequent accesses to the secondRefNum will return an error, since the file 'secondFile ' was closed. The File Control Blocks are reused, and since they are just offsets, it is possible to get the same file RefNum back for two different files. In this case, firstRefNum == secondRefNum since 'firstFile ' was closed before opening 'secondFile ' and the same FCB record was reused for 'secondFile '. | There are any number of nasty cases that can arise if a file is closed twice, reusing an old RefNum . A common programming practice is to have an error handler or cleanup routine that goes through the files that a program creates and closes them all, even if some may already be closed. If an FCB element was not reused, the Close will return the expected fnOpnErr . If the FCB had been reused, then the Close could be closing the wrong file. This can be very dangerous. As a particularly nasty example, think of what can happen if a program were to close a file, then the user inserted an HFS floppy disk. The FCB could be reused for the Catalog File on that HFS disk. If the program had a generic error handler that closed all of its files, it could inadvertently close "its" file again. If it thought "its" file was still open it would do the close, which could close the Catalog file on the HFS disk. This is catastrophic for the disk since the file could easily be closed in an inconsistent state. The result is a bad disk that needs to be reformatted. |