1. 程式人生 > >《unix環境高階程式設計》--- 資料庫函式庫

《unix環境高階程式設計》--- 資料庫函式庫

大多數訪問資料庫的函式庫使用兩個檔案儲存資訊:索引檔案和資料檔案。索引檔案包含索引值(鍵)和指向資料檔案中對應資料記錄的指標。為提高按鍵查詢的速度和效率,可用雜湊法和B+樹組織索引檔案,本文采用固定大小的散列表,
並採用連結串列法解決雜湊衝突。
由於只有一個索引檔案,所以每條資料記錄只能有一個鍵(不支援第二個鍵)。
索引檔案由三部分組成:空閒連結串列指標、散列表和索引記錄。索引指標(ptr)欄位中實際儲存的是以ASCII碼數字形式記錄的檔案中的偏移量。
這裡寫圖片描述
當給定一個鍵要做資料庫中尋找一條記錄是,db_fetch根據該鍵值即使雜湊值,由此雜湊值可確定散列表中的一條雜湊鏈(連結串列指標欄位可以為0,表示一條空的雜湊鏈)。沿著這條雜湊鏈,可以找到所有具有該雜湊值的索引記錄。當遇到一條索引記錄的連結串列指標欄位為0是,表示到達了此雜湊鏈的末尾。

t4.c

#include "apue.h"
#include "apue_db.h"
#include <fcntl.h>

int main(void)
{
    DBHANDLE db;

    if((db = db_open("db4", O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) == NULL)
        err_sys("db_open error");

    if(db_store(db, "Alpah", "data1", DB_INSERT) != 0)
        err_quit("db_store error for alpha"
); if(db_store(db, "beta", "Data for beta", DB_INSERT) != 0) err_quit("db_store error for beta"); if(db_store(db, "gamma", "record3", DB_INSERT) != 0) err_quit("db_store error for gamma"); db_close(db); exit(0); }

結果:
這裡寫圖片描述

結果解釋:
雜湊連結串列的大小為137,其中有三條雜湊連結串列寫了資料。
共三條記錄,每條記錄都是所在連結串列的頭節點。
第一條記錄的偏移量為829, 即“ 0 10Alpah:0:6”的偏移量為829

0 10Alpha:0:6
第一個0:位於本連結串列中的下一條記錄的偏移,0表示空
10:記錄長度,即”Alpha:0:6”的長度為10
0:資料在資料檔案,即db4.dat中的偏移
6:資料的長度,即”data1”的長度

apue_db.h

#ifndef _APUE_DB_H  /* 保證只包含該標頭檔案一次 */
#define _APUE_DB_H

typedef void * DBHANDLE;  /* 對資料庫的一個有效引用,隔離應用程式和資料庫的實現細節 */

DBHANDLE db_open(const char *pathname, int oflag, ...);
void     db_close(DBHANDLE h);
char    *db_fetch(DBHANDLE h, const char *key);
int      db_store(DBHANDLE h, const char *key, const char *data, int flag);
int      db_delete(DBHANDLE h, const char *key);
void     db_rewind(DBHANDLE h);
char    *db_nextrec(DBHANDLE h, char *key);

/* 可以傳送給db_store函式的合法標誌 */
#define DB_INSERT  1  /* insert new record only */
#define DB_REPLACE 2  /* replace existing record */
#define DB_STORE   3  /* replace or insert */

/* 實現的基本限制,為支援更大的資料庫可更改這些限制 */
#define IDXLEN_MIN 6     /* key, sep, start, sep, length, \n    --- 最小索引長度
                            1位元組鍵、1位元組分隔符、1位元組起始偏移量、
                            另一個位元組分隔符、1位元組長度和1位元組換行符。*/
#define IDXLEN_MAX 1024  /* arbitrary */
#define DATLEN_MIN 2     /* data byte, newline */
#define DATLEN_MAX 1024  /* arbitrary */

#endif  /* _APUE_DB_H */

db.c

#include "apue.h"
#include "apue_db.h"
#include <fcntl.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/uio.h>

#define IDXLEN_SZ   4      /* 索引記錄長度 */
#define SEP         ':'    /* 用某些字元(例如冒號、換行符)作為資料庫中的分隔符 */
#define SPACE       ' '    /* 當刪除一條記錄時,在其中全部填入空格符 */
#define NEWLINE     '\n'

/* The following definitions are for hash chains and free
   list chain in the index file */
#define PTR_SZ      6     /* size of ptr field in hash chain */
#define PTR_MAX  999999   /* max file offset = 10**PTR_SZ - 1 */
#define NHASH_DEF   137   /* default hash table size */
#define FREE_OFF    0     /* free list offset in index file */
#define HASH_OFF PTR_SZ   /* hash table offset in index file */

typedef unsigned long DBHASH;  /* hash values */
typedef unsigned long COUNT;   /* unsigned counter */

/* 記錄開啟資料的所有資訊, db_open函式返回DB結構的指標DBHANDLE值 */
typedef struct
{
    int   idxfd;      /* fd for index file */
    int   datfd;      /* fd for data file */
    char  *idxbuf;    /* malloc'ed buffer for index record */
    char  *datbuf;    /* malloc'ed buffer for data record */
    char  *name;      /* name db was opend under */
    off_t idxoff;     /* offset in index file of index record */
              /* key is at (idxoff + PTR_SZ + IDXLEN_SZ) */
    size_t idxlen;    /* length of index record */
              /* excludes IDXLEN_SZ bytes at front of record */
    off_t  datoff;    /* offset in data file of data record */
    size_t datlen;    /* length of data record */
              /* includes newline at end */
    off_t  ptrval;    /* contents of chain ptr in index record */   
    off_t  ptroff;    /* chain ptr offset pointing to this idx record */
    off_t  chainoff;  /* offset of hash chain for this index record */
    off_t  hashoff;   /* offset in index file of hash table */
    DBHASH nhash;     /* current hash table size */

    /* 對成功和不成功的操作計數 */
    COUNT  cnt_delok;    /* delete OK */
    COUNT  cnt_delerr;   /* delete error */
    COUNT  cnt_fetchok;  /* fetch OK */
    COUNT  cnt_fetcherr; /* fetch error */
    COUNT  cnt_nextrec;  /* nextrec */
    COUNT  cnt_stor1;    /* store: DB_INSERT, no empty, appended */
    COUNT  cnt_stor2;    /* store: DB_INSERT, found empty, reused */
    COUNT  cnt_stor3;    /* store: DB_REPLACE, diff len, appended */
    COUNT  cnt_stor4;    /* store: DB_REPLACE, same len, overwrote */
    COUNT  cnt_storerr;  /* store error */
}DB;

/* 內部私有函式。宣告為static,只有同一檔案中的其他函式才能呼叫 */
static DB *_db_alloc(int);
static void _db_dodelete(DB *);
static int _db_find_and_lock(DB *, const char *, int);
static int _db_findfree(DB *, int, int);
static void _db_free(DB *);
static DBHASH _db_hash(DB *, const char *);
static char *_db_readdat(DB *);
static off_t _db_readidx(DB *, off_t);
static off_t _db_readptr(DB *, off_t);
static void _db_writedat(DB *, const char *, off_t, int);
static void _db_writeidx(DB *, const char *, off_t, int, off_t);
static void _db_writeptr(DB *, off_t, off_t);

/* 如果呼叫者想要建立資料庫檔案,那麼用可選擇的第3個引數指定檔案許可權。
   db_open開啟索引檔案和資料檔案,在必要時初始化索引檔案。
   成功返回資料庫控制代碼,出錯返回NULL。
   如果成功返回,則將建立兩個檔案:pathname.idx和pathname.dat */
DBHANDLE db_open(const char *pathname, int oflag, ...)
{
    DB *db;
    int len, mode;
    size_t i;
    char asciiptr[PTR_SZ + 1], hash[(NHASH_DEF + 1) * PTR_SZ + 2];
    struct stat statbuff;

    /* 為DB結構分配空間 */
    len = strlen(pathname);
    if((db = _db_alloc(len)) == NULL)
        err_dump("db_open: _db_alloc error for DB");

    db->nhash = NHASH_DEF;  /* hash table size */
    db->hashoff = HASH_OFF; /* offset in index file of hash table */
    strcpy(db->name, pathname);  
    strcat(db->name, ".idx");  /* 資料庫索引檔名 */

    if(oflag & O_CREAT)
    {
        va_list ap;

        /* 如果想建立資料庫檔案,找到可選的第3個引數 */
        va_start(ap, oflag);
        mode = va_arg(ap, int);
        va_end(ap);

        db->idxfd = open(db->name, oflag, mode);
        strcpy(db->name + len, ".dat");
        db->datfd = open(db->name, oflag, mode);
    }
    else
    {
        db->idxfd = open(db->name, oflag);
        strcpy(db->name + len, ".dat");
        db->datfd = open(db->name, oflag);
    }

    /* 如果開啟或建立任意資料庫檔案時出錯 */
    if(db->idxfd < 0 || db->datfd < 0)
    {
        _db_free(db);  /* 清除DB結構 */
        return (NULL);
    }

    if((oflag & (O_CREAT | O_TRUNC)) == (O_CREAT | O_TRUNC))
    {
        /* 
        如果正在建立資料庫,則必須正確地加鎖 
        Write lock the entire file so that we can stat it, 
        check its size, and initialize it, atomically
        */
        if(writew_lock(db->idxfd, 0, SEEK_SET, 0) < 0)
            err_dump("db_open: writew_lock error");

        if(fstat(db->idxfd, &statbuff) < 0)
            err_sys("db_open: fstat error");

        /* 
        如果索引檔案長度為0,那麼該檔案是剛剛被建立的,
        所以需初始化它所包含的空閒連結串列和雜湊連結串列指標。
        */
        if(statbuff.st_size == 0)
        {
            /*
            字串%*d將資料庫指標從整型變為ASCII字串,
            這一格式告訴sprintf取PTR_SZ引數,用它作為下一個引數的最小欄位寬度,此處為0(因為
            正在建立一個新的資料庫)。其作用是強迫建立的字串至少包含PTR_SZ個字元(在左邊用
            空格填充。在_db_writeidx和_db_writeptr中,將傳送一個非0值,但首先將驗證指標指不
            大於PTR_MAX,以保證寫入資料庫的指標字串恰好為PTR_SZ(6)個字元。
            */
            sprintf(asciiptr, "%*d", PTR_SZ, 0);

            /* 構造散列表,並寫到索引檔案中 */
            hash[0] = 0;
            for(i = 0; i< NHASH_DEF+1; i++)
                strcat(hash, asciiptr);
            strcat(hash, "\n");
            i = strlen(hash);
            if(write(db->idxfd, hash, i) != i)
                err_dump("db_open: index file init write error");
        }
        /* 解鎖索引檔案 */
        if(un_lock(db->idxfd, 0, SEEK_SET, 0) < 0)
            err_dump("db_open: un_lock error");
    }
    /* 清除資料庫檔案指標 */
    db_rewind(db);

    /* 返回DB結構指標作為控制代碼,以邊呼叫者以後用於其他資料庫函式 */
    return (db);
}

/* db_open呼叫_db_alloc為DB結構分配空間,包括一個索引緩衝和一個數據緩衝 */
static DB *_db_alloc(int namelen)
{
    DB *db;

    /* 為DB分配儲存區,並將該區各單元全部置初值為0 */
    if((db = calloc(1, sizeof(DB))) == NULL)
        err_dump("_db_alloc: calloc error for DB");
    db->idxfd = db->datfd = -1;  /* 重新置為-1,以表示它們至此還不是有效的 */

    /* 
    Allocate room for the name.
    +5 for ".idx" or ".dat" plus null at end
    */
    if((db->name = malloc(namelen + 5)) == NULL)
        err_dump("_db_alloc: malloc error for name");

    /*
    Allocate an index buffer and a data buffer.
    +2 for newline and null at end.
    */
    if((db->idxbuf = malloc(IDXLEN_MAX + 2)) == NULL)
        err_dump("_db_alloc: malloc error for index buffer");
    if((db->datbuf = malloc(DATLEN_MAX + 2)) == NULL)
        err_dump("_db_alloc: malloc error for data buffer");
    return (db);
}

/* Relinquish access to the database */
void db_close(DBHANDLE h)
{
    _db_free((DB *) h);  /* close fds, free buffers * struct */
}

/* db_open在開啟索引檔案和資料檔案時如果發生錯誤,則呼叫_db_free釋放資源;
   應用程式在結束對資料庫的使用後,db_close也呼叫_db_free。 */
static void _db_free(DB *db)
{
    /* 關閉檔案 */
    if(db->idxfd >= 0)
        close(db->idxfd);
    if(db->datfd >= 0)
        close(db->datfd);
    /* 釋放動態分配的緩衝 */
    if(db->idxbuf != NULL)
        free(db->idxbuf);
    if(db->datbuf != NULL)
        free(db->datbuf);
    if(db->name != NULL)
        free(db->name);
    /* 釋放DB結構佔用的儲存區 */
    free(db);
}


/* 根據給定的鍵讀取一條記錄 */
char *db_fetch(DBHANDLE h, const char *key)
{
    DB *db = h;
    char *ptr;

    /* 查詢該記錄 */
    if(_db_find_and_lock(db, key, 0) < 0)  
    {
        ptr = NULL;
        db->cnt_fetcherr++;
    }
    else  /* 成功 */
    {
        ptr = _db_readdat(db);  /* 讀相應的記錄 */
        db->cnt_fetchok++;
    }

    if(un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
        err_dump("db_fetch: un_lock error");
    return (ptr);
}

/* 在函式庫內部按給定的鍵查詢記錄 */
/* 如果想在索引檔案上加一把寫鎖,則將writelock引數設定為非0;否則加讀鎖 */
static int _db_find_and_lock(DB *db, const char *key, int writelock)
{
    off_t offset, nextoffset;

    /* 將鍵變為雜湊值,用其計算在檔案中相應雜湊鏈的起始地址(chainoff)*/
    db->chainoff = (_db_hash(db, key) * PTR_SZ) + db->hashoff;
    db->ptroff = db->chainoff;

    /* 只鎖該雜湊鏈開始處的第1個位元組,允許多個程序同時搜尋不同的
           雜湊鏈,因此增加了併發性 */
    if(writelock)
    {
        if(writew_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
            err_dump("_db_find_and_lock: writew_lock error");
    }
    else
    {
        if(readw_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
            err_dump("_db_find_and_lock: readw_lock error");
    }

    /* 讀雜湊鏈中的第一個指標, 如果返回0,該雜湊鏈為空 */
    offset = _db_readptr(db, db->ptroff);
    while(offset != 0)
    {
        /* 讀取每條索引記錄。將當前記錄的鍵填入DB結構的idxbuf欄位。
           如果_db_readidx返回0,則已打到雜湊鏈的最後一個記錄項。*/
        nextoffset = _db_readidx(db, offset);
        if(strcmp(db->idxbuf, key) == 0)
            break;  /* found a match */
        db->ptroff = offset;  /* offset of this (unequal) record */
        offset = nextoffset;  /* next one to compare */
    }

    /* offset為0,那麼已到雜湊鏈末端並且沒有找到匹配鍵 */
    return (offset == 0 ? -1 : 0);
}

/* 根據給定的鍵計算雜湊值 */
static DBHASH _db_hash(DB *db, const char *key)
{
    DBHASH hval = 0;
    char c;
    int i;

    /* 將鍵中的每個ASCII字元乘以這個字元在字串中以1開始的索引號,
       將這些結果相加,除以散列表記錄項數,將餘數作為鍵的雜湊值。
       散列表記錄項數為137,是一個素數,提供良好的分佈特性。 */
    for(i=1; (c = *key++) != 0; i++)
        hval += c * i;
    return (hval % db->nhash);
}

/*
讀取以下三種不同連結串列指標的任意一種:
1、索引檔案中指向空閒連結串列的第一條索引記錄的指標
2、散列表中指向雜湊鏈的第一條索引記錄的指標
3、存放在每條索引記錄開始處、指向下一條記錄的指標
呼叫前,需加鎖
*/
static off_t _db_readptr(DB *db, off_t offset)
{
    char asciiptr[PTR_SZ + 1];

    if(lseek(db->idxfd, offset, SEEK_SET) == -1)
        err_dump("_db_readptr: lseek error to ptr field");
    if(read(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ)
        err_dump("_db_readptr: read error of ptr field");
    asciiptr[PTR_SZ] = 0;     /* null terminate */
    return (atol(asciiptr));  /* 將指標從ASCII形式變換為長整型 */
}

/*
從索引檔案的指定偏移量處讀取索引記錄。
如果成功,返回連結串列中下一條記錄的偏移量,並填充DB結構的許多欄位:
idxoff:索引檔案中當前記錄的偏移量
ptrval:在雜湊連結串列中下一索引項的偏移量
idxlen:當前索引記錄的長度
idxbuf:實際索引記錄
datoff:資料檔案中該記錄的偏移量
datlen:該資料記錄的長度
*/
static off_t _db_readidx(DB *db, off_t offset)
{
    ssize_t i;
    char *ptr1, *ptr2;
    char asciiptr[PTR_SZ + 1], asciilen[IDXLEN_SZ + 1];
    struct iovec iov[2];

    /*
    按呼叫者提供的引數,查詢索引檔案偏移量,並記錄在DB結構中。
    為此即使呼叫者想要在當前檔案偏移處讀記錄,仍需要呼叫lseek確定當前偏移量。
    因為在索引檔案中,索引記錄決不會放在偏移記錄為0處,所以可用0表示“從當前偏移量處讀”
    */
    if((db->idxoff = lseek(db->idxfd, offset, offset == 0 ? 
        SEEK_CUR : SEEK_SET)) == -1)
        err_dump("_db_readidx: lseek error");

    /*
    呼叫readv讀在索引記錄開始處的兩個定長欄位:指向下一條索引記錄的連結串列指標
    和該索引記錄餘下部分的長度(餘下部分是不定長的)
    */
    iov[0].iov_base = asciiptr;
    iov[0].iov_len = PTR_SZ;
    iov[1].iov_base = asciilen;
    iov[1].iov_len = IDXLEN_SZ;
    if((i = readv(db->idxfd, &iov[0], 2)) != PTR_SZ + IDXLEN_SZ)    
    {
        if(i == 0 && offset == 0)
            return (-1);  /* EOF for db_nextrec */
        err_dump("_db_readidx: readv error of index record");
    }

    /* This is our return value; always >= 0 */
    asciiptr[PTR_SZ] = 0;         /* null terminate */
    db->ptrval = atol(asciiptr);  /* offset of next key in chain */

    asciilen[IDXLEN_SZ] = 0;      /* null terminate */
    if((db->idxlen = atoi(asciilen)) < IDXLEN_MIN || 
        db->idxlen > IDXLEN_MAX)
        err_dump("_db_readidx: invalid length");

    /* 將索引記錄的不定長部分讀入DB的idxbuf欄位 */
    if((i = read(db->idxfd, db->idxbuf, db->idxlen)) != db->idxlen)
        err_dump("_db_readidx: read error of index record");
    if(db->idxbuf[db->idxlen-1] != NEWLINE)  /* 該記錄應以換行符結束 */
        err_dump("_db_readidx: missing newline");
    db->idxbuf[db->idxlen-1] = 0;  /* 將換行符替換為NULL */

    /* 索引記錄劃分為三個欄位:鍵、對應資料記錄的偏移量和資料記錄的長度 */
    if((ptr1 = strchr(db->idxbuf, SEP)) == NULL)
        err_dump("_db_readidx: missing first separator");
    *ptr1++ = 0;  /* replace SEP with null */

    if((ptr2 = strchr(ptr1, SEP)) == NULL)
        err_dump("_db_readidx: missing second separator");
    *ptr2++ = 0;

    if(strchr(ptr2, SEP) != NULL)
        err_dump("_db_readidx: too many separators");

    /* 將資料記錄的偏移量和長度變為整型 */
    if((db->datoff = atol(ptr1)) < 0)
        err_dump("_db_readidx: starting offset < 0");
    if((db->datlen = atol(ptr2)) <= 0 || db->datlen > DATLEN_MAX)
        err_dump("_db_readidx: invalid length");

    return (db->ptrval);   /* 雜湊鏈中的下一條記錄的偏移量 */
}

/* 在datoff和datlen已獲正確值後,_db_readdat將資料記錄
的內容讀入DB結構中的datbuf欄位指向的緩衝區 */
static char *_db_readdat(DB *db)
{
    if(lseek(db->datfd, db->datoff, SEEK_SET) == -1)
        err_dump("_db_readdat: lseek error");
    if(read(db->datfd, db->datbuf, db->datlen) != db->datlen)
        err_dump("_db_readdat: read error");
    if(db->datbuf[db->datlen-1] != NEWLINE)
        err_dump("_db_readdat: missing newline");
    db->datbuf[db->datlen-1] = 0;
    return (db->datbuf);
}

/* 刪除與給定鍵匹配的一條記錄 */
int db_delete(DBHANDLE h, const char *key)
{
    DB *db = h;
    int rc = 0;

    /* 判斷在資料庫中該記錄是否存在
       如果存在,呼叫_db_dodelete執行刪除該記錄的操作 
       第3個引數控制對雜湊鏈是加讀鎖還是寫鎖 
       因為可能執行刪除該記錄的操作,所以要加一把寫鎖 */
    if(_db_find_and_lock(db, key, 1) == 0)
    {
        _db_dodelete(db);
        db->cnt_delok++;
    }
    else
    {
        rc = -1;
        db->cnt_delerr++;
    }
    /* 不管是否找到所需記錄,都除去這把鎖 */
    if(un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
        err_dump("db_delete: un_lock error");
    return (rc);
}

/* 執行從資料庫刪除一條記錄的所有操作。
此函式的大部分工作僅僅是更新兩個連結串列:空閒連結串列以及與
鍵對應的雜湊鏈。當一條記錄被刪除後,將其鍵和資料記錄設為空。*/
static void _db_dodelete(DB *db)
{
    int i;
    char *ptr;
    off_t freeptr, saveptr;

    /* Set data buffer and key to all blanks */
    for(ptr = db->datbuf, i = 0; i < db->datlen - 1; i++)
        *ptr++ = SPACE;
    *ptr = 0;   
    ptr = db->idxbuf;
    while(*ptr)
        *ptr++ = SPACE;

    /* We have to lock the free list */
    if(writew_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("_db_dodelete: writew_lock error");

    /* Write the data record with all blanks */
    _db_writedat(db, db->datbuf, db->datoff, SEEK_SET);

    /* 讀空閒連結串列指標。讓這條記錄成為空閒連結串列的第一條記錄 */
    freeptr = _db_readptr(db, FREE_OFF);

    /* 被_db_writeidx修改之前先儲存雜湊鏈中的當前記錄 */
    saveptr = db->ptrval;

    /* 用被刪除的索引記錄的偏移量更新空閒連結串列指標,
           也就使其指向當前刪除的這條記錄, 從而將該被刪除記錄加到了空閒連結串列之首 */
    _db_writeidx(db, db->idxbuf, db->idxoff, SEEK_SET, freeptr);

    /* write the new free list pointer */
    _db_writeptr(db, FREE_OFF, db->idxoff);

    /* 修改雜湊鏈中前一條記錄的指標,使其指向正刪除記錄之後的一條記錄,
       這樣便從雜湊鏈中撤除了要刪除的記錄 */
    _db_writeptr(db, db->ptroff, saveptr);

    /* 對空閒連結串列解鎖 */
    if(un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("_db_dodelete: un_lock error");
}

/* 寫一條資料記錄, 當刪除一條記錄是,清空資料記錄 */
static void _db_writedat(DB *db, const char *data, off_t offset, int whence)
{
    struct iovec iov[2];
    static char newline = NEWLINE;

    /* If we're appending, we have to lock before dong the lseek and 
       write to make the two an atomic operation. If we're overwriting
       an existing record, we don't have to lock */
    if(whence == SEEK_END)
        if(writew_lock(db->datfd, 0, SEEK_SET, 0) < 0)
            err_dump("_db_writedat: writew_lock error");

    if((db->datoff = lseek(db->datfd, offset, whence)) == -1)
        err_dump("_db_writedat: lseek error");
    db->datlen = strlen(data) + 1;  /* datlen includes new line */

    /* 不能想當然地認為呼叫者緩衝區尾端有空間可以加換行符,
           所以先將換行符送入另一個緩衝,然後再從該緩衝寫到資料記錄 */
    iov[0].iov_base = (char *)data;
    iov[0].iov_len = db->datlen - 1;
    iov[1].iov_base = &newline;
    iov[1].iov_len = 1;
    if(writev(db->datfd, &iov[0], 2) != db->datlen)
        err_dump("_db_writedat: writev error of dat record");

    if(whence == SEEK_END)
        if(un_lock(db->datfd, 0, SEEK_SET, 0) < 0)
            err_dump("_db_writedat: un_lock error");
}

/* 寫一條索引記錄 
   _db_writedat is called before this function to set datoff and datlen
   fields in the DB structure, which we need to write the index record  */
static void _db_writeidx(DB *db, const char *key, off_t offset, 
                         int whence, off_t ptrval)
{
    struct iovec iov[2];
    char asciiptrlen[PTR_SZ + IDXLEN_SZ + 1];
    int len;
    char *fmt;

    if((db->ptrval = ptrval) < 0 || ptrval > PTR_MAX)
        err_quit("_dbwriteidx: invalid ptr: %d", ptrval);
    if(sizeof(off_t) == sizeof(long long))
        fmt = "%s%c%lld%c%d\n";
    else
        fmt = "%s%c%ld%c%d\n";
    sprintf(db->idxbuf, fmt, key, SEP, db->datoff, SEP, db->datlen);
    if((len = strlen(db->idxbuf)) < IDXLEN_MIN || len > IDXLEN_MAX)
        err_dump("_db_writeidx: invalid length");
    sprintf(asciiptrlen, "%*ld%*d", PTR_SZ, ptrval, IDXLEN_SZ, len);

    /* If we're appending, we have to lock before dong the lseek
       and write to make the two an atomic operation */
    if(whence == SEEK_END)
        if(writew_lock(db->idxfd, ((db->nhash+1)*PTR_SZ) + 1, 
                   SEEK_SET, 0) < 0)
            err_dump("_db_writeidx: writew_lock error");

    /* 設定索引檔案偏移量,從此處開始寫索引記錄,
           將該偏移量存入DB結構的idxoff欄位 */
    if((db->idxoff = lseek(db->idxfd, offset, whence)) == -1)
        err_dump("_db_writeidx: lseek error");

    iov[0].iov_base = asciiptrlen;
    iov[0].iov_len = PTR_SZ + IDXLEN_SZ;
    iov[1].iov_base = db->idxbuf;
    iov[1].iov_len = len;
    if(writev(db->idxfd, &iov[0], 2) != PTR_SZ + IDXLEN_SZ + len)
        err_dump("_db_writeidx: writev error of index record");

    if(whence == SEEK_END)
        if(un_lock(db->idxfd, ((db->nhash+1)*PTR_SZ)+1, 
               SEEK_SET, 0) < 0)
            err_dump("_db_writeidx: un_lock error");

}

/* 將一個連結串列指標寫到索引檔案 */
static void _db_writeptr(DB *db, off_t offset, off_t ptrval)
{
    char asciiptr[PTR_SZ + 1];

    /* 驗證該指標再索引檔案的邊界範圍內,然後將它變成ASCII字串 */
    if(ptrval < 0 || ptrval > PTR_MAX)
        err_quit("_db_writeptr: invalid ptr: %d", ptrval);
    sprintf(asciiptr, "%*ld", PTR_SZ, ptrval);

    /* 按指定的偏移量在索引檔案中定位,接著將該指標ASCII字串寫入索引檔案 */
    if(lseek(db->idxfd, offset, SEEK_SET) == -1)
        err_dump("_db_writeptr: lseek error to ptr field");
    if(write(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ)
        err_dump("_db_writeptr: write error of ptr field");
}

/* 將一條記錄新增到資料庫 */
int db_store(DBHANDLE h, const char *key, const char *data, int flag)
{
    DB *db = h;
    int rc, keylen, datlen;
    off_t ptrval;

    /* 首先驗證flag,然後查明資料記錄長度是否有效,無效則構造core檔案退出 */
    if(flag != DB_INSERT && flag != DB_REPLACE && flag != DB_STORE)
    {
        errno = EINVAL;
        return (-1);
    }
    keylen = strlen(key);
    datlen = strlen(data) + 1;
    if(datlen < DATLEN_MIN || datlen > DATLEN_MAX)
        err_dump("db_store: invalid data length");

    /* 檢視該記錄是否已經存在。如果記錄不存在且指定標誌位DB_INSERT或DB_STORE,
       或者記錄存在且指定標誌位DB_REPLACE或DB_STORE,均合法
       因為db_store可能會修改雜湊鏈,所以用_db_find_and_lock的最後一個引數指明要對
           雜湊鏈加寫鎖 */
    if(_db_find_and_lock(db, key, 1) < 0)  /* record not found */
    {
        if(flag == DB_REPLACE)
        {
            rc = -1;
            db->cnt_storerr++;
            errno = ENOENT;   /* error, record does not exist */
            goto doreturn;
        }

        /* _db_find_and_lock locked the hash chain for us; 
        read the chain ptr to the first index record on hash chain */
        ptrval = _db_readptr(db, db->chainoff);

        if(_db_findfree(db, keylen, datlen) < 0)  
        {
            /* 沒有在空閒連結串列中找到鍵長度和資料長度與keylen和datlen相同的記錄,
            需將這條新記錄新增到索引檔案和資料檔案的末尾 */
            _db_writedat(db, data, 0, SEEK_END);  /* 寫資料 */
            _db_writeidx(db, key, 0, SEEK_END, ptrval); /* 寫索引 */

            /* 將新記錄加到對應的雜湊鏈的鏈首 */
            _db_writeptr(db, db->chainoff, db->idxoff);
            db->cnt_stor1++;  /* 計數器加1 */
        }
        else
        {
            /* 找到了鍵長度和資料長度與keylen和datlen相同的記錄,將這條空記
                錄從空閒連結串列上移下來,寫入新的索引記錄和資料記錄,將新記錄加到
                對應的雜湊鏈的鏈首,並將此種情況的計數器加1 */
            _db_writedat(db, data, db->datoff, SEEK_SET);
            _db_writeidx(db, key, db->idxoff, SEEK_SET, ptrval);
            _db_writeptr(db, db->chainoff, db->idxoff);
            db->cnt_stor2++;
        }
    }
    else  /* 具有相同鍵的記錄在資料庫中已存在 */
    {
        if(flag == DB_INSERT)
        {
            rc = 1;
            db->cnt_storerr++;
            goto doreturn;
        }

        /* 替換一條現存的記錄,而新資料記錄的長度與已存在記錄的長度不一樣 */
        if(datlen != db->datlen)
        {
            /* 刪除舊記錄, 將該刪除記錄存放在空閒連結串列的鏈首 */
            _db_dodelete(db);  

            /* Reread the chain ptr in the hash table
               (it may change with the deletion) */
            ptrval = _db_readptr(db, db->chainoff);

            /* 將新激流新增到索引檔案和資料檔案的末尾 */
            _db_writedat(db, data, 0, SEEK_END);
            _db_writeidx(db, key, 0, SEEK_END, ptrval);

            /* 將新記錄新增到對應雜湊鏈的鏈首 */
            _db_writeptr(db, db->chainoff, db->idxoff);
            db->cnt_stor3++; /* 此種情況的計數器加1 */
        }
        else /* 替換一條記錄,而新資料記錄的長度與已存在的記錄的長度恰好一樣 */
        {
            /* 只需重寫資料記錄,並將計數器加1 */
            _db_writedat(db, data, db->datoff, SEEK_SET);
            db->cnt_stor4++;
        }
    }
    rc = 0;  /* OK */

 doreturn:
    if(un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
        err_dump("db_store: unclock error");
    return (rc);
}

/* 試圖找到一個指定大小的空閒索引記錄和相關聯的資料記錄 */
static int _db_findfree(DB *db, int keylen, int datlen)
{
    int rc;
    off_t offset, nextoffset, saveoffset;

    /* 避免與其他使用空閒連結串列的程序互相干擾 */
    if(writew_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("_db_findfree: writew_lock error");

    /* 得到空閒連結串列頭指標 */
    saveoffset = FREE_OFF;
    offset = _db_readptr(db, saveoffset);

    /* 迴圈遍歷空閒連結串列 */
    while(offset != 0)
    {
        nextoffset = _db_readidx(db, offset);
        if(strlen(db->idxbuf) == keylen && db->datlen == datlen)
            break;  /* found a match */
        saveoffset = offset;
        offset = nextoffset;
    }

    if(offset == 0) /* 未找到所要求的鍵長度和資料長度的可用記錄 */
    {
        rc = -1;
    }
    else
    {
        /* 否則,將已找到記錄的下一個連結串列指標值寫到前一記錄的連結串列指標,
           這樣就從空閒連結串列中移除了該記錄 */
        _db_writeptr(db, saveoffset, db->ptrval);
        rc = 0;
    }

    /* 結束對空閒連結串列的操作,釋放寫鎖 */
    if(un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("_db_findfree: un_lock error");
    return (rc);  /* 返回狀態碼 */
}

/* 把資料庫重置到“起始狀態” */
void db_rewind(DBHANDLE h)
{
    DB *db = h;
    off_t offset;

    /* 將索引檔案的檔案偏移量定位在索引檔案的第一條索引記錄
      (緊跟在散列表之後) */
    offset = (db->nhash + 1) * PTR_SZ;  

    if((db->idxoff = lseek(db->idxfd, offset+1, SEEK_SET)) == -1)
        err_dump("db_rewind: lseek error"); 
}

/* 返回資料庫的下一條記錄,返回值是指向資料緩衝的指標
   如果key非空,則相應的鍵複製到該緩衝中 */
char *db_nextrec(DBHANDLE h, char *key)
{
    DB *db = h;
    char c;
    char *ptr;

    /* 對空閒連結串列加讀鎖,使得正在讀該連結串列時,其他程序不能刪除 */
    if(readw_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("db_nextrec: readw_lock error");

    do
    {
        if(_db_readidx(db, 0) < 0) /* 讀下一條記錄 */
        {
            ptr = NULL;
            goto doreturn;
        }

        ptr = db->idxbuf;

        /* 可能會讀到已刪除記錄,僅返回有效記錄,所以跳過全空格記錄 */
        while((c = *ptr++) != 0 && c == SPACE);
    }while(c == 0);

    if(key != NULL)  /* 找到一個 有效鍵 */
        strcpy(key, db->idxbuf);

    /* 讀資料記錄,返回值未指向包含資料記錄的內部緩衝的指標值 */
    ptr = _db_readdat(db); 
    db->cnt_nextrec++;

 doreturn:
    if(un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
        err_dump("db_nextrec: un_lock error");
    return (ptr);
}