错误的观点

关于linux中“每个TCP连接占用多少内存”的问题,中文网络中一直流传一种说法:TCP连接建立的时候就会分配接收缓冲区和发送缓冲区,各4KB,一共是8KB。如果加上TCP协议控制块(protocol control block)的2KB, 一共是10KB。而且使用tcp_mem和tcp_wmen的值可以佐证这个观点,这种说法是错误的。

  1. $ sysctl -A |grep tcp_.mem
  2. net.ipv4.tcp_rmem = 4096 87380 6291456
  3. net.ipv4.tcp_wmem = 4096 16384 4194304

以上说法错误的原因在于TCP连接在建立时并不会去分配接受缓冲区和发送缓冲区,后文我们会谈到这点。

原理和源码分析

这里的TCP连接,准确地说是一个TCP socket,一条TCP连接实际上对应两个TCP socke。调用socket(2)会返回一个文件描述符,socket(AF_INET, SOCK_STRERM, 0), 影响的内核数据结构示意图如下。

image.png
在内核中,TCP协议控制是struct top_sock, 每个TCP socket会对应一个tcp_sock对象,除此之外,为了能从文件描述符映射到tcp_sock, 还需要一些其它struct, 包括:

  • strcut file, 对应每个打开的文件
  • struct dentry, 文件所在的目录
  • struct socket_alloc, 包含struct socket和socket inode两个成员,是连接VFS和tcp_sock的桥梁
  • strcut socket_wq,用于wait queue,例如阻塞IO时挂起当前线程

从逻辑上说,通过文件描述符sockfd找到对应的tcp_sock的途径是current->files->fdt->fd[sockfd]->private_data->sk。这个每个struct的大小可以通过/proc/slabinfo找到,本文以陈硕大佬家某台机器上运行Debian 8 x86-64为例,linux内核版本3.16:

  1. | struct | size | slab cache name |
  2. | ---------------- | ---- | ------------------ |
  3. | file | 256 | "filp" |
  4. | dentry | 192 | "dentry" |
  5. | socket_alloc | 640 | "sock_inode_cache" |
  6. | tcp_sock | 1792 | "TCP" |
  7. | socket_wq | 64 | "kmalloc-64" |
  8. | ---------------- | ---- | ------------------ |
  9. | inet_bind_bucket | 64 | "tcp_bind_bucket" |
  10. | epitem | 128 | "eventpoll_epi" |
  11. | tcp_request_sock | 256 | "request_sock_TCP" |

因此,每个TCP socket占用的内存最少是256 + 192 + 640 + 1792 + 64 = 2944 bytes。后面的实验表明,实际占用的字节数会比这个略大,

  • SLAB的额外开销(overhead), 以tcp_sock为例,sizeof(struct tcp_sock) == 1792, 对于4KB的page, 每个page只能放2 tcp_sock, 因为每个tcp_sock的实际开销是2048字节。
  • 每建立一个客户端会使用一个ephemeral port, 每个被占用的tcp port会对应一个inet_bind_bucket, 占用64字节,所以不会为每个accept()得到的连接建立inet_bind_bucket对象,就是说服务端连接占用的内存比客户端连接略小,这和绝大数的想象相反
  • 通常的网络编程会使用epoll来处理多个并发连接,用epoll_ctl_add往epollfd每添加一个sockfd会创建一个epitem对象,大小为128字节
  • 对于接受缓冲区和发送缓冲区,如果没有数据,是不占用内存的。具体来说,对于接受缓冲区,只有当有数据可读但应用程序尚未读取数据的时候才会占用内存(就是epoll_wait返回epoll_in之后,程序调用read()之前的那一段时间)。换句话说,只要服务器总是及时读取数据,接受缓冲区基本不占用内存。对于发送缓冲区,只有等待发送的数据和发送之后尚未收到ACK的数据才占用内存。
  • 服务端再收到SYN之后不会立刻创建tcp_sock,而是会创建tcp_request_sock来处理三次握手,后者要小得多(256字节)。等到收到三路握手最后的ACK才创建tcp_sock。主动关闭TCP socket会进入TIME_WAIT状态,tcp_sock会被释放,取而代之的是小得多inet_timewait_sock对象(192字节)。因此,TIME_WAIT并不占用多少资源。总之,Linux协议栈尽可能缩小tcp_sock的生命周期,以节约内容。

    参考

    Linux 中每个 TCP 连接最少占用多少内存?