gen_tcp接收缓冲区易混淆概念纠正

来源:互联网 发布:篮球服淘宝 编辑:程序博客网 时间:2024/05/16 08:57

Erlang的每个TCP网络链接是由相应的gen_tcp对象来表示的,说白了就是个port, 实现Erlang网络相关的逻辑,其实现代码位于erts/emulator/drivers/common/inet_drv.c

参照inet:setopts文档,它有三个buffer相关的选项,非常让人费解:

{buffer, Size}
Determines the size of the user-level software buffer used by the driver. Not to be confused with sndbuf and recbuf options which correspond to the kernel socket buffers. It is recommended to have val(buffer) >= max(val(sndbuf),val(recbuf)). In fact, the val(buffer) is automatically set to the above maximum when sndbuf or recbuf values are set.

{recbuf, Size}
Gives the size of the receive buffer to use for the socket.

{sndbuf, Size}
Gives the size of the send buffer to use for the socket.

其中sndbuf, recbuf选项比较好理解, 就是设置gen_tcp所拥有的socket句柄的内核的发送和接收缓冲区,从代码可以验证:

 
/* inet_drv.c */
#define INET_OPT_SNDBUF     6   /* set send buffer size */
#define INET_OPT_RCVBUF     7   /* set receive buffer size */
staticintinet_set_opts(inet_descriptor* desc, char* ptr,intlen)
{
...
        caseINET_OPT_SNDBUF:    type = SO_SNDBUF;
            DEBUGF(("inet_set_opts(%ld): s=%d, SO_SNDBUF=%d\r\n",
                    (long)desc->port, desc->s, ival));
            break;
        caseINET_OPT_RCVBUF:    type = SO_RCVBUF;
            DEBUGF(("inet_set_opts(%ld): s=%d, SO_RCVBUF=%d\r\n",
                    (long)desc->port, desc->s, ival));
            break;
...
        res = sock_setopt           (desc->s, proto, type, arg_ptr, arg_sz);
...
}

那buffer是什么呢,他们三者之间的关系? 从文档的描述来看:
It is recommended to have val(buffer) >= max(val(sndbuf),val(recbuf)). In fact, the val(buffer) is automatically set to the above maximum when sndbuf or recbuf values are set.

再对照源码:

 
/* inet_drv.c */
  
#define INET_DEF_BUFFER     1460        /* default buffer size */
#define INET_MIN_BUFFER     1           /* internal min buffer */
#define INET_LOPT_BUFFER      20  /* min buffer size hint */
  
staticintinet_set_opts(inet_descriptor* desc, char* ptr,intlen)
{
...
       caseINET_LOPT_BUFFER:
            DEBUGF(("inet_set_opts(%ld): s=%d, BUFFER=%d\r\n",
                    (long)desc->port, desc->s, ival));
            if(ival < INET_MIN_BUFFER) ival = INET_MIN_BUFFER;
            desc->bufsz = ival;
            continue;
  
        DEBUGF(("inet_set_opts(%ld): s=%d returned %d\r\n",
                (long)desc->port, desc->s, res));
        if(type == SO_RCVBUF) {
            /* make sure we have desc->bufsz >= SO_RCVBUF */
            if(ival > desc->bufsz)
                desc->bufsz = ival;
        }
...
}
  
/* Allocate descriptor */
staticErlDrvData inet_start(ErlDrvPort port,intsize,intprotocol)
{
...
    desc->bufsz = INET_DEF_BUFFER;
...
}

我们从源码看到在实现上inet:setopts的二点要素:
1. make sure we have desc->bufsz >= SO_RCVBUF
2. desc->bufsz min buffer size hint
3. 接收缓冲区默认长度 1460, 刚好是一个mtu长度
4. 最小的缓冲区大小为1
5. bufsz不继承

但是关系还是没搞明白。

好吧,通读inet_drv源码,我们可以看出gen_tcp接收包的流程:

1. 当socket上面有数据的时候,epoll会通知到port,最终导致tcp_inet_drv_input被调用。
2. tcp_inet_drv_input 发现如果连接已建立,就会调用tcp_recv来处理网络封包。
3. tcp_recv在调用sock_recv真正准备接收数据包前:
a. 如果发现接收缓冲区是空的话,会分配一个缓冲区。如果包大小已知,缓冲区大小就是包大小,否则的话为desc->bufsz。
b. 如果缓冲区非空,这时候看看是否已经收了一个以上完整的包,如果是就通过tcp_deliver往上层投递包,投递后如果缓冲区里面除了完整包以外,没有其他数据的话,就会调用tcp_clear_input把输入缓冲区释放掉。
c. 如果缓冲区非空,而且缓冲区的大小无法容纳包的话,就会调用tcp_expand_buffer来把缓冲区扩大到包大小。
4. 接收好网络封包后,根据packet类型进行进一步整理,投递给上层,同时释放接收缓存区。

/* clear CURRENT input buffer */
staticvoidtcp_clear_input(tcp_descriptor* desc)
{
    if(desc->i_buf != NULL)
        free_buffer(desc->i_buf);
    desc->i_buf = NULL;
    desc->i_remain    = 0;
    desc->i_ptr       = NULL;
    desc->i_ptr_start = NULL;
    desc->i_bufsz     = 0;
}
/*                                                                                                                        
** Set new size on buffer, used when packet size is determined                                                            
** and the buffer is to small.                                                                                            
** buffer must have a size of at least len bytes (counting from ptr_start!)                                               
*/
staticinttcp_expand_buffer(tcp_descriptor* desc, intlen)
{
   ...
    if(desc->i_bufsz >= ulen)/* packet will fit */
        return0;
    elseif(desc->i_buf->orig_size >= ulen) { /* buffer is large enough */
        desc->i_bufsz = ulen; /* set "virtual" size */
        return0;
    }
  
    offs1 = desc->i_ptr_start - desc->i_buf->orig_bytes;
    offs2 = desc->i_ptr - desc->i_ptr_start;
  
    if((bin = driver_realloc_binary(desc->i_buf, ulen)) == NULL)
        return-1;
  
    desc->i_buf = bin;
    desc->i_ptr_start = bin->orig_bytes + offs1;
    desc->i_ptr       = desc->i_ptr_start + offs2;
...
}
/*                                                                                                                        
** Deliver all packets ready                                                                                              
** if len == 0 then check start with a check for ready packet                                                             
*/
staticinttcp_deliver(tcp_descriptor* desc, intlen)
{
...
    while(len > 0) {
        intcode;
  
        inet_input_count(INETP(desc), len);
  
        /* deliver binary? */
        if(len*4 >= desc->i_buf->orig_size*3) {/* >=75% */
            code = tcp_reply_binary_data(desc, desc->i_buf,
                                         (desc->i_ptr_start -
                                          desc->i_buf->orig_bytes),
                                         len);
            if(code < 0)
                returncode;
  
            /* something after? */
            if(desc->i_ptr_start + len == desc->i_ptr) {/* no */
                tcp_clear_input(desc);
            }
            else{/* move trail to beginning of a new buffer */
                ErlDrvBinary* bin = alloc_buffer(desc->i_bufsz);
                char* ptr_end = desc->i_ptr_start + len;
                intsz = desc->i_ptr - ptr_end;
  
                memcpy(bin->orig_bytes, ptr_end, sz);
                free_buffer(desc->i_buf);
                desc->i_buf = bin;
                desc->i_ptr_start = desc->i_buf->orig_bytes;
                desc->i_ptr = desc->i_ptr_start + sz;
                desc->i_remain = 0;
...
}
}

除了上面的逻辑外,这里需要强调几点:
1. 默认情况下gen_tcp建立的时候,接收缓冲区是空的。
2. 接收完整的包投递后,释放接收缓冲区。
3. 接收缓冲区大小由包的大小决定,如果包未知,由desc->bufsz决定。
4. INET_LOPT_BUFFER仅仅影响接收缓冲区,发送无需缓冲区,因为发送的时候,sendv可以直接发送队列里面的数据。
5. INET_LOPT_BUFFER只是给个缓冲区大小的hint, 而非强制。

分析到这里为止,我们可以把这三个缓冲区的概念搞清楚了。接下来就是如何用好这些缓冲区的实践了:

1. INET_LOPT_BUFFER由于指示的是inet_drv这个层面接收缓冲区的默认大小,所以这个缓冲区最好是比操作内核SO_RCVBUF指示的接收缓冲区要大。
2. INET_LOPT_BUFFER只是个hint, 在包大小未知的情况下,影响接收缓冲区的大小,而如果要接收的包大于接收缓冲区的时候,就要扩展缓冲区,通过realloc来实现的。所以通过统计包的平均大小,设置一个比较合理的hint, 减少expand缓冲区的发生。inets:getstat(Socket, [recv_avg]). 可以帮我们统计到平均包大小。

这里还需要指出个问题,通过前面的分析,我们知道接收缓冲区不停的分配,释放,这对内存分配器造成很大的压力。 所以inet_drv实现了一套小型的内存分配池。为了减少冲突,每个CPU一个分配池. 每个池维护最近使用的buffer, 达到最快分配到buffer的目的。

参看代码如下:

staticErlDrvBinary* alloc_buffer(ErlDrvSizeT minsz)
{
    InetDrvBufStk *bs = get_bufstk();
    if(bs && bs->buf.pos > 0) {
        longsize;
        ErlDrvBinary* buf = bs->buf.stk[--bs->buf.pos];
        size = buf->orig_size;
        bs->buf.mem_size -= size;
  
        if(size >= minsz)
            returnbuf;
  
        driver_free_binary(buf);
    }
  
    returndriver_alloc_binary(minsz);
}
  
staticvoidrelease_buffer(ErlDrvBinary* buf)
{
...
    bs = get_bufstk();
    if(!bs
        || (bs->buf.mem_size + size > BUFFER_STACK_MAX_MEM_SIZE)
        || (bs->buf.pos >= BUFFER_STACK_SIZE)) {
    free_binary:
        driver_free_binary(buf);
    }
    else{
        bs->buf.mem_size += size;
        bs->buf.stk[bs->buf.pos++] = buf;
    }
...
}

有了高速的内存分配器,gen_tcp的接收缓冲区的管理的代价就不算太大。gen_tcp这样设计接收缓冲区的目的是为了能够在大量网络链接的情况下,尽可能的节约内存,典型的用时间换空间的设计。

小结: 源码是最好的答案,文档不是。
祝玩得开心!

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 八字没有正财怎么办 八字无根怎么办啊 开车撞死一条狗怎么办 小麦收割机滚筒轴老折怎么办 圆珠笔画白墙上怎么办 白墙上圆珠笔怎么办洗 按扣皮箱扣不上怎么办 拉链一边掉了怎么办 拉链一半掉了怎么办 去北戴河怎么办安检证 电脑空格键打字失灵怎么办 国外玩游戏延迟怎么办 韩服lol要求输韩文怎么办 智齿碰到牙神经怎么办 合影中有人去世怎么办 在家中遇到蛇怎么办 去澳门手机充电怎么办 商标撕下来胶怎么办 商标被提出异议怎么办 商标被别人用怎么办 商标申请不下来怎么办 注册商标有近似商标怎么办 电话话打不通怎么办 公司卖了商标怎么办 商标撕不下来怎么办 商标撕不下来时怎么办 商标还没下来怎么办 商标揭不下来怎么办 手机程序出现异常怎么办 公众号企业名称是*怎么办 家人生命受到威胁怎么办 海淘看不懂英文怎么办 对英语不感兴趣怎么办 装修无合同起诉怎么办 上海离职后档案怎么办 公司注销了银行帐户怎么办 360借条注销了怎么办 注销营业执照公章丢失怎么办 工厂招聘信息有假怎么办 个体餐饮怎么办核名 见父母后接下来怎么办