TCP/IP协议栈在Linux内核中的运行时序分析
一、TCP/IP协议栈介绍
1.OSI模型
在介绍TCP/IP协议之前,让我们先来了解一下OSI模型。
开放式系统互联模型(英语:Open System Interconnection Model,缩写:OSI;简称为OSI模型)是一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。定义于ISO/IEC 7498-1。
该模型将通信系统中的数据流划分为七个层,从跨通信介质传输位的物理实现到分布式应用程序数据的最高层表示。每个中间层为其上一层提供功能,其自身功能则由其下一层提供。功能的类别通过标准的通信协议在软件中实现。开放式系统互联模型的开发始于上世纪70年代后期,用以支持各种计算机联网方法的出现。在上世纪80年代,该模型成为国际标准化组织(ISO)开放系统互连小组的工作产品。
-
物理层(Physical Layer)在局部局域网上传送数据帧(Data Frame),它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。
-
数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成信息框(Data Frame)。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。分为两个子层:逻辑链路控制(logical link control,LLC)子层和介质访问控制(Media access control,MAC)子层。
- 网络层(Network Layer)决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络资料。例如:互联网协议(IP)等。
- 传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP)等。
- 会话层(Session Layer)负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。
- 表示层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式。
- 应用层(Application Layer)提供为应用软件而设计的接口,以设置与另一应用软件之间的通信。例如:HTTP、HTTPS、FTP、Telnet、SSH、SMTP、POP3等。
OSI模型下的信息传递如下图:
2.TCP/IP协议栈
互联网协议套件(英语:Internet Protocol Suite,缩写IPS)[1]是网络通信模型,以及整个网络传输协议家族,为网际网络的基础通信架构。它常通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols),简称TCP/IP[2]。因为该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准[3]。由于在网络通讯协议普遍采用分层的结构,当多个层次的协议共同工作时,类似计算机科学中的堆栈,因此又称为TCP/IP协议栈(英语:TCP/IP Protocol Stack)[4][5] 。这些协议最早发源于美国国防部(缩写为DoD)的ARPA网项目,因此也称作DoD模型(DoD Model)[6]。这个协议族由互联网工程任务组负责维护。
TCP/IP提供了点对点链接的机制,将资料应该如何封装、寻址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式,分别实现出不同通信协议。协议族下的各种协议,依其功能不同,分别归属到这四个层次结构之中[7][8],常视为是简化的七层OSI模型。
- 网络访问(链接)层实际上并不是因特网协议组中的一部分,但是它是数据包从一个设备的网络层传输到另外一个设备的网络层的方法。这个过程能够在网卡的软件驱动程序中控制,也可以在韧体或者专用芯片中控制。这将完成如添加报头准备发送、通过实体介质实际发送这样一些数据链路功能。另一端,链路层将完成数据帧接收、去除报头并且将接收到的包传到网络层。 然而,链路层并不经常这样简单。它也可能是一个虚拟专有网络(VPN)或者隧道,在这里从网络层来的包使用隧道协议和其他(或者同样的)协议组发送而不是发送到实体的接口上。VPN和通道通常预先建好,并且它们有一些直接发送到实体接口所没有的特殊特点(例如,它可以加密经过它的数据)。由于现在链路“层”是一个完整的网络,这种协议组的递归使用可能引起混淆。但是它是一个实现常见复杂功能的一个优秀方法。(尽管需要注意预防一个已经封装并且经隧道发送下去的数据包进行再次地封装和发送)。
- 网络层解决在一个单一网络上传输数据包的问题。类似的协议有X.25和ARPANET的Host/IMP Protocol。 随着因特网思想的出现,在这个层上添加附加的功能,也就是将数据从源网络传输到目的网络
- 传输层(transport layer)的协议,能够解决诸如端到端可靠性(“数据是否已经到达目的地?”)和保证数据按照正确的顺序到达这样的问题。在TCP/IP协议组中,传输协议也包括所给数据应该送给哪个应用程序。 在TCP/IP协议组中技术上位于这个层的动态路由协议通常认为是网络层的一部分;一个例子就是OSPF(IP协议89)。 TCP(IP协议6)是一个“可靠的”、面向链接的传输机制,它提供一种可靠的字节流保证数据完整、无损并且按顺序到达。TCP尽量连续不断地测试网络的负载并且控制发送数据的速度以避免网络过载。另外,TCP试图将数据按照规定的顺序发送。这是它与UDP不同之处,这在实时数据流或者路由高网络层丢失率应用的时候可能成为一个缺陷。 较新的SCTP也是一个“可靠的”、面向链接的传输机制。它是面向记录而不是面向字节的,它在一个单独的链接上提供通过多路复用提供的多个子流。它也提供多路自寻址支持,其中链接终端能够以多个IP地址表示(代表多个实体接口),这样的话即使其中一个连接失败了也不中断。它最初是为电话应用开发的(在IP上传输SS7),但是也可以用于其他的应用。 UDP(IP协议号17)是一个无链接的数据报协议。它是一个“尽力传递”(best effort)或者说“不可靠”协议——不是因为它特别不可靠,而是因为它不检查数据包是否已经到达目的地,并且不保证它们按顺序到达。如果一个应用程序需要这些特性,那它必须自行检测和判断,或者使用TCP协议。 UDP的典型性应用是如流媒体(音频和视频等)这样按时到达比可靠性更重要的应用,或者如DNS查找这样的简单查询/响应应用,如果创建可靠的链接所作的额外工作将是不成比例地大。 DCCP目前正由IETF开发。它提供TCP流动控制语义,但对于用户来说保留UDP的数据报服务模型。 TCP和UDP都用来支持一些高层的应用。任何给定网络地址的应用通过它们的TCP或者UDP端口号区分。根据惯例使一些大众所知的端口与特定的应用相联系。 RTP是为如音频和视频流这样的实时数据设计的数据报协议。RTP是使用UDP包格式作为基础的会话层,然而据说它位于因特网协议栈的传输层。
- 应用层包括所有和应用程序协同工作,利用基础网络交换应用程序专用的数据的协议。 应用层是大多数普通与网络相关的程序为了通过网络与其他程序通信所使用的层。这个层的处理过程是应用特有的;数据从网络相关的程序以这种应用内部使用的格式进行传送,然后编码成标准协议的格式。
3.TCP/IP协议栈与OSI模型之间的的对应
人们已经进行一些讨论关于如何将TCP/IP协议栈映射到OSI模型。
二、Linux内核网络协议栈
协议栈实现层级:
-
硬件层(Physical device hardware):又称驱动程序层,提供连接硬件设备的接口。
-
设备无关层(Device agnostic interface):又称设备接口层,提供与具体设备无关的驱动程序抽象接口。这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如 open,close,init 等,这一层可以屏蔽底层不同的驱动程序。
-
网络协议层(Network protocols):对应 IP layer 和 Transport layer。毫无疑问,这是整个内核网络协议栈的核心。这一层主要实现了各种网络协议,最主要的当然是 IP,ICMP,ARP,RARP,TCP,UDP 等。
-
协议无关层(Protocol agnostic interface),又称协议接口层,本质就是 SOCKET 层。这一层的目的是屏蔽网络协议层中诸多类型的网络协议(主要是 TCP 与 UDP 协议,当然也包括 RAW IP, SCTP 等等),以便提供简单而同一的接口给上面的系统调用层调用。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个 SOCKET,这个 SOCKET 其实是一个巨大的 sock 结构体,它和下面的网络协议层联系起来,屏蔽了不同的网络协议,通过系统调用接口只把数据部分呈献给应用层。系统调用接口层(System call interface),实质是一个面向用户空间(User Space)应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。
- BSD(Berkeley Software Distribution)socket:BSD Socket 层,提供统一的 SOCKET 操作接口,与 socket 结构体关系紧密。
- INET(指一切支持 IP 协议的网络) socket:INET socket 层,调用 IP 层协议的统一接口,与 sock 结构体关系紧密。
协议栈的数据结构:
-
msghdr:描述了从应用层传递下来的消息格式,包含有用户空间地址,消息标记等重要信息。
-
iovec:描述了用户空间地址的起始位置。
-
file:描述文件属性的结构体,与文件描述符一一对应。
-
file_operations:文件操作相关结构体,包括
read()
、write()
、open()
、ioctl()
等。 -
socket:向应用层提供的 BSD socket 操作结构体,协议无关,主要作用为应用层提供统一的 Socket 操作。
-
sock:网络层 sock,定义与协议无关操作,是网络层的统一的结构,传输层在此基础上实现了 inet_sock。
-
sock_common:最小网络层表示结构体。
-
inet_sock:表示层结构体,在 sock 上做的扩展,用于在网络层之上表示 inet 协议族的的传输层公共结构体。
-
proto_ops:BSD socket 层到 inet_sock 层接口,主要用于操作 socket 结构
-
udp_sock:传输层
-
proto:inet_sock 层到传输层操作的统一接口,主要用于操作 sock 结构。
-
net_proto_family:用于标识和注册协议族,常见的协议族有 IPv4、IPv6。
-
softnet_data:内核为每个 CPU 都分配一个这样的 softnet_data 数据空间。每个 CPU 都有一个这样的队列,用于接收数据包。
-
UDP 协议专用 sock 结构,在传输层 inet_sock 上扩展。
-
sk_buff:描述一个帧结构的属性,包含 socket、到达时间、到达设备、各层首部大小、下一站路由入口、帧长度、校验和等等。
-
sk_buff_head:数据包队列结构。
-
net_device:这个巨大的结构体描述一个网络设备的所有属性,数据等信息。
-
inet_protosw:向 IP 层注册 socket 层的调用操作接口。
-
inetsw_array:socket 层调用 IP 层操作接口都在这个数组中注册。
-
sock_type:socket 类型。
-
IPPROTO:传输层协议类型 ID。
-
net_protocol:用于传输层协议向 IP 层注册收包的接口
-
packet_type:以太网数据帧的结构,包括了以太网帧类型、处理方法等。
-
rtable:路由表结构,描述一个路由表的完整形态。
-
rt_hash_bucket:路由表缓存。
-
dst_entry:包的去向接口,描述了包的去留,下一跳等路由关键信息
网络协议栈初始化流程
内核初始r化过程中,start_kernel()的过程中会执行socket_init()来完成协议栈的初始化。sock_init()
包含了内核协议栈的初始化工作:
- sock_init:Initialize sk_buff SLAB cache,注册 SOCKET 文件系统。
- net_inuse_init:为每个 CPU 分配缓存。
- proto_init:在 /proc/net 域下建立 protocols 文件,注册相关文件操作函数。
- net_dev_init:建立 netdevice 在 /proc/sys 相关的数据结构,并且开启网卡收发中断;为每个 CPU 初始化一个数据包接收队列(softnet_data),包接收的回调;注册本地回环操作,注册默认网络设备操作。
- inet_init:注册 INET 协议族的 SOCKET 创建方法,注册 TCP、UDP、ICMP、IGMP 接口基本的收包方法。为 IPv4 协议族创建 proc 文件。此函数为协议栈主要的注册函数:
rc = proto_register(&udp_prot, 1);
:注册 INET 层 UDP 协议,为其分配快速缓存。(void)sock_register(&inet_family_ops);
:向static const struct net_proto_family *net_families[NPROTO]
结构体注册 INET 协议族的操作集合(主要是 INET socket 的创建操作)。inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0;
:向externconst struct net_protocol *inet_protos[MAX_INET_PROTOS]
结构体注册传输层 UDP 的操作集合。static struct list_head inetsw[SOCK_MAX]; for (r = &inetsw[0]; r < &inetsw[SOCK_MAX];++r) INIT_LIST_HEAD(r);
:初始化 SOCKET 类型数组,其中保存了这是个链表数组,每个元素是一个链表,连接使用同种 SOCKET 类型的协议和操作集合。for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
:inet_register_protosw(q);
:向 sock 注册协议的的调用操作集合。
arp_init();
:启动 ARP 协议支持。ip_init();
:启动 IP 协议支持。udp_init();
:启动 UDP 协议支持。dev_add_pack(&ip_packet_type);
:向ptype_base[PTYPE_HASH_SIZE];
注册 IP 协议的操作集合。socket.c
提供的系统调用接口。
协议栈初始化完成后再执行 dev_init()
,继续设备的初始化。
三、TCP/IP协议栈在Linux内核中运行分析
1.测试代码
本文基于以下代码进行调试分析:
server:
//#include <stdio.h> //#include <winsock2.h> //#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll #include "wrapper.h" int main(){ //初始化 DLL WSADATA wsaData; startup(wsaData); //创建套接字 SOCKET servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //绑定套接字 memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充 sockAddr.sin_family = AF_INET; //使用IPv4地址 sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址 sockAddr.sin_port = htons(1234); //端口 BIND(servSock,sockAddr,sizeof(sockAddr)); //进入监听状态 listen(servSock, 20); //接收客户端请求 socklen_t nSize = sizeof(clntAddr); SOCKET clntSock = ACCEPT(servSock, clntAddr, nSize); //向客户端发送数据 char *str =(char*) "Hello World!"; SEND(clntSock, str, strlen(str)+sizeof(char)); // //关闭套接字 // closesocket(clntSock); // closesocket(servSock); // // //终止 DLL 的使用 // WSACleanup(); CLOSE(servSock,clntSock); return 0; }
client端:
#include "wrapper.h" int main(){ //初始化DLL WSADATA wsaData; startup(wsaData); //创建套接字 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //向服务器发起请求 memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充 sockAddr.sin_family = AF_INET; sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); sockAddr.sin_port = htons(1234); CONNECT(sock, sockAddr, sizeof(sockAddr)); //接收服务器传回的数据 char szBuffer[1024] = {0}; RECV(sock, szBuffer); //输出接收到的数据 printf("Message form server: %s\n", szBuffer); // //关闭套接字 // closesocket(sock); // // //终止使用 DLL // WSACleanup(); CLOSECLITE(sock); return 0; }
2.send过程
-
应用层
调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。对于TCP,应用接着调用 connect()函数,使得客户端和服务器端通过该 socket 建立一个连接。然后可以调用send函数发出一个 message 给接收端。send会调用sock_sendmsg函数int sock_sendmsg(struct socket *sock, struct msghdr *msg) { int err = security_socket_sendmsg(sock, msg, msg_data_left(msg)); return err ?: sock_sendmsg_nosec(sock, msg); }
sock_sendmsg函数会调用security_socket_sendmsg()和sock_send_nosec()函数,又会调用到inet_sendmsg
-
传输层
数据到了传输层的处理,以TCP协议为例。TCP主要处理:(1)构造 TCP segment (2)计算 checksum (3)发送回复(ACK)包 (4)滑动窗口(sliding windown)等保证可靠性的操作。tcp协议会调用tcp_sendmsg,tcp_sendmsg()的主要工作是把用户层的数据,填充到skb中。然后调用tcp_push()来发送,tcp_push函数调用tcp_write_xmit()函数,其又将调用发送函数__tcp_transmit_skb,所有的SKB都经过该函数进行发送。最后进入到ip_queue_xmit到网络层。因为tcp会进行重传控制,所以有tcp_write_timer函数,进行定时。
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size) { ... //检测三次握手是否成功 if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) && ! (tcp_passive_fastopen(sk)) { /* 等待连接的建立,成功时返回值为0 */ if ((err = sk_stream_wait_connect(sk, &timeo)) != 0) goto do_error; } ... }
static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; skb = tcp_write_queue_tail(sk); if (!skb) return; if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); tcp_mark_urg(tp, flags); if (tcp_should_autocork(sk, skb, size_goal)) { /* avoid atomic op if TSQ_THROTTLED bit is already set */ if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING); set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags); } /* It is possible TX completion already happened * before we set TSQ_THROTTLED. */ if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize) return; } if (flags & MSG_MORE) nonagle = TCP_NAGLE_CORK; __tcp_push_pending_frames(sk, mss_now, nonagle); }
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; unsigned int tso_segs, sent_pkts; int cwnd_quota; int result; bool is_cwnd_limited = false, is_rwnd_limited = false; u32 max_segs; /*统计已发送的报文总数*/ sent_pkts = 0; ...... /*若发送队列未满,则准备发送报文*/ while ((skb = tcp_send_head(sk))) { unsigned int limit; if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) { /* "skb_mstamp_ns" is used as a start point for the retransmit timer */ skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache; list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue); tcp_init_tso_segs(skb, mss_now); goto repair; /* Skip network transmission */ } if (tcp_pacing_check(sk)) break; tso_segs = tcp_init_tso_segs(skb, mss_now); BUG_ON(!tso_segs); /*检查发送窗口的大小*/ cwnd_quota = tcp_cwnd_test(tp, skb); if (!cwnd_quota) { if (push_one == 2) /* Force out a loss probe pkt. */ cwnd_quota = 1; else break; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; ...... }
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt) { skb_push(skb, tcp_header_size); skb_reset_transport_header(skb); ...... /* 构建TCP头部和校验和 */ th = (struct tcphdr *)skb->data; th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(rcv_nxt); tcp_options_write((__be32 *)(th + 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out += tcp_skb_pcount(skb); tp->bytes_sent += skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out += tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); ...... }
- 网络层
ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,就会去选择一个路由。
填充IP包的各个字段,比如版本、包头长度、TOS等。当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码
ip的宏定义://IP首部长度 #define IP_HEADER_LEN 20 //IP版本号位置 以太网首部2+6+6,与下面那个在用的时候上区别下 #define IP_HEADER_LEN_VER_P 0xe //IP版本号位置 以太网首部2+6+6 #define IP_P 0xe //IP 16位标志位置 #define IP_FLAGS_P 0x14 //IP 生存时间位置 #define IP_TTL_P 0x16 //IP协议类型位置,如ICMP,TCP,UDP 1个字节 #define IP_PROTO_P 0x17 //首部校验和 #define IP_CHECKSUM_P 0x18 // IP源地址位置 14+12 #define IP_SRC_P 0x1a // IP目标地址位置 14+12+4 #define IP_DST_P 0x1e //IP总长度 #define IP_TOTLEN_H_P 0x10 #define IP_TOTLEN_L_P 0x11 //协议类型 #define IP_PROTO_ICMP_V 0x01 #define IP_PROTO_TCP_V 0x06 #define IP_PROTO_UDP_V 0x11
ip_queue_xmit最终也是调用ip_local_out发送本机的数据包。
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl) { struct sock *sk = skb->sk; struct inet_sock *inet = inet_sk(sk); struct ip_options_rcu *inet_opt; struct flowi4 *fl4; struct rtable *rt; struct iphdr *iph; int res; /* 判断数据包是否有路由,如果已经有了,就直接跳到 packet_routed */ rcu_read_lock(); inet_opt = rcu_dereference(inet->inet_opt); fl4 = &fl->u.ip4; rt = skb_rtable(skb); if (rt != NULL) goto packet_routed; /* 从套接字获得合法的路由(需要检查是否过期) */ rt = (struct rtable *)__sk_dst_check(sk, 0); if (rt == NULL) { __be32 daddr; daddr = inet->inet_daddr; /* 如果有 IP 严格路由选项,则使用选项中的地址作为目的地址进行路由查询 */ if (inet_opt && inet_opt->opt.srr) daddr = inet_opt->opt.faddr; /* 进行路由查找 */ rt = ip_route_output_ports(sock_net(sk), fl4, sk, daddr, inet->inet_saddr, inet->inet_dport, inet->inet_sport, sk->sk_protocol, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if); if (IS_ERR(rt)) goto no_route; /* 根据路由的接口的特性设置套接字特性 */ sk_setup_caps(sk, &rt->dst); } /* 给数据包设置路由 */ skb_dst_set_noref(skb, &rt->dst); packet_routed: /* 如果有 IP严格路由选项 */ if (inet_opt && inet_opt->opt.is_strictroute && fl4->daddr != rt->rt_gateway) goto no_route; /* 分配 IP首部和选项空间 */ skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0)); /* 设置 IP首部位置 */ skb_reset_network_header(skb); /* 得到数据包 IP首部的指针 */ iph = ip_hdr(skb); /* 构建 IP首部 */ *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); /* 如不能分片,则在 IP首部设置 IP_DF标志 */ if (ip_dont_fragment(sk, &rt->dst) && !skb->local_df) iph->frag_off = htons(IP_DF); else iph->frag_off = 0; iph->ttl = ip_select_ttl(inet, &rt->dst); iph->protocol = sk->sk_protocol; iph->saddr = fl4->saddr; iph->daddr = fl4->daddr; /* Transport layer set skb->h.foo itself. */ /* 构建 IP选项 */ if (inet_opt && inet_opt->opt.optlen) { iph->ihl += inet_opt->opt.optlen >> 2; ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0); } /* 选择合适的 IP identifier */ ip_select_ident_more(iph, &rt->dst, sk, (skb_shinfo(skb)->gso_segs ?: 1) - 1); /* 根据套接字选项,设置数据包的优先级和标记 */ skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; /* 发送数据包 */ res = ip_local_out(skb); rcu_read_unlock(); return res; no_route: rcu_read_unlock(); IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); kfree_skb(skb); return -EHOSTUNREACH; }
- 链路层
数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。这一层数据的单位称为帧(frame)。从dev_queue_xmit函数开始,位于net/core/dev.c文件中。
gdb调试跟踪:
2.recv过程
- 链路层
包到达时会触发一个中断通过DMA传送到内存中的rx_ring。中断会将数据帧copy到skb_buff缓冲区。之后通知内核接收到新的数据帧。进入软中断处理流程,之后再调用net_rx_action 函数。该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。 - 网络层
ip_rcv函数调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。做完各种检查后,到达ip_rcv_finish函数。ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃: (1)如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。(2)如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 <1>处理 Netfilter Hook<2>执行 IP fragmentation<3>调用 dev_queue_xmit,进入链路层处理流程。
/* * IP receive entry point */ int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { struct net *net = dev_net(dev); skb = ip_rcv_core(skb, net); if (skb == NULL) return NET_RX_DROP; return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish); }
- 传输层
tcp_v4_rcv 函数做 TCP header 检查等处理。调用 _tcp_v4_lookup,查找该package的open socket。如果找不到,该package会被丢弃。接下来检查 socket 和 connection 的状态。如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
/* * From tcp_input.c */ //网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() int tcp_v4_rcv(struct sk_buff *skb) { struct net *net = dev_net(skb->dev); const struct iphdr *iph; const struct tcphdr *th; bool refcounted; struct sock *sk; int ret; //如果不是发往本地的数据包,则直接丢弃 if (skb->pkt_type != PACKET_HOST) goto discard_it; /* Count it even if it's bad */ __TCP_INC_STATS(net, TCP_MIB_INSEGS); ////包长是否大于TCP头的长度 if (!pskb_may_pull(skb, sizeof(struct tcphdr))) goto discard_it; //tcp头 --> 不是很懂为何老是获取tcp头 th = (const struct tcphdr *)skb->data; if (unlikely(th->doff < sizeof(struct tcphdr) / 4)) goto bad_packet; if (!pskb_may_pull(skb, th->doff * 4)) goto discard_it; /* An explanation is required here, I think. * Packet length and doff are validated by header prediction, * provided case of th->doff==0 is eliminated. * So, we defer the checks. */ if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo)) goto csum_error; //得到tcp的头 --> 不是很懂为何老是获取tcp头 th = (const struct tcphdr *)skb->data; //得到ip报文头 iph = ip_hdr(skb); /* This is tricky : We move IPCB at its correct location into TCP_SKB_CB() * barrier() makes sure compiler wont play fool^Waliasing games. */ memmove(&TCP_SKB_CB(skb)->header.h4, IPCB(skb), sizeof(struct inet_skb_parm)); barrier(); TCP_SKB_CB(skb)->seq = ntohl(th->seq); TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4); TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq); TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th); TCP_SKB_CB(skb)->tcp_tw_isn = 0; TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph); TCP_SKB_CB(skb)->sacked = 0; lookup: //根据源端口号,目的端口号和接收的interface查找sock对象------>先在建立连接的哈希表中查找------>如果没找到就从监听哈希表中找 //对于建立过程来讲肯是监听哈希表中才能找到 sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source, th->dest, &refcounted); //如果找不到处理的socket对象,就把数据报丢掉 if (!sk) goto no_tcp_socket; process: //检查sock是否处于半关闭状态 if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait; if (sk->sk_state == TCP_NEW_SYN_RECV) { struct request_sock *req = inet_reqsk(sk); struct sock *nsk; sk = req->rsk_listener; if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) { sk_drops_add(sk, skb); reqsk_put(req); goto discard_it; } if (unlikely(sk->sk_state != TCP_LISTEN)) { inet_csk_reqsk_queue_drop_and_put(sk, req); goto lookup; } /* We own a reference on the listener, increase it again * as we might lose it too soon. */ sock_hold(sk); refcounted = true; nsk = tcp_check_req(sk, skb, req, false); if (!nsk) { reqsk_put(req); goto discard_and_relse; } if (nsk == sk) { reqsk_put(req); } else if (tcp_child_process(sk, nsk, skb)) { tcp_v4_send_reset(nsk, skb); goto discard_and_relse; } else { sock_put(sk); return 0; } } if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) { __NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP); goto discard_and_relse; } if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) goto discard_and_relse; if (tcp_v4_inbound_md5_hash(sk, skb)) goto discard_and_relse; nf_reset(skb); if (tcp_filter(sk, skb)) goto discard_and_relse; //tcp头部 --> 不是很懂为何老是获取tcp头 th = (const struct tcphdr *)skb->data; iph = ip_hdr(skb); skb->dev = NULL; //如果socket处于监听状态 --> 我们重点关注这里 if (sk->sk_state == TCP_LISTEN) { ret = tcp_v4_do_rcv(sk, skb); goto put_and_return; } sk_incoming_cpu_update(sk); bh_lock_sock_nested(sk); tcp_segs_in(tcp_sk(sk), skb); ret = 0; //查看是否有用户态进程对该sock进行了锁定 //如果sock_owned_by_user为真,则sock的状态不能进行更改 if (!sock_owned_by_user(sk)) { if (!tcp_prequeue(sk, skb)) //--------------------------------------------------------> ret = tcp_v4_do_rcv(sk, skb); } else if (tcp_add_backlog(sk, skb)) { goto discard_and_relse; } bh_unlock_sock(sk); put_and_return: if (refcounted) sock_put(sk); return ret; no_tcp_socket: if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) goto discard_it; if (tcp_checksum_complete(skb)) { csum_error: __TCP_INC_STATS(net, TCP_MIB_CSUMERRORS); bad_packet: __TCP_INC_STATS(net, TCP_MIB_INERRS); } else { tcp_v4_send_reset(NULL, skb); } discard_it: /* Discard frame. */ kfree_skb(skb); return 0; discard_and_relse: sk_drops_add(sk, skb); if (refcounted) sock_put(sk); goto discard_it; do_time_wait: if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { inet_twsk_put(inet_twsk(sk)); goto discard_it; } if (tcp_checksum_complete(skb)) { inet_twsk_put(inet_twsk(sk)); goto csum_error; } switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) { case TCP_TW_SYN: { struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev), &tcp_hashinfo, skb, __tcp_hdrlen(th), iph->saddr, th->source, iph->daddr, th->dest, inet_iif(skb)); if (sk2) { inet_twsk_deschedule_put(inet_twsk(sk)); sk = sk2; refcounted = false; goto process; } /* Fall through to ACK */ } case TCP_TW_ACK: tcp_v4_timewait_ack(sk, skb); break; case TCP_TW_RST: tcp_v4_send_reset(sk, skb); inet_twsk_deschedule_put(inet_twsk(sk)); goto discard_it; case TCP_TW_SUCCESS:; } goto discard_it; }
- 应用层
应用调用 read 或者 recv 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recvmsg 函数。对于 INET 类型的 socket,/net/ipv4/af_inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。TCP 会调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到buffer。
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; }
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags) { int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags); return err ?: sock_recvmsg_nosec(sock, msg, flags); } EXPORT_SYMBOL(sock_recvmsg);
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags) { struct sock *sk = sock->sk; int addr_len = 0; int err; if (likely(!(flags & MSG_ERRQUEUE))) sock_rps_record_flow(sk); err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg, sk, msg, size, flags & MSG_DONTWAIT, flags & ~MSG_DONTWAIT, &addr_len); if (err >= 0) msg->msg_namelen = addr_len; return err; }
gdb调试跟踪:
四、时序图