域套接字sendto errno -11分析

  • 域套接字sendto errno -11分析已关闭评论
  • 125 次浏览
  • A+
所属分类:linux技术
摘要

errno -11在内核代码中代表EAGAIN(再试⼀次),域套接字sendto过程中 sendto->sock_sendmsg->unix_dgram_sendmsg,在unix_dgram_sendmsg中有两处会返回 EAGAIN:
第1处:sock_alloc_send_pskb
第2处:
other!=sk&&unlikely(unix_peer(other)!=sk&&unix_recvq_full_lockless(other))
unix_peer(sk)!=other||unix_dgram_peer_wake_me(sk,other)
当以上两个条件都满⾜时也会返回 EAGAIN。
另外需要注意的是unix_dgram_sendmsg中直接通过skb_queue_tail(&other->sk_receive_queue,skb)将数据放⼊了对端的接收队列中。


sendto errno -11代码分析

errno -11在内核代码中代表EAGAIN(再试⼀次),域套接字sendto过程中 sendto->sock_sendmsg->unix_dgram_sendmsg,在unix_dgram_sendmsg中有两处会返回 EAGAIN:
第1处:sock_alloc_send_pskb
第2处:
other!=sk&&unlikely(unix_peer(other)!=sk&&unix_recvq_full_lockless(other))
unix_peer(sk)!=other||unix_dgram_peer_wake_me(sk,other)
当以上两个条件都满⾜时也会返回 EAGAIN。
另外需要注意的是unix_dgram_sendmsg中直接通过skb_queue_tail(&other->sk_receive_queue,skb)将数据放⼊了对端的接收队列中。
域套接字sendto errno -11分析

第1处
sock_alloc_send_pskb函数中当socket发送缓冲区满时( sk_wmem_alloc_get(sk)>=sk->sk_sndbuf)将返回 EAGAIN。
域套接字sendto errno -11分析
第2处

在 Linux 内核源代码中,unix_peer(other) != sk 表⽰另⼀个 Unix 域套接字( other )的对端套接字(peer socket)不等于当前套接字( sk )本⾝。
在 Unix 域套接字通信中,每个发起连接的进程(或线程)都必须创建两个⽂件描述符,⼀个⽤于客⼾端(client),称为客⼾端套接字(client socket),另⼀个⽤于服务器端
(server),称为服务器套接字(server socket)。这两个套接字通过 Unix 域⽂件系统中的某个路径名进⾏连接(bind)。
当对等⽅成功建⽴连接后,两个套接字中的⼀个将⾃动成为对⽅的对端套接字(peer socket)。这意味着两个对等⽅都有⼀个指向对⽅套接字的结构体,也就是所谓的“peer
socket”。
因此,unix_peer(other) != sk 表⽰当前套接字( sk )不是另⼀个 Unix 域套接字( other )的对端套接字。如果这个条件成⽴,那么就不能向 other 套接字发送数据,因为 other 并不是当前套接字的对端套接字,这种情况下发送数据可能会引发错误或者产⽣不确定的结果

unix_peer() 函数尝试返回指向当前 Unix Domain 套接字的对端套接字的指针,如果当前套接字不是连接状态或者没有对端套接字则返回空指针。该函数通常⽤于判断当前
Unix Domain 套接字是否有对端套接字,以决定是否可以进⾏数据发送。

  1. other!=sk, 因为 other=unix_peer_get(sk)(其实就是other=sk->peer) ,该条件意味着 sk->peer!=sk,在域套接字中 sk代表通信的⼀端,sk->peer代表通信的另⼀端,该条件是为了避免循环引⽤。本⽂档中默认 sk是客⼾端,other是服务端。
  2. unlikely(unix_peer(other)!=sk&&unix_recvq_full_lockless(other)),⾸先 unix_peer(other)!=sk意味着 sk->peer->peer!=sk说明 sk客⼾端所指向的服务端发⽣了变化(⽐如在客⼾端发送的过程中⼜有⼀个新的客⼾端与服务端建⽴了连接),其次是 unix_recvq_full_lockless(other)如下⾯代码所⽰,当条件满⾜时代表着 other服务端接收队列深度⼤于sk_max_ack_backlog
af_unix.c: static inline int unix_recvq_full_lockless(conststructsock*sk) { 	return skb_queue_len_lockless(&sk->sk_receive_queue)> 		READ_ONCE(sk->sk_max_ack_backlog); } 
  1. unix_peer(sk)!=other||unix_dgram_peer_wake_me(sk,other)unix_peer(sk)!=other⽤于判断当前 Unix Domain 套接字( sk )是否为另⼀个 Unix Domain 套接字(other )的对端套接字,这⾥只能是other发⽣了变化 ;在 unix_dgram_peer_wake_me中只有other端接收队列深度⼤于 sk_max_ack_backlog时才会 return 1
af_unix.c: static inline int unix_recvq_full(conststructsock*sk) { 	return skb_queue_len(&sk->sk_receive_queue) > sk->sk_max_ack_backlog; } static int unix_dgram_peer_wake_me(structsock*sk, structsock*other) { 	int connected; 	connected = unix_dgram_peer_wake_connect(sk,other); 	if(unix_recvq_full(other)) 		return 1; 	if(connected) 		unix_dgram_peer_wake_disconnect(sk,other); 	return 0; } 

总结

域套接字sendto errno -11存在以下可能:

  1. socket发送缓冲区满(可复现)。
  2. other的对端不是sk(本客⼾端)并且 unix_recvq_full_lockless成⽴
    2.1 sk(本客⼾端)的对端也不是other。
    2.2 other接收队列深度⼤于 sk_max_ack_backlog(可复现)。

条件2中 unix_recvq_full_lockless,代表other接收队列深度⼤于 sk_max_ack_backlog,不过这⾥ unix_recvq_full_lockless调⽤的是 skb_queue_len_lockless是不加锁的,因此这⾥存在不确定性,但⾄少内核得到的信息是other接收队列深度⼤于 sk_max_ack_backlog

条件2.1和2.2成⽴的前提是条件2先成⽴。
针对条件2.1成⽴的可能性:
1)sk 与 other 建链,此时 sk->peer==other,other==sk->peer
2)new_sk(新的客⼾端)与other建链,此时 sk->peer==other,other->peer!=sk,other->peer==new_sk,new_sk->peer==other
3)new_sk⾼速发消息到other使 unix_recvq_full_lockless条件满⾜。
4)sk发消息进⼊unix_dgram_sendmsg内部并到达unlikely(unix_peer(other)!=sk&&unix_recvq_full_lockless(other))之后并且在 unix_peer(sk)!=other|| unix_dgram_peer_wake_me(sk,other)之前的位置时,other端重新初始化了,条件 unix_peer(sk)!=other满⾜。

在sendto的过程中重新初始化other,⽬前没有很好的复现⽅法。

当客⼾环境并没有使⽤ socketpairconnect ,那么sk->peer和other->peer并没有相互引⽤,并且 other->peer==NULL。因此other的对端不是sk(本客⼾端)并且sk(本客⼾端)的对端
也不是other,在这种情况下当other接收队列深度⼤于 sk_max_ack_backlog时,将返回 EAGAIN(error -11)。
事实上,unix_dgram_sendto返回 EAGAIN时,要么 socket发送缓冲区满 ,要么 other接收队列深度⼤于sk_max_ack_backlog,因为如果第1个if不成⽴(条件2),那么第2个if也不会成⽴(条件2.1、条件2.2)。
域套接字sendto errno -11分析

调试手段

#sysctl-a|grepunix net.unix.max_dgram_qlen=512 #sudosysctl-a|grep"net.core.wmem" net.core.wmem_default=2097152 net.core.wmem_max=2097152 

net.unix.max_dgram_qlen 代表缓冲区队列深度(缓冲区中有多少个数据包)
net.core.wmem_max 代表缓冲区最⼤⼤小(所有数据包的总⻓度)
针对errno -11, 可适当增加
net.unix.max_dgram_qlen 的⼤小。

发送缓冲区溢出判断

#include<sys/ioctl.h> #include<linux/sockios.h> long outq=0; ioctl(sockfd,SIOCOUTQ,&outq); 

errno -11 前后可根据该代码获取发送缓冲区的⼤小并与net.core.wmem_max 对⽐。

kprobe 监控socket 缓冲区是否溢出

kprobe监控sock_alloc_send_pskb、unix_dgram_peer_wake_me

      add_kprobe_event 'r:probeE1 sock_alloc_send_pskb $retval'      set_kprobe_event_filter 'arg1 == 0' probeE1      add_kprobe_event 'r:probeE2 unix_dgram_peer_wake_me $retval'      set_kprobe_event_filter 'arg1 == 1' probeE2      add_kprobe_event 'r:probeE3 unix_dgram_sendmsg $retval'      set_kprobe_event_filter 'arg1 == 0xfffffff5' probeE3       enable_trace_event 'sock/sock_rcvqueue_full'      enable_trace_event 'sock/sock_exceed_buf_limit' 

skb_queue_len 和 skb_queue_len_lockless区别

在 Linux 内核中,skb_queue_len 和 skb_queue_len_lockless 都是⽤于获取 sk_buff 队列⻓度的函数。这两个函数的差异在于函数调⽤时是否⽀持锁机制,具体描述如下:
skb_queue_len() 是⼀个基于锁的队列⻓度计算函数,当需要获取 sk_buff 队列⻓度时,该函数会获取队列的⾃旋锁来保证队列在计算期间不发⽣并发修改。因为该函数对队列
实⾏锁机制,当需要查询 sk_buff 队列⻓度时可能会受到锁的竞争和等待时延等因素的影响。
skb_queue_len_lockless() 是⼀个不⽀持锁的队列⻓度计算函数,该函数可以在不加锁的情况下,快速获取 sk_buff 队列的实际⻓度。由于该函数不需要获取队列的锁,因此其
执⾏速度快,但也可能在并发写⼊数据时因为⽆法保证数据⼀致性而产⽣错误的队列⻓度计算结果。
根据内核实现,当我们希望检查队列的⻓度时,⼀般建议使⽤ skb_queue_len 而不是 skb_queue_len_lockless ,因为前者可以确保计算结果的准确性,并通过⾃旋锁确保计算
过程不受并发修改的影响。而后者则主要⽤于在不需要确保 sk_buff 队列准确性的情况下快速计算其⻓度,⽐如有些地⽅例如数据处理中可能观察者只需要⼀个近似值,⽤
skb_queue_len_lockless() 可以显著降低系统的开销。

unix_socketpair

Unix 域套接字提供了⼀种可靠的进程间通信机制,其中 unix_socketpair 是其中的⼀个函数。该函数⽤于创建⼀对相互连接的套接字,这两个套接字可以⽤于进程间的通信。
具体来说,unix_socketpair 函数创建⼀对套接字(⼜称为 socket pair),这两个套接字在创建时已经互相连接。这意味着,对其中⼀个套接字进⾏任何操作都会直接影响另⼀个
套接字。因此,可以使⽤这对套接字进⾏进程间通信,⽐如在两个进程之间传递⽂件描述符、管道、消息等。
两个套接字在创建时有以下特点:
套接字对是由内核创建的,不依赖于⽂件系统中存在的⽂件。
套接字对是⼀对⼀的,即⼀个套接字只能被⼀个进程所拥有,而另⼀个套接字只能被另⼀个进程所拥有。
套接字对是双向的,即它们既可以⽤于读也可以⽤于写。
套接字对是数据传输的最小单位,因此数据交换的时候也是传输整块数据,不能传输部分数据。
unix_socketpair 函数的调⽤⽅式如下:
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
其中 domain 参数⽤于指定套接字协议簇,type 参数⽤于指定套接字类型,protocol 参数⽤于指定协议类型。sv 参数是⼀个已经分配好的数组,⽤于返回 socket pair 的描述
符。调⽤成功将返回 0,否则返回 -1。

connect操作

在 Unix 域套接字中,unix_dgram_connect 函数⽤于建⽴连接并指定该连接的⽬标套接字地址。该函数主要⽤于 Datagram 套接字的客⼾端,能够为该套接字指定⼀个默认
的⽬标地址,以便在后续的发送操作中⽆需再指定⽬标地址。
unix_dgram_connect 函数的具体作⽤如下:
建⽴连接。连接建⽴后,套接字就可以直接向指定的⽬标地址发送数据,不再需要每次都指定⽬标地址。
指定默认⽬标地址。连接建⽴后,可以使⽤ unix_dgram_sendmsg 等函数向默认⽬标地址发送数据。
接收数据。经过 unix_dgram_connect 函数连接的套接字也可以接收从指定的⽬标地址发送过来的数据。
请注意,unix_dgram_connect 函数并不是必须的,如果在套接字操作中指定了⽬标地址,则会⾃动建⽴连接。但是通过 unix_dgram_connect 函数建⽴连接可以使得发送数
据等操作更加⽅便。
unix_dgram_connect 函数的函数原型如下:
#include <sys/socket.h>
int unix_dgram_connect(int sockfd, const struct sockaddr_un* addr, socklen_t addrlen);
其中 sockfd 参数是待连接套接字的⽂件描述符,addr 参数是⽬标套接字地址,addrlen 参数是⽬标套接字地址的⻓度。调⽤成功将返回 0,否则返回 -1。

sendto
sendto时如果other不存在,则会主动通过地址和路径去寻找other,然后通过 unix_may_send是否可以发送,允许发送的条件是 other->peer==NULL or other->peer==sk
域套接字sendto errno -11分析