TCP/IP协议栈在Linux内核中的运行时序分析
1.网络体系结构模型
1.1.OSI七层模型
OSI模型是由国际化标准组织ISO提出的网络体系结构模型。被称为开放系统互联参考模型。OSI模型总共有7层。自上而下依次为应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。七层模型结构清晰。共同完成数据的传输和处理工作。
在OSI模型中,各层的功能大致如下:
应用层为特定类型的网络应用提供访问OSI环境的手段。
表示层主要处理在两个通信系统中交换信息的表示方式。
会话层允许不同主机上的各个进程之间进行会话。
传输层负责不同主机之间两个进程的通信。功能是为端到端连接提供可靠的传输服务。
网络层的主要任务是把网络层的协议数据单元从源端传递到目的端,为分组交换网络上的不同主机提供通信服务。
数据链路层的主要任务是将网络层传来的IP数据报封装成帧,透明传输和差错控制等。
物理层的任务是透明的传输比特流。
1.2.TCP/IP分层模型
ARPA在研究ARPAnet时提出了TCP/IP模型,模型从低到高依次为网络接口层(对应OSI参考模型中的物理层和数据链路层)、网际层、传输层和应用层(对应OSI参考模型中的会话层、表示层和应用层)。TCP/IP由于得到广泛应用而成为事实上的国际标准。其中,TCP/IP模型和OSI模型的层次结构及相关的协议如图所示
网络接口层的功能类似于OSI的物理层和数据链路层。它表示与物理网络的接口,但实际上TCP/IP本身并未真正描述这一部分,只是指出主机必须使用某种协议与网络连接,以便在其上传递IP分组。
网际层(主机-主机)是TCP/IP体系结构的关键部分。它和OSI网络层在功能上非常相似。网际层将分组发往任何网络,并为之独立地选择合适的路由,但它不保证各个分组有序地到达,各个分组的有序交付由高层负责。网际层定义了标准的分组格式和协议,即IP。当前采用的IP协议是第4版,即IPv4,它的下一-版本是IPv6。
传输层(应用-应用或进程进程)的功能同样和OSI中的传输层类似,即使得发送端和目的端主机上的对等实体进行会话。传输层主要使用以下两种协议:
TCP:它是面向连接的,能够提供可靠的交付。
UDP:它是无连接的,不保证可靠的交付,只能提供尽最大努力的交付
应用层包含所有的高层协议,如Telnet、FTP、DNS、SMTP、HTTP等协议。
由上图可以看出,IP协议是因特网的核心协议;TCP/IP可以为各式各样的应用提供服务,正因为如此,因特网才会发展到今天的规模。
2.Linux网络子系统
Linux 网络子系统的顶部是系统调用接口层。它为为应用程序提供访问内核网络子系统的方法,如Socket系统调用。位于其下面的是一个协议无关层,实现一组通用函数来访问各种不同的协议:通过socket实现。然后是具体协议的实现,网络协议层用于实现各种具体的网络协议,如:TCP、UDP 等,当然还有IP。然后是设备无关层,设备无关接口将协议与各种网络设备驱动连接在一起。这一层提供一组通用函数供底层网络设备驱动程序使用,让它们可以对高层协议栈进行操作。最下面是设备驱动程序, 负责管理物理网络设备。
3.套接字Socket
3.1.Socket简介
Socket 是指网络上的两个程序过一个双向的通讯连接实现数据的交换,这个 双向链路的一端称之为一个 socket,socket 通常用来实现客户方和服务方的连接。 socket 是 TCP/IP 协议的一个十分流行的编程界面,一个 Socket 由一个 IP 地址和 一个端口号唯一确定。 但是,Socket 所支持的协议种类也不光 TCP/IP 一种,因此两者之间是没有必 然联系的。在 Java 中,socket 编程主要是基于 TCP/IP 协议的网络编程。现在, TCP/IP 协议族中的主要 socket 类型为流套接字(使用 TCP 协议)和数据报套接字 (使用 UDP 协议)以及raw套接字。
3.2.Socket数据结构
用户使用socket系统调用编写应用程序时,通过一个数字来表示一个socket,所有的操作都在该数字上进行,这个数字称为套接字描述符。在系统调用 的实现函数里,这个数字就会被映射成一个表示socket的结构体,该结构体保存了该socket的所有属性和数据。
struct socket { socket_state state; //描述套接字的状态 unsigned long flags; //套接字的一组标志位 const struct proto_ops *ops; //将套接字层调用于传输层协议对应 struct fasync_struct *fasync_list; //存储异步通知队列 struct file *file; //套接字相关联的文件指针 struct sock *sk; //套接字相关联的传输控制块 wait_queue_head_t wait; //等待套接字的进程列表 short type; //套接字的类型 }; state用于表示socket所处的状态,是一个枚举变量,其类型定义如下: typedef enum { SS_FREE = 0, //该socket还未分配 SS_UNCONNECTED, //未连向任何socket SS_CONNECTING, //正在连接过程中 SS_CONNECTED, //已连向一个socket SS_DISCONNECTING //正在断开连接的过程中 }socket_state;
3.3.Socketcall系统调用
socketcall()是Linux操作系统中所有的socket系统调用的总入口。sys_soketcall()函数的第一个参数即为具体的操作码,其中,每个数字可以代表一个操作码,一共17种,函数中正是通过操作码来跳转到真正的系统调用函数的。具体操作码对应情况如下:
#define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ #define SYS_CONNECT 3 /* sys_connect(2) */ #define SYS_LISTEN 4 /* sys_listen(2) */ #define SYS_ACCEPT 5 /* sys_accept(2) */ #define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */ #define SYS_GETPEERNAME 7 /* sys_getpeername(2) */ #define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */ #define SYS_SEND 9 /* sys_send(2) */ #define SYS_RECV 10 /* sys_recv(2) */ #define SYS_SENDTO 11 /* sys_sendto(2) */ #define SYS_RECVFROM 12 /* sys_recvfrom(2) */ #define SYS_SHUTDOWN 13 /* sys_shutdown(2) */ #define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */ #define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */ #define SYS_SENDMSG 16 /* sys_sendmsg(2) */ #define SYS_RECVMSG 17 /* sys_recvmsg(2) */ sys_socketcall()函数的源代码在net/socket.c中,具体如下: asmlinkage long sys_socketcall(int call, unsigned long __user *args) { unsigned long a[6]; unsigned long a0, a1; int err; .......................................... a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = sys_listen(a0, a1); break; case SYS_ACCEPT: err = do_accept(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: err = sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; ..................................... return err; }
可以看到sys_socketcall()函数就是通过传递进来的call类型,来调用相应的socket相关的函数。而这里并没有出现我们所熟知得write、read、aio、poll等系统调用函数,这是因为socket上面其实还有一层VFS层,内核把socket当做一个文件系统来处理,并且实现了相应得VFS方法。
下图显示了使用上述系统调用函数的顺序,图中蓝色大方框中的IO系统调用函数可以在任何时候调用。注意,给出的图中不是一个完整的状态流程图,仅显示了一些常见的系统调用函数:
4.应用层分析
4.1发送数据
应用层发送数据时,sys_send实际只是对sys_sendto的简单封装。sys_sendto函数的作用是将数据报发送至指定的目的地址。而sys_sendto是通过调用sys_sendmsg函数进行发送数据的。sys_sendmsg函数的作用是将用户空间的信息复制到内核空间中,然后再逐级调用发包接口发送数据。 sys_sendmsg最终调用了sock_sendmsg()。
sys_sendmsg函数部分源代码如下:
asmlinkage long sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags) {
.......
//64位系统向32位系统兼容时,需将控制缓存信息进行复制 if ((MSG_CMSG_COMPAT & flags) && ctl_len) { err = cmsghdr_from_user_compat_to_kern(&msg_sys, ctl, sizeof(ctl)); if (err) goto out_freeiov; ctl_buf = msg_sys.msg_control; } else if (ctl_len) { //在32位系统中复制控制缓存信息 if (ctl_len > sizeof(ctl)) { ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL); if (ctl_buf == NULL) goto out_freeiov; } err = -EFAULT; /* * Careful! Before this, msg_sys.msg_control contains a user pointer. * Afterwards, it will be a kernel pointer. Thus the compiler-assisted * checking falls down on this. */ if (copy_from_user(ctl_buf, (void __user *) msg_sys.msg_control, ctl_len)) goto out_freectl; msg_sys.msg_control = ctl_buf; } msg_sys.msg_flags = flags; //设置数据发送标识 if (sock->file->f_flags & O_NONBLOCK) msg_sys.msg_flags |= MSG_DONTWAIT; //调用sock_sendmsg函数发送报文 err = sock_sendmsg(sock, &msg_sys, total_len); out_freectl: //释放临时申请的缓冲区 if (ctl_buf != ctl) sock_kfree_s(sock->sk, ctl_buf, ctl_len); out_freeiov: if (iov != iovstack) sock_kfree_s(sock->sk, iov, iov_size); out_put: sockfd_put(sock); out: return err; }
......
sock_sendmsg()又调用了__sock_sendmsg.__sock_sendmsg实际上又调用了sock->ops->sendmsg函数
static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { int err = security_socket_sendmsg(sock, msg, size); return err ?: __sock_sendmsg_nosec(iocb, sock, msg, size); } static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { struct sock_iocb *si = kiocb_to_siocb(iocb); si->sock = sock; si->scm = NULL; si->msg = msg; si->size = size; /* 调用Socket层的操作函数,如果是SOCK_STREAM,则proto_ops为inet_stream_ops, * 函数指针指向inet_sendmsg()。 */ return sock->ops->sendmsg(iocb, sock, msg, size); }
sock-ops->sendmsg的意思就是根据不同的协议调用不同的sendmsg函数,对于 TCP ,调用tcp_sendmsg函数。
通过以上的分析,且由于应用层的系统调用过程比价清晰,给出sendmsg系统调用的过程如图所示:
gdb跟踪调试结果如下:
4.2接收数据
接收数据与发送数据的系统调用过程十分相似,它们都归结到sock_sendmsg()和sock_recvmsg()两个函数上。不同的只是数据的流向相反而已。下面是recvmsg的系统调用过程:
gdb跟踪调试结果如下:
5.传输层分析
5.1发送数据
TCP协议对发送数据相关系统调用内核实现,虽然发送相关的系统调用接口由很多,但是到了TCP协议层,都统一由tcp_sendmsg()处理。tcp_sendmsg()函数要完成的工作就是将应用程序要发送的数据组织成skb,然后调用tcp_push函数。
发送数据部分的源代码:
... //查看是否立即发送数据包 if (forced_push(tp)) { //设置立即发送数据包标志PSH tcp_mark_push(tp, skb); //发送数据包,实际调用的是ip_queue_xmit __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH); } else if (skb == tcp_send_head(sk)) tcp_push_one(sk, mss_now); continue; wait_for_sndbuf: //缓冲数据段,等到一定数量再发送 set_bit(SOCK_NOSPACE, &sk->sk_socket->flags); wait_for_memory: if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH); if ((err = sk_stream_wait_memory(sk, &timeo)) != 0) goto do_error; mss_now = tcp_send_mss(sk, &size_goal, flags); } } ...
tcp_push()在判断了是否需要设置PUSH标记位之后,会调用__tcp_push_pending_frames()
static inline void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_send_head(sk)) { //判断是否需要设置PUSH标记 struct sk_buff *skb = tcp_write_queue_tail(sk); if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); //MSG_OOB相关,忽略 tcp_mark_urg(tp, flags, skb); //调用__tcp_push_pending_frames()尝试发送 __tcp_push_pending_frames(sk, mss_now, (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle); } } __tcp_push_pending_frames()调用调用tcp_write_xmit()完成发送。 /* Push out any pending frames which were held back due to * TCP_CORK or attempt at coalescing tiny packets. * The socket must be locked by the caller. */ void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) { struct sk_buff *skb = tcp_send_head(sk); //如果有新数据可供发送,调用tcp_write_xmit()发送 if (skb) { if (tcp_write_xmit(sk, cur_mss, nonagle)) //和PMTU相关 tcp_check_probe_timer(sk); } }
tcp_write_xmit()该函数是TCP发送新数据的核心函数,包括发送窗口判断、拥塞控制判断等核心操作都是在该函数中完成。它又将调用发送函数tcp_transmit_skb函数。
tcp_transmit_skb的作用是复制或者拷贝skb,构造skb中的tcp首部,并将调用网络层的发送函数发送skb;在发送前,首先需要克隆或者复制skb,因为在成功发送到网络设备之后,skb会释放,而tcp层不能真正的释放,是需要等到对该数据段的ack才可以释放;然后构造tcp首部和选项;最后调用网络层提供的发送回调函数发送skb,ip层的回调函数为ip_queue_xmit;
gdb调试结果如下:
5.2接收数据
tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数。tcp_v4_rcv函数只要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段;当状态为TCP_LISTEN调用tcp_v4_do_rcv
/* LISTEN状态处理 */ if (sk->sk_state == TCP_LISTEN) { ret = tcp_v4_do_rcv(sk, skb); goto put_and_return; }
对于tcp_v4_do_rcv()函数,如果状态为ESTABLISHED,即已连接状态,就会调用tcp_rcv_established()函数
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ struct dst_entry *dst = sk->sk_rx_dst; sock_rps_save_rxhash(sk, skb); sk_mark_napi_id(sk, skb); if (dst) { if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif || !dst->ops->check(dst, 0)) { dst_release(dst); sk->sk_rx_dst = NULL; } } tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len); return 0; }
tcp_rcv_established用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径;在快路中,对是有有数据负荷进行不同处理;在慢路中,会进行更详细的校验,然后处理ack,处理紧急数据,接收数据段,其中数据段可能包含乱序的情况,最后进行是否有数据和ack的发送检查;当一切正常时,调用tcp_data_queue()方法将报文放入队列中。
tcp_data_queue作用为数据段的接收处理,当存在预期接收的数据段,就会有以下几种处理方式:
预期接收的数据段,a. 进行0窗口判断;b. 进程上下文,复制数据到用户空间;c. 不满足b或者b未完整拷贝此skb的数据段,则加入到接收队列;d. 更新下一个期望接收的序号;e. 若有fin标记,则处理fin;f. 乱序队列不为空,则处理乱序;g. 快速路径的检查和设置;h. 唤醒用户空间进程读取数据;
部分代码如下:
......
/* 预期接收的数据段 */ if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) { /* 窗口为0,不能接收数据 */ if (tcp_receive_window(tp) == 0) goto out_of_window; /* Ok. In sequence. In window. */ /* 进程上下文 */ /* 当前进程读取数据 */ if (tp->ucopy.task == current && /* 用户空间读取序号与接收序号一致&& 需要读取的数据不为0 */ tp->copied_seq == tp->rcv_nxt && tp->ucopy.len && /* 被用户空间锁定&& 无紧急数据 */ sock_owned_by_user(sk) && !tp->urg_data) { /* 带读取长度和数据段长度的较小值 */ int chunk = min_t(unsigned int, skb->len, tp->ucopy.len); /* 设置running状态 */ __set_current_state(TASK_RUNNING); /* 拷贝数据 */ if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) { tp->ucopy.len -= chunk; tp->copied_seq += chunk; /* 完整读取了该数据段 */ eaten = (chunk == skb->len); /* 调整接收缓存和窗口 */ tcp_rcv_space_adjust(sk); } } ....... }
.......
gdb调试结果如下:
6.网络层分析:
6.1.发送数据
ip_queue_xmit()是ip层提供给tcp层发送回调,大多数tcp发送都会使用这个回调,tcp层使用tcp_transmit_skb封装了tcp头之后,调用该函数,该函数提供了路由查找校验、封装ip头和ip选项的功能。
ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成.ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)。
ip_queue_xmit调用函数__ip_queue_xmit()。在__ip_queue中,会调用skb_rtable函数来检查skb是否已被路由,即获取其缓存信息,将缓存信息保存在变量rt中,如果rt不为空,就直接进行packet_routed函数,如果rt不为空,就会自行ip_route_output_ports查找路由缓存。
packet_routed代码段首先先进行严格源路由选项的处理。如果存在严格源路由选项,并且数据包的下一跳地址和网关地址不一致,则丢弃该数据包(goto no_route)。如果没问题,就进行ip头部设置,设置完成后调用ip_local_out函数。ip_local_out
函数内部调用__ip_local_out
。发现返回一个nf_hook
函数,里面调用了dst_output。
packet_routed: if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)//处理严路由选项,下一跳地址必须为网管地址 goto no_route; iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));//移动skb->data向上,准备填写ip报头 *((__u16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));//设置报头长度为20字节,选项的长度,在ip_options_build中增加 iph->tot_len = htons(skb->len);//总长度,skb中所有数据的长度,包括报头,选项,有效载荷 if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)//检查禁止了分片 iph->frag_off = htons(IP_DF);//设置报头指示禁止分片 else iph->frag_off = 0;//分片中的偏移量 iph->ttl = ip_select_ttl(inet, &rt->u.dst);//生存周期 iph->protocol = sk->sk_protocol;//指示ip上层的l4协议类型 iph->saddr = rt->rt_src;//源地址,目的地址 iph->daddr = rt->rt_dst; skb->nh.iph = iph; if (opt && opt->optlen) {//sock中设置了选项 iph->ihl += opt->optlen >> 2; ip_options_build(skb, opt, inet->daddr, rt, 0);//处理选项 } ip_select_ident_more(iph, &rt->u.dst, sk, skb_shinfo(skb)->tso_segs);//为ip选择id ip_send_check(iph);//ip报头校验和 skb->priority = sk->sk_priority;//skb的优先级为sock的优先级,在dev_queue_xmit中用于在规则队列中排队 return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output);//调用dst->output,由ip_route_output_flow设置
ip_local_out函数调用了__ip_local_out函数,而__ip_local_out函数最终调用的是nf_hook函数并在里面调用了dst_output函数。
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { struct iphdr *iph = ip_hdr(skb); /* 设置总长度 */ iph->tot_len = htons(skb->len); /* 计算校验和 */ ip_send_check(iph); /* if egress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_out(sk, skb); if (unlikely(!skb)) return 0; /* 设置ip协议 */ skb->protocol = htons(ETH_P_IP); /* 经过NF的LOCAL_OUT钩子点 */ return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)->dev, dst_output); }
dst_output函数调用了ip_output函数,而ip_output函数调用了ip_finish_output函数。这个函数实际上调用的是__ip_finish_output函数。紧接着, ip_finish_output调用了 ip_finish_output2(),ip_finish_output2函数会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数neigh_output进行输出。输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。不管执行哪个函数,最终都会调用dev_queue_xmit将数据包传入数据链路层。
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb) { const struct hh_cache *hh = &n->hh; /* 连接状态 &&缓存的头部存在,使用缓存输出 */ if ((n->nud_state & NUD_CONNECTED) && hh->hh_len) return neigh_hh_output(hh, skb); /* 使用邻居项的输出回调函数输出,在连接或者非连接状态下有不同的输出函数 */ else return n->output(n, skb); }
gdb调试结果如下
6.2.接收数据
网络IP层的入口函数在ip_rcv函数。ip_rcv函数调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。ip_rcv数首先会检查检验和等各种字段,如果数据包的长度超过最大传送单元MTU的话,会进行分片,最终到达 ip_rcv_finish 函数。
部分代码如下:
/* 取得传输层头部 */ skb->transport_header = skb->network_header + iph->ihl*4; /* Remove any debris in the socket control block */ /* 重置cb */ memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)); /* 保存输入设备信息 */ IPCB(skb)->iif = skb->skb_iif; /* Must drop socket now because of tproxy. */ skb_orphan(skb); /* 经过PRE_ROUTING钩子点 */ return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish);
ip_rcv_finish最终会调用dst_input函数,这个函数调用的是ip_input函数,当缓存查找没有匹配路由时将调用ip_route_input_slow(),决定该 package 将会被发到本机还是会被转发还是丢弃。
如果是发到本机的将会执行ip_local_deliver函数。可能会做 de-fragment(合并多个包),并调用ip_local_deliver_finish。ip_local_deliver_finish会调用ip_protocol_deliver_rcu函数
/* 如果忽略掉原始套接字和IPSec,则该函数仅仅是根据IP头部中的协议字段选择上层L4协议,并交给它来处理 */ static int ip_local_deliver_finish(struct sk_buff *skb) { /* 跳过IP头部 */ __skb_pull(skb, ip_hdrlen(skb)); /* Point into the IP datagram, just past the header. */ /* 设置传输层头部位置 */ skb_reset_transport_header(skb); rcu_read_lock(); { /* Note: See raw.c and net/raw.h, RAWV4_HTABLE_SIZE==MAX_INET_PROTOS */ int protocol = ip_hdr(skb)->protocol; int hash; struct sock *raw_sk; struct net_protocol *ipprot; resubmit: /* 这个hash根本不是哈希值,仅仅只是inet_protos数组中的下表而已 */ hash = protocol & (MAX_INET_PROTOS - 1); raw_sk = sk_head(&raw_v4_htable[hash]); /* If there maybe a raw socket we must check - if not we * don't care less */ /* 原始套接字?? 忽略... */ if (raw_sk && !raw_v4_input(skb, ip_hdr(skb), hash)) raw_sk = NULL; /* 查找注册的L4层协议处理结构。 */ if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) { int ret; /* 启用了安全策略,则交给IPSec */ if (!ipprot->no_policy) { if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); goto out; } nf_reset(skb); } /* 调用L4层协议处理函数 */ /* 通常会是tcp_v4_rcv, udp_rcv, icmp_rcv和igmp_rcv */ /* 如果注册了其他的L4层协议处理,则会进行相应的调用。 */ ret = ipprot->handler(skb); if (ret < 0) { protocol = -ret; goto resubmit; } IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); } else { if (!raw_sk) { /* 无原始套接字,提交给IPSec */ if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } } else IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); kfree_skb(skb); } } out: rcu_read_unlock(); return 0; }
ip_protocol_deliver_rcu将输入数据包从网络层传递到传输层。
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol) { const struct net_protocol *ipprot; int raw, ret; resubmit: raw = raw_local_deliver(skb, protocol); ipprot = rcu_dereference(inet_protos[protocol]); if (ipprot) { if (!ipprot->no_policy) { if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); return; } nf_reset_ct(skb); } ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb); if (ret < 0) { protocol = -ret; goto resubmit; } __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); } else { if (!raw) { if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { __IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } kfree_skb(skb); } else { __IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS); consume_skb(skb); } } }
gdb调试结果如下:
7.数据链路层分析:
7.1.发送数据
数据链路层的入口是dev_queue_xmit函数。这个函数调用的是__dev_queue_xmit函数。__dev_queue_xmit函数对不同类型的数据包进行不同的处理。__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb。
__dev_queue_xmit的部分代码如下
/*此处是设备没有Qdisc的,实际上没有enqueue/dequeue的规则,无法进行拥塞控制的操作, *对于一些loopback/tunnel interface比较常见,判断下设备是否处于UP状态*/ if (dev->flags & IFF_UP) { int cpu = smp_processor_id(); /* ok because BHs are off */ if (txq->xmit_lock_owner != cpu) { if (__this_cpu_read(xmit_recursion) > RECURSION_LIMIT) goto recursion_alert; skb = validate_xmit_skb(skb, dev); if (!skb) goto drop; HARD_TX_LOCK(dev, txq, cpu); /*这个地方判断一下txq不是stop状态,那么就直接调用dev_hard_start_xmit函数来发送数据*/ if (!netif_xmit_stopped(txq)) { __this_cpu_inc(xmit_recursion); skb = dev_hard_start_xmit(skb, dev, txq, &rc); __this_cpu_dec(xmit_recursion); if (dev_xmit_complete(rc)) { HARD_TX_UNLOCK(dev, txq); goto out; } } HARD_TX_UNLOCK(dev, txq); net_crit_ratelimited("Virtual device %s asks to queue packet!\n", dev->name); } else { /* Recursion is detected! It is possible, * unfortunately */ recursion_alert: net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n", dev->name); } }
dev_hard_start_xmit函数会循环调用xmit_one函数,直到将待输出的数据包提交给网络设备的输出接口,完成数据包的输出。
在xmit_one中调用__net_dev_start_xmit函数。一旦网卡完成报文发送,将产生中断通知 CPU,然后驱动层中的中断处理程序就可以删除保存的 skb。
static int xmit_one(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq, bool more) { unsigned int len; int rc; /*如果有抓包的工具的话,这个地方会进行抓包,such as Tcpdump*/ if (!list_empty(&ptype_all)) dev_queue_xmit_nit(skb, dev); len = skb->len; trace_net_dev_start_xmit(skb, dev); /*调用netdev_start_xmit,快到driver的tx函数了*/ rc = netdev_start_xmit(skb, dev, txq, more); trace_net_dev_xmit(skb, rc, dev, len); return rc; }
7.2.接收数据
接受数据的入口函数是net_rx_action,在net_rx_action函数中会去调用设备的napi_poll函数, 它是设备自己注册的.
static void net_rx_action(struct softirq_action *h) { struct list_head *list = &__get_cpu_var(softnet_data).poll_list; /*设置软中断处理程序一次允许的最大执行时间为2个jiffies*/ unsigned long time_limit = jiffies + 2; int budget = netdev_budget;//轮询配额 当数据包数超过此值 进行轮询接收 此值初始为300 void *have; //禁止本地cpu中断 local_irq_disable(); while (!list_empty(list)) { struct napi_struct *n; int work, weight; if (unlikely(budget <= 0 || time_after(jiffies, time_limit))) goto softnet_break; local_irq_enable(); //取得等待轮询设备的结构 n = list_entry(list->next, struct napi_struct, poll_list); have = netpoll_poll_lock(n); //获得权重 处理此设备n的最大帧数 weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { //对于NAPI设备如e1000驱动此处为poll为e1000_clean() 进行轮询处理数据 //对于非NAPI设备此处poll为process_backlog() 看net_dev_init()中的初始化 work = n->poll(n, weight); trace_napi_poll(n); } //返回值work需要小于等于weight work返回的是此设备处理的帧个数不能超过weight WARN_ON_ONCE(work > weight); budget -= work; //禁止本地中断 local_irq_disable(); //消耗完了此设备的权重 if (unlikely(work == weight)) { //若此时设备被禁止了 则从链表删除此设备 if (unlikely(napi_disable_pending(n))) { //开启本地cpu中断 local_irq_enable(); napi_complete(n); //关闭本地cpu中断 local_irq_disable(); } else//否则将设备放到链表的最后面 list_move_tail(&n->poll_list, list); } netpoll_poll_unlock(have); } out: //开启本地cpu中断 local_irq_enable(); #ifdef CONFIG_NET_DMA dma_issue_pending_all(); #endif return; softnet_break: __get_cpu_var(netdev_rx_stat).time_squeeze++; //在这里触发下一次软中断处理 __raise_softirq_irqoff(NET_RX_SOFTIRQ); goto out; }
在设备的napi_poll函数中, 它负责调用napi_gro_receive函数。
static int napi_poll(struct napi_struct *n, struct list_head *repoll) { void *have; int work, weight; list_del_init(&n->poll_list); have = netpoll_poll_lock(n); weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { work = n->poll(n, weight); //调用网卡注册的poll函数 trace_napi_poll(n, work, weight); } WARN_ON_ONCE(work > weight); if (likely(work < weight)) goto out_unlock; if (unlikely(napi_disable_pending(n))) { napi_complete(n); goto out_unlock; } if (n->gro_list) { napi_gro_flush(n, HZ >= 1000); } if (unlikely(!list_empty(&n->poll_list))) { pr_warn_once("%s: Budget exhausted after napi rescheduled\n", n->dev ? n->dev->name : "backlog"); goto out_unlock; } list_add_tail(&n->poll_list, repoll); out_unlock: netpoll_poll_unlock(have); return work; }
napi_gro_receive用来将网卡上的数据包发给协议栈处理。它会调用 netif_receive_skb_core。而它会调用__netif_receive_skb_one_core,将数据包交给上层 ip_rcv 进行处理。
8.时序图