庖丁解牛,从源码角度来深入tcp/ip。----《TCP/IP解读卷2:实现》。
一、简介和介绍
ip合同的是tcp和udp的根本,通常我们只须要了解子网界定,路由转发机制即可。并且底层是如何实现的呢?毫无疑惑linux社区,一个顶尖的服务端工程师应当从根本从把知识点分析清楚。下边就让我们从源码的角度解剖ip合同。(关于netinet中的函数,现今的linux代码已然重塑过了,所以采用4.4BSD版本的代码。)
(我找了好久)Github地址:
image.png
《TCP/IP解读卷2:实现》在第四章中阐明了一个数据包通过网路插口发生硬件中断时,数据包都会放进iprinrq队列中,如上图。而文章主要述说的是在路由器中,ip层的函数是怎样实现的,大体的组织方式如下。
image.png
若果是路由器linux内核源码剖析:tcp/ip实现,当分组来到ipintrq而且发生软件中断时
iprintrq中的分组数据都会传递到ipintr函数让其进行分组验证,转发等等的操作。ip_forward会依照ip分段和路由表来定位下一条。最后在ip_ouput就是构造首部,选择路由和分片。
假如是主机的话,数据来到iprintr的时侯才会包ip报文传送都网路层。熟悉osi7层结构的应当很清楚,这儿就不再详述了。
一、iprintr
我们可以晓得当软中断发生的时侯,内核都会调用iprintr把数据包从队列中获取下来。这个函数比较复杂。首先我们先看一下iprintr函数的开头,代码如下。
void
ipintr()
{
register struct ip *ip;
register struct mbuf *m;
register struct ipq *fp;
register struct in_ifaddr *ia;
int hlen, s;
next:
/*
* Get next datagram off input queue and get IP header
* in first mbuf.
*/
s = splimp();
IF_DEQUEUE(&ipintrq, m);
splx(s);
if (m == 0)
return;
...
我们可以看见iprintr调用IF_DEQUEUE从队列中获取分组数据,iprintr从iprintrq中移走分组,并对以处理直至整个队列为空为止。之后接出来就是分别对分组进行验证,选项处理和转发,重装和分用。
1.分组验证
把分组从ipintrq中取出,验证它们对内容以后。毁坏可有差错的分组会被手动扔掉。
1.1.验证ip版本
if (in_ifaddr == NULL)
goto bad;
ipstat.ips_total++;
if (m->m_len ip_v != IPVERSION) {
ipstat.ips_badvers++;
goto bad;
}
当网段插口没有配置好的时侯,ip地址会为空。所以分组来到这儿的时侯才会被中断,跳到bad。可以看见在4.4的BSD实现中,ip版本必须是ipv4的。不过如今早已支持ipv6了。
1.2.IP校准和
if (ip->ip_sum = in_cksum(m, hlen)) {
ipstat.ips_badsum++;
goto bad;
}
一个完整的IP数据包必需要有完整的校准和,我们可以看见内核早已封装了一个in_cksum来进行校准。
校准和检验是一个很历时的操作,关于这个in_cksum函数的实现似乎有好多优化的地方。这儿有好多论文才研究这个算法。这儿就不展开了。
1.3.字节次序
个人觉得底层最无聊,也是最麻烦的地方就是字节问题。由于缺德的主机有大端跟小端之分,网路字节次序跟主机字节次序不一致的问题贼麻烦。不过操作系统早已帮我们解决了,谢谢stevens,以及各个位linux贡献的前辈们。
NTOHS(ip->ip_len);
if (ip->ip_len ip_id);
NTOHS(ip->ip_off);
首先把几个16bit的值先转为主机次序,内核封装了一个宏NTOHS来进行转换。假如首部宽度不满足要求,这么都会跳到bad分支。
1.4分组宽度
if (m->m_pkthdr.len ip_len) {
ipstat.ips_tooshort++;
goto bad;
}
if (m->m_pkthdr.len > ip->ip_len) {
if (m->m_len == m->m_pkthdr.len) {
m->m_len = ip->ip_len;
m->m_pkthdr.len = ip->ip_len;
} else
m_adj(m, ip->ip_len - m->m_pkthdr.len);
}
分组的宽度是由链路的最小mtu决定,所以是有可能出现分组逻辑宽度小于mbuf的数据量的(mbuf是tcp/ip底层储存数据的数据结构,参考《TCP/IP解读卷2实现》第一章)
2.选项处理与转发
选项处理实在是太过复杂,这儿是介绍不了如此多的。IP数据包有40个字节储存选项,仅仅是RFC定义的IP选项就有8个。我实在是没有精力去看这儿了,除非我要做合同栈。至于转发就比较好理解,就是按照按照Internet地址表,决定是否有分组目的地匹配的地址。
/*
* Check our list of addresses, to see if the packet is for us.
*/
for (ia = in_ifaddr; ia; ia = ia->ia_next) {
#define satosin(sa) ((struct sockaddr_in *)(sa))
if (IA_SIN(ia)->sin_addr.s_addr == ip->ip_dst.s_addr)
goto ours;
if (
#ifdef DIRECTED_BROADCAST
ia->ia_ifp == m->m_pkthdr.rcvif &&
#endif
(ia->ia_ifp->if_flags & IFF_BROADCAST)) {
u_long t;
if (satosin(&ia->ia_broadaddr)->sin_addr.s_addr ==
ip->ip_dst.s_addr)
goto ours;
if (ip->ip_dst.s_addr == ia->ia_netbroadcast.s_addr)
goto ours;
t = ntohl(ip->ip_dst.s_addr);
if (t == ia->ia_subnet)
goto ours;
if (t == ia->ia_net)
goto ours;
}
}
if (IN_MULTICAST(ntohl(ip->ip_dst.s_addr))) {
struct in_multi *inm;
#ifdef MROUTING
extern struct socket *ip_mrouter;
if (ip_mrouter) {
ip->ip_id = htons(ip->ip_id);
if (ip_mforward(m, m->m_pkthdr.rcvif) != 0) {
ipstat.ips_cantforward++;
m_freem(m);
goto next;
}
ip->ip_id = ntohs(ip->ip_id);
if (ip->ip_p == IPPROTO_IGMP)
goto ours;
ipstat.ips_forward++;
}
#endif
IN_LOOKUP_MULTI(ip->ip_dst, m->m_pkthdr.rcvif, inm);
if (inm == NULL) {
ipstat.ips_cantforward++;
m_freem(m);
goto next;
}
goto ours;
}
其实了,这儿选定下一跳的代码在卷1中是有比较详尽的说明的。
2.重装代码
我们可以晓得,从网段得到的数据包是早已被分片了的。所以iprintr函数最后是须要把代码重装的。要理解怎么重装,首先要理解分片后的ip数据包。如右图。
觉得说再多也不够上图直观,IP分片就是把原先的IP报文分割成若干个更小的IP报文,而且每一个小报文须要重新添加IP首部。但是要对分片重装远比对IP分片复杂得多。再下一篇文章再总结。
回到第一个函数,iprintr函数主要是做验证,处理,重装和分用等等功能。其中转发和重装逻辑很复杂,日后再详尽总结。
二、ip_forward函数
这个函数主要是拿来对重装后的代码进行重装,不过我不晓得为什么在iprintr函数中还要查一遍地址表。=。=#。这个函数主要有三个用途:
1)判定分组转发的合法性
2)降低TTL
3)定位下一跳
void
ip_forward(m, srcrt)
struct mbuf *m;
int srcrt;
{
register struct ip *ip = mtod(m, struct ip *);
register struct sockaddr_in *sin;
register struct rtentry *rt;
int error, type = 0, code;
struct mbuf *mcopy;
n_long dest;
struct ifnet *destifp;
dest = 0;
#ifdef DIAGNOSTIC
if (ipprintfs)
printf("forward: src %x dst %x ttl %xn", ip->ip_src,
ip->ip_dst, ip->ip_ttl);
#endif
if (m->m_flags & M_BCAST || in_canforward(ip->ip_dst) == 0) {
ipstat.ips_cantforward++;
m_freem(m);
return;
}
HTONS(ip->ip_id);
if (ip->ip_ttl ip_ttl -= IPTTLDEC;
第一个用途不用解释了查看linux是什么系统,简而言之就是对“链路层广播,环回广播或则其他轮询查询参数是否正确“。
至于第二点,我们看下边代码。
if (ip->ip_ttl ip_ttl -= IPTTLDEC;
系统是不接受TTL为0的数据包的,由于每一跳都约定了TTL要降低起码1s,所以实现中就降低IPTTLDEC(宏为1)。假如大于1linux内核源码剖析:tcp/ip实现,这么就向源地址发送ICMP超时报文。
第三点,定位下一跳。
我们看下边代码。
sin = (struct sockaddr_in *)&ipforward_rt.ro_dst;
if ((rt = ipforward_rt.ro_rt) == 0 ||
ip->ip_dst.s_addr != sin->sin_addr.s_addr) {
if (ipforward_rt.ro_rt) {
RTFREE(ipforward_rt.ro_rt);
ipforward_rt.ro_rt = 0;
}
sin->sin_family = AF_INET;
sin->sin_len = sizeof(*sin);
sin->sin_addr = ip->ip_dst;
rtalloc(&ipforward_rt);
if (ipforward_rt.ro_rt == 0) {
icmp_error(m, ICMP_UNREACH, ICMP_UNREACH_HOST, dest, 0);
return;
}
rt = ipforward_rt.ro_rt;
}
里面那段代码是检测是否是须要发送重定向报文。出现重定向的缘由是上一台主机的路由表太久了,形成了错误的转发。接出来就是选择合适路由器来发送差错报文。
三、ip_output(略)四、总结
ip合同的处理中,首先是ipintr函数对分组报文进行验证和重装;之后ip_forward对数据包进行转发和定位;最后ip_output对数据包进行分片发送。
源码我花了不少时间去找,最后意识到源码是4.4BSD标准的,跟linux版本无关。原本想说清楚的,并且限于篇幅和自己的理解问题,比较难展开。个人觉得要理解tcp/ip,卷1就可以了。并且要深入tcp/ip,要把socket用好,用透,还是须要从源码起来阅读和理解。
假如说C++是一个骄傲的信仰,这么就让我执著一次自己的信仰吧。
image.png