由于这里贴代码效果不太好,而且很多东西讲起来不是很清楚,所以我把自己重写一次,然后加上注释的代码放在这里,有意思的可以看下。

本文基于APUE2e第20章,准确的说是自己对这一章的一个解读,如果需要了解更详细的东西,请参考书本。

本章开发的函数库类似于ndbm函数库,但是增加了并发控制机制,从而允许多进程同时更新同一数据库,主要接口如下

typedef void DBHANDLEDBHANDLE db_open(const char pathname, int, …); //用来打开数据库
void db_close(DBHANDLE db); //用来关闭数据库
char db_fetch(DBHANDLE db, const char key); //用来取特定数据库中特定key所对应的数据
int db_store(DBHANDLE db, const char key, const char data, int flag); //用来更新数据库(插入或者更新)
int db_delete(DBHANDLE db, const char* key); //用来删除指定数据库中key所对应的记录
void db_rewind(DBHANDLE db); //回滚到数据库的第一条记录

char db_nextrec(DBHANDLE db, char key); //取下一条记录(不保证访问顺序,只保证每条记录访问一次)

这里给的程序把索引和数据单独存在不同的文件里面。分别对应为 pathname.idx 和 pathname.dat ,对于组织索引,常用的方法有散列(又叫哈希)和B+树,这里用的是固定大小的散列,并采用链表来解决散列冲突(类似于图的邻接表),大致关系图如下:

上图中给出了索引文件的格式,以及索引文件怎么和数据文件结合起来的。通过这张图对数据库函数库的整体有一个了解。

对于有多个进程访问同一数据库时,有两种方法可实现库函数:(1)集中式:由一个进程作为数据库管理者,所有的数据库访问工作由此进程完成。其他进程通过IPC机制与此中心进程进行联系。(2)非集中式:每个库函数独立申请并发控制(加锁),然后自己调用I/O函数。使用第一种方法,需要使用IPC,不过可以控制不同进程的优先级,另外在出错的情况下也更容易进行复原。这里使用的是第二种方法。

因为我们使用了索引文件数据文件两个文件,所以在加锁的情况下,就有两种情况:(1)粗锁:对其中一个文件上锁,然后控制整个过程,这样的缺点是限制了最大程度的并发,因为不能有多个进程同时对数据库进行只读访问。(2):细锁:如果对数据库进行读/写访问的时候,先获得数据所在散列链的读锁/写锁,允许对同一条散列链有多个读进程,但只能有一个写进程。一个写进程在操作空闲链表前,必须获得空闲链表的写锁。当 db_store 向索引文件或数据文件末尾追加一条新纪录时,必须获得对应文件相应区域的写锁。

接下来是源代码的分析,借鉴本书的注释,另外加上自己的理解,争取把整个程序讲清楚。

1. 如果我们想在C里面实现私有函数(类似于C++的 private 函数,那么可以用 static,这样函数就只能在本文件里访问了),上面我们提供了7个对数据库进行操作的函数,当然我们希望这7个函数通过调用其他辅助函数来完成实际的工作,这样能够更好的实现模块化和重用。但是辅助函数我们希望只能够在本文件进行访问,这里就可以用 static 来进行控制了。

2. 需要说明的是DB结构,该结构用来记录一个打开数据库的所有信息

typedef struct {
int idxfd / 记录文件的文件描述符 /
int datfd / 数据文件的文件描述符 /
char idxbuf / 为单条记录信息分配的内存 /
char datbuf / 为单条数据信息分配的内存/
char name / 当前打开的数据库(有 .idx/.dat 后缀) 用来打开对应的文件/
off_t idxoff / 索引文件中索引记录的偏移量 /
/ key is at (idxoff + PTR_SZ + IDXLEN_SZ) 其中PTR_SZ标识链表指针的字节数,IDXLEN_SZ标识索引记录长度,见上图/
size_t idxlen / 索引记录长度 /
/ 从key开始到’\n’结尾 具体的见上图 /
off_t datoff / 当前数据记录的偏移量 /
size_t datlen / 当前数据记录长度,包括后面的’\n’/
off_t ptrval / contents of chain ptr in index record /
off_t ptroff / 该条记录链表(已散列)中下一条记录的偏移量 /
off_t chainoff / 该条记录散列之后所对应的链表的偏移量 /
off_t hashoff / 散列表的偏移量 /
DBHASH nhash / 当前的散列表大小,DBHASH是unsigned long的typedef /
COUNT cnt_delok / delete OK, COUNT是unsigned long的typedef 这些COUNT是统计效率用的,比如删除成功多少次,失败多少次/
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

接下来就是打开数据库函数 db_open, 这个函数负责分析调用参数(oflag 是否带 O_CREAT ), 然后打开相应的索引文件和数据文件, 利用一个私有函数 _db_alloc(int) 该函数负责分配一个DB结构所需要的内存,然后返回给调用者。然后打开之后我们需要对数据库进行初始化,这里需要进行加锁,如果不加锁的话,可能会导致数据出错(多进程同时访问一个数据库)。代码如下:

if ((oflag (O_CREAT | O_TRUNC)) == (O_CREAT | O_TRUNC)) { //如果有O_CREAT 和 O_TRUNC 标识
/
If the database was created, we have to initialize
it. 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”);

if (statbuff.st_size == 0) { //如果索引文件长度为0
/
We have to build a list of (NHASH_DEF + 1) chain
ptrs with a value of 0. The +1 is for the free
list pointer that precedes the hash table.
/
sprintf(asciiptr, “%d”, PTR_SZ, 0); //全部设置为0, 其中 %*d, 标识一共占PTR_SZ个位置,值为0
hash[0] = 0
for (i = 0 i < NHASH_DEF + 1; i++) //所有的散列链都置为0, 标识所有散列链都没有数据
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_close(DBHANDLE h), 这个函数调用了一个内部函数 _db_free(DB*); 对打开的文件描述符进行关闭(在初始化的时候把文件描述符置为-1,在这里就派上用场了),释放分配的内存。 _db_free 函数在其他很多地方也会被调用,比如数据库打开出错的情况下,需要释放分配的内存,然后返回出错。

接下来是 db_fetch(DBHANDLE, const char) 函数,这个函数返回 指定数据库的特定 key 值的数据项,在这里调用 _db_find_and_lock(DB, const char key, int writelock) 函数进行加锁以及查询, 如果查询成功,那么就使用 _db_readdat(DB)进行数据读取。在处理完成之后,需要在 db_fetch 函数中对加锁文件进行解锁

_db_find_and_lock(DB db, const char key, int writelock) 对指定指定索引进行查询,并加锁。如果 writelock 非0则加写锁,否则加读锁。这里的锁只加在 key 散列之后所在的散列链上(这样运行不同的进程同时访问不同的散列链,从而增加并发性)。 然后调用 _db_readptr(DB*, off_t offset) 得到散列链中的第一个指针,如果这个函数返回0, 表示散列链为空。然后对散列链进行遍历,一查看是否存在一条需要查询的记录。

接下来是 db_delete() 函数,首先进行加锁并查找, 如果查找成功,则用 _db_dodelete() 进行删除, 最后不管成功与否,都需要对加锁的数据段进行解锁。因为可能需要更改散列链,所以这里加的是一把写锁。

_db_dodelete 函数用来实际进行删除操作。操作过程中更新空闲链表以及对应的散列链。更新索引文件和数据为空格(这里在后面的 db_nextrec 会用到)

_db_writedate 实际进行一个数据的更新,把数据写到相应的内存位置,如果是有 db_store 进行调用,且是追加数据的话,需要对文件进行加写锁。

_db_writeidx 更新索引数据,与上一个函数类似

_db_writeptr 将一个链表指针写至索引文件中。

db_store() 对数据库进行操作,插入,更新。这里面首先需要进行加锁,查找。然后分为四种情况:第一种没有查找到,所以需要添加记录,添加的时候,通过查找我们以前删除过的记录,它的键长度和数据长度与当前的键长度和数据长度一致,如果没有找到,就将这条数据添加到索引文件和数据文件的末尾,然后更新数据,索引部分。第二种,我们在以前删除过的记录中查找到了,那么直接重用就行了。第三种,替换已有数据,且新记录的长度和已有记录的长度不一样,那么直接删除旧数据(前面已经有了),再添加新数据就行了。第四种,新纪录的长度和已有记录的长度一样,直接更新记录即可。

_db_findfree(DB* db, int keylen, int datlen) 查找一块已经删除了的指定大小的数据块。通过遍历空闲链表,查找合适的数据块,找到就从空闲链表中删除,找不到的话就返回一个出错信息。

db_rewind() 将索引文件的文件偏移量置为第一条索引记录

db_nextrec() 遍历索引文件,返回下一条记录。返回的是一个只想DB结构里面的内存区域。

Comments