From yonge.csri.toronto.edu!sis.yorku.ca!oz Tue Mar 15 17:07:59 1994 To: lee@sq.com Subject: Re: paper In-Reply-To: Your message of "Tue, 15 Mar 94 11:39:22 EST." <9403151639.AA03054@sqrex.sq.com> Date: Tue, 15 Mar 1994 17:00:34 -0500 From: "ozan s. yigit" here is the torek article you are looking for. cheers. oz ---- Subject: Re: dbm.a and ndbm.a archives Newsgroups: comp.unix References: <976@altos86.UUCP> dbm and ndbm use the same format; dbm is implemented as a compatibility layer on top of ndbm. One description suffices for both. The key idea behind dbm is a variant of a database technique known as `extensible hashing'. In dbm, it works like this: For each object that you wish to find again later (call this a `key'), compute a 32 bit hash number. Make the hash function depend on the bits in the key, but sufficiently `random-looking' that two nearly- identical keys give radically different hashes. DES encryption, for instance, would give a fairly well-spread-out bit spectrum. For each key, the 32 bit hash number tells us in which `block' that key can be found. (A database can thus consist of up to 4 billion blocks.) But, since we do not want to scatter eight keys across an entire file system, we modify it thus: Instead of using all 32 bits, we use only as many bits as needed to make all the keys fit in their blocks. Initially, we use no bits, and all keys fit in block zero: (hash & 0) is always 0. Every store goes to block 0. Eventually, block 0 gets full. We then declare that, for everything that was in block 0, we will now use another bit. Now some keys fit in block 0, and some in block 1: (hash & 1) is either 0 or 1. Move all the keys that were in block 0, but should now be in block 1, to block 1. (This is called a `split'.) Now either block 0 or block 1 can get full. If block 0 fills, we declare that, for everything that (was in block 0 for 0 bits) and (was in block 0 for 1 bit) should have two bits treated. Change the mask from 1 to 3, and instead of (hash&1)==0, we will have (hash&3) which will be either 0 or 2. (It cannot be 3 since (hash&1)==0.) Move the appropriate keys to block 2. If block 1 fills instead, we do the same, but for block 1 rather than zero; some of these keys move to block 3. If both blocks fill, we wind up doing this for both. Now blocks 0, 1, 2, or 3 could fill. If any one does, we declare that, for everything that (was in block 0 for 0 bits) and (was in block 0/1 for 1 bit) and (was in block 0/1/2/3 for 2 bits) should have three bits treated, and we change the mask from 3 to 7. If all goes well, about half the keys move to another block (from 0 to 4, 1 to 5, 2 to 6, or 3 to 7). We repeat this process as often as necessary. To find a key, we find its block number. First, compute the 32 bit hash value for that key. Next, see if we declared that (block 0 for 0 bits) split. If so, see if we declared that (block (hash&1) for 1 bit) split; if so, see if (block (hash&3) for 2 bits) split; and so on, until we finally reach a block that has not split. The key is either in this block, or not in the database at all. The way we tell whether (block B for K bits) has split is to keep a bit array (`.dir' file) with zeroes where blocks have not split and ones where they have. (Block B for K bits) is denoted by bit number (B + (1<>= 1) == 0) { /* ran out of bits */ return 0; /* no more blocks */ } if ((hash & bit) == 0) return (hash | bit); /* do this block next */ hash &= ~bit; } I am not sure how to explain this intuitively, but it really does hit all the blocks that have been used (and each only once). This `walks the tree' of hash-zero values (0, 4, 2, 1 in the example above), inserting between each walk any blocks due to splits from 4, 2, or 1. It does occasionally try a block that is empty, but this is not a problem. Within a block, the order is up to the person implementing firstkey() and nextkey(); dbm produces them sorted, with the sort being based on shortest first, then strcmp()-like order within same-length keys: if (key1.size < key2.size) return (-1); /* less */ if (key1.size > key2.size) return (1); /* greater */ return (compare(key1.dptr, key2.dptr, key1.dsize)); /* where compare() is like strcmp() but does not treat \0 specially */ This has the advantage of needing no state across calls. Last-minute details: the .pag block size is 1024 bytes; the dir file does not really have a block size, but 4.3BSD uses 4096 bytes (which is 32768 bits, and so covers up to about 32 MB directly). The external functions are #define dbm_dirfno(db) ((db)->dbm_dirf) #define dbm_pagfno(db) ((db)->dbm_pagf) typedef struct { char *dptr; int dsize; } datum; /* * flags to dbm_store() */ #define DBM_INSERT 0 #define DBM_REPLACE 1 DBM *dbm_open(char *name, int flags, int mode); void dbm_close(DBM *db); datum dbm_fetch(DBM *db, datum key); datum dbm_firstkey(DBM *db); datum dbm_nextkey(DBM *db, datum prevkey); int dbm_delete(DBM *db, datum key); int dbm_store(DBM *db, datum key, datum content, int flags);