Total Pageviews

Friday 9 June 2017

从ip addr add和ifconfig的区别看linux网卡的ip地址的结构

一个老外在邮件列表上问了一个问题,就是ip addr add和ifconfig的区别,我给他进行了解答,可能因为英语不好吧,解答的很简单,因此我还是要在这里详细说明一下。其实它们之间没有什么区别,只 是表述方式不同罢了。如果你非常理解网络协议的原理以及网络的分层架构那么我想你就不会有这个问题,实际上,每一个网卡设备都有一个mac地址,但是却可 以有多个网络层地址,比如IP地址,然而这个事实无法很好地像用户提供操作接口,所以就引出了ip别名(IP aliases)和辅助ip(secondary IP addresses)的概念。其实很容易理解这个事实,按照分层的思想,下层总是为上层服务,也就是为上层提供舞台,上层利用下层的服务,而不必让下层知 道自己的情况,如果一个拥有合理mac地址的网卡没有配置网络层地址(比如IP地址)这件事合理的话,那么为这个设备配置多个IP地址也是合理的,正好像 一个ip可以对应多个应用层端口一样,也就是说,下层对上层总是一对多的关系,在分层架构中这种关系是合理的。下面我们就看一下linux的网卡的ip地 址结构。刚才说了在linux中,一个网卡可以有多个IP,那么这多个ip有什么关系呢?其实这些ip组成了一个吊链结构,所谓吊链结构就是一些节点链接 成一条链,然后每个节点带有自己的一条链,如下图所示:
clip_image001clip_image002clip_image003clip_image004
每个节点代表的ip地址标识一个网段,这个节点的ip就是这个网段的 Primary地址,它下面所带的ip就是这个网段的Secondary地址,也就是说一个网卡可以带有各个节点所带链表长度之和个ip地址,而且这些 ip不是线形的,而是上述的吊链结构。我们看一下这么做有什么好处。玩过Cisco路由器的朋友可能都知道有个Secondary IP的概念,这个特性可以创建逻辑子网,也就是说在一个物理网口上连接两个子网,这咋看起来好像不可思议,其实很简单,比如这个网口接到一台交换机上,如 果这个网口没有配置Secondary IP的话,那么这台交换机只能连接一个网段的主机,比如192.168.1.1/24,但是,如果它配置了Secondary IP,那么就可以连接两个网段的主机,比如192.168.1.1/24和10.0.0.1/24,道理就是这么简单,但是却很有用,该机制可以被路由汇 总策略所使用。注意上面这个例子中的Secondary IP不是这里说的linux的Secondary address,在linux中恰恰相反,只要一个网卡上配置的ip不是一个网段的,那么都是Primary IP,就是吊链结构中上面的那条主链中的IP,linux中的Secondary address是主链结点的子链结点中的IP,这一点一定注意,概念是不能混淆的。前面说的只是吊链中主链的作用,那么子链呢?其实想象一下也很简单,比 如一台机器上运行着一个代理服务器或者负载均衡服务,代理服务器或者负载均衡服务和主服务器要监听相同的端口,那么就可以用secondary address来解决了,只要需要在同一网段监听同一个端口的应用都是吊链中子链存在的原因,因此可以说,主链对外部或者说对下面链路层虚拟了多块网卡, 而子链向上层虚拟了多台机器,配置了吊链结构的linux主机如果说只有一块网卡,那么外部会认为它有多块网卡,对于内部,应用层会认为彼此在不同的主机 上,这就是效果。
除了上面大体的介绍之外,还有很多细节,吊链在主链上是没有主次的,子链除了第一个节点其它节点也不分主次,都是平行的关系,但是子链中的第一个节点总是 链接在主链中,它们携带的地址就是primary地址,它们下面隶属的子链携带的地址就是这个primary地址的secondary地址,如此看来,一 旦主链上一个节点被删除了,那么它的子链也将不复存在,所谓皮之不存毛将焉附。但是这种策略总是显得不是那么优美,因为父亲犯错,儿子也要受连累,这在现 代社会早就不时行了,那么就需要改变机制了,因此linux中特意有了一个选项,就是当一个primary地址被删除时,如果它有secondary地址 的话,那么它的第一个secondary地址(长子)继承被删除的primary地址的位置成为primary地址,这样就显得很合理了,要不然在删除 primary地址的时候,如果有程序用secondary地址,那么要么延迟删除,要么程序崩溃,采用自动提升策略的话就不会出现问题。
至于说IP aliases,那是以前版本有的了,就是一个实现问题,解决的问题和现在的secondary IP机制一样,它主要就是在物理网卡名字后面加上后缀从而成为虚拟网络接口,本质上和secondary IP机制没有区别,区别就是IP aliases显得不是那么直观,而secondary IP却是真正让应用看到了一个网卡的多个地址,比如你要是用IP aliases的话,有的时候你总是会问eth0:0是什么?我就曾经在内核里面拼命找eth0:0这个网络设备的注册代码,都要疯掉了也没有找到,其实 我并不是很傻,但是我却因为那个该死的名字作出了傻事。
下面就可以看看linux内核的实现代码了,首先弄明白一些数据结构,最重要的就是net_device,其次就是in_device,然后就是in_ifaddr,明白了这三个数据结构,一切就明白了,这是真的。
struct net_device
{
...
     void                    *ip_ptr;       //指向一个in_device结构,这个字段从net_device中分离表明一个网卡可以支持多种网络层协议的
...
}
struct in_device
{
         struct net_device       *dev;           //指向它隶属的net_device,也就是网卡
         atomic_t                refcnt;         //引用计数
         int                     dead;
         struct in_ifaddr        *ifa_list;      //所有的ip地址链表
...
};
struct in_ifaddr   //代表一个ip地址
{
         struct in_ifaddr        *ifa_next;       //上面的in_device中的ifa_list字段就是靠这个字段连成链的
         struct in_device        *ifa_dev;        //回指in_device结构
         struct rcu_head         rcu_head;
         u32                     ifa_local;       //ip地址
         u32                     ifa_address;
         u32                     ifa_mask;        //掩码
         u32                     ifa_broadcast;   //广播地址
         u32                     ifa_anycast;
         unsigned char           ifa_scope; 
         unsigned char           ifa_flags;           //只有IFA_F_SECONDARY标志,因为除了这个就是primary地址了
         unsigned char           ifa_prefixlen;
         char                    ifa_label[IFNAMSIZ]; //名字,在ip aliases时代,它就可能是ethx:y的形式,在secondary ip时代,它统一就是ethx
};
注 意,上面的结构并没有将linux网卡的ip地址结构表示为吊链结构,所谓的吊链结构只是逻辑上的,在数据结构上,一个网卡所有的ip地址全部都在 ifa_list中被链接成一个线性的链表,至于是primary地址还是secondary地址就看in_ifaddr的ifa_flags字段了。每 当有新的地址被设置的时候,inet_insert_ifa总是被调用,linux为何没有在代码上将ip地址表示为吊链结构呢?我也不知道,个人感觉一 个net_device带有一个primary ip链表,然后每个primary ip节点带有一个secondary ip链表,这样会更好一些的,我觉得inet_insert_ifa实现的十分拙劣。添加地址可以通过两个用户空间程序搞定,一个是ifconfig,另 一个是ip addr add,ifconfig是基于ioctl进行地址添加的,而ip程序是基于netlink进行地址添加的,不管哪一种方式都可以达到目的,现在就可以看 看另一个问题了:为何用ip addr add添加的ip地址用ifconfig看不到,而ifconfig设置的地址ip addr show却是可以看到。这个问题通过看代码一眼就可以明白,在ifconfig获得ip地址的时候,代码:
for (ifap = &in_dev->ifa_list; (ifa = *ifap) != NULL; ifap = &ifa->ifa_next)
{
    if (!strcmp(ifr.ifr_name, ifa->ifa_label) && sin_orig.sin_addr.s_addr == ifa->ifa_address)
    {
        break;
    }
}
取 的是这个被找到的ifa的ip地址,而我们知道,所有的ifa链接成一个线性链表,那么找到了第一个就不会再往后走了,因此只能得到一个结果,就是链表最 前面的那个,而ip add show就不同了,具体在函数inet_dump_ifaddr中实现,该函数遍历所有的ifa,并且传到用户空间缓冲区。这里可以做一个实验:首先用 ip addr add添加几个不在同一个网段的primary ip地址,然后再ifconfig一个和前面的ip都不在一个网段的ip,然后可以用ifconfig查看一下,发现不是刚刚用ifconfig设置进去 的那个ip,而是用ip addr add添加进去的,这就说明ifconfig永远都是取的ifa链表最前面的那一个,还有一点要注意,就是如果你用ip addr add添加了很多的secondary ip地址,那么恰好你用ifconfig设置的ip地址和那些secondary ip在一个网段,那么所有的secondary ip都将被删除,这些都是sencondary ip的规范决定的,而且在代码中也有体现。另外还要注意,路由表的表项都是基于primary ip的,因为所有的操作都是以primary ip为主的,比如在添加路由的时候:
void fib_add_ifaddr(struct in_ifaddr *ifa)
{
         struct in_device *in_dev = ifa->ifa_dev;
         struct net_device *dev = in_dev->dev;
         struct in_ifaddr *prim = ifa;
...
         if (ifa->ifa_flags&IFA_F_SECONDARY) {   //如果ifa是个sencondary地址,那么就找到它隶属的primary地址后然后以这个primary为主进行设置
                 prim = inet_ifa_byprefix(in_dev, prefix, mask);
                 if (prim == NULL) {
                         printk(KERN_DEBUG "fib_add_ifaddr: bug: prim == NULL/n");
                         return;
                 }
         }
         fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);    //添加进路由表
...
}
到 此为止我们知道了不少东西,最重要的就是linux中网卡ip地址的吊链结构以及这么设计的好处,另外就是设置ip地址的方式有ioctl和 netlink。其实网卡拥有多个ip并不会带来什么冲突,本质上ip和网卡没有什么关系,它们唯一的关系就是靠网络分层模型联系在一起的,细节上就是靠 路由联系在一起的,比如我添加路由的时候指定了一个目的地址和下一跳ip地址以及一个网卡出口,那么内核会根据提供的目的地址将路由插在合式的位置,然后 将nh的网络设备设置为你提供的网卡出口,等到传输数据的时候就会查找路由从而找到出口,就是这么简单,你自己手动设置的路由可以随意设置,即使完全错误 内核也会将之加入路由表的,还有一种路由是内核自动生成的,就是在网卡刚刚up的时候,这时通过网卡的net_device找到其in_device然后 找到其ip地址,这样的路由称为链路路由。
通过secondary IP机制,你可以认为你的机器有很多网卡,对于应用,监听同一端口的应用会认为它们在局域网中不同的机器上,你可以随意使用这些ip地址而不会发生混乱,路由和底层的arp会处理好这一切,当然前提是你将路由设置对。
附: 用户空间有ifup/ifdown,/sbin/ip,ifconfig,还有netplugd守护进程,这些有何关系吗?这中间ip程序是最基本的,没 有任何策略,策略就是参数指定,要么就是别的程序调用它,而netplugd就是一个监控守护进程,通过netlink监控网卡状态,然后根据不同的监控 结果调用/etc/netplug.d/netplug脚本,进而可能调用ifup/ifdown脚本,而后者就是脚本,其中会调用ifup-eth脚 本,最终整理好参数后调用ip程序(典型的就是:ip link set eth0 up/down),当然ip程序完全可以自己调用,比如ip addr add以及ip route add等等,而ifconfig没有那么绕圈子,就是通过ioctl进行设置,可以通过strace来观察。这其中奥妙大了去了,说白了就是策略和机制分 离,另外还体现出linux中的很多功能都是很小的程序组合而成的。 Linux的ip地址的吊链结构以及ip地址的寻址特性(详见《关于IP网段间互访的问题—路由是根本》)充分说明了linux的协议栈实现多么的完美,完全符合分层和封装模型,使得下层的逻辑和上层的逻辑完全解除耦合,也就是说ip层完全不依赖链路层以及物理层的物理布局,最后记住,ip层事情比如寻址路由只由ip层实现,之所有有链路层发现的路由,完全是为了方便。

No comments:

Post a Comment