一、TCP中的数据收发过程
TCP协议简介
TCP/IP是面向连接的、可靠的、基于字节流的传输层通信协议。TCP的全称Transport Control Protocal即(数据)传输控制协议,其主要包含了建立连接(三次握手)、滑动窗口机制和拥塞控制。TCP数据段的首部如下图所示:
下面我们略过TCP建立连接的部分,对TCP滑动窗口和拥塞控制进行介绍
TCP滑动窗口
TCP是全双工的协议,会话双方可以同时收发信息,所以滑动窗口分为发送窗口和接收窗口。发送窗口包括了“已发送但未收到确认”和“未发送但对方允许发送”的数据,接收窗口包括了“未接收但允许对方发送”的数据。用滑动窗口实现字节流的传输的可靠性来源于确认重传机制,发送窗口只有在收到对已发送字节的确认ACK后才会移动左边界,接收窗口只有在前面字节都已接收的情况下才会移动左边界(不能空缺)。滑动窗口机制图示如下:
TCP拥塞控制
TCP有流量控制的功能,可以根据网络拥塞情况调整收发速度。实现这一功能的算法主要有四种:慢启动、拥塞避免、快重传和快恢复。引入三个参数:拥塞窗口(cwnd)限制了发送端接收到确认前可以发送的最大数据量,接收端通知窗口(rwnd)限制了接收端可接收数据量,慢启动阈值(ssthresh)决定了传输方式是用慢启动还是拥塞避免。cwnd与rwnd即对应TCP滑动窗口中的发送窗口和接收窗口。cwnd每收到一次确认增长一次,在慢启动阶段为二次指数增长,在达到ssthresh后改变为线性增长(拥塞避免),出现网络超时后重新回到慢启动,而在收到3-ACK后采用快重传与快恢复,即cwnd减半后线性增长。整个机制的具体描述如下图:
二、send和recv源代码分析
TCP SOCKET的系统调用的总入口位于linux/net/socket.c
中的SYSCALL_DEFINE2
函数,查看后发现send、sento与recv、recvfrom其实只对应两个系统调用:__sys_sendto
和__sys_recvfrom
。
查看这两个函数的源代码:
__sys_sendto
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg;
struct iovec iov;
int fput_needed;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
__sys_recvfrom
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
struct sockaddr_storage address;
int err, err2;
int fput_needed;
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don't copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2;
}
fput_light(sock->file, fput_needed);
out:
return err;
}
可以看到,这两个系统调用只是简单的分配了发送缓冲区和接收缓冲区的文件指针,并未涉及滑动窗口和拥塞控制。
然而我们发现在SYSCALL_DEFINE2
中还有4个与收发数据有关的调用,__sys_sendmsg
、__sys_sendmmsg
、__sys_recvmsg
和__sys_recvmmsg
查看它们的源代码:
__sys_sendmsg
long __sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned int flags,
bool forbid_cmsg_compat)
{
int fput_needed, err;
struct msghdr msg_sys;
struct socket *sock;
if (forbid_cmsg_compat && (flags & MSG_CMSG_COMPAT))
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL, 0);
fput_light(sock->file, fput_needed);
out:
return err;
}
__sys_sendmmsg
int __sys_sendmmsg(int fd, struct mmsghdr __user *mmsg, unsigned int vlen,
unsigned int flags, bool forbid_cmsg_compat)
{
int fput_needed, err, datagrams;
struct socket *sock;
struct mmsghdr __user *entry;
struct compat_mmsghdr __user *compat_entry;
struct msghdr msg_sys;
struct used_address used_address;
unsigned int oflags = flags;
if (forbid_cmsg_compat && (flags & MSG_CMSG_COMPAT))
return -EINVAL;
if (vlen > UIO_MAXIOV)
vlen = UIO_MAXIOV;
datagrams = 0;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
return err;
used_address.name_len = UINT_MAX;
entry = mmsg;
compat_entry = (struct compat_mmsghdr __user *)mmsg;
err = 0;
flags |= MSG_BATCH;
while (datagrams < vlen) {
if (datagrams == vlen - 1)
flags = oflags;
if (MSG_CMSG_COMPAT & flags) {
err = ___sys_sendmsg(sock, (struct user_msghdr __user *)compat_entry,
&msg_sys, flags, &used_address, MSG_EOR);
if (err < 0)
break;
err = __put_user(err, &compat_entry->msg_len);
++compat_entry;
} else {
err = ___sys_sendmsg(sock,
(struct user_msghdr __user *)entry,
&msg_sys, flags, &used_address, MSG_EOR);
if (err < 0)
break;
err = put_user(err, &entry->msg_len);
++entry;
}
if (err)
break;
++datagrams;
if (msg_data_left(&msg_sys))
break;
cond_resched();
}
fput_light(sock->file, fput_needed);
/* We only return an error if no datagrams were able to be sent */
if (datagrams != 0)
return datagrams;
return err;
}
__sys_recvmsg
long __sys_recvmsg(int fd, struct user_msghdr __user *msg, unsigned int flags,
bool forbid_cmsg_compat)
{
int fput_needed, err;
struct msghdr msg_sys;
struct socket *sock;
if (forbid_cmsg_compat && (flags & MSG_CMSG_COMPAT))
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
err = ___sys_recvmsg(sock, msg, &msg_sys, flags, 0);
fput_light(sock->file, fput_needed);
out:
return err;
}
__sys_recvmmsg
int __sys_recvmmsg(int fd, struct mmsghdr __user *mmsg,
unsigned int vlen, unsigned int flags,
struct __kernel_timespec __user *timeout,
struct old_timespec32 __user *timeout32)
{
int datagrams;
struct timespec64 timeout_sys;
if (timeout && get_timespec64(&timeout_sys, timeout))
return -EFAULT;
if (timeout32 && get_old_timespec32(&timeout_sys, timeout32))
return -EFAULT;
if (!timeout && !timeout32)
return do_recvmmsg(fd, mmsg, vlen, flags, NULL);
datagrams = do_recvmmsg(fd, mmsg, vlen, flags, &timeout_sys);
if (datagrams <= 0)
return datagrams;
if (timeout && put_timespec64(&timeout_sys, timeout))
datagrams = -EFAULT;
if (timeout32 && put_old_timespec32(&timeout_sys, timeout32))
datagrams = -EFAULT;
return datagrams;
}
观察以上代码,发现__sys_sendmmsg
给出了发送窗口的基本结构,__sys_recvmmsg
中涉及了超时重传,其他滑动窗口与拥塞控制的细节都未出现,说明其实现并不在这一层级中。这些机制的实现应该在tcp协议栈的初始化过程中就已经完成。
三、运行跟踪
给__sys_sendto
、__sys_recvfrom
、__sys_sendmsg
、__sys_sendmmsg
、__sys_recvmsg
和__sys_recvmmsg
都打上断点,然后运行reply/hi
发现只有__sys_sendto
和__sys_recvfrom
被调用了两次,说明在tcp的send与recv过程中,只需要分配好发送和接收方的文件指针(分配符),即可完成数据的收发。而控制完整数据收发过程的相应机制的实现,与这一层级无关。