用户态发送函数列表
1 | ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
发送函数之间差别
send 有连接协议发送数据使用,send第四个参数flags为0时候,等价于write
send(sockfd, buf, len, 0) 等价 write(sockfd, buf, len)
send是sendto一部分,send可被sendto替换
send(sockfd, buf, len, flags) 等价于 sendto(sockfd, buf, len, flags, NULL, 0)
sendto 无连接和有连接发包都可以使用
sendmsg 可替换上树所有的发包函数
1
2
3
4
5
6
7
8
9 struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags (unused) */
};
/proc/sys/net/core/optmem_max可控制每个socket的msg_control大小
sendmsg不使用msg_flags参数
send发包过程概述
阻塞模式下
调用send函数时候,比较要发送数据和套接字发送缓冲区长度(net.ipv4.tcp_wmem);如果发送缓冲区较小,函数直接返回SOCKET_ERR;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20if send_len <= tcp_wmem{
if is sending{
wait
if network err
return SCOKET_ERR
}
else{
if len > tcp_wmem left{
wait
if network err
return SCOKET_ERR
}
else{
copy data to tcp buf
if copy err
return SCOKET_ERR
return copy data size
}
}
}剩余缓冲区能容纳发送数据,则直接将数据拷贝到缓冲区中,send直接返回。如果剩余缓冲区不足,发送端阻塞等待,对端在协议栈层接收到数据后会发送ack确认,发送方接收到ack后释放缓冲区空间;如果此时剩余缓冲区大小可放置要发送数据,则直接将数据拷入缓冲区,返回。
Tips:阻塞模式下,数据发送正常,其返回的数据长度一定是发送数据的长度。
- 非阻塞模式下
send函数将数据拷入协议栈缓冲区,如果缓冲区不足,则send尽力拷贝,并返回拷贝大小;如果缓冲区满则返回-1,同时errno为EAGAIN,让发送端再次尝试发送数据。
发送缓冲区设置
socklen_t sendbuflen = 0;
socklen_t len = sizeof(sendbuflen);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("default,sendbuf:%d\n", sendbuflen);
sendbuflen = 10240;
setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("now,sendbuf:%d\n", sendbuflen);
send发包实例解析
实际socket使用过程中,常用的是非阻塞模式,我们就以非阻塞模式为例进行分析,预设多种场景如下:
场景1:发送端10k数据已经安全放入缓冲区,已实际发出2k(收到对端ack),接收端正在处理数据,此时发送端因为10k数据发送完毕,关闭了socket。
场景分析:
发送端关闭socket,主动fin告诉对端发送端数据发送完毕想关闭TCP连接,发送完fin后发送端处于fin wait1状态等待接收端ack确认;发送端协议栈剩余8k数据依然在独立发送,待数据发送完成后,协议栈才会把fin发给接收端;接收端在接收ack完10k数据后,且收到fin信号后,接收端回复ack确认fin信号,两者协商关闭socket。
场景2:发送端预期发送10k数据,已将2k数据拷入缓冲区并实际发出拷入的2k数据(收到对端ack),接收端正在处理数据,此时发送端又发送了8k新数据;(缓冲区充足(8k新数据会被拷入缓冲区)情况我们不讨论)缓冲区不足时候会发生什么?
场景分析
新发送的10k数据会尽力拷入缓冲区,send返回拷入缓冲区数据长度2k,如果此时缓冲区剩余空间为0时候,客户端强制send数据,会收到EAGAIN信号;其实这种情况客户端正确处理方式是读出缓冲区可写信号再发送数据,而不是自己进行发送尝试。
场景3:发送端10k数据已经安全放入缓冲区,已实际发出2k(收到对端ack),接收端正在处理接收到1k数据,处理完成后数据接收端关闭了socket,会发发生什么?
场景分析
- 数据发送端有监听机制,数据发送端用户态会得到接收端端关闭信号(socket可读信号),这时候用户正确打开方式是调用close关闭socket
- 如果数据发送端未处理该关闭信号,且数据接收端没有rst强制关闭连接,数据发送端仍然可正常发送数据
- 如果数据发送端未处理该关闭信号,但是数据接收端已经rst强制关闭连接,数据发送端仍然在send发送数据,send将返回-1
- 如果是阻塞情况,但是因缓冲区满正在阻塞,如果接收端发送rst,阻塞发送端会退出阻塞返回,发送成功字节数,如果在此调用send,将返回-1
场景4:发送端10k数据已经安全放入缓冲区,已实际发出2k(收到对端ack),接收端正在处理接收到1k数据,此时网络出现异常
场景分析
接收应用程序在处理完已收到的1k数据后,会继续从缓存区读取余下的1k数据,然后就表现为无数据可读的现象,这种情况需要应用程序来处理超时.一般做法是设定一个select等待的最大时间,如果超出这个时间依然没有数据可读,则认为socket已不可用.
发送应用程序会不断的将余下的数据发送到网络上,但始终得不到确认,所以缓存区的可用空间持续为0,这种情况也需要应用程序来处理.如果不由应用程序来处理这种情况超时的情况,也可以通过tcp协议本身来处理,具体可以查看sysctl项中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time
send特点
- send只是将数据放入缓冲区中,并不是真正已经发给对方
- 非阻塞发送字节可以是1-n,其发送多少完全依赖于剩余的发送缓冲区
socket发送函数解析
发送流程图
send
sendto
sendmmsg
sendmsg
上述流程调用过程如下:
->socketcall ->sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg(inet_sendmsg)
->[tcp_prot]tcp_sendmsg
内核系统调用
send 、sendto、sendmsg、sendmmsg发送函数由glibc提供,声明于/usr/include/sys/socket.h
用户态在调用后会进入到sys_socketcall系统调用中,下面代码部分就是其入口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
...
switch (call) {
...
case SYS_SEND:
err = sys_send(a0, (void __user *)a1, a[2], a[3]);
break;
case SYS_SENDTO:
err = sys_sendto(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4], a[5]);
break;
...
case SYS_SENDMSG:
err = sys_sendmsg(a0, (struct msghdr __user *)a1, a[2]);
break;
case SYS_SENDMMSG:
err = sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3]);
break;
...
default:
err = -EINVAL;
break;
}
return err;
}
- send 是sendto的一种特殊情况,(sendto发送地址为NULL发送地址长度为0)
1 | SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len, |
- sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg(inet_sendmsg)
1 | SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len, |
- sendmsg 和sendmmsg 完成用户态数据拷贝到内核态后,最终也是调用inet_sendmsg处理,在此就拿sendto情况详细分析
sendto源码实现分析
sendto -> sock_sendmsg -> “sock_sendmsg” ->”sock_sendmsg_nosec” -> sock->ops->sendmsg(inet_sendmsg)
- 首先分析sock_sendmsg实现
1 | int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) |
- 其次分析inet_autobind ,获取可用端口并给,获取后的端口会赋值给inet->inet_sport/inet_num
1 | static int inet_autobind(struct sock *sk) |
- 最后分析tcp_sendmsg
1 | int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, |
tcp_sendmsg()做了以下事情:
- 如果使用了TCP Fast Open,则会在发送SYN包的同时携带上数据。
- 如果连接尚未建立好,不处于ESTABLISHED或者CLOSE_WAIT状态,
那么进程进行睡眠,等待三次握手的完成。 - 获取当前的MSS、网络设备支持的最大数据长度size_goal。
如果支持GSO,size_goal会是MSS的整数倍。 - 遍历用户层的数据块数组:
4.1 获取发送队列的最后一个skb,如果是尚未发送的,且长度尚未达到size_goal,
4.2 否则需要申请一个新的skb来装载数据。那么可以往此skb继续追加数据。
4.2.1 如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,
4.2.2 申请一个skb,其线性数据区的大小为:或者发送缓存中尚未发送的数据量超过了用户的设置值: 设置同步发送时发送缓存不够的标志。 如果此时已有数据复制到发送队列了,就尝试立即发送。 等待发送缓存,直到sock有发送缓存可写事件唤醒进程,或者等待超时。
4.2.3 如果以上两步成功了,就更新skb的TCP控制块字段,把skb加入到sock发送队列的尾部,通过select_size()得到的线性数据区中TCP负荷的大小 + 最大的协议头长度。 如果申请skb失败了,或者虽然申请skb成功,但是从系统层面判断此次申请不合法, 等待可用内存,等待时间为2~202ms之间的一个随机数。
4.3 接下来就是拷贝消息头中的数据到skb中了。增加发送队列的大小,减小预分配缓存的大小。
4.4 如果skb的线性数据区已经用完了,那么就使用分页区:如果skb的线性数据区还有剩余空间,就复制数据到线性数据区中,同时计算校验和。
4.4.1 检查分页是否有可用空间,如果没有就申请新的page。如果申请失败,说明系统内存不足。
4.4.2 判断能否往最后一个分页追加数据。不能追加时,检查分页数是否达到了上限、之后会设置TCP内存压力标志,减小发送缓冲区的上限,睡眠等待内存。
4.4.3 从系统层面判断此次分页发送缓存的申请是否合法。或网卡不支持分散聚合。如果是的话,就为此skb设置PSH标志。 然后跳转到4.2处申请新的skb,来继续填装数据。
4.4.4 拷贝用户空间的数据到skb的分页中,同时计算校验和。
4.4.5 如果把数据追加到最后一个分页了,更新最后一个分页的数据大小。否则初始化新的分页。更新skb的长度字段,更新sock的发送队列大小和预分配缓存。
4.5 拷贝成功后更新:发送队列的最后一个序号、skb的结束序号、已经拷贝到发送队列的数据量。
4.6 尽可能的将发送队列中的skb发送出去。