从源码角度剖析tcp/ip---ip协议
本文最后更新于1240天前,其中的信息可能已经有所发展或是发生改变。

庖丁解牛,从源码角度来深入tcp/ip。----《TCP/IP详解 卷2:实现》。

一、简介和介绍

ip协议的是tcp和udp的根本,一般我们只需要了解子网划分,路由转发机制即可。但是底层是怎么实现的呢?毫无疑问,一个顶级的服务端工程师应该从根本从把知识点剖析清楚。下面就让我们从源码的角度解剖ip协议。(关于netinet中的函数,现在的linux代码已经重构过了,所以采用4.4BSD版本的代码。)
Github地址:https://github.com/neilss/4.4BSD-Lite

IMG_256

《TCP/IP详解 卷2:实现》在第四章中阐述了一个数据包通过网络接口发生硬件中断时,数据包就会放进iprinrq队列中,如上图。而文章主要讲述的是在路由器中,ip层的函数是如何实现的,大体的组织形式如下。

IMG_256

如果是路由器,当分组来到ipintrq并且发生软件中断时

iprintrq中的分组数据就会传递到ipintr函数让其进行分组验证,转发等等的操作。

  1. ip_forward会根据ip分段和路由表来定位下一条。
  2. 最后在ip_ouput就是构造首部,选择路由和分片。

如果是主机的话,数据来到iprintr的时候就会包ip报文传送都网络层。熟悉osi7层结构的应该很清楚,这里就不再阐述了。

一、iprintr

我们可以知道当软中断发生的时候,内核就会调用iprintr把数据包从队列中获取出来。这个函数比较复杂。首先我们先看一下iprintr函数的开头,代码如下。

voidipintr(){

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 < sizeof (struct ip) &&

(m = m_pullup(m, sizeof (struct ip))) == 0) {

ipstat.ips_toosmall++;

goto next;

}

ip = mtod(m, struct ip *);

if (ip->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 < hlen) {
ipstat.ips_badlen++;
goto bad;
}
NTOHS(ip->ip_id);
NTOHS(ip->ip_off);

首先把几个16bit的值先转为主机顺序,内核封装了一个宏NTOHS来进行转换。如果首部长度不满足要求,那么就会跳到bad分支。

1.4 分组长度

if (m->m_pkthdr.len < ip->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 socka ddr_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)定位下一跳

voidip_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 %x\n", 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 <= IPTTLDEC) {

icmp_error(m, ICMP_TIMXCEED, ICMP_TIMXCEED_INTRANS, dest, 0);

return;

}

ip->ip_ttl -= IPTTLDEC;

第一个用途不用解释了,简而言之就是对“链路层广播,环回广播或者其他寻址查询参数是否正确“。
至于第二点,我们看下面代码。

if (ip->ip_ttl <= IPTTLDEC) {
icmp_error(m, ICMP_TIMXCEED, ICMP_TIMXCEED_INTRANS, dest, 0);
return;
}

ip->ip_ttl -= IPTTLDEC;

系统是不接受TTL为0的数据包的,因为每一跳都约定了TTL要减少至少1s,所以实现中就减少IPTTLDEC(宏为1)。如果小于1,那么就向源地址发送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用好,用透,还是需要从源码上去阅读和理解。

点击数:53

    暂无评论

    发送评论 编辑评论

    
    				
    |´・ω・)ノ
    ヾ(≧∇≦*)ゝ
    (☆ω☆)
    (╯‵□′)╯︵┴─┴
     ̄﹃ ̄
    (/ω\)
    ∠( ᐛ 」∠)_
    (๑•̀ㅁ•́ฅ)
    →_→
    ୧(๑•̀⌄•́๑)૭
    ٩(ˊᗜˋ*)و
    (ノ°ο°)ノ
    (´இ皿இ`)
    ⌇●﹏●⌇
    (ฅ´ω`ฅ)
    (╯°A°)╯︵○○○
    φ( ̄∇ ̄o)
    ヾ(´・ ・`。)ノ"
    ( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
    (ó﹏ò。)
    Σ(っ °Д °;)っ
    ( ,,´・ω・)ノ"(´っω・`。)
    ╮(╯▽╰)╭
    o(*////▽////*)q
    >﹏<
    ( ๑´•ω•) "(ㆆᴗㆆ)
    😂
    😀
    😅
    😊
    🙂
    🙃
    😌
    😍
    😘
    😜
    😝
    😏
    😒
    🙄
    😳
    😡
    😔
    😫
    😱
    😭
    💩
    👻
    🙌
    🖕
    👍
    👫
    👬
    👭
    🌚
    🌝
    🙈
    💊
    😶
    🙏
    🍦
    🍉
    😣
    Source: github.com/k4yt3x/flowerhd
    颜文字
    Emoji
    小恐龙
    花!
    上一篇
    下一篇