【1】自己写数据库函数库 — 整体概况

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

这里的长度都是指字符数。


接下来是用户测试函数test.c:

#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;
}


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值