TCP
TCP通过校验和、序列号、确认应答、重发控制、连接管理和窗口控制实现可靠传输。
TCP通过确认应答ACK来实现有保障的数据传输,但是由于各种原因,目标主机可能无法收到ACK信号,导致源主机不停重发数据。为此,引入序列号与确认信号相结合,实现有效的重发控制。
作为面向连接的协议,TCP在数据通信前,通过TCP首部发送一个SYN包作为建立连接的请求等待确认应答。一个连接的建立与断开至少要来回发送7个包才能完成。其中建立连接三次握手,需要发送3个包。切断连接四次挥手,需要发送4个包。
LINUX & TCP
下图展示了LINUX中TCP客户机和服务器交互时调用的基本套接字函数。
(图源:UNIX网络编程-第三版)
一个进程要进行网络读写,第一个执行的就是socket()
,它定义在socket.h
文件中:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
其中family
指定协议类型簇(IPv4、IPv6、Unix域协议、路由套接字、密钥套接字),type
指定套接字类型,protocol
指定协议的类型常量(TCP、UDP、SCTP)。socket()
函数返回套接字描述符。
跟踪调用栈
(图源:https://www.jianshu.com/p/5d82a685b5b6)
启动qemuOS,打开GDB调试,连接到远程。在__sys_socket()
函数处打上断点,追踪函数调用栈:
在gdb中根据情况交替使用next
和step
命令,追踪程序运行。函数调用了__sock_create()
实际执行socket创建任务。对__socket_create()
进行追踪。
在__sock_create()
函数中
sock_alloc()
在__socket_create()
中,又调用了sock_alloc()
函数将socket分配给一个结构体变量sock
。
这里,sock
的类型为struct socket*
,在linux源码目录下使用正则表达式find -name "*.h" | xargs grep "strcut socket {" -rn
找到该结构体定义在./linux-5.0.1/include/linux/net.h
中,如下:
struct socket {
socket_state state;
short type;
unsigned long flags;
struct socket_wq *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
state
是socket的状态,type
是类型,flags
是标志位,eq
是等待队列,file
是指针列表。sock
是一个重要的结构体,同理可以找到结构体sock的定义,根据注释描述,它表示一个网络层的socket。
new_inode_pseudo()
接着调用了new_inode_pseudo()
函数,在相应的文件系统中创建一个inode。其代码位于./linux-5.0.1/fs/inode.c
中:
struct inode *new_inode_pseudo(struct super_block *sb)
{
struct inode *inode = alloc_inode(sb);
if (inode) {
spin_lock(&inode->i_lock);
inode->i_state = 0;
spin_unlock(&inode->i_lock);
INIT_LIST_HEAD(&inode->i_sb_list);
}
return inode;
}
inode为socket文件系统中的节点。inode和socket对象是连结在一起的,可以根据inode取得socket对象。分配inode后,应用程序可以通过文件描述符对socket进行读写访问。
协议簇初始化socket
继续往下,有一条语句:
pf = rcu_dereference(net_families[family]);
用于查找内核初始化时注册的协议簇。当传入的family
字段存在于协议簇中时,又继续执行一条语句:
err = pf->create(net, sock, protocol, kern);
这一步调用的是inet_create()
函数,它位于./net/ipv4/Af_inet.c
中。
inet_create()
这个函数主要完成四个工作:
1 将socket的state设置为SS_UNCONNECTED
sock->state = SS_UNCONNECTED;
2 根据type字段找到对应套接字类型
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
if (unlikely(err)) {
if (try_loading_module < 2) {
rcu_read_unlock();
/*
* Be more specific, e.g. net-pf-2-proto-132-type-1
* (net-pf-PF_INET-proto-IPPROTO_SCTP-type-SOCK_STREAM)
*/
if (++try_loading_module == 1)
request_module("net-pf-%d-proto-%d-type-%d",
PF_INET, protocol, sock->type);
/*
* Fall back to generic, e.g. net-pf-2-proto-132
* (net-pf-PF_INET-proto-IPPROTO_SCTP)
*/
else
request_module("net-pf-%d-proto-%d",
PF_INET, protocol);
goto lookup_protocol;
} else
goto out_rcu_unlock;
}
err = -EPERM;
if (sock->type == SOCK_RAW && !kern &&
!ns_capable(net->user_ns, CAP_NET_RAW))
goto out_rcu_unlock;
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_flags = answer->flags;
rcu_read_unlock();
3 初始化socket的sk
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
if (!sk)
goto out;
err = 0;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = SK_CAN_REUSE;
4 建立socket与sock之间的关系
inet = inet_sk(sk);
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
inet->nodefrag = 0;
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1;
}
if (net->ipv4.sysctl_ip_no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->inet_id = 0;
sock_init_data(sock, sk);
5 使用具体协议初始化socket
这一步通过hook机制(回调函数)实现。
回到__sys_socket()
在函数的最后使用sock_map_fd()
,将初始化好的socket与文件系统关联,对于TCP和其他协议来说,区别在于socket结构体中family字段的不同:
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));