从源码角度剖析tcp/ip---ip协议(1)

2017-05-19  本文已影响0人  小胖子善轩

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

一、简介和介绍

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

image.png

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

image.png

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

  1. iprintrq中的分组数据就会传递到ipintr函数让其进行分组验证,转发等等的操作。
  2. ip_forward会根据ip分段和路由表来定位下一条。
  3. 最后在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 < 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 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 %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用好,用透,还是需要从源码上去阅读和理解。


如果说C++是一个骄傲的信仰,那么就让我执着一次自己的信仰吧。

image.png
上一篇下一篇

猜你喜欢

热点阅读