本工程是根据《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; }
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; }