EAGAIN、EWOULDBLOCK、EINTR与非阻塞 长连接
EWOULDBLOCK用于非阻塞模式,不需要重新读或者写
EINTR指操作被中断唤醒,需要重新读/写
在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返 回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
Linux - 非阻塞socket编程处理EAGAIN错误
在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这是什么意思?
这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。 对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
另外,如果出现EINTR即errno为4,错误描述Interrupted system call,操作也应该继续。
最后,如果recv的返回值为0,那表明连接已经断开,我们的接收操作也应该结束。
对于在基于 UNIX 的环境中的 TCP/IP 用户,下表列出了某些最常见的错误原因码 (errno)。它不是完整的错误列表。可以在文件 /usr/include/sys/errno.h 中找到 Errno。针对每一种操作系统给出了errno 号码。
基于 UNIX 的 TCP/IP errno
Errno
Errno号码
AIX HP-UX Solaris UnixWare Linux
说明
EINTR 4 4 4 4 4 系统调用中断。
EAGAIN 11 11 11 11 11 资源临时不可用。
EBUSY 16 16 16 16 16 资源正忙。
EMFILE 24 24 24 24 24 每个进程文件描述符表已满。
EPIPE 32 32 32 32 32 管道断开。
EADDRINUSE 67 226 125 125 98 已经在使用指定的地址。
ENETDOWN 69 228 127 127 100 网络已停止。
ENETUNREACH 70 229 128 128 101 没有到达网络的可用路由。
ENETRESET 71 230 129 129 102 重设时网络已卸下了连接。
ECONNRESET 73 232 131 131 104 伙伴已重设了连接。
ENOBUFS 74 233 132 132 105 系统中没有足够的缓冲区空间资源可用来完成调用。
ENOTCONN 76 235 134 134 107 未连接套接字。
ETIMEDOUT 78 238 145 145 110 连接超时。
ECONNREFUSED 79 239 146 146 111 连接已被拒绝。若您正在尝试与数据库相连,则检查是否已成功启动了服务器上的数据库管理程序和 TCP/IP 协议支持。 若使用 SOCKS 协议支持,则还要确保在 SOCKS 服务器上已成功启动了 TCP/IP 协议支持。
EHOSTDOWN 80 241 147 147 147 主机已停机。
EHOSTUNREACH 81 242 148 148 113 没有到达主机的可用路由。
——————————————————————————–
对于 OS/2 环境中的 TCP/IP 用户,下面的列表显示了最常见的 errno。它不是完整的错误列表。可以在文件 nerrno.h 中找到 Errno。此文件是 TCP/IP 产品包含文件的一部分。若尚未安装这些文件,则在您的系统上可能不会出现。errno 号码本身是用括号括起来的。
SOCEINTR (10003):系统调用被中断。
SOCEMFILE (10024):打开的文件太多。
SOCEPIPE (10032):管道被断开。
EADDRINUSE (10048):已经在使用指定的地址。
ENETDOWN (10050):网络已停止。
ENETUNREACH (10051):没有到达网络的可用路由。
ENETRESET (10052):重设时网络已卸下了连接。
SOCECONNABORTED (10053):软件导致连接异常中止。
ECONNRESET (10054):伙伴已重设了连接。
ENOBUFS (10055):无缓冲区空间可用。
ENOTCONN (10057):未连接套接字。
ETIMEDOUT (10060): 在进行连接之前,建立连接超时。
ECONNREFUSED (10061):连接已被拒绝。若您正在尝试与数据库相连,则检查是否已成功启动了服务器上的数据库管理程序和 TCP/IP 协议支持。
若使用 SOCKS 协议支持,则还要确保在 SOCKS 服务器上已成功启动了 TCP/IP 协议支持。
EHOSTDOWN (10064):主机已停机。
EHOSTUNREACH (10065):没有到达主机的可用路由。
SOCEOS2ERR (10100):OS/2 错误。
有关 OS/2 TCP/IP 通信错误的详情,参考 OS/2 TCP/IP 文档。
——————————————————————————–
对于 Windows 95、Windows 98 或 Windows NT 下的 TCP/IP 或 IPX/SPX 用户,下面的列表显示了最常见的错误码。它不是完整的错误列表。可以在文件 winsock.h 中找到由 WSAGetLastError() 返回的错误。
WSAEINVAL (10022):若在函数 WSASTARTUP 上接收到此错误,则此 DLL 不支持应用程序所支持的 Windows Sockets 版本。
WSAEMFILE (10024):没有可用的文件描述符。
WSAEWOULDBLOCK (10035):套接字标记为未分块,而操作将分块。
WSAEINPROGRESS (10036):正在进行分块 Windows Sockets 操作。
WSAENOPROTOOPT (10042):该选项是未知的或不受支持的。
WSAEADDRINUSE (10048):已经在使用指定的地址。
WSAENETDOWN (10050):网络子系统已失败。
WSAENETUNREACH (10051):此时不能从此主机到达网络。
WSAENETRESET (10052):由于重设了远程主机,所以连接已断开。
WSAECONNABORTED (10053):由于超时或其他故障,导致虚拟电路异常中止。重设时网络已卸下了连接。
WSAECONNRESET (10054):伙伴已重设了连接。
WSAENOBUFS (10055):无缓冲区空间可用,连接太多。
WSAENOTCONN (10057):未连接套接字。
WSAETIMEDOUT (10060):在进行连接之前,建立连接超时。
WSAECONNREFUSED (10061):连接已被拒绝。若您正在尝试与数据库相连,则检查在服务器上是否已成功启动了数据库管理程序和 TCP/IP 协议支持。
WSAEHOSTUNREACH (10065):此时不能从此主机到达网络。
WSASYSNOTREADY (10091):基础网络子系统未准备好进行网络通信。
WSAVERNOTSUPPORTED (10092):此特定的 Windows Sockets 实现未提供请求的 Windows Sockets API 支持的版本。
WSAHOST_NOT_FOUND (11001):找不到主机。
WSATRY_AGAIN (11002):找不到主机。请求从名称服务器中检索主机名的 IP 地址失败。
WSANO_DATA (11004):名称无效,没有请求的类型的数据记录。名称服务器或 hosts 文件不识别主机名,或者在 services 文件中未指定服务名。
有关 Windows 下的 TCP/IP 通信错误的详情,参考 Windows Sockets 文档。
查看网络的状态有很多中方法,如之前介绍的 网络状态查看命令 netstat VS. ss,也即使用 netlink 机制;还有 procfs 。相比来说,前者效率更高,但是后者查看会更加方便。
在此仅介绍 procfs 的相关实现,内核的相关实现在 net/core/net-procfs.c 中,所以如果对 /proc/net 下的文件内容不熟悉,可以直接查看内核中的统计项。
查看丢包
网络丢包会有多种可能,例如,交换机,上连和下连端口的流量跑满或链路有问题,那么数据包就有可能会被交换机丢掉;负载均衡设备,包括了硬件设备以及软件的负载均衡。
在此,我们仅查看本机可能导致的掉包。
操作系统处理不过来,丢弃数据
有两种情况,一是网卡发现操作系统处理不过来,丢数据包,可以读取下面的文件:
$ cat /proc/net/dev
每个网络接口一行统计数据,第 4 列(errs)是接收出错的数据包数量,第 5 列(drop)是接收不过来丢弃的数量。
第二部分是传统非 NAPI 接口实现的网卡驱动,每个 CPU 有一个队列,当在队列中缓存的数据包数量超过 net.core.netdev_max_backlog 时,网卡驱动程序会丢掉数据包,这个见下面的文件:
$ cat /proc/net/softnet_stat
每个 CPU 有一行统计数据,第二列是对应 CPU 丢弃的数据包数量。
应用程序处理不过来,操作系统丢弃
内核中记录了两个计数器:
ListenOverflows:当 socket 的 listen queue 已满,当新增一个连接请求时,应用程序来不及处理;
ListenDrops:包含上面的情况,除此之外,当内存不够无法为新的连接分配 socket 相关的数据结构时,也会加 1,当然还有别的异常情况下会增加 1。
分别对应下面文件中的第 21 列(ListenOverflows)和第 22 列(ListenDrops),可以通过如下命令查看:
$ cat /proc/net/netstat | awk ‘/TcpExt/ { print $21,$22 }’
如果使用 netstat 命令,有丢包时会看到 “times the listen queue of a socket overflowed” 以及 “SYNs to LISTEN sockets ignored” 对应行前面的数字;如果值为 0 则不会输出对应的行。
Out of memory
直接看代码,在内核中,对应的代码如下。
bool tcp_check_oom(struct sock *sk, int shift){
bool too_many_orphans, out_of_socket_memory;
too_many_orphans = tcp_too_many_orphans(sk, shift);
out_of_socket_memory = tcp_out_of_memory(sk);
if (too_many_orphans)
net_info_ratelimited("too many orphaned sockets\n");
if (out_of_socket_memory)
net_info_ratelimited("out of memory -- consider tuning tcp_mem\n");
return too_many_orphans || out_of_socket_memory;}static int tcp_out_of_resources(struct sock *sk, bool do_reset){
struct tcp_sock *tp = tcp_sk(sk);
int shift = 0;
/* If peer does not open window for long time, or did not transmit
* anything for long time, penalize it. */
if ((s32)(tcp_time_stamp - tp->lsndtime) > 2*TCP_RTO_MAX || !do_reset)
shift++;
/* If some dubious ICMP arrived, penalize even more. */
if (sk->sk_err_soft)
shift++;
if (tcp_check_oom(sk, shift)) {
/* Catch exceptional cases, when connection requires reset.
* 1. Last segment was sent recently. */
if ((s32)(tcp_time_stamp - tp->lsndtime) <= TCP_TIMEWAIT_LEN ||
/* 2. Window is closed. */
(!tp->snd_wnd && !tp->packets_out))
do_reset = true;
if (do_reset)
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_done(sk);
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONMEMORY);
return 1;
}
return 0;} 如上所示,出现内存不足可能会有两种情况:
有太多的 orphan sockets,通常对于一些前端的服务器经常会出现这种情况。
分配给 TCP 的内存确实较少,从而导致内存不足。
内存不足
这个比较好排查,只需要查看一下实际分配给 TCP 多少内存,现在时用了多少内存即可。需要注意的是,通常的配置项使用的单位是 Bytes,在此用的是 Pages,通常为 4K 。
先查看下给 TCP 分配了多少内存。
$ cat /proc/sys/net/ipv4/tcp_mem
183474 244633 366948
简单来说,三个值分别表示进入 无压力、压力模式、内存上限的值,当到达最后一个值的时候就会报错。
接下来查看一下当前使用的内存。
$ cat /proc/net/sockstat
sockets: used 855
TCP: inuse 17 orphan 1 tw 0 alloc 19 mem 3
UDP: inuse 16 mem 10
UDPLITE: inuse 0
RAW: inuse 1
FRAG: inuse 0 memory 0
其中的 mem 表示使用了多少 Pages,如果相比 tcp_mem 的配置来说还很小,那么就有可能是由于 orphan sockets 导致的。
orphan sockets
首先介绍一下什么是 orphan sockets,简单来说就是该 socket 不与任何一个文件描述符相关联。例如,当应用调用 close() 关闭一个链接时,此时该 socket 就成为了 orphan,但是该 sock 仍然会保留一段时间,直到最后根据 TCP 协议结束。
实际上 orphan socket 对于应用来说是无用的,因此内核希望尽可能减小 orphan 的数量。对于像 http 这样的短请求来说,出现 orphan 的概率会比较大。
对于系统允许的最大 orphan 数量,以及当前的 orphan 数量可以通过如下方式查看:
$ cat /proc/sys/net/ipv4/tcp_max_orphans
32768
$ cat /proc/net/sockstat 16-05-30 14:11
… …
TCP: inuse 37 orphan 14 tw 8 alloc 39 mem 9
… …
你可能会发现,sockstat 中的 orphan 数量要远小于 tcp_max_orphans 的数目。
实际上,可以从代码中看到,实际会有个偏移量 shift,该值范围为 [0, 2] 。
static inline bool tcp_too_many_orphans(struct sock *sk, int shift){
struct percpu_counter *ocp = sk->sk_prot->orphan_count;
int orphans = percpu_counter_read_positive(ocp);
if (orphans << shift > sysctl_tcp_max_orphans) {
orphans = percpu_counter_sum_positive(ocp);
if (orphans << shift > sysctl_tcp_max_orphans)
return true;
}
return false;} 也就是说,在某些场景下会对 orphan 做些惩罚,将 orphan 的数量 2x 甚至 4x,这也就解释了上述的问题。
如果是这样,那么就可以根据具体的情况,将 tcp_max_orphans 值适当调大。
总结
除了可能真正出现内存不足的情况之外,还有可能是由于内核的惩罚措施,导致 orphan 的误报。
Procfs 文件系统
/proc/net/tcp:记录 TCP 的状态信息。