For an introduction to CVS locks focusing on user-visible behavior, see Concurrency. The following section is aimed at people who are writing tools which want to access a CVS repository without interfering with other tools accessing the same repository. If you find yourself confused by concepts described here, like read lock, write lock, and deadlock, you might consult the literature on operating systems or databases.
Any file in the repository with a name starting with #cvs.rfl. is a read lock. Any file in the repository with a name starting with #cvs.wfl is a write lock. Old versions of CVS (before CVS 1.5) also created files with names starting with #cvs.tfl, but they are not discussed here. The directory #cvs.lock serves as a master lock. That is, one must obtain this lock first before creating any of the other locks.
To obtain a read lock, first create the #cvs.lock directory. This operation must be atomic (which should be true for creating a directory under most operating systems). If it fails because the directory already existed, wait for a while and try again. After obtaining the #cvs.lock lock, create a file whose name is #cvs.rfl. followed by information of your choice (for example, hostname and process identification number). Then remove the #cvs.lock directory to release the master lock. Then proceed with reading the repository. When you are done, remove the #cvs.rfl file to release the read lock.
To obtain a write lock, first create the #cvs.lock directory, as with read locks. Then check that there are no files whose names start with #cvs.rfl.. If there are, remove #cvs.lock, wait for a while, and try again. If there are no readers, then create a file whose name is #cvs.wfl followed by information of your choice (for example, hostname and process identification number). Hang on to the #cvs.lock lock. Proceed with writing the repository. When you are done, first remove the #cvs.wfl file and then the #cvs.lock directory. Note that unlike the #cvs.rfl file, the #cvs.wfl file is just informational; it has no effect on the locking operation beyond what is provided by holding on to the #cvs.lock lock itself.
Note that each lock (write lock or read lock) only locks a single directory in the repository, including Attic and CVS but not including subdirectories which represent other directories under version control. To lock an entire tree, you need to lock each directory (note that if you fail to obtain any lock you need, you must release the whole tree before waiting and trying again, to avoid deadlocks).
Note also that CVS expects write locks to control access to individual foo,v files. RCS has a scheme where the ,foo, file serves as a lock, but CVS does not implement it and so taking out a CVS write lock is recommended. See the comments at rcs_internal_lockfile in the CVS source code for further discussion/rationale.