Permalink: 2014-10-14 18:53:04 by ning in misc tags: all

我们经常纠结这种代码:

some_small_struct *ptr=(some_small_struct *) malloc(sizeof(some_small_struct));
ptr->some_member= ...;
  • 正方: 检查有用:
    • 如果不检查, 没内存时会出现非预期行为.
  • 反方: 检查没用:
    • 因为这是一个小程序, 基本不可能超内存, 我们还需要检查malloc是否成功么?
    • linux 上, 如果打开了 overcommit, 基本上所有的alloc都会返回说有内存, 等到真正写到这片的时候才可能通过oom-killer 杀掉某个(也许是其它)进程.
    • linux上, 如果打开了部分swap, alloc通常也会返回有内存,
    • 就算我们检查, 我们也不能避免栈空间不够之类的错误.

overcommit参考 man malloc:

http://linux.die.net/man/3/malloc

By default, Linux follows an optimistic memory allocation strategy. This means that when malloc() returns non-NULL there is no guarantee that the memory really is available. This is a really bad bug. In case it turns out that the system is out of memory, one or more processes will be killed by the infamous OOM killer.

但是这时候malloc也是可能返回NULL的, 比如 address space is full.

1   我们想要什么

我们的期望分成4级:

  1. 没内存时, 程序依然能够正常工作, 比如http服务器能拒绝掉部分请求而保证另一部分请求正常.

  2. 没内存时, 优雅退出, 防止: - 因为文件未关闭造成数据丢失 (如果我们写了文件, 未关闭, 操作系统会保证sync到磁盘么 -- 应该是不能保证) - socket未关闭导致对端长等待. - 比如是一个文件编辑器, 需要保存用户的工作先.

  3. 没内存时, 通过ASSERT显示core掉

  4. 发生非预期异常(可能core).
    • 比如alloc了一个NULL, 但是不是alloc的时候报错, 而是往里面写东西的时候报错, 就很难追查了.

很显然, 1是最好的情况, 是一个有尊严的程序员所期望的, 4是一定不能发生的. 2,3是需要权衡的.

3 是基线, 做到3可能保证出错时知道原因.

其它:

  • 对嵌入式系统: 内存有限, 所以应该总是检查
  • 对c++: 尽量使用new, 这样有exception.

2   现有程序做法

我们看看一些有名程序的做法

2.1   mysql

多数地方都做到1:

HP_INFO *heap_open_from_share(HP_SHARE *share, int mode)
{
  HP_INFO *info;
  DBUG_ENTER("heap_open_from_share");

  if (!(info= (HP_INFO*) my_malloc((uint) sizeof(HP_INFO) +
                  2 * share->max_key_length,
                  MYF(MY_ZEROFILL))))
  {
    DBUG_RETURN(0);
  }

有的地方只能做到4:

storage/innobase/handler/ha_innodb.cc:

    field_lengths = (ulint*) my_malloc(sizeof(ulint) * n_fields,
            MYF(MY_FAE));

    namebuf = (char*) my_malloc((uint) len + 2, MYF(0));
    memcpy(namebuf, ptr, len);

innobase_rename_table(
    ...
    norm_to = (char*) my_malloc(strlen(to) + 64, MYF(0));
    norm_from = (char*) my_malloc(strlen(from) + 64, MYF(0));

storage/myisam/myisampack.c:

static my_bool open_isam_files(PACK_MRG_INFO *mrg, char **names, uint count)
{
  mrg->file=(MI_INFO**) my_malloc(sizeof(MI_INFO*)*count,MYF(MY_FAE));

2.2   redis

做到3:

void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
    block = zmalloc(sizeof(*block));
    block->free = AOF_RW_BUF_BLOCK_SIZE;

不过在zmalloc里面做到了assert:

static void zmalloc_default_oom(size_t size) {
    fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
        size);
    fflush(stderr);
    abort();
}

static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;

void *zmalloc(size_t size) {
    void *ptr = malloc(size+PREFIX_SIZE);

    if (!ptr) zmalloc_oom_handler(size);

但是在cli等不重要代码里面做到4:

static void cliInitHelp() {
    tmp.argv = malloc(sizeof(sds));
    tmp.argv[0] = sdscatprintf(sdsempty(),"@%s",commandGroups[i]);

2.3   nginx

alloc 里面打日志, 不是ASSERT (不过ngx_log_error里面应该还会malloc):

void *
ngx_alloc(size_t size, ngx_log_t *log)
{
    void  *p;

    p = malloc(size);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
                      "malloc(%uz) failed", size);
    }

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);

    return p;
}

使用时检查(90%的地方都有检查):

env = ngx_palloc(cycle->pool, (n + 1) * sizeof(char *));
if (env == NULL) {
    return NULL;
}

overflow_list = ngx_alloc(sizeof(struct pollfd) * rtscf->overflow_events,
                          cycle->log);
if (overflow_list == NULL) {
    return NGX_ERROR;
}

也有不检查:

ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
    var = ngx_alloc(sizeof(NGINX_VAR)
                    + cycle->listening.nelts * (NGX_INT32_LEN + 1) + 2,
                    cycle->log);

    p = ngx_cpymem(var, NGINX_VAR "=", sizeof(NGINX_VAR));

2.5   twemproxy

和nginx一样:

void *
_nc_alloc(size_t size, const char *name, int line)
{
    void *p;

    ASSERT(size != 0);

    p = malloc(size);
    if (p == NULL) {
        log_error("malloc(%zu) failed @ %s:%d", size, name, line);
    } else {
        log_verb("malloc(%zu) at %p @ %s:%d", size, p, name, line);
    }

    return p;
}

3   小结

  • nginx 和mysql, redis 做的差不多, 尽量检查.

  • 推荐的做法:
    • alloc里面如果有NULL打日志, 上层每次检查, 发现NULL处理错误
    • 对小的alloc, alloc 后写ASSERT.

Comments