programming in lua (11)

第 29 章和 31 章。这两章都是讲资源管理的,就放一起看了。

userdata 可能是用户自己分配的一段内存(内存里可能有一个域是文件描述符,可能是一个指向另外一段内存的指针,等等),当这段 userdata 被释放时,其中包含的其它资源也需要被释放(如需要关闭文件,释放另一段内存等)。某些语言提供了一种叫做“finalizer”的机制来完成这件事,在 lua 中对应的是一个 __gc 的域,当某个 userdata 被 lua 回收时,如果它对应的 metatable 中有 __gc 这个域,则这个域所指向的函数(通常指向的是函数)将会被调用,这样用户就能释放和 userdata 相关联的内容。

目录遍历迭代器

第 26 章实现过一个目录遍历的程序,程序每次返回一个包含所有目录项的 table。这里我们将要实现一个迭代器,每次只返回一个目录:

for dname in dir(".") do
    print(dname)
end

c 语言中遍历目录的步骤:首先使用 opendir() 函数打开目录返回一个指向 DIR 结构体的指针,然后使用 readdir() 每次读取一个目录,读取完所有目录后使用 closedir() 释放 DIR 指针。在以前的实现中我们在一个 c 函数里获取所有目录,但是在迭代器中每次只获取一个目录。并且在函数中我们不一定每次都是获取所有目录之后才退出,有可能是找到我们需要的目录后就退出。为了能在使用完 DIR 后及时释放,我们为 DIR 指针定义了一个 userdata,并且为其注册了一个 __gc 函数,这样当这个 userdata 被 lua 回收的时候能够及时释放 DIR 的资源。

#include <stdio.h>
#include <errno.h>

#include <sys/types.h>
#include <dirent.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_dir_iter(lua_State* l)
{
    struct dirent* entry;
    DIR* d = *(DIR**)lua_touserdata(l, lua_upvalueindex(1));

    entry = readdir(d);
    if (entry) {
        lua_pushstring(l, entry->d_name);
        return 1;
    }

    return 0; /* no more values to return */
}

static int l_dir(lua_State* l)
{
    DIR** d;
    const char* path = luaL_checkstring(l, 1);

    /* create a userdatum to store a DIR address */
    d = (DIR**)lua_newuserdata(l, sizeof(DIR*));

    /* set its metatable */
    luaL_getmetatable(l, "LuaBook.dir");
    lua_setmetatable(l, -2);

    /* try to open the given directory */
    *d = opendir(path);
    if (*d == NULL) /* error opening the directory? */
        luaL_error(l, "cannot open %s: %s", path, strerror(errno));

    /* creates and returns the iterator function;
     * its sole upvalue, the directory userdatum,
     * is already on the stack top */
    lua_pushcclosure(l, l_dir_iter, 1);

    return 1;
}

static int l_dir_gc(lua_State* l)
{
    DIR* d = *(DIR**)lua_touserdata(l, 1);
    if (d)
        closedir(d);

    fprintf(stderr, "----------> lua dir gc function is called.\n");

    return 0;
}

int luaopen_dir(lua_State* l)
{
    luaL_newmetatable(l, "LuaBook.dir");

    /* set its __gc field */
    lua_pushstring(l, "__gc");
    lua_pushcfunction(l, l_dir_gc);
    lua_settable(l, -3);

    /* register the 'dir' function */
    lua_pushcfunction(l, l_dir);
    lua_setglobal(l, "dir");

    return 0;
}

int main(void)
{
    lua_State* l;

    l = luaL_newstate();
    luaL_openlibs(l);

    luaopen_dir(l);

    if (luaL_loadfile(l, "./dir_iter.lua") != 0) {
        fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    if (lua_pcall(l, 0, 0, 0) != 0)
        fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));

end:
    lua_close(l);
    return 0;
}

和第 26 章实现的程序有几点不同:首先是在 l_dir() 函数中并不是一次获取所有目录项,而是只打开一个 DIR 结构,具体的读取工作在 l_dir_iter 中,每次只读取一个目录项。另外把 DIR 指针作为一个 userdata,这样可以把它和 metatable 关联起来并为其设置 gc 函数。当该 userdata 被回收时就会调用 gc 函数关闭 DIR 结构。

lua 的内存管理

lua 中绝大部分数据结构都是动态分配的,所有结构(table,字符串,函数,线程等)都在 gc(garbage collection)的管理之下。lua 提供了对 gc 的一些调整机制,例如可以设置 lua 分配内存的函数,或者设置 gc 的过程。

内存分配函数

lua 内部使用唯一的内存分配函数来进行内存分配。我们之前使用的 luaL_State() 使用 malloc-realloc-free 作为内存管理函数。函数

lua_State *lua_newstate (lua_Alloc f, void *ud);

可以用来创建一个带有自定义内存分配函数的 lua_State。与返回的 lua_State 相关的所有内存操作都由函数指针 f 指向的函数来完成:

typedef void * (*lua_Alloc) (void *ud,
                             void *ptr,
                             size_t osize,
                             size_t nsize);

lua_Alloc 中的第一个参数就是传递给 lua_newstate() 的第二个参数;第二个参数指向要重新分配或释放的内存;第三个参数是 ptr 指向内存的大小;第四个参数是需要的内存大小。返回值为指向一块 nsize 大小内存的指针。

如果 ptr 不为 NULL,lua 认为它指向的内存块大小就是 osize;如果 ptr 为 NULL 则认为 osize 为 0。当 nsize 为 0 时,ptr 指向的内存会被释放并且返回 NULL。如果 osize 和 nsize 都不为 0,其行为相当于 realloc()。

luaL_newstate() 使用的内存分配函数为:

void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
    if (nsize == 0) {
        free(ptr);
        return NULL;
    }
    else
        return realloc(ptr, nsize);
}

可以通过

lua_Alloc lua_getallocf (lua_State *L, void **ud);

来获取某个 lua_State 对应的内存管理函数,如果 ud 不为空,那么对应的 userdata 的地址保存在 *ud 中。也可以通过

void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);

来动态改变某个 lua_State 的内存管理函数。要注意的是新的函数还要负责释放旧函数分配的内存。这个函数的典型用途是把旧的函数包装一下,添加一些额外的信息作为调试用途。

lua 并不会对空闲的内存进行重复利用(应该是说一块内存使用完了就调用函数把它释放了,不会留着下一次用),这个功能应该是由内存管理函数来实现的。

为每个 lua_State 设置单独的内存管理函数的好处是,当有多个 lua_State 同时申请内存资源时,从各自独立的内存池中申请能避免同步开销。

garbage collector

在 5.0 及以前的版本中,lua 使用一种简单的 mark-and-sweep 方法来清理不再使用的资源(可能是释放内存或关闭不使用的打开的文件)。每次清理的时候都要打断整个解析器的运行。这种方法包括四步:mark,cleaning,sweep和finalization。

  • mark:把 lua 能够直接访问到的 object(registry 和主线程)标记为 alive;存储在这些 object 中的 object(例如在当前脚本中的变量)因为能被程序直接访问,这些 object 也被标记为 alive。当所有能被程序访问到的 object 都标记为 alive 后这一步就结束了;
  • cleaning:首先查找没有被标记到的 userdata,调用 __gc() 函数释放它们,被标记为 alive 的 userdata 则被放到一个单独的链表中供 finalization 时使用;然后 lua 遍历 weak tables,释放没有被标记的 key 或者 value;
  • sweep:遍历所有的 object,把有没被标记的 object 收集起来准备下一阶段释放;被标记的 object 重新设置为未标记,为下一次 mark 阶段做准备(初始的时候所有 object 都是 clean 状态,只有经过 mark 阶段后才能确定哪些是 alive 的)。
  • finalization:没什么好说的,就是调用 __gc() 清理。

从 5.1 版本开始 lua 采用了一种增量的方法,步骤仍然是这四步,但是并不是一次做完,而是每次要申请内存的时候做一部分工作。lua 把每次要完成的部分工作作为一个“原子操作”,这部分操作不能被打断,也就是说执行这部分工作的时候整个 lua 进程还是阻塞的,只是时间短了些,感觉没那么明显。主要的“原子操作”是遍历 table 和 cleaning 阶段。

在遍历 table 的时候可能会花比较长的时间,因此使用 table 的时候应该把元素分成独立的组,然后放到一个 subtable 中(可能每次遍历一个 subtable,不用一次遍历完整个大的 table)。对于在 cleaning 阶段的 weak table 也是一样,如果有大量的 userdata 或者 weak table 中有很多元素也有类似问题。

最后 lua 提供了一些 api 来更精确地控制 gc 的过程,这个不看了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注