# Socket
:套接字,是类 Unix 系统提供的一套 API ,用于进行 TCP/UDP 通信,工作在会话层。
# 用法
- 调用 Socket API 时,会在内存中创建一个 Socket 类型的文件,没有实际存储在磁盘上。
- 程序读、写 Socket 文件时,操作系统会自动基于 TCP/UDP 协议接收、发送数据,不需要程序自己实现 TCP/UDP 协议的具体逻辑。
- Linux 内核会为每个 Socket 自动分配一些内存空间,创建一个接收缓冲区(Recv buffer)、一个发送缓冲区(Send buffer),用于缓冲基于 TCP/UDP 协议接收、发送的 payload 数据。
- Socket 主要有两种用法:
- Unix Domain Socket
- :用于本机的进程之间通信。
- 例如 mysqld 进程在启动时会创建一个 /var/lib/mysql/mysql.sock 文件,位于同一主机的其它进程可读写该文件,从而与 mysqld 进程通信。
- Network Socket
- :用于不同主机的进程之间通信。
- 通信双方需要各自创建一个 Socket 文件,向一方的 Socket 文件写入数据,就会被操作系统自动进行 TCP/UDP 传输,然后可以在另一方的 Socket 文件读取到数据。
- client 向 server 请求建立 TCP 连接时,需要知道 server 的
IP:PORT
地址,比如10.0.0.1:80
。- IP :IPv4 或 IPv6 地址,用于定位主机。
- PORT :端口号,用于定位主机上的进程。
- 同一 IP 的主机上可能有多个进程,分别监听了不同的 TCP 端口。
- Unix Domain Socket
# API
# 创建 Socket
创建 Socket 的 API :
#include <sys/socket.h> int socket(int domain, int type, int protocol); // 创建一个 Socket ,返回一个文件描述符 sockfd int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 将一个 Socket 绑定到指定的 IP:PORT int listen(int sockfd, int backlog); // TCP server 一方的程序会调用该函数,让 Socket 进入 LISTEN 状态,等待 client 连接 // backlog :accept 队列的最大长度 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // TCP client 一方的程序会调用该函数,连接到 server int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen); // 从指定 Socket 的 accept 队列取出一个 TCP 握手成功的连接,为它创建并绑定一个新 Socket ,返回新的 sockfd // 源 Socket 为 LISTEN 状态,新 Socket 为 ESTABLISHED 状态 // 如果 accept 队列为空,则 accept() 函数会一直阻塞等待 // addr :请求连接的客户端地址 // addrlen :客户端地址的长度,即 sizeof(addr) int shutdown(int sockfd , int how); // 用于停止 Socket 的通信,但并不会关闭 Socket // how :表示操作类型,可取值: // SHUT_RD :停止接收,并丢弃接收缓冲区中的数据 // SHUT_WR :停止发送,但继续传输发送缓冲区中的数据 // SHUT_RDWR :停止接收和发送
例:基于 Socket 开发一个 TCP server 程序的代码流程
- 调用 socket() 创建一个 Socket ,然后用 bind() 绑定,用 listen() 监听。
- 该 Socket 用于被动监听 TCP 端口,等待 client 连接,不能用于正式的 TCP 通信。
- client 程序可调用 socket() 创建一个 Socket ,然后调用 connect() 主动连接到 server 。
- 调用 accept() ,为每个 TCP 握手成功的 client 创建一个 Socket 。
- 该 Socket 用于正式的 TCP 通信。
- 一个 server 可能被多个 client 连接,会分别创建一个 Socket ,分别用一对接收、发送缓冲区进行 TCP 通信。
- 调用 read()、write() 读写 Socket 。
- 调用 close() 关闭 Socket 。
- 调用 socket() 创建一个 Socket ,然后用 bind() 绑定,用 listen() 监听。
调用 bind() 时,如果端口已被本机的其它进程绑定,则会报错:
bind() failed: Address already in use
- 同一 IP 的主机上可能有多个进程先后调用 bind() 。如果它们的 Socket 使用相同 protocol ,则只能绑定不同端口。如果使用不同 protocol ,则可以绑定相同端口。
- 端口号在 TCP/UDP 数据包中存储为 16 bit 的无符号整数,因此取值范围为 0~65535 。
- 小于 1024 的端口号通常被系统服务使用,不建议普通进程使用,比如 SSH 协议采用 TCP 22 端口。
- 调用 bind() 时,如果端口号小于 1024 ,则需要 root 权限才能绑定。
- 调用 bind() 时,如果端口号为 0 ,则会被随机分配一个可用端口号。例:
[root@CentOS ~]# python3 -m http.server 0 Serving HTTP on 0.0.0.0 port 33720 (http://0.0.0.0:33720/) ...
- 如果一个进程绑定 IP 为 127.0.0.1 并监听一个端口,则只会接收本机发来的数据包,因为其它主机发来的数据包的目标 IP 不可能是本机环回地址。
- 如果一个进程绑定 IP 为 0.0.0.0 并监听一个端口,则会接收所有数据包,不限制目标 IP ,只要目标端口一致。
- 小于 1024 的端口号通常被系统服务使用,不建议普通进程使用,比如 SSH 协议采用 TCP 22 端口。
当 server 调用 listen() 监听 TCP 端口时,Linux 内核会自动为该 Socket 维护 SYN、accept 两个连接队列。
- 如果一个 client 请求连接当前 server ,开始三次握手。则等 TCP 连接变为 SYN_RECV 状态时,将连接信息存入 SYN 队列,又称为半连接队列。
- 此时每个连接占用 304 bytes 内存。
- 查看半连接的数量:
netstat | grep SYN_RECV | wc -l
- 当 TCP 连接变为 ESTABLISHED 状态时,将它从 SYN 队列取出,存入 accept 队列,又称为全连接队列。
- 等到 server 调用 accept() 函数,就从 accept 队列取出 TCP 连接。
- 如果队列满了,则 Socket 不能接收新连接。
- Linux 内核默认会在 SYN 队列满了时启用 SYN Cookies 功能,从而抵抗 SYN Flood 攻击。
- 原理:将 SYN_RECV 状态的连接信息不存入 SYN 队列,而是在 server 回复的 SYN+ACK 包中包含一个 cookie 信息。client 之后发出 ACK 包时如果包含该 cookie ,则允许建立连接。
- 不过该功能不符合 TCP 协议,与某些服务不兼容。
- Linux 内核默认会在 SYN 队列满了时启用 SYN Cookies 功能,从而抵抗 SYN Flood 攻击。
- 如果一个 client 请求连接当前 server ,开始三次握手。则等 TCP 连接变为 SYN_RECV 状态时,将连接信息存入 SYN 队列,又称为半连接队列。
调用 accept() 时,会根据五元组 protocol、src_addr、src_port、dst_addr、dst_port 创建一个 Socket ,只要其中一项元素不同,就会创建不同的 Socket 文件。
- 当 server 监听一个端口、被不同 IP 的 client 分别请求连接时,这些 Socket 的 src_addr、src_port 不同,因此创建的 Socket 有
255^4 * 65535
种可能性,几乎无限。 - 当 server 监听一个端口、被同一 IP 的 client 多次请求连接时,这些 Socket 的 src_addr 相同、src_port 默认的取值范围为
10000 ~ 65535
,因此创建的 Socket 有 55535 种可能性,一般足够使用。- 一般情况下,同一 client 向同一 server 建立的 TCP 并发连接数只有一个,或者几个。
- 有的情况下,client 会频繁访问 server ,每次创建一个新的 TCP 连接,传输完数据就关闭连接。而 client 作为主动关闭方, Socket 要等 2*MSL 时长才能关闭,因此新、旧 Socket 会同时存在,平均每秒最多能创建
55535/60=925
个新 Socket 。此时建议 client 创建 TCP 连接之后不立即关闭,而是复用一段时间。
- Linux 内核收到一个发向本机的 TCP/UDP 数据包时,会检查其 dst_addr、dst_port ,判断属于哪个 Socket ,然后暂存到该 Socket 的接收缓冲区。
- 如果不存在该 Socket ,则回复一个 RST 包,表示拒绝连接。
- 当 server 监听一个端口、被不同 IP 的 client 分别请求连接时,这些 Socket 的 src_addr、src_port 不同,因此创建的 Socket 有
# 读写 Socket
Socket 是在内存中创建的文件,因此可调用文件的 API 来读写 Socket :
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
也可以调用 recv()、send() 等 API 来读写 Socket ,详见 IO 模型。
# 关闭 Socket
- 关闭 Socket 的几种方式:
- 等待创建该 Socket 的进程主动调用 close() 。
#include <unistd.h> int close(int fd);
- 不允许其它进程关闭该 Socket ,即使是 root 用户。
- 终止创建该 Socket 的进程,内核会自动回收其创建的所有 Socket 。
- 通过 gdb ,调试创建该 Socket 的进程,调用 close() 。如下:
ss -tapn | grep $PORT # 找出监听某个端口的进程的 PID lsof -p $PID | grep TCP # 找出该进程创建的 Socket 的文件描述符 FD gdb -p $PID # 调试该进程 call close($FD) # 关闭 Socket
- 等待创建该 Socket 的进程主动调用 close() 。
# IO 模型
- 读写 Socket 的 API 有多种,根据 IO 模型分为以下几类:
- 阻塞(blocking) IO
- 非阻塞(nonblocking) IO
- IO 复用(multiplexing)
- 信号驱动(signal driven) IO
- 异步(asynchronous) IO :简称为 AIO
# 阻塞 IO
:程序调用 API 读写 Socket 时,会阻塞执行。
- 调用 API 接收数据时,要等数据从 Socket 接收缓冲区拷贝到 buf 进程内存空间,API 才返回。
- 调用 API 发送数据时,也要等数据从 buf 进程内存空间拷贝到 Socket 发送缓冲区,API 才返回。
优点:用法简单。
缺点:调用 API 时可能长时间阻塞,导致当前线程不能执行其它任务。
- 函数阻塞时 CPU 是空闲的,可执行其它线程。
相关 API :
#include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 接收数据。将 Socket 接收缓冲区中的数据,拷贝到 buf ,然后返回实际读取的字节数 // sockfd :指定 Socket 的文件描述符 // buf :指向一块内存空间的指针,用作读缓冲。内核会从 Socket 接收缓冲区读取数据,写入其中 // len :声明 buf 内存空间的长度 ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict src_addr, socklen_t *restrict addrlen); // 与 recv() 相比,只接收来自指定源地址的数据 ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 发送数据。将 buf 中的数据,拷贝到 Socket 发送缓冲区,然后返回实际拷贝的字节数 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // 与 send() 相比,只发送数据到指定目标地址
- 调用 read()、write() 读写 Socket 时,相当于调用 recv()、send() 且 flags 为 0 。
- 调用
recv(sockfd, buf, len, flags);
时,相当于调用recvfrom(sockfd, buf, len, flags, NULL, NULL);
。- TCP 通信时,通常使用 recv()、send() ,因为远程主机的地址已经绑定到 Socket 了。
- UDP 通信时,通常使用 recvfrom()、sendto() ,从而区分不同地址的远程主机。
例:假设两个程序 A、B 分别位于两个 Linux 主机上,已建立 TCP 连接,可调用 send()、recv() 函数来发送、接收数据。流程如下:
- 程序 A 调用 send() ,请求操作系统发送一批 bytes 数据。
- 程序 A 的操作系统先将 bytes 数据暂存到发送缓冲区,然后取出这些数据作为 payload ,自动封装为一个或多个 TCP 包,最后发送。
- 程序 B 的操作系统收到一个或多个 TCP 包,自动解包,将其中的 payload 数据暂存到接收缓冲区。
- 这些数据会一直保存在接收缓冲区,直到程序调用 recv() 取出这些数据。另外,关闭 Socket 也会销毁缓冲区,如果缓冲区还有数据则会丢失。
- 如果接收缓冲区满了,则不能接收包含 payload 的新 TCP 包,只能丢包。为了避免这种情况,可以提高调用 recv() 的频率,或者增加
net.core.rmem_default
内核参数。
- 程序 B 调用
recv(int sockfd, void *buf, size_t len, int flags);
,请求接收数据。- 这个 buf 是进程动态分配的一块内存空间,可以被进程直接访问。而 Socket 发送、接收缓冲区是 Linux 内核自动分配的一块内存空间,不能被进程直接访问。
- 如果接收缓冲区中有数据,则操作系统会从中取出 len 长度的数据,拷贝到进程的 buf 内存空间,然后结束执行 recv() 函数。之后进程可以自由处理 buf 中的数据。
- 如果接收缓冲区中没有数据,则 recv() 函数会一直阻塞等待。
假设程序多次调用 send() ,分别发送 1、2、3 bytes 长度的数据:
- 如果是 TCP 通信,则多次 send() 的数据会暂存在发送缓冲区中,前后拼接成一段数据(该例为 6 bytes 长度)。累计的数据量达到 Nagle 算法的阈值之后,才封装为一个 TCP 数据包并发送。因此 recv() 时只收到一段数据,不能区分多次 send() 的数据。
- 该问题称为 TCP 黏包。常见的解决方案是,程序每次 send() 时,主动给数据添加一些前缀标识,从而明确每条数据的边界。例如 HTTP 协议发送的每个报文,都有固定格式的头部,还通过 Content-Length 字段声明了 http body 的长度。
- 有时 TCP 黏包不会影响数据传输。例如将一个文件的数据分多次 send() 发送,本来就需要拼接成一段数据,不存在边界。
- 如果是 UDP 通信,则每次调用 send() ,都会立即发送 UDP 数据包,不存在黏包的问题。
- 如果是 TCP 通信,则多次 send() 的数据会暂存在发送缓冲区中,前后拼接成一段数据(该例为 6 bytes 长度)。累计的数据量达到 Nagle 算法的阈值之后,才封装为一个 TCP 数据包并发送。因此 recv() 时只收到一段数据,不能区分多次 send() 的数据。
# 非阻塞 IO
- :程序调用 API 读写 Socket 时,API 会立即返回一个错误码,然后程序需要轮询 IO 操作是否完成。
- 优点:调用 API 的耗时很短。
- 缺点:需要多次轮询,占用更多 CPU 。
# IO 复用
:程序调用 API 阻塞监视多个 Socket ,等任一 Socket 缓冲区变为可读或可写时返回。然后程序再调用 recv() 或 send() 。
优点:使用单线程就可同时监视多个 Socket 的状态。
- 例如 TCP server 一般会被多个 client 连接,绑定了多个 Socket ,需要检查每个 Socket 是否接收了新数据。如果 server 采用阻塞式 IO ,则一个线程同时只能检查一个 Socket 的缓冲区是否可读,轮询一遍全部 Socket 的耗时较久。解决方案是创建多线程,或采用 IO 复用。
select() 是最老的一种 IO 复用 API ,大部分操作系统都支持:
#include <sys/select.h> int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict timeout); // 阻塞监视多个文件描述符,等任一文件描述符满足条件时才返回 // nfds :取值等于监视的最大一个文件描述符 + 1 // readfds :一组文件描述符,当任一变为可读时,函数返回 // writefds :一组文件描述符,当任一变为可写时,函数返回 // exceptfds :一组文件描述符,当任一发生异常时,函数返回 // timeout :函数阻塞的超时时间
- 等待文件描述符变为可读、可写,是指可以立即进行 IO 操作而不会阻塞。
- select() 将监视的所有文件描述符记录在一个数组中,数组长度固定为 FD_SETSIZE=1024 。
poll() 是一种改进的 API :
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); // 阻塞监视多个文件描述符 // fds :一个指针,记录一组 pollfd 结构体的首地址 // nfds :pollfd 结构体的数量 struct pollfd { int fd; // 文件描述符 short events; // 监视的事件,比如文件可读、可写等 short revents; // 实际发生的事件。调用 poll() 时内核会赋值该变量 };
- poll() 不限制文件描述符的数量,而且监视的事件比 select() 更多。
Linux 系统独有地,将 poll() 改进为了 epoll 系列 API :
#include <sys/epoll.h> int epoll_create(int size); // 创建一个 epoll 实例,返回其文件描述符 epfd // size :允许 epoll 实例监视的文件描述符总数。Linux 2.6 开始会自动调整 size ,因此传入 size>0 即可 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 控制一个 epoll 实例,管理对一个文件描述符 fd 的监视事件 // fd :一个文件描述符,如果有多个文件描述符需要监视,则需要多次调用 epoll_ctl() // op :操作类型。比如添加、删除监视的事件 // event :一组事件。epoll_event 结构体中记录了一个文件描述符、监视的事件 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 阻塞监视一个 epoll 实例,等任一事件发生时,返回满足条件的事件数量 // 超时时,函数返回 0 // 出错时,函数返回 -1 ,并设置 errno // events :一个指针。调用 epoll_wait() 时,内核会将发生的的一组事件的首地址赋值给该指针 // maxevents :最多返回的事件数 // timeout :阻塞的超时时间,单位 ms 。取值为 0 则立即返回,适合轮询。取值为 -1 则会一直阻塞。
- epoll 实例以文件形式保存,使用完之后应该 close() 。
- epoll 只返回发生的事件,不需要检查所有文件描述符,比 select() 和 poll() 更高效,更适合监视大量文件描述符。
- epoll 是线程安全的,而 select() 和 poll() 不是。
- 当一个线程阻塞等待 epoll 实例时,其它线程可以向该 epoll 实例添加文件描述符。如果新的文件描述符满足条件,则会解除阻塞。
# 信号驱动 IO
- :进程调用 sigaction() 时,函数会立即返回。等 Socket 变为可读或可写时,内核会发送 SIGIO 信号通知该进程。然后进程再调用 recv() 或 send() 。
# 异步 IO
:进程调用 API 读写 Socket 时,API 会立即返回。等内核执行完 IO 操作之后,再发送信号通知该进程。
相当于另外创建了一个线程,去执行 recv() 或 send() 。
其它四种 IO 模型都属于同步 IO ,进程需要等待 IO 完成,才能执行其它任务。
同步通信(Synchronous)
- :指一个程序对外发送消息之后,要等收到回复,才能执行其它任务。
- 在等待回复的期间,该程序属于阻塞(Block)状态。
异步通信(Asynchronous)
- :指一个程序对外发送消息之后,不必等收到回复,就能执行其它任务。
- 例如:打电话属于同步通信,需要一边说话一边听对方的回复。而发短信属于异步通信。
- CPU 的运行速度远高于磁盘 IO 、网络 IO 速度。因此采用同步 IO 时,程序经常会阻塞,不占用 CPU 。采用异步 IO 可以大幅提高 CPU 的使用率。
# 相关命令
# sockstat
- 通过
/proc/net/sockstat
文件可查看各种状态的 socket 数量:各字段的含义:[CentOS ~]# cat /proc/net/sockstat sockets: used 1101 TCP: inuse 44 orphan 0 tw 260 alloc 402 mem 37 UDP: inuse 2 mem 10 UDPLITE: inuse 0 RAW: inuse 0 FRAG: inuse 0 memory 0
orphan # 无主的 Socket ,即不绑定任何进程 tw # TIME-WAIT 状态的 socket 数量 alloc # 已绑定进程的 socket 数量 mem # 占用内存,单位为内存页 pages
# netstat
:用于查看本机网络连接的状态。
- 命令:
netstat -a # 显示所有 socket -l # 只显示被进程 listen 的 socket -t # 只显示 tcp socket -u # 只显示 udp socket -x # 只显示 unix socket -e # 增加显示 User、Inode 列 -p # 增加显示 PID/Program name 列,表示使用每个 socket 的进程 -n # 取消将端口号显示成服务名(比如默认会把 22 端口显示成 ssh)
# ss
:全称为 socket statistics 。该命令与 netstat 类似,但执行速度更快。
命令:
ss # 显示 established 状态的 socket -a # 显示所有状态的 socket -l # 只显示被进程 listen 的 socket -t # 只显示 tcp socket -u # 只显示 udp socket -x # 只显示 unix socket -p # 显示绑定每个 socket 的进程名 -m # 显示每个 socket 占用的内存 -n # 取消将端口号显示成服务名 -i # 显示 tcp 协议的详细信息,包括 mss、cwnd、ssthresh 等 -s # 显示各种 Socket 的统计数量
例:查看各种 Socket 的统计数量
[root@CentOS ~]# ss -s Total: 1101 (kernel 1405) # Total 表示存在的 Socket 数。不过 kernel 中存在的 Socket 数较多一些,因为有些 Socket 已关闭,但尚未回收 TCP: 697 (estab 140, closed 538, orphaned 0, synrecv 0, timewait 295/0), ports 0 Transport Total IP IPv6 * 1405 - - RAW 0 0 0 UDP 3 2 1 # 统计 UDP 类型的 Socket ,包括总数、IPv4 数量、IPv6 数量 TCP 159 44 115 INET 162 46 116 FRAG 0 0 0
- 执行
ss -a
时不会显示 closed 状态的 Socket ,但它们的确存在,占用了文件描述符。 - 这里 closed 状态的 Socket ,不是指被 close() 关闭的 Socket (它们会被内核自动回收),而是指没有通信的 Socket 。比如:
- 程序创建 Socket 之后,没有成功调用 connect() ,导致该 Socket 从未进行通信。
- 程序调用了 shutdown() ,但没有调用 close() ,导致该 Socket 停止通信。
- 执行
例:查看所有 TCP 端口的信息
[root@CentOS ~]# ss -tapn | cat # 加上 cat 使显示的 users 列不换行 State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 127.0.0.1:34186 *:* users:(("node",pid=15647,fd=19)) LISTEN 0 128 *:111 *:* users:(("systemd",pid=1,fd=51)) LISTEN 0 128 *:22 *:* users:(("sshd",pid=3057,fd=3))
- 不指定 Socket 类型时,默认显示的第一列是 Netid ,表示 Socket 类型,取值包括:
TCP UDP u_str # Unix Stream u_dgr # UNIX datagram nl # net link p_raw # raw packet p_dgr # datagram packet
- LISTEN 状态时:
- Recv-Q 表示当前 accept 队列的长度。
- Send-Q 表示 accept 队列允许的最大长度。
- 非 LISTEN 状态时:
- Recv-Q 表示接收队列的长度,即已接收、尚未被进程读取的字节数。
- Send-Q 表示发送队列的长度,即已发送、尚未收到对方 ACK 的字节数。
- 上述两个值为 0 时最好,说明内核缓冲区没有堆积。
- 最右端的一列 users 表示监听每个端口的进程。
- 不指定 Socket 类型时,默认显示的第一列是 Netid ,表示 Socket 类型,取值包括:
例:查看所有 Socket 占用的内存
[root@CentOS ~]# ss -tapnmi | cat State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:22 *:* users:(("sshd",pid=1173,fd=3)) skmem:(r0,rb87380,t0,tb16384,f0,w0,o0,bl0,d0) cubic rto:1000 mss:536 cwnd:10 segs_in:146 lastsnd:988678939 lastrcv:988678939 lastack:988678939 SYN-SENT 0 1 10.0.0.1:52052 10.0.0.2:9094 users:(("filebeat",pid=1215,fd=8)) skmem:(r0,rb87380,t0,tb16384,f4294966016,w1280,o0,bl0,d0) cubic rto:16000 backoff:4 mss:524 rcvmss:88 advmss:1460 cwnd:1 ssthresh:7 segs_out:5 lastsnd:988678939 lastrcv:988678939 lastack:988678939 unacked:1 retrans:1/4 lost:1
- skmem 表示 Socket 占用的内存,其中各个字段的含义:
rmem_alloc # 接收缓冲区已分配的内存。通常缓冲区不需要存储数据时,Socket 就不会向 MMU 申请分配内存 rcv_buf # 接收缓冲区的容量,即最多允许分配的内存。Linux 可能自动增减缓冲区的容量 wmem_alloc # 发送缓冲区已分配的内存 snd_buf # 发送缓冲区的容量 fwd_alloc # 该 Socket 已分配,但尚未用于 rmem_alloc、wmem_alloc 的空闲内存。这部分内存尚未写入数据,因此并未占用 RSS 内存 wmem_queued # 发送缓冲区已分配的另一块内存,已写入数据,正在准备发送 opt_mem # 用于存储 socket option 的内存 back_log # sk backlog 队列占用的内存。当进程正在接收数据包时,新的数据包会被暂存到 sk backlog 队列,以便被进程立即接收 sock_drop # 该 Socket 丢弃的数据包的数量
- 每个 Socket 占用的总内存等于 rmem_alloc + wmem_alloc (+ fwd_alloc) + wmem_queued + opt_mem + back_log 。
- 这里 rto 的单位为 ms ,
- skmem 表示 Socket 占用的内存,其中各个字段的含义: