Copyright (c) Hyperion Entertainment and contributors.
Cooperative Record Locking
Contents
Description
The Amiga's cooperative record locking scheme allows an application to lock regions of a file rather than locking an entire file. These regions are known as records.
The Amiga thinks of a record in terms of size and offset. The size refers to the length of the record in bytes. The offset refers to the starting position of the record from the beginning of the file. The size and offset can vary from record to record. The only limit on a record's size and offset are the limits the file system has on the size of the file.
File System or DOS?
Starting with version 53.x of the DOS Library (dos.library), the record locking feature is handled entirely by AmigaDOS. DOS record locking is independent of the file system and is always available. Prior to this version, record locking was available only if each individual file system implemented the feature. If a file system does not record locking, the record locking functions described below will always fail.
Record Lock Types
There are two basic types of record lock, exclusive and shared. An exclusive lock gives an application exclusive access to a record. While an application holds an exclusive record lock, no other process can obtain a record lock that overlaps the exclusively locked region. The file system can only grant an exclusive record lock on a region that is not part of an existing record lock.
The other type of record lock, the shared lock, gives an application shared access to a record. While an application holds a shared record lock, other applications can also obtain a shared record lock that overlaps the original shared record lock, but no other process can obtain an exclusive record lock that overlaps the shared record lock.
Locking and Unlocking Records
To lock (or unlock) a record within a file, an application needs a valid file handle on the file. Two DOS Library functions handle individual record locks:
BOOL LockRecord(BPTR myFH, ULONG my_offset, ULONG my_length, ULONG my_mode, ULONG my_timeout); BOOL UnLockRecord(BPTR myFH, ULONG my_offset, ULONG my_length);
In both functions, the myFH field refers to the valid file handle mentioned above, and the my_offset and my_length fields refer to the record's offset and size.
The LockRecord() function has two additional parameters: my_mode and my_timeout. The my_mode field refers to the type of record. DOS supports four record types:
- REC_EXCLUSIVE
- Create an exclusive record lock. If the record is not immediately available, the file system will make a second record lock attempt when the timeout expires. The timeout (the my_timeout field from the RecordLock() prototype above) is in DOS ticks (1/50 of a second).
- REC_EXCLUSIVE_IMMED
- This record type is like REC_EXCLUSIVE, but the attempt to lock a record will fail if the record is not immediately available. In this case the file system ignores the timeout.
- REC_SHARED
- Create a shared record lock. If the record is not immediately available, the file system will make a second record lock attempt when the timeout expires. The timeout (the my_timeout field from the RecordLock() prototype above) is in DOS ticks (1/50th of a second).
- REC_SHARED_IMMED
- This record type is like REC_SHARED, but the attempt to lock a record will fail if the record is not immediately available. In this case the file system ignores the timeout.
The Amiga record locking scheme is cooperative--the file system does not prevent any process from accessing any part of a data file. The record locking scheme has nothing to do with other actions of a file system. The file system will let other processes read and write a data file regardless of any existing record locks on the data file.
Also, when an application attempts to create a record lock, the file system's record locking mechanism only makes sure the record won't conflict with any existing record locks on the same data file. The file system doesn't check the validity of the locked region. This makes it possible to lock records in an empty data file and also to lock records that are well beyond the end of a data file. This feature may be useful to an application that needs to lock records that don't exist yet.
When releasing a record lock, an application must provide exactly the same parameters it used when it locked the record. Attempting to unlock a record using a different offset or length will fail. If the application does not successfully unlock a record, the record lock will remain in effect. This means further requests to lock that record can fail, depending on the type of record lock. The record lock will remain in effect until the data file is removed or the system restarted. As with most Amiga resources, an application should release a record lock as quickly as possible. This helps out other applications that might be waiting to access the locked record.
Locking Multiple Records
DOS Library offers a function to lock an array of record locks:
BOOL LockRecords(struct RecordLock *recordarray, ULONG multi_timeout);
LockRecords() locks a group of records using one function call. LockRecords() accepts a pointer to an array of RecordLock structures (as defined in <dos/record.h>):
struct RecordLock { BPTR rec_FH; /* The file handle of the data file */ ULONG rec_Offset; /* The record offset in the data file */ ULONG rec_Length; /* The length of the record */ ULONG rec_Mode; /* The mode of the record lock */ };
The fields in each RecordLock structure correspond to the parameters from the LockRecord() function. The records do not have to be in the same file. The array is terminated by a dummy RecordLock with a NULL file handle (rec_FH).
The RecordLock structure does not include a timeout. Instead, LockRecords() applies the same timeout (multi_timeout from the LockRecords() prototype above) to each RecordLock in the array. If LockRecords() fails to lock any of the records in its array, LockRecords() releases any successful record locks from the array, and return DOSFALSE.
To unlock records locked by LockRecords(), use the dos.library function UnLockRecords():
BOOL UnLockRecords(struct RecordLock *recordarray);
This function accepts the same array used to lock the records with LockRecords().
Note that it is possible to use UnLockRecord() on a record locked by LockRecords(). However, if an application uses UnLockRecord() to unlock one record in the RecordLock array, it should use UnLockRecord() to unlock all of the records in the array.
Locking Records Using DOS Packets
Don't use DOS Packets |
---|
DOS Library now handles record locking as of version 53.?? so that file systems are no longer burdened with implementing this functionality. The information below is being provided for reference and for users of previous versions of the DOS Library. |
To lock a record using the DOS packet interface, send an ACTION_LOCK_RECORD (2008) packet to the file system. This packet uses the following arguments:
ARG1: BTPR The file handle of the data file in which you wish to lock a record. ARG2: ULONG The offset (in bytes) in the file of the record. ARG3: ULONG The length (in bytes) of the record. ARG4: ULONG The mode with which you wish to lock record. ARG5: ULONG Time (in ticks) you are will to wait for the record to become available. RES1: BOOL Upon return RES1 will contain the success/failure status: DOSTRUE if the record lock was successfully locked. DOSFALSE if the record lock failed. RES2: CODE Failure code if the record lock failed for a reason other than denied access (collision for example).
To unlock a record using the DOS packet interface, send an ACTION_FREE_RECORD (2009) packet. This packet uses the following arguments:
ARG1: BPTR The file handle of the data file in which you locked the record. ARG2: ULONG The offset (in bytes) in the file of the record with which you locked the record. ARG3: ULONG The length (in bytes) of the record with which you locked the record. RES1: BOOL Upon return RES1 will contain the success/failure status: DOSTRUE if the record lock was successfully unlocked. DOSFALSE if there was no lock to unlock. RES2: CODE Possible failure code if the record lock could not be unlocked.
Examples
LockRecord.c
// LockRecord.c // // LockRecord()/UnLockRecord() example. #include <exec/memory.h> #include <exec/lists.h> #include <dos/dosextens.h> #include <dos/rdargs.h> #include <dos/record.h> #include <utility/tagitem.h> #include <proto/dos.h> #include <proto/exec.h> #include <proto/utility.h> void GetCommandLine(BPTR, struct RDArgs *rdargs, UBYTE *cmdbuffer); void DoLockRecord(BPTR, struct RDArgs *rdargs); void DoUnLockRecord(BPTR fh, struct RDArgs *rdargs); void ListRecordLocks(void); struct LockNode *FindRecordLock(ULONG offset, ULONG length); /* List and node structures to keep track of record locks */ struct LockNode { struct LockNode *ln_Succ; struct LockNode *ln_Pred; ULONG ln_Counter; ULONG ln_Offset; ULONG ln_Length; ULONG ln_Mode; }; struct LockList { struct LockNode *lh_Head; struct LockNode *lh_Tail; struct LockNode *lh_TailPred; ULONG lh_Counter; }; /* Pseudo data file */ #define TESTFILE "t:locktest" #define LOCK_TEMPLATE "OFFSET/K/N,LENGTH/K/N,EXCLUSIVE/S,IMMEDIATE/S,TIMEOUT/K/N" #define UNLOCK_TEMPLATE "OFFSET/K/N,LENGTH/K/N" #define OFFSET_POS 0 #define LENGTH_POS 1 #define EXCLUSIVE_POS 2 #define IMMEDIATE_POS 3 #define TIMEOUT_POS 4 struct LockList *locklist; int main() { BPTR fh; struct RDArgs *rdargs; struct CSource *csource; UBYTE *cmdbuffer; struct LockNode *lnode, *nnode; LONG error = RETURN_OK; if (locklist = AllocVecTags(sizeof(struct LockList), AVT_ClearWithValue, 0, TAG_END)) { IExec->NewList((struct List *)locklist); /* Allocate RDArgs structure to parse command lines */ if (rdargs = IDOS->AllocDosObject(DOS_RDARGS, TAG_END)) { csource = &rdargs->RDA_Source; /* Get buffer to read command lines in */ if (csource->CS_Buffer = IExec->AllocVecTags(512, AVT_ClearWithValue, 0, TAG_END)) { csource->CS_Length = 512; csource->CS_CurChr = 0; /* Buffer to isolate command keyword */ if (cmdbuffer = IExec->AllocVecTags(80, AVT_ClearWithValue, 0, TAG_END)) { /* Open a testfile, create it if necessary */ if (fh = IDOS->Open(TESTFILE, MODE_READWRITE)) { /* Process command lines */ GetCommandLine(fh, rdargs, cmdbuffer); /* Try to get rid of outstanding record locks */ lnode = locklist->lh_Head; while (nnode = lnode->ln_Succ) { /* Try to unlock pending locks */ if ((IDOS->UnLockRecord(fh, lnode->ln_Offset, lnode->ln_Length)) == DOSFALSE) { IDOS->Printf("Error unlocking record %ld with offset %ld length %ld\n", lnode->ln_Counter, lnode->ln_Offset, lnode->ln_Length); if (IDOS->IoErr()) IDOS->PrintFault(IDOS->IoErr(), NULL); } /* Remove node no matter what */ IExec->FreeVec(lnode); lnode = nnode; }; IDOS->Close(fh); } IExec->FreeVec(cmdbuffer); } else IDOS->SetIoErr(ERROR_NO_FREE_STORE); IExec->FreeVec(csource->CS_Buffer); } else IDOS->SetIoErr(ERROR_NO_FREE_STORE); IDOS->FreeDosObject(DOS_RDARGS, rdargs); } else IDOS->SetIoErr(ERROR_NO_FREE_STORE); IExec->FreeVec(locklist); } else IDOS->SetIoErr(ERROR_NO_FREE_STORE); error = IDOS->IoErr(); if (error) { IDOS->PrintFault(IDOS->IoErr(), NULL); error = RETURN_FAIL; } return(error); } void GetCommandLine(BPTR fh, struct RDArgs *rdargs, UBYTE *cmdbuffer) { struct CSource *csource = &rdargs->RDA_Source; UBYTE *cmdlinebuffer = csource->CS_Buffer; LONG error; /* Prompt for command line */ IDOS->Write(IDOS->Output(), "Cmd> ", 5); /* Loop forever, waiting for commands */ for (;;) { /* Get command line */ if ((IDOS->FGets(IDOS->Input(), cmdlinebuffer, 512)) != NULL) { /* Use ReadItem() to isolate actual command */ error = IDOS->ReadItem(cmdbuffer, 80, csource); /* Break on error */ if (error == ITEM_ERROR) break; /* Make sure I've got something */ else if (error != ITEM_NOTHING) { /* cmdbuffer now contains the command: * * KNOWN COMMANDS: * QUIT * LIST * LOCKRECORD * UNLOCKRECORD */ if ((IUtility->Stricmp("QUIT", cmdbuffer)) == 0) break; else if ((IUtility->Stricmp("HELP", cmdbuffer)) == 0) { IDOS->Printf("Available commands:\n"); IDOS->Printf("LOCKRECORD %s\n", LOCK_TEMPLATE); IDOS->Printf("UNLOCKRECORD %s\n", UNLOCK_TEMPLATE); IDOS->Printf("LIST\n"); IDOS->Printf("QUIT\n"); } else if ((IUtility->Stricmp("LIST", cmdbuffer)) == 0) ListRecordLocks(); /* Show all current locks */ else { /* Note that I've already isolated the command * keyword, so I'm using Source->CS_CurChr to point * after it. */ csource->CS_Buffer += csource->CS_CurChr; csource->CS_CurChr = 0; if ((IUtility->Stricmp("LOCKRECORD", cmdbuffer)) == 0) DoLockRecord(fh, rdargs); else if ((IUtility->Stricmp("UNLOCKRECORD", cmdbuffer)) == 0) DoUnLockRecord(fh, rdargs); else IDOS->PrintFault(ERROR_NOT_IMPLEMENTED, cmdbuffer); /* Reset CSource */ csource->CS_Buffer = cmdlinebuffer; } /* Output new prompt. Make sure csource is OK. */ IDOS->Write(IDOS->Output(), "Cmd> ", 5); csource->CS_CurChr = 0; } } else break; } } void DoLockRecord(BPTR fh, struct RDArgs *rdargs) { struct RDArgs *readargs; LONG rargs[5]; ULONG offset, length, timeout, mode; ULONG result; struct LockNode *lnode; offset = length = timeout = mode = 0; rargs[0] = rargs[1] = rargs[2] = rargs[3] = rargs[4] = 0; if (readargs = IDOS->ReadArgs(LOCK_TEMPLATE, rargs, rdargs)) { if (rargs[OFFSET_POS]) offset = *((LONG *)rargs[OFFSET_POS]); if (rargs[LENGTH_POS]) length = *((LONG *)rargs[LENGTH_POS]); if (rargs[TIMEOUT_POS]) timeout = *((LONG *)rargs[TIMEOUT_POS]); /* Type of locking */ if (rargs[EXCLUSIVE_POS]) { if (rargs[IMMEDIATE_POS]) mode = REC_EXCLUSIVE_IMMED; else mode = REC_EXCLUSIVE; } else { if (rargs[IMMEDIATE_POS]) mode = REC_SHARED_IMMED; else mode = REC_SHARED; } rargs[0] = offset; rargs[1] = length; switch (mode) { case REC_EXCLUSIVE_IMMED: rargs[2] = (LONG)"REC_EXCLUSIVE_IMMED"; break; case REC_EXCLUSIVE: rargs[2] = (LONG)"REC_EXCLUSIVE"; break; case REC_SHARED_IMMED: rargs[2] = (LONG)"REC_SHARED_IMMED"; break; case REC_SHARED: rargs[2] = (LONG)"REC_SHARED"; break; } rargs[3] = timeout; /* Show what I'm going to do */ IUtility->VFPrintf(Output(), "LockRecord: Offset %ld, Length %ld, Mode %s, Timeout %ld...", rargs); IDOS->FFlush(IDOS->Output()); /* Lock the record. Parameters are not checked. It is f.e. possible to * specify an offset larger than the size of the file. Possible since * Record Locks are not related to the file itself, only the means for * you to do arbitration. * * Note that the timeout value is in ticks... */ result = IDOS->LockRecord(fh, offset, length, mode, timeout); if (result == DOSTRUE) { IDOS->Write(IDOS->Output(), "OK\n", 3); /* Add a node to track this record lock */ if (lnode = IExec->AllocVec(sizeof(struct LockNode), AVT_ClearWithValue, 0, TAG_END)) { lnode->ln_Counter = locklist->lh_Counter++; lnode->ln_Offset = offset; lnode->ln_Length = length; lnode->ln_Mode = mode; IExec->AddTail((struct List *)locklist, (struct Node *)lnode); } else { /* Not enough memory for node. You're on your own... */ IDOS->Write(IDOS->Output(), "Not enough memory to track record lock.\n", 40); } } else { IDOS->Write(IDOS->Output(), "FAILED\n", 7); if (IDOS->IoErr()) IDOS->PrintFault(IDOS->IoErr(), NULL); } /* Release memory associated with readargs */ IDOS->FreeArgs(readargs); } else IDOS->PrintFault(IDOS->IoErr(), NULL); } void DoUnLockRecord(BPTR fh, struct RDArgs *rdargs) { struct RDArgs *readargs; LONG rargs[2]; ULONG offset, length; ULONG result; struct LockNode *lnode; offset = length = 0; rargs[0] = rargs[1] = 0; if (readargs = IDOS->ReadArgs(LOCK_TEMPLATE, rargs, rdargs)) { if (rargs[OFFSET_POS]) offset = *((LONG *)rargs[OFFSET_POS]); if (rargs[LENGTH_POS]) length = *((LONG *)rargs[LENGTH_POS]); rargs[0] = offset; rargs[1] = length; /* Show what I'm going to do */ IUtility->VFPrintf(Output(), "UnLockRecord: Offset %ld, Length %ld...", rargs); IDOS->FFlush(IDOS->Output()); /* Unlock indicated record with indicated offset and length. * If the same record (same offset/length) is locked multiple times, * only one, the first one in the list , will be unlocked. */ result = IDOS->UnLockRecord(fh, offset, length); if (result == DOSTRUE) { IDOS->Write(IDOS->Output(), "OK\n", 3); /* Remove node associated with this lock */ if (lnode = FindRecordLock(offset, length)) { IExec->Remove((struct Node *)lnode); FreeVec(lnode); } } else { IDOS->Write(IDOS->Output(), "FAILED\n", 7); /* Keep locknode */ if (IDOS->IoErr()) IDOS->PrintFault(IDOS->IoErr(), NULL); } /* Release memory associated with readargs */ IDOS->FreeArgs(readargs); } else IDOS->PrintFault(IDOS->IoErr(), NULL); } void ListRecordLocks(void) { struct LockNode *lnode; LONG rargs[4]; for (lnode = locklist->lh_Head; lnode->ln_Succ; lnode = lnode->ln_Succ) { rargs[0] = lnode->ln_Counter; rargs[1] = lnode->ln_Offset; rargs[2] = lnode->ln_Length; switch (lnode->ln_Mode) { case REC_EXCLUSIVE_IMMED: rargs[3] = (LONG)"REC_EXCLUSIVE_IMMED"; break; case REC_EXCLUSIVE: rargs[3] = (LONG)"REC_EXCLUSIVE"; break; case REC_SHARED_IMMED: rargs[3] = (LONG)"REC_SHARED_IMMED"; break; case REC_SHARED: rargs[3] = (LONG)"REC_SHARED"; break; } IUtility->VFPrintf(IDOS->Output(), "RecordLock #%ld: Offset %ld Length %ld Mode %s\n", rargs); } IDOS->FFlush(IDOS->Output()); } struct LockNode *FindRecordLock(ULONG offset, ULONG length) { struct LockNode *lnode; for (lnode = locklist->lh_Head; lnode->ln_Succ; lnode = lnode->ln_Succ) { if ((lnode->ln_Offset == offset) && lnode->ln_Length == length) return(lnode); } return(NULL); }
ExRecLock1.c
// ExRecLock1.c // // This is a simple example of using record locking to create an exclusive record // lock on a file, and writing to that record. The example ExRecLock2 is almost // exactly the same as this example, except ExRecLock2 uses the record lock directly // after ExRecLock1's record. If you try to run ExRecLock1 (or ExRecLock2) while // another instance of ExRecLock1 (or ExRecLock2) is running, the second record lock // attempt will fail. #include <protob/dos.h> #define RECORDSIZE 12 #define RECORDOFFSET 0 UBYTE *string = "ExRecLock1\n"; /* This string will be the */ /* contents of the record. */ int main() { BPTR fh; if (fh = IDOS->Open("t:testRLock", MODE_READWRITE)) /* Open the file, creating */ { /* it if necessary. */ if (DOSTRUE == IDOS->LockRecord(fh, /* Lock the record as exclusive, */ RECORDOFFSET, /* and do not wait if it is not */ RECORDSIZE, /* available immediately. */ REC_EXCLUSIVE_IMMED, 0)) { LONG error = RECORDOFFSET; /* If the record is beyond the end of the file, */ if (IDOS->Seek(fh, 0, OFFSET_END) < RECORDOFFSET) /* lengthen the file. */ error = IDOS->SetFileSize(fh, RECORDOFFSET, OFFSET_BEGINNING); if (error == RECORDOFFSET) /* If there was no error with the file */ { /* file size, continue. */ if (IDOS->Seek(fh, RECORDOFFSET, OFFSET_BEGINNING) < 0) IDOS->PrintFault(IDOS->IoErr(), "Seek() error"); if (IDOS->Write(fh, string, RECORDSIZE) < 0) IDOS->PrintFault(IDOS->IoErr(), "Write() error"); else IDOS->PutStr("Write successful, "); } IDOS->PutStr("Waiting 10 seconds...\n"); IDOS->Delay(10 * 50); IDOS->UnLockRecord(fh, RECORDOFFSET, RECORDSIZE); } else IDOS->PrintFault(IDOS->IoErr(), "Record Lock Failed"); IDOS->Close(fh); } else IDOS->PrintFault(IDOS->IoErr(), "Open Failed"); }
ExRecLock2.c
// ExRecLock2.c // // This is a simple example of using record locking to create an exclusive record // lock on a file, and writing to that record. The example ExRecLock1 is almost // exactly the same as this example, except ExRecLock1 uses the record lock directly // before ExRecLock2's record. If you try to run ExRecLock2 (or ExRecLock1) while // another instance of ExRecLock2 (or ExRecLock1) is running, the second record lock // attempt will fail. #include <proto/dos.h> #define RECORDSIZE 12 #define RECORDOFFSET 12 UBYTE *string = "ExRecLock2\n"; /* This string will be the */ /* contents of the record. */ int main() { BPTR fh; if (fh = IDOS->Open("t:testRLock", MODE_READWRITE)) /* Open the file, creating */ { /* it if necessary. */ if (DOSTRUE == IDOS->LockRecord(fh, /* Lock the record as exclusive, */ RECORDOFFSET, /* and do not wait if it is not */ RECORDSIZE, /* available immediately. */ REC_EXCLUSIVE_IMMED, 0)) { LONG error = RECORDOFFSET; /* If the record is beyond the end of the file, */ if (IDOS->Seek(fh, 0, OFFSET_END) < RECORDOFFSET) /* lengthen the file. */ error = IDOS->SetFileSize(fh, RECORDOFFSET, OFFSET_BEGINNING); if (error == RECORDOFFSET) /* If there was no error with the file */ { /* file size, continue. */ if (IDOS->Seek(fh, RECORDOFFSET, OFFSET_BEGINNING) < 0) IDOS->PrintFault(IDOS->IoErr(), "Seek() error"); if (IDOS->Write(fh, string, RECORDSIZE) < 0) IDOS->PrintFault(IDOS->IoErr(), "Write() error"); else IDOS->PutStr("Write successful, "); } IDOS->PutStr("Waiting 10 seconds...\n"); IDOS->Delay(10 * 50); IDOS->UnLockRecord(fh, RECORDOFFSET, RECORDSIZE); } else IDOS->PrintFault(IDOS->IoErr(), "Record Lock Failed"); IDOS->Close(fh); } else IDOS->PrintFault(IDOS->IoErr(), "Open Failed"); }