Berkeley DB 4の1.85互換API
FreeBSDのdbopen(3)
はBerkeley DBバージョン1.85のAPIである。このAPIはBerkeley DBバージョン4系においても「バージョン1.85互換API」として利用可能だが、いくつかの点で両者の振る舞いが異なることがわかった。
以下の実験は、次の環境でそれぞれ動作を確認した。
- FreeBSD 6.2: OS付属のBerkeley DBバージョン1.85と、バージョン4.5(
db45-4.5.20.0
) - openSUSE 10.3:
db1-devel-1.85-161
と、libdb-4_4-devel-4.4.20-28
1 dbopen(3)にO_CREATとO_RDONLYを同時に指定したときの振る舞い
dbopen(3)
で、第1引数のデータベースファイルに存在しないパスを指定し、第2引数のフラグにO_RDONLY
とO_CREAT
を指定してみる。これは、データベースファイルを読み込み専用でオープンするが、指定したファイルが存在しないときは第3引数のモードで生成する、という意味である。
1.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdlib.h>
#include <fcntl.h>
#include <err.h>
int main(void)
{
DB *db;
if ((db = dbopen("./test.db", O_CREAT | O_RDONLY, 0666, DB_BTREE, NULL))
== NULL) {
err(1, "dbopen() failed");
}
exit(0);
return 0;
}
次のようにコンパイルして実行すると、カレントディレクトリに0バイトのファイルtest.db
が生成される。
% gcc -Wall -W test-db1.c
% rm -f test.db
% ./a.out
% wc test.db
0 0 0 test.db
1.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、エラーになることがわかる。しかも、空のファイルは生成されない。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% rm -f test.db
% ./a.out
a.out: dbopen() failed: Invalid argument
% wc test.db
wc: test.db: open: No such file or directory
2 del()の第3引数にR_CURSORを指定したとき、第2引数を評価するかどうかの振る舞い
btreeのデータベースに対し、seq()
で最初から最後まで順番にキーとデータのペアを取り出しながら、del()
の第3引数にR_CURSOR
を指定して、ペアを削除していく。最終的には、すべてのキーとデータのペアが削除され、データベースは空になる。このとき、del()
の第2引数に指定するキーは利用されないので、第2引数にはNULL
を指定する。
2.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdlib.h>
#include <fcntl.h>
#include <err.h>
static int putString(DB *db, const char *s, DBT *val)
{
DBT key;
key.data = (char *)s;
key.size = strlen(s) + 1;
return db->put(db, &key, val, 0);
}
int main(void)
{
DB *db;
DBT key, val;
int s;
if ((db = dbopen(NULL, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
val.data = NULL;
val.size = 0;
if (putString(db, "alpha", &val) < 0
|| putString(db, "beta", &val) < 0
|| putString(db, "gamma", &val) < 0) {
err(1, "db#put() failed");
}
for (s = db->seq(db, &key, &val, R_FIRST); s == 0;
s = db->seq(db, &key, &val, R_NEXT)) {
if (db->del(db, NULL, R_CURSOR) != 0) {
err(1, "db#del() failed");
}
}
if (db->close(db) < 0) {
err(1, "db#close() failed");
}
exit(0);
return 0;
}
次のようにコンパイルして実行すると、正常に終了する。
% gcc -Wall -W test-db1.c
% ./a.out
2.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、クラッシュすることがわかる。おそらく、第2引数を参照しているようである。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% ./a.out
Segmentation fault (core dumped)
次のように第2引数に参照しても問題のないキーを指定すると、クラッシュすることはなくなる。また、バージョン1.85でも動作する。
% cat test-db4-fix.c
...
for (s = db->seq(db, &key, &val, R_FIRST); s == 0;
s = db->seq(db, &key, &val, R_NEXT)) {
key.data = NULL;
key.size = 0;
if (db->del(db, &key, R_CURSOR) != 0) {
err(1, "db#del() failed");
}
}
...
3 put()の第4引数にR_SETCURSORを指定したときの振る舞い
btreeのデータベースに対し、put()
でキーとデータのペアを格納する。このとき、第4引数のflags
にR_SETCURSOR
を指定して、格納したペアの位置にカーソルを設定する。
さらに、カーソルが正しく設定されたことを確認するため、seq()
の第4引数にR_NEXT
を指定して、カーソルの位置の次のペアを取得しようとすると、seq()
が1を返すか確認する(カーソルが設定されていなければ、R_FIRST
を指定したのと同じことになるので、seq()
は0を返す)。
3.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <err.h>
static void setString(DBT *t, const char *s)
{
t->data = (char *)s;
t->size = strlen(s) + 1;
}
int main(void)
{
DB *db;
DBT key, val;
if ((db = dbopen(NULL, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
setString(&key, "foo");
setString(&val, "bar");
if (db->put(db, &key, &val, R_SETCURSOR) < 0) {
err(1, "db#put() failed");
}
if (db->seq(db, &key, &val, R_NEXT) != 1) {
err(1, "db#seq() failed");
}
if (db->close(db) < 0) {
err(1, "db#close() failed");
}
exit(0);
return 0;
}
次のようにコンパイルして実行すると、put()
がEINVAL
で失敗する。
% gcc -Wall -W test-db1.c
% ./a.out
a.out: db#put() failed: Invalid argument
%
FreeBSDのソースコードでput()
の動作を確認してみる。/usr
の__bt_put()
は次のようになっている。
...
68 int
69 __bt_put(dbp, key, data, flags)
70 const DB *dbp;
71 DBT *key;
72 const DBT *data;
73 u_int flags;
74 {
...
99 switch (flags) {
100 case 0:
101 case R_NOOVERWRITE:
102 break;
103 case R_CURSOR:
104 /*
105 * If flags is R_CURSOR, put the cursor. Must already
106 * have started a scan and not have already deleted it.
107 */
108 if (F_ISSET(&t->bt_cursor, CURS_INIT) &&
109 !F_ISSET(&t->bt_cursor,
110 CURS_ACQUIRE | CURS_AFTER | CURS_BEFORE))
111 break;
112 /* FALLTHROUGH */
113 default:
114 errno = EINVAL;
115 return (RET_ERROR);
116 }
...
248 success:
249 if (flags == R_SETCURSOR)
250 __bt_setcur(t, e->page->pgno, e->index);
251
252 F_SET(t, B_MODIFIED);
253 return (RET_SUCCESS);
254 }
したがって、R_SETCURSOR
は必ずEINVAL
になるようだ(dbopen(3)
の説明と実装は異なる)。ただし、249行目でflags
とR_SETCURSOR
を比較しているくらいなので、説明が間違っているというよりも、case
が1つ不足しているだけのバグなのかもしれない。
3.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、正しく実行できることがわかる。バージョン1.85にあったバグは修正されているようだ。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% ./a.out
%
4 put()の第4引数にR_CURSORを指定したときの振る舞い
btreeのデータベースに対し、いくつかのキーとデータのペアを格納し、そのいずれかのペアにカーソルをセットしておく。次に、put()
の第4引数flags
にR_CURSOR
を指定して、カーソルの位置にキーとデータのペアを格納する。
4.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <err.h>
static void putString(DB *db, const char *k, const char *v, int flags)
{
DBT key, val;
key.data = (char *)k;
key.size = strlen(k) + 1;
val.data = (char *)v;
val.size = strlen(v) + 1;
if (db->put(db, &key, &val, flags) != 0) {
err(1, "db#put() failed");
}
}
int main(void)
{
DB *db;
DBT key, val;
int s;
if ((db = dbopen(NULL, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
putString(db, "a", "alpha", 0);
putString(db, "b", "beta", 0);
putString(db, "c", "gamma", 0);
if (db->seq(db, &key, &val, R_FIRST) != 0
|| db->seq(db, &key, &val, R_NEXT) != 0) {
err(1, "db#seq() failed");
}
putString(db, "d", "delta", R_CURSOR);
for (s = db->seq(db, &key, &val, R_FIRST); s == 0;
s = db->seq(db, &key, &val, R_NEXT)) {
printf("%s:%s\n", (char *)key.data, (char *)val.data);
}
if (db->close(db) < 0) {
err(1, "db#close() failed");
}
exit(0);
return 0;
}
次のようにコンパイルして実行すると、結果が表示される。
% gcc -Wall -W test-db1.c
% ./a.out
a:alpha
d:delta
c:gamma
%
カーソルがあった2番目のペアに上書きされることは確認できた。しかし、キーの順序性が破壊されている。R_CURSOR
を指定するput
を呼び出す直前では、データベースは次のようになっていた。
# CURSOR KEY VALUE
- ------ --- -----
1 a alpha
2 -> b beta
3 c gamma
ここでput()
を呼び出し、カーソルの位置に「キーがd
、データがdelta
」のペアを上書きすることで、データベースは次のようになる。
# CURSOR KEY VALUE
- ------ --- -----
1 a alpha
2 -> d delta
3 c gamma
このように順序性が破壊されると、正しくデータベースを操作できなくなる。この例では、「キーがc
、データがgamma
」のペアをget()
でデータベースから取り出すことができなくなる。ただし、順序性が破壊されても、seq()
で先頭から順番にペアを取り出せば、すべてのペアを取得できるようである。また、カーソルに対する操作も正しく実行できるようだ。
4.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、結果が表示される。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% ./a.out
a:alpha
b:delta
c:gamma
%
カーソルがあった2番目のペアのうち、データだけが置き換わるようだ。したがって、順序性は破壊されない。また、このときput()
の第2引数の値は利用されないようだ(ただし第2引数にNULL
を渡すとクラッシュする)。
5 fd()の振る舞い
dbopen(3)
の第1引数にNULL
を指定してデータベースを作成したあと、fd()
でファイルディスクリプタを取得してみる。fd()
が−1を返し、errno
にENOENT
を設定することを確認する。
5.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdlib.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
int main(void)
{
DB *db;
if ((db = dbopen(NULL, O_CREAT | O_RDWR, 0666, DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
if (db->fd(db) != -1) {
err(1, "db#fd() failed");
}
if (errno != ENOENT) {
errx(1, "errno is not ENOENT.");
}
if (db->close(db) < 0) {
err(1, "db#close() failed");
}
exit(0);
return 0;
}
次のようにコンパイルして実行すると、正しく終了することがわかる。
% gcc -Wall -W test-db1.c
% ./a.out
%
5.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、クラッシュすることがわかる。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% ./a.out
Segmentation fault (core dumped)
%
gdbでコアをロードして、バックトレースをみると、Berkeley DBの中でクラッシュしていることがわかる。
% gdb ./a.out ./a.out.core
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd"...(no debugging symbols found)...
Core was generated by `a.out'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/local/lib/libdb-4.5.so.0...(no debugging symbols found)...done.
Loaded symbols for /usr/local/lib/libdb-4.5.so.0
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/libpthread.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/libpthread.so.2
Reading symbols from /libexec/ld-elf.so.1...(no debugging symbols found)...done.
Loaded symbols for /libexec/ld-elf.so.1
#0 0x2815b86e in __os_fsync () from /usr/local/lib/libdb-4.5.so.0
[New LWP 100118]
(gdb) bt
#0 0x2815b86e in __os_fsync () from /usr/local/lib/libdb-4.5.so.0
#1 0x28158330 in __memp_sync_int () from /usr/local/lib/libdb-4.5.so.0
#2 0x2815882a in __mp_xxx_fh () from /usr/local/lib/libdb-4.5.so.0
#3 0x28113d9a in __db_fd_pp () from /usr/local/lib/libdb-4.5.so.0
#4 0x2809de1c in db185_fd () from /usr/local/lib/libdb-4.5.so.0
#5 0x080485cd in main ()
(gdb)
dbopen(3)
の第1引数にNULL
を指定しない場合は、正しく動作するので、これはバージョン4のバグだろう。
6 O_RDONLYでオープンしたデータベースを変更しようとしたときの振る舞い
dbopen(3)
の第2引数にO_RDONLY
を指定してデータベースをオープンし、put()
でデータベースを変更をしてみる。put()
が−1を返すことと、errno
にを設定される値を確認する。
6.1 バージョン1.85の場合
次のBerkeley DBバージョン1.85用のプログラムを用意する。
% cat test-db1.c
#include <db.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
static void
putString(DB *db, const char *k, const char *v)
{
DBT key, val;
key.data = (char *)k;
key.size = strlen(k) + 1;
val.data = (char *)v;
val.size = strlen(v) + 1;
if (db->put(db, &key, &val, 0) != 0) {
err(1, "db#put() failed: %d", errno);
}
}
int main(void)
{
DB *db;
if ((db = dbopen("./test.db", O_CREAT | O_RDWR | O_TRUNC, 0666,
DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
putString(db, "a", "alpha");
putString(db, "b", "beta");
putString(db, "c", "gamma");
if (db->close(db) < 0) {
err(1, "db#close() failed");
}
if ((db = dbopen("./test.db", O_RDONLY | O_EXCL, 0666,
DB_BTREE, NULL)) == NULL) {
err(1, "dbopen() failed");
}
putString(db, "d", "delta");
exit(0);
return 0;
}
次のようにコンパイルして実行すると、errno
にはEPERM
が設定されることがわかる。
% gcc -Wall -W test-db1.c
% ./a.out
a.out: db#put() failed: 1: Operation not permitted
%
6.2 バージョン4.5の場合
今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.h
の代わりにdb_185.h
をインクルードするように変更しただけで、残りは同じである。
% cat test-db4.c
#include <db_185.h>
...
次のようにコンパイルして実行すると、errno
にはEACCES
が設定されることがわかる。
% gcc -Wall -W -I/usr/local/include/db45 -L/usr/local/lib/db45 -ldb test-db4.c
% ./a.out
a.out: db#put() failed: 13: Permission denied
%
put()
に限らず、del()
でも同様の結果になる。