Linux kernel tcp overview
一直想找机会梳理一下 kernel 的网络子系统,不如现在开始动手做,在梳理 kernel 前,先回顾一下操作系统提供的网络编程 API。
0x00 用户如何使用 想了解一下 Linux 下的 tcp 个人认为 socket API 是肯定要介绍的,自 bsd 4.2 引入到如今已经 30 多年了,核心 api 是非常稳定见如下表格
C/S
API
服务器端:
socket
,bind
,listen
,accept
,shutdown
等
————
—————————————————-
客户端:
socket
, connect
, recv
,close
等
————
—————————————————-
几个简单的接口有效的控制了网络编程的复杂度。
这些 api 围绕着一个 socket 文件操作,这个文件挂载在相对简单的 sockfs 文件系统下面 (在 socket.c 中实现),下面这个文件操作符结构体实现描述了这个类型文件支持的文件操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static const struct file_operations socket_file_ops = { .owner = THIS_MODULE, .llseek = no_llseek, .read_iter = sock_read_iter, .write_iter = sock_write_iter, .poll = sock_poll, .unlocked_ioctl = sock_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = compat_sock_ioctl, #endif .mmap = sock_mmap, .release = sock_close, .fasync = sock_fasync, .sendpage = sock_sendpage, .splice_write = generic_splice_sendpage, .splice_read = sock_splice_read, };
上面这个就是体现 unix 哲学 Everything is a file
的体现,通过 vfs 抽象将函数指针放到结构体中,当对对应的文件调用就回调这个文件系统实现的回调函数,比如说我对 socket 文件进行 mmap
调用,到具体的文件系统中就是调用了 sock_mmap
这个文件系统实现。
这个 struct 已经暴露了能对 socket 的操作了,不过并不打算对这个 struct 上纠结太多。
一般的使用场景是,首先用户通过 socket 系统调用创建 ipv4 面向字节流套接字,也就是指的 TCP 套接字,当套接字创建完成过后就可以像操作文件一样操作套接字。
我们这里关注的是 ipv4 tcp 套接字是如何建立的,如何传输数据的,如何关闭的,这三个问题。
tcp linux 实现[^TCP_Implementation] tcp 虚链路的建立,有效的关闭是学习 TCP 的基础中的基础。
多场景下高效率的传输学习和研究的难点,多场景的例子有卫星链路,其特点是带宽大延迟高;广域网,其特点是背景丢包率,IDC 内部,低延迟高带宽。
tcp 套接字的建立 众所周知的三次握手,客户端发起 syn,服务端 ack + syn,服务端 ack。这里一共三次,为什么是三次是因为 2 次不能进行双向确认,4 次显的没有效率多一次发包。
系统调用 socket
, 通过两天函数,对应内核函数 __sock_create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { int err; struct socket *sock ; const struct net_proto_family *pf ; ... pf = rcu_dereference(net_families[family]); err = -EAFNOSUPPORT; if (!pf) goto out_release; ... err = pf->create(net, sock, protocol, kern); if (err < 0 ) goto out_module_put; ...
看上面代码是__sock_create
调用已经在系统中注册的协议提供的create
方法创建sock
函数。
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 26 27 28 29 30 31 32 int sock_register (const struct net_proto_family *ops) { int err; if (ops->family >= NPROTO) { pr_crit("protocol %d >= NPROTO(%d)\n" , ops->family, NPROTO); return -ENOBUFS; } spin_lock(&net_family_lock); if (rcu_dereference_protected(net_families[ops->family], lockdep_is_held(&net_family_lock))) err = -EEXIST; else { rcu_assign_pointer(net_families[ops->family], ops); err = 0 ; } spin_unlock(&net_family_lock); pr_info("NET: Registered protocol family %d\n" , ops->family); return err; } EXPORT_SYMBOL(sock_register);
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 static int __init inet_init (void ) { struct inet_protosw *q ; struct list_head *r ; int rc = -EINVAL; sock_skb_cb_check_size(sizeof (struct inet_skb_parm)); rc = proto_register(&tcp_prot, 1 ); if (rc) goto out; rc = proto_register(&udp_prot, 1 ); if (rc) goto out_unregister_tcp_proto; rc = proto_register(&raw_prot, 1 ); if (rc) goto out_unregister_udp_proto; rc = proto_register(&ping_prot, 1 ); if (rc) goto out_unregister_raw_proto; (void )sock_register(&inet_family_ops); #ifdef CONFIG_SYSCTL ip_static_sysctl_init(); #endif if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0 ) pr_crit("%s: Cannot add ICMP protocol\n" , __func__); if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0 ) pr_crit("%s: Cannot add UDP protocol\n" , __func__); if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0 ) pr_crit("%s: Cannot add TCP protocol\n" , __func__); #ifdef CONFIG_IP_MULTICAST if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0 ) pr_crit("%s: Cannot add IGMP protocol\n" , __func__); #endif for (r = &inetsw[0 ]; r < &inetsw[SOCK_MAX]; ++r) INIT_LIST_HEAD(r); for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q) inet_register_protosw(q); arp_init(); ip_init(); tcp_init(); udp_init(); udplite4_register(); raw_init(); ping_init(); if (icmp_init() < 0 ) panic("Failed to create the ICMP control socket.\n" ); #if defined(CONFIG_IP_MROUTE) if (ip_mr_init()) pr_crit("%s: Cannot init ipv4 mroute\n" , __func__); #endif if (init_inet_pernet_ops()) pr_crit("%s: Cannot init ipv4 inet pernet ops\n" , __func__); if (init_ipv4_mibs()) pr_crit("%s: Cannot init ipv4 mibs\n" , __func__); ipv4_proc_init(); ipfrag_init(); dev_add_pack(&ip_packet_type); ip_tunnel_core_init(); rc = 0 ; out: return rc; out_unregister_raw_proto: proto_unregister(&raw_prot); out_unregister_udp_proto: proto_unregister(&udp_prot); out_unregister_tcp_proto: proto_unregister(&tcp_prot); goto out; } fs_initcall(inet_init);
tcp 的终止 众所周知的四次挥手,为什么是四次挥手关闭呢?其实这里的四次挥手的关闭指的是两个套接字两个方向的关闭,两个套接字指的是客户端和服务端,两个方向分别是发送方和接收方。
经典正确场景:当客户端 c 准备结束数据发送了,首先发起 FIN,服务端 s 收到客户端发来 FIN 信息并返回 ACK,当前阶段 c 客户端不发送数据但是还可以接收服务端发来的数据。当 s 把数据发送完成过后准备关闭这个客户的套接字,发送 FIN 给 c,这时候服务端不能发送数据。当 c 接受接收到 s 发来的 FIN,会回复一下 ACK,当 s 收到了 ack 套接字被正常关闭。
更多描述参考^wiki .
[^TCP_Implementation]: TCP Implementation in Linux: A Brief Tutorial