Berkeley DB 4の1.85互換API

FreeBSDのdbopen(3)はBerkeley DBバージョン1.85のAPIである。このAPIはBerkeley DBバージョン4系においても「バージョン1.85互換API」として利用可能だが、いくつかの点で両者の振る舞いが異なることがわかった。

以下の実験は、次の環境でそれぞれ動作を確認した。


1. dbopen(3)にO_CREATとO_RDONLYを同時に指定したときの振る舞い

dbopen(3)で、第1引数のデータベースファイルに存在しないパスを指定し、第2引数のフラグにO_RDONLYO_CREATを指定してみる。これは、データベースファイルを読み込み専用でオープンするが、指定したファイルが存在しないときは第3引数のモードで生成する、という意味である。

バージョン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

バージョン4.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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;
}

次のようにコンパイルして実行すると、エラーになることがわかる。しかも、空のファイルは生成されない。

% 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を指定する。

バージョン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

バージョン4.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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;
}

次のようにコンパイルして実行すると、クラッシュすることがわかる。おそらく、第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引数のflagsR_SETCURSORを指定して、格納したペアの位置にカーソルを設定する。

さらに、カーソルが正しく設定されたことを確認するため、seq()の第4引数にR_NEXTを指定して、カーソルの位置の次のペアを取得しようとすると、seq()が1を返すか確認する(カーソルが設定されていなければ、R_FIRSTを指定したのと同じことになるので、seq()は0を返す)。

バージョン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/src/lib/libc/db/btree/bt_put.c__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行目でflagsR_SETCURSORを比較しているくらいなので、説明が間違っているというよりも、caseが1つ不足しているだけのバグなのかもしれない。

バージョン4.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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;
}

次のようにコンパイルして実行すると、正しく実行できることがわかる。バージョン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引数flagsR_CURSORを指定して、カーソルの位置にキーとデータのペアを格納する。

バージョン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.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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 -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を返し、errnoENOENTを設定することを確認する。

バージョン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
% 

バージョン4.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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 -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にを設定される値を確認する。

バージョン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
% 

バージョン4.5の場合

今度は同様のプログラムをバージョン4.5用に用意する。といっても、1行目でdb.hの代わりにdb_185.hをインクルードするように変更しただけで、残りは同じである。

% cat test-db4.c
#include <db_185.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には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()でも同様の結果になる。