本工程是根据《unix环境高级编程》中第20章的例子写的一个数据库函数库。代码大部分来自于书上,然后结合自己的理解添加了大量注释。
首先是头文件my_db.h,大致浏览一下提供的用户接口:
#ifndef __MY_DB_H__
#define __MY_DB_H__
typedef void* DBHANDLE;
/* 用户接口 */
DBHANDLE db_open(const char *, int, ...); /* 打开数据库 */
void db_close(DBHANDLE); /* 关闭数据库 */
char *db_fetch(DBHANDLE, const char *); /* 取一条记录 */
int db_store(DBHANDLE, const char *, const char *, int); /* 插入或替换操作 */
int db_delete(DBHANDLE, const char *); /* 删除一条记录 */
void db_rewind(DBHANDLE); /* 回滚到数据库第一条记录 */
char *db_nextrec(DBHANDLE, char *); /* 顺序取出每条记录,rewind函数配合使用 */
/* db_store函数的标志位 */
#define DB_INSERT 1 /* 插入 */
#define DB_REPLACE 2 /* 替换 */
#define DB_STORE 3 /* 替换或插入 */
#define IDXLEN_MIN 6 /* 一条索引记录最小长度 */
#define IDXLEN_MAX 1024 /* 一条索引记录最大长度 */
#define DATLEN_MIN 2 /* 一条数据最小长度 */
#define DATLEN_MAX 1024 /* 一条数据最大长度 */
#endif
这里的长度都是指字符数。
#include <stdio.h>
#include <fcntl.h>
#include "my_db.h"
int main()
{
DBHANDLE db; /* 数据库句柄 */
char *str;
char key[IDXLEN_MAX];
/* 打开数据库 */
db = db_open("my_db", O_RDWR | O_CREAT | O_TRUNC);
db_store(db, "a", "hello", DB_INSERT);
db_store(db, "b", "good", DB_INSERT);
db_store(db, "c", "google", DB_INSERT);
db_store(db, "d", "world", DB_INSERT);
db_store(db, "e", "people", DB_INSERT);
db_store(db, "f", "Nestle", DB_INSERT);
// 回滚到第一条索引记录
db_rewind(db);
while ((str = db_nextrec(db, key)) != NULL) /* 遍历数据库,key保存一条记录的键值 */
printf("%s\n", str);
db_close(db);
return 0;
}
在Ubuntu上编译:
gcc -o test test.c my_db.c
运行结果如下:
可以看到,此测试程序成功地打印出了数据库中存放的所有记录。
本数据库使用了一个索引文件和一个数据文件,索引文件负责存储散列表(采用分离链接法)、键值、实际数据偏移值、实际数据长度等信息;数据文件则只存储实际的数据。数据库中的所有寻址操作,都是根据数据在文件中的偏移量来获得的,也就是调用lseek函数。索引文件如下:
第一行中第一个数代表空闲链表,剩余部分表示散列表,表中的每个指针元素占6个字符,每一个数字代表落在此链中的索引记录在索引文件中的偏移量。例如数字829表示该条索引记录在索引文件中的偏移量为829,也就是第二行的起始处,其它以此类推。第二行开头的数字表示该散列链中下一条记录的偏移量,这里为0表示该记录为最后一条记录。继续分析第一条索引记录,跟在指针后面的6(占4个字符宽度)表示本行剩余部分的长度,包括换行符,单位为字节。接下来是a表示键值(长度不固定),分隔符(1字符宽度),数据记录偏移量(长度不固定),分隔符(1字符宽度),数据记录长度(长度不固定),换行符(1字符宽度)。正因索引记录后面的一些域的长度是不固定的,所以才需要一个记录剩余部分长度的域。
数据文件如下:
第一条索引记录就对应第一行的数据"hello\n",根据上面分析的一条索引记录的内容可以看出,实际数据放在了数据文件中正确的位置上(偏移量为0,长度为6)。其它索引记录对应下面的实际数据,原理类似。严格定义每一个字段的字符宽度,加之每个字段在文件中的偏移量,就可以找到任何位置任何字段的信息。这是本数据库所采取的基本方法。
以下是整个函数库的所有源码:
#include <stdio.h>
#include <fcntl.h>
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/uio.h>
#include "my_db.h"
/* 记录所相关函数 */
int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock; /* 定义一个记录所 */
lock.l_type = type; /* 锁类型(F_RDLCK, F_WRLCK, F_UNLCK) */
lock.l_start = offset; /* 从whence处的偏移量 */
lock.l_whence = whence; /* 起始点 */
lock.l_len = len; /* 加锁区域长度 */
/* cmd:
* F_GETLK:测试能够建立一把锁
* F_SETLK:试图建立一把锁,不会阻塞
* F_SETLKW:试图建立一把锁,会阻塞
*/
return (fcntl(fd, cmd, &lock));
}
/* 非阻塞型读锁 */
#define read_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
/* 阻塞型读锁 */
#define readw_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
/* 非阻塞型写锁 */
#define write_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
/* 阻塞型写锁 */
#define writew_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
/* 解锁 */
#define un_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
/* 索引记录常量 */
#define IDXLEN_SZ 4 /* 索引记录长度所占字节数 */
#define SEP ':' /* 分隔符 */
#define SPACE ' ' /* 空格符 */
#define NEWLINE '\n' /* 换行符 */
/* 散列表相关常量 */
#define PTR_SZ 6 /* 指针所占字符数 */
#define PTR_MAX 999999 /* 文件最大偏移量(六位数最大值) */
#define NHASH_DEF 137 /* 散列表大小 */
#define FREE_OFF 0 /* 空链表在索引文件中的偏移量 */
#define HASH_OFF PTR_SZ /* 散列表在索引文件中的偏移量 */
typedef unsigned long DBHASH;
typedef unsigned long COUNT;
/* 记录一个打开数据库的所有信息 */
typedef struct {
int idxfd; /* 索引文件描述符 */
int datfd; /* 数据文件描述符 */
char *idxbuf; /* 实际索引记录,包括键值、数据记录偏移量、数据记录长度 */
char *datbuf;
char *name;
off_t idxoff; /* 当前索引记录的偏移量 */
size_t idxlen; /* 当前索引记录长度 */
off_t datoff; /* 数据文件中该记录的偏移量 */
size_t datlen; /* 该数据记录长度 */
off_t ptrval; /* 下一条索引记录的偏移量 */
off_t ptroff; /* 前一条索引记录的偏移量 */
off_t chainoff; /* 一条散列链的偏移量 */
off_t hashoff; /* 散列表的起始位置 */
DBHASH nhash; /* 散列表大小,这里为137 */
/* 操作成功或失败的记录 */
COUNT cnt_delok;
COUNT cnt_delerr;
COUNT cnt_fetchok;
COUNT cnt_fetcherr; /* 获取数据失败记录 */
COUNT cnt_nextrec;
COUNT cnt_stor1; /* 插入操作情况1 */
COUNT cnt_stor2; /* 插入操作情况2 */
COUNT cnt_stor3; /* 插入操作情况3 */
COUNT cnt_stor4; /* 插入操作情况4 */
COUNT cnt_storerr;
} DB;
/* 内部函数 */
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);
/* 打开或建立一个数据库
* 调用完此函数后,索引文件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]; /* +2表示空字符和换行符 */
struct stat statbuff;
len = strlen(pathname);
if ((db = _db_alloc(len)) == NULL) /* 给db结构体分配空间 */
{
printf("%d\n", __LINE__);
exit(-1);
}
db->nhash = NHASH_DEF; /* 散列表大小 = 137 */
db->hashoff = HASH_OFF; /* 跳过第一个空闲链表,散列表起始位置 = 6 */
if (oflag & O_CREAT) /* 创建文件 */
{
/* 因为参数是在栈内连续存放,所以可以根据可变参数之前的那个参数地址,
* 推算出所有可变参数的地址,从而获得每个可变参数
*/
va_list ap; /* 实质是一个指针 */
va_start(ap, oflag); /* 初始化ap,使它指向可变参数列表中第一个参数 */
mode = va_arg(ap, int); /* 获取可变参数,mode保存有访问权限,知道类型就能够获取值 */
va_end(ap); /* 关闭指针,使它为NULL */
/* 创建文件 */
strcpy(db->name, pathname);
strcat(db->name, ".idx");
db->idxfd = open(db->name, oflag, mode); /* 创建索引文件pathname.idx */
strcpy(db->name + len, ".dat");
db->datfd = open(db->name, oflag, mode); /* 创建数据文件pathname.dat */
}
else /* 打开现有数据库 */
{
strcpy(db->name, pathname);
strcat(db->name, ".idx");
db->idxfd = open(db->name, oflag);
strcpy(db->name + len, ".dat");
db->datfd = open(db->name, oflag);
}
/* 这里一次性检查上面所有的open是否成功,节省了代码空间 */
if (db->idxfd < 0 || db->datfd < 0)
{
/* 打开失败 */
printf("%d\n", __LINE__);
_db_free(db); /* 清理DB结构 */
return NULL; /* 打开失败,返回NULL */
}
/* 注意,要两个条件同时满足 */
if ((oflag & (O_CREAT | O_TRUNC)) == (O_CREAT | O_TRUNC))
{
/* 初始化新创建的数据库 */
/* 长度为0表示锁住整个文件 */
if (writew_lock(db->idxfd, 0, SEEK_SET, 0) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (fstat(db->idxfd, &statbuff) < 0) /* 这个函数一定要上锁 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if (statbuff.st_size == 0)
{
/* 初始化索引文件,*表示占位符,占用PTR_SZ个字符 */
/* 先构造散列表,然后写入索引文件 */
sprintf(asciiptr, "%*d", PTR_SZ, 0); /* asciiptr = "_ _ _ _ _ 0" */
hash[0] = 0; /* 为了让strcat函数覆盖此空字符 */
for (i = 0; i < NHASH_DEF + 1; i++)
strcat(hash, asciiptr); /* strcat会覆盖hash尾部的'\0' */
strcat(hash, "\n"); /* hash和asciiptr尾部都没有空字符 */
i = strlen(hash); /* i = 829 = 138 * 6 + 1 */
if (write(db->idxfd, hash, i) != i) /* 哈希表写入索引文件 */
{
printf("%d\n", __LINE__);
exit(-1);
}
}
if (un_lock(db->idxfd, 0, SEEK_SET, 0) < 0) /* 文件解锁 */
{
printf("%d\n", __LINE__);
exit(-1);
}
}
db_rewind(db); /* 跳过散列表,使db->idxoff指向第一条索引记录 */
return db;
}
/* 分配、初始化DB结构体 */
static DB *_db_alloc(int namelen)
{
DB *db;
if ((db = calloc(1, sizeof(DB))) == NULL)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* calloc将全部成员变成了0,这里重新设置为-1表示此时文件描述符无效 */
db->idxfd = db->datfd = -1; /* _db_free 为了检测是否需要关闭 */
if ((db->name = malloc(namelen + 5)) == NULL) /* +5保存".idx"或".dat"和空字符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if ((db->idxbuf = malloc(IDXLEN_MAX + 2)) == NULL) /* +2保存空字符和换行符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if ((db->datbuf = malloc(DATLEN_MAX + 2)) == NULL) /* +2保存空字符和换行符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
return db;
}
void db_close(DBHANDLE h)
{
_db_free((DB *)h);
}
/* 释放资源和DB结构 */
static void _db_free(DB *db)
{
/* _db_alloc 函数把描述符都设成了-1
* 只要打开了就一定非负
*/
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);
free(db); /* 销毁整个DB结构体 */
}
/* 根据给定的键读取一条记录 */
char *db_fetch(DBHANDLE h, const char *key)
{
DB *db = h;
char *ptr;
/* 参数三:0表示读,1表示写,根据此函数决定加什么锁 */
if (_db_find_and_lock(db, key, 0) < 0)
{
/* 返回-1表示未找到记录 */
ptr = NULL;
db->cnt_fetcherr++;
}
else /* 返回0表示查找成功 */
{
ptr = _db_readdat(db); /* 返回找到的数据 */
db->cnt_fetchok++;
}
if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return ptr;
}
/* writelock:0表示读,1表示写
* 返回值:0查找成功,-1查找失败
*/
static int _db_find_and_lock(DB *db, const char *key, int writelock)
{
off_t offset, nextoffset;
/* 根据散列函数定位桶的起始字符,这里要跳过第一个空闲链表 */
db->chainoff = (_db_hash(db, key) * PTR_SZ) + db->hashoff;
db->ptroff = db->chainoff; /* 同样指向某个桶 */
if (writelock) /* 写锁 */
{
/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
if (writew_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
else /* 读锁 */
{
/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
if (readw_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 知道偏移量,知道指针宽度,atol将指针转换成数字类型 */
offset = _db_readptr(db, db->ptroff); /* 直接读取桶中的元素,即第一个节点的偏移量 */
/* 沿着桶向下查找 */
while (offset != 0) /* 如果为0则该散列链为空 */
{
nextoffset = _db_readidx(db, offset); /* 读取一条索引记录存入db->idxbuf */
/* db->idxbuf = 键值 '\0' 数据记录偏移量 '\0' 数据记录长度 '\0' */
if (strcmp(db->idxbuf, key) == 0)
break;
db->ptroff = offset; /* ptroff记录前一索引记录的地址 */
offset = nextoffset;
}
return (offset == 0 ? -1 : 0); /* offset = 0则没有找到记录 */
}
/* 散列函数 */
static DBHASH _db_hash(DB *db, const char *key)
{
DBHASH hval = 0;
char c;
int i;
/* 以下为散列函数 */
for (i = 1; (c = *key++) != 0; i++)
hval += c * i;
return (hval % db->nhash); /* db->nhash = 137 */
}
/* 读取一个指针并转换为数值型,凡是要获取指针值都要调用这个函数
* 根据offset知道指针所在位置,指针所占字节宽度也已知,就能读取一个指针的值了
* 注意:此函数未进行加锁操作,需要手动进行
*/
static off_t _db_readptr(DB *db, off_t offset)
{
char asciiptr[PTR_SZ + 1];
if (lseek(db->idxfd, offset, SEEK_SET) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (read(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ) /* PTR_SZ = 6,一次读6个字符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
asciiptr[PTR_SZ] = 0; /* 追加一个空字符,为了atol函数辨识结尾处 */
/* atol会跳过前面的空格,遇到非数字或字符串结束时停止转换 */
return (atol(asciiptr));
}
/* 根据索引记录的偏移值offset读取一条索引记录,填充DB结构许多成员
* 返回值:下一条记录的偏移量
*/
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];
/* 保存当前索引记录偏移量
* 如果offset为0,表示从当前偏移量处读
*/
if ((db->idxoff = lseek(db->idxfd, offset, offset == 0 ? SEEK_CUR : SEEK_SET)) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 散布读,第一部分放asciiptr中,第二部分放asciilen中 */
iov[0].iov_base = asciiptr;
iov[0].iov_len = PTR_SZ; /* 6 */
iov[1].iov_base = asciilen;
iov[1].iov_len = IDXLEN_SZ; /* 4 */
if ((i = readv(db->idxfd, &iov[0], 2)) != PTR_SZ + IDXLEN_SZ)
{
/* 返回读取的字节数,结尾返回-1 */
if (i == 0 && offset == 0)
return -1; /* 这个返回值是给函数db_nextrec使用的,_db_find_and_lock永远不可能返回-1 */
else
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 下一条索引记录偏移量 */
asciiptr[PTR_SZ] = 0;
db->ptrval = atol(asciiptr);
/* 当前索引记录长度 */
asciilen[IDXLEN_SZ] = 0;
if ((db->idxlen = atoi(asciilen)) < IDXLEN_MIN || db->idxlen > IDXLEN_MAX)
{
/* 索引记录长度必须在6~1024字节之间 */
printf("%d\n", __LINE__);
exit(-1);
}
/* 下面读取实际的索引记录,文件指针已经在调用readv后指向正确位置,即key开头 */
if ((i = read(db->idxfd, db->idxbuf, db->idxlen)) != db->idxlen)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (db->idxbuf[db->idxlen -1] != NEWLINE)
{
printf("%d\n", __LINE__);
exit(-1);
}
db->idxbuf[db->idxlen - 1] = 0; /* 把换行符替换为空字符,为了atol函数做准备 */
/* 找出分隔符 */
if ((ptr1 = strchr(db->idxbuf, SEP)) == NULL)
{
printf("%d\n", __LINE__);
exit(-1);
}
*ptr1++ = 0; /* 分隔符替换为空字符,ptr1现在指向数据记录的偏移量 */
if ((ptr2 = strchr(ptr1, SEP)) == NULL)
{
printf("%d\n", __LINE__);
exit(-1);
}
*ptr2++ = 0; /* 分隔符替换为空字符,ptr2现在指向数据记录的长度 */
if (strchr(ptr2, SEP) != NULL) /* 只有两个分隔符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 保存实际数据的偏移量 */
if ((db->datoff = atol(ptr1)) < 0) /* atol遇到空字符结束 */
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 保存实际数据的长度,范围必须在2~1024字节之间 */
if ((db->datlen = atol(ptr2)) <= 0 || db->datlen > DATLEN_MAX)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* db->idxbuf = "key \0 datoff \0 datlen \0" */
return (db->ptrval);
}
/* 读取实际数据到缓存db->datbuf中
* 返回值:NULL结尾的实际数据
*/
static char* _db_readdat(DB *db)
{
if (lseek(db->datfd, db->datoff, SEEK_SET) == -1) /* 定位 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if (read(db->datfd, db->datbuf, db->datlen) != db->datlen) /* 读入缓存 */
{
printf("%d\n", __LINE__);
exit(-1);
}
if (db->datbuf[db->datlen - 1] != NEWLINE) /* 数据文件中的一个条必以换行符结尾 */
{
printf("%d\n", __LINE__);
exit(-1);
}
db->datbuf[db->datlen - 1] = 0; /* 换行符替换成空字符 */
return (db->datbuf);
}
/* 根据键值删除一条记录 */
int db_delete(DBHANDLE h, const char *key)
{
DB *db = h;
int rc = 0;
/* 因为可能要修改记录,所以加写锁 */
if (_db_find_and_lock(db, key, 1) == 0)
{
/* 查找成功,执行删除操作 */
_db_dodelete(db);
db->cnt_delok++;
}
else
{
/* 查找失败 */
rc = -1;
db->cnt_delerr++;
}
/* _db_find_and_lock中加了锁,这里要解锁 */
if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return rc;
}
/* 实际的删除记录的函数
* 可由db_delete或db_store函数调用
*/
static void _db_dodelete(DB *db)
{
int i;
char *ptr;
off_t freeptr, saveptr;
/* 将数据缓冲填充空格符,后面在向文件中写的时候还会做调整 */
for (ptr = db->datbuf, i = 0; i < db->datlen - 1; i++)
*ptr++ = SPACE;
*ptr = 0;
/* 将索引记录填充空格符,后面在向文件中写的时候还会做调整
* _db_find_and_lock函数调用了_db_readidx函数
* db->idxbuf = "key \0 datoff \0 datlen \0"
*/
ptr = db->idxbuf;
while (*ptr)
*ptr++ = SPACE;
/* db->idxbuf = "_ _ _ _ \0 datoff \0 datlen \0" */
/* 锁住空闲链表,防止多个进程同时删除从而影响空闲链表 */
if (writew_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 向数据文件中写入空白行,_db_readidx中已经设置好了db->datoff
* 这里无需加锁,因为db_delete对这条散列链加了写锁
* db->datbuf = "_ _ _ _ _ _ '\0'"
*/
_db_writedat(db, db->datbuf, db->datoff, SEEK_SET);
freeptr = _db_readptr(db, FREE_OFF); /* freeptr = 0 */
saveptr = db->ptrval; /* 下一条索引记录的偏移量 */
/* 修改要删除条目的索引记录,使它的指向下一条记录的指针指向空闲链表的头节点freeptr
* 相当于在空闲链表头插入要删除的条目
* db->idxbuf = "_ _ _ _ \0 datoff \0 datlen \0"
*/
_db_writeidx(db, db->idxbuf, db->idxoff, SEEK_SET, freeptr);
/* 修改空闲链表,使之指向被删除条目,idxoff指向一条索引记录开头 */
_db_writeptr(db, FREE_OFF, db->idxoff);
/* 使上一条记录ptroff跳过要删除记录,指向下一条记录saveptr */
_db_writeptr(db, db->ptroff, saveptr);
if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 向数据文件中写入数据,以换行符结尾
*/
static void _db_writedat(DB *db, const char *data, off_t offset, int whence)
{
struct iovec iov[2];
static char newline = NEWLINE;
if (whence == SEEK_END) /* 追加,则锁住整个数据文件 */
if (writew_lock(db->datfd, 0, SEEK_SET, 0) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
if ((db->datoff = lseek(db->datfd, offset, whence)) == -1) /* 在数据文件中定位 */
{
printf("%d\n", __LINE__);
exit(-1);
}
db->datlen = strlen(data) + 1; /* +1容纳换行符 */
/* 聚集写
* 写入到文件中的数据,要以换行符结尾
*/
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)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (whence == SEEK_END)
if (un_lock(db->datfd, 0, SEEK_SET, 0) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 向索引文件写一条索引记录 */
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;
/* ptrval是下一条记录的偏移量 */
if ((db->ptrval = ptrval) < 0 || ptrval > PTR_MAX)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (sizeof(off_t) == sizeof(long long))
fmt = "%s%c%lld%c%d\n";
else
fmt = "%s%c%ld%c%d\n";
/* datoff和datlen已经在_db_writedat函数中被设置好了
* 实际上,被删除的索引记录依然指向原来的数据,但key部分已经变为空了
*/
sprintf(db->idxbuf, fmt, key, SEP, db->datoff, SEP, db->datlen); /* 索引记录后半部分 */
if ((len = strlen(db->idxbuf)) < IDXLEN_MIN || len > IDXLEN_MAX)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* ptrval为下一条记录的偏移量 */
sprintf(asciiptrlen, "%*ld%*d", PTR_SZ, ptrval, IDXLEN_SZ, len); /* 索引记录前半部分 */
if (whence == SEEK_END) /* 追加,则需要加写锁 */
/* 跳过空闲链表和散列表,锁定下一行至结尾 */
if (writew_lock(db->idxfd, ((db->nhash + 1) * PTR_SZ) + 1, SEEK_SET, 0) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
if ((db->idxoff = lseek(db->idxfd, offset, whence)) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 聚集写 */
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)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (whence == SEEK_END)
if (un_lock(db->idxfd, ((db->nhash + 1) * PTR_SZ) + 1, SEEK_SET, 0) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 将一个链表指针写入索引文件 */
static void _db_writeptr(DB *db, off_t offset, off_t ptrval)
{
char asciiptr[PTR_SZ + 1];
if (ptrval < 0 || ptrval > PTR_MAX)
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 数值型转换成字符型写入缓冲区 */
sprintf(asciiptr, "%*ld", PTR_SZ, ptrval);
/* 写之前的定位 */
if (lseek(db->idxfd, offset, SEEK_SET) == -1)
{
printf("%d\n", __LINE__);
exit(-1);
}
if (write(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ)
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 将一条记录写到数据库中
* 返回值:
* 0 —— 成功
* 1 —— 记录已存在
* -1 —— 错误
*/
int db_store(DBHANDLE h, const char *key, const char *data, int flag)
{
DB *db = h;
int rc, keylen, datlen;
off_t ptrval;
/* 先验证flag的值 */
if (flag != DB_INSERT && flag != DB_REPLACE && flag != DB_STORE)
{
errno = EINVAL;
return -1;
}
keylen = strlen(key);
datlen = strlen(data) + 1; /* +1保存换行符 */
if (datlen < DATLEN_MIN || datlen > DATLEN_MAX) /* 数据长度必须在2~1024字节之间 */
{
printf("%d\n", __LINE__);
exit(-1);
}
/* 先查看记录是否已存在 */
if (_db_find_and_lock(db, key, 1) < 0)
{
/* 记录不存在 */
if (flag == DB_REPLACE)
{
rc = -1; /* 出错 */
db->cnt_storerr++;
errno = ENOENT;
goto doreturn;
}
/* 指向一条散列链的开头,chainoff已经在_db_find_and_lock函数中设置妥当 */
ptrval = _db_readptr(db, db->chainoff);
/* 在空闲链表中搜索一条已删除的记录
* 它的键长度和数据长度跟传入的参数相等
*/
if (_db_findfree(db, keylen, datlen) < 0)
{
/* 情况1 */
/* 没有找到这样的空闲记录,则要添加新的记录到这条散列链末尾
* 注意这里的顺序,是先写数据文件,设置好datoff和datlen之后再写索引文件
*/
_db_writedat(db, data, 0, SEEK_END);
/* 新记录的下一条记录为ptrval,相当于把新记录插入到散列链开头 */
_db_writeidx(db, key, 0, SEEK_END, ptrval);
/* idxoff已经在_db_writeidx函数中被设置妥当
* 使散列链开头指向新插入的节点
*/
_db_writeptr(db, db->chainoff, db->idxoff);
db->cnt_stor1++;
/* 注意,上述操作确实是在文件末尾添加新索引记录和数据记录
* 但逻辑上,是插入到散列链的开头,这是修改各个指针域的数值来完成的
*/
}
else
{
/* 情况2 */
/* 找到了对应大小的空记录,那么只需修改空记录即可
* _db_findfree函数会从空闲链表移除这条即将被使用的空记录
*/
_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
{
/* 情况3 */
/* 记录已存在 */
if (flag == DB_INSERT)
{
rc = 1; /* 1表示记录已存在 */
db->cnt_storerr++;
goto doreturn;
}
/* 以下执行替换操作 */
if (datlen != db->datlen)
{
/* 新记录长度和已存在记录长度不一样
* 那么会先删除现有记录,然后和情况1一样重新添加一个新的记录
*/
_db_dodelete(db); /* 先删除老记录,放入空闲链表开头 */
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++;
}
else
{
/* 情况4 */
/* 长度相同,直接执行替换操作 */
_db_writedat(db, data, db->datoff, SEEK_SET);
db->cnt_stor4++;
}
}
rc = 0; /* 0表示成功 */
doreturn:
/* _db_find_and_lock加了锁,这里要解锁 */
if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return rc;
}
/* 在空闲链表中查找键长度和数据记录长度跟传入参数相匹配的记录
* 注意,这里只需要比较key长度和datlen,因为另外一些字段长度固定
* 返回值:
* 0 —— 查找成功
* -1 —— 查找失败
*/
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)
{
printf("%d\n", __LINE__);
exit(-1);
}
saveoffset = FREE_OFF;
offset = _db_readptr(db, saveoffset); /* 第一条空闲记录的偏移地址 */
/* 循环遍历空闲链表 */
while (offset != 0)
{
/* 读取索引记录
* db->idxbuf = "key \0 datoff \0 datlen \0"
* strlen函数遇到空字符则停止,所以能够正确的计算key的长度
*/
nextoffset = _db_readidx(db, offset);
if (strlen(db->idxbuf) == keylen && db->datlen == datlen)
break; /* 找到了 */
saveoffset = offset;
offset = nextoffset;
}
if (offset == 0)
rc = -1; /* 没有找到 */
else
{
/* 从空闲链表中删除找到的记录,使上一条记录指向下一条记录
* saveoffset保存了上一条记录的偏移量
* db->ptrval保存了上一条记录的偏移量
*/
_db_writeptr(db, saveoffset, db->ptrval);
rc = 0;
}
/* 解锁空闲链表 */
if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return rc;
}
/* 将索引文件的文件偏移量定位在索引文件的第一条索引记录开头 */
void db_rewind(DBHANDLE h)
{
DB *db = h;
off_t offset;
/* db->nhash = 137 */
offset = (db->nhash + 1) * PTR_SZ; /* +1表示空闲链表 */
if ((db->idxoff = lseek(db->idxfd, offset + 1, SEEK_SET)) == -1) /* +1跳过换行符 */
{
printf("%d\n", __LINE__);
exit(-1);
}
}
/* 返回数据库的下一条记录,返回值指向数据记录
* 对应记录的key放到参数key中,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)
{
printf("%d\n", __LINE__);
exit(-1);
}
do {
if (_db_readidx(db, 0) < 0) /* 0表示从当前位置开始读 */
{
ptr = NULL;
goto doreturn;
}
ptr = db->idxbuf; /* ptr现在指向key字段开头 */
while ((c = *ptr++) != 0 && c == SPACE)
; /* key为空则跳过 */
} while (c == 0); /* 跳过空记录 */
if (key != NULL)
strcpy(key, db->idxbuf); /* 只复制key字段 */
/* db->idxbuf = "key \0 datoff \0 datlen \0" */
ptr = _db_readdat(db); /* 读实际数据 */
db->cnt_nextrec++;
doreturn:
if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0)
{
printf("%d\n", __LINE__);
exit(-1);
}
return ptr;
}