前篇分析了给本机系统配置IP地址的过程,本篇介绍一下loopback接口的“配置”过程,之所以用引号,是因为此配置不完全是用户自己控制的,为什么不先介绍loopback的配置,原因也在此。上篇我们已经对配置的过程一步一步做了分解,那么我们可以一下子来了解loopback接口的初始化及配置过程,这也是对普通设备的初始化和配置过程的一个回顾。
要使Loopback接口起作用得分为2个步骤:
第一步是在系统初始化的时候就创建一个in_dev{}
结构给loopback接口,参照前篇,对于用户配置普通设备的IP地址,这个动作应该是在ioctl->...... ->inet_set_ifa
函数中完成。这是两种不同设备的配置上的区别:loopback设备是自动创建,而普通设备是“用户”手动创建。
而其这种自动还转了一道手,它实际是在ip_netdev_notifier
收到NETDEV_REGISTER
事件后才去调用inetdev_init
创建了in_dev{}
结构,此函数请参考前篇。而普通设备则是在ioctl的上下文中调用inet_set_ifa
函数创建的。
第二个步骤就是当系统初始化结束之后根据系统配置打开loopback接口,使之UP。如同普通设备的操作,系统必须调用dev_open
打开looback接口,从而发送NETDEV_UP
事件给ip_netdev_notifier
。这个notifier对于普通设备的事件还是不是太感兴趣,啥也不做;但是非常关心loopback接口的UP事件,它创建了in_ifaddr{}
结构。这有是loopback接口配置自动化的证明了。ip_netdev_notifier
的事件处理函数是inetdev_event
,其相关代码如下:
inetdev_event
{
......
case NETDEV_UP:
if (dev == &loopback_dev) {
struct in_ifaddr *ifa;
if ((ifa = inet_alloc_ifa()) != NULL) {
/*这里面每个赋值都很重要,直接决定loopback的性质 INADDR_LOOPBACK宏就是0x7f000001-- 127.0.0.1 */
ifa->ifa_local = ifa->ifa_address = htonl(INADDR_LOOPBACK);
ifa->ifa_prefixlen = 8;
ifa->ifa_mask = inet_make_mask(8);
ifa->ifa_dev = in_dev;
/*该地址是属于本机范围的,不属于外部地址,我们会在后面说到这个值*/
ifa->ifa_scope = RT_SCOPE_HOST;
memcpy(ifa->ifa_label, dev->name, IFNAMSIZ);
inet_insert_ifa(ifa);
}
}
......
}
值得关注的是这段代码又调用了inet_insert_ifa
函数,它发送NETDEV_UP
事件给fib_inetaddr_notifier
。
是否loopback设备的配置可以改变?可以的!让我们跑到Linux的/etc/sysconfig/ network目录下(目前是Redhat,其它版本的Linux不一定是此目录,但都大同小异),看到有ifcfg-lo
这么一个文件,内容如下:
DEVICE=lo
IPADDR=127.0.0.1 #可以改变这个IP地址
NETMASK=255.0.0.0
NETWORK=127.0.0.0
......
BROADCAST=127.255.255.255
NAME=loopback
你可以改动这个文件里的某些项,然后执行#ifup lo
就把里面的内容作为ioctl的参数传给了内核,把缺省的配置改成你想要的。上图中看到fib_netdev_notifier
收到NETDEV_UP
事件(确切的来说此时只有loopback接口),于搜索in_dev->fa_list
然后循环调用fib_add_ifaddr
,实际在缺省情况下在系统启动之后,只有loopback接口才有in_ifaddr{}
结构,所以这个动作对于普通设备没有意义。我们将在以后分析fib_add_ifaddr
函数,这个函数非常重要,它是打开路由系统的钥匙
为了能处理IP别名,所以要引入ip命令。
ip是iproute2软件包里面的一个强大的网络配置工具,Iproute2是一个在Linux下的高级网络管理工具软件。实际上,它是通过rtnetlink sockets方式动态配置内核的一些小工具组成的,从Linux2.2内核开始,就实现了通过rtnetlink sockets用来配置网络协议栈,它是一个现代的强大的接口。最吸引人的特色就是它用完整而有机制的简单命令替代了之前以下命令的功能,如ifconfig,arp,route,iptunnel,而且还添加了其它不少的功能。如今,Iproute2已经在很多主要的发行版里被默认安装。它在配置隧道的时候非常有用。当然本书不打算讨论它们。我们要讨论的IP别名,通过iproute2这个工具可以对它进行设置。
IP 别名有时候也称为网络接口别名(network interface aliasing) 或逻辑接口(logical interface)。IP 别名的概念是:可以在一个网络接口上配置多个IP 地址。这样就能够在使用单一接口的同一个主机上进行负载平衡以支持多种服务。比如你的主机上只有少量网卡接口,而又要求进行多路接入的扩展,你可以如下配置:
假设A和B与服务器之间的数据不相关,在这种配置下,你的服务器为了给两个不同的请求服务,要么使机器上的端口(指TCP/UDP的端口,比如FTP使用的21,HTTP使用80)不一样,要么使机器上的IP地址不一样——如果大家都要使用FTP,而且请求端口还是21,那只好使用多个逻辑IP了。当然读者还有更多的解决方法,但本书提出这样一个概念只是告诉你Linux“可以”用这种解决办法,而非“必须”。
例如,如果在物理单元号为eth0 的以太网卡上已经配置了一个现有的IP 地址,那么可以通过添加一个逻辑单元号:1 来创建IP 别名,可以通过递增逻辑单元号来添加更多的IP 地址。
配置的基本过程如下: 假设我们已经有一个IP地址了,如果配置IP为192.168.18.2。
# ip addr add 192.168.0.11/24 label eth0:1 dev eth0
检查一下是否设置成功
# ip addr show eth0
2: eth0: mtu 1500 qdisc pfifo_fast qlen 100
link/ether 00:48:54:1b:25:30 brd ff:ff:ff:ff:ff:ff
inet 192.168.18.2/24 brd 192.168.18.255 scope global eth0
inet 192.168.0.11/24 scope global secondary eth0:1
ip addr命令和ifconfig在内核的实现路径不一致,后者是通过ioctl,前者则是通过netlink消息接口,内核中采用inet_rtm_newaddr
来响应这个消息。此函数把应用层传过来的参数进行一次拷贝之后,创建in_ifaddr{}
,就直接调用inet_insert_ifa
。
static int inet_rtm_newaddr(struct sk_buff *skb, struct nlmsghdr *nlh, void *arg)
{
struct rtattr **rta = arg;
struct net_device *dev;
struct in_device *in_dev;
struct ifaddrmsg *ifm = NLMSG_DATA(nlh);
struct in_ifaddr *ifa;
int rc = -EINVAL;
// 有效性检查
// 网路地址部分 <= 32
if (ifm->ifa_prefixlen > 32 || !rta[IFA_LOCAL - 1])
goto out;
rc = -ENODEV;
//通过设备index获取设备描述符
if ((dev = __dev_get_by_index(ifm->ifa_index)) == NULL)
goto out;
rc = -ENOBUFS;
//获取设备配置信息
if ((in_dev = __in_dev_get(dev)) == NULL) {
//第一次配置设备,则创建配置信息
in_dev = inetdev_init(dev);
}
//分配ip地址
if ((ifa = inet_alloc_ifa()) == NULL)
goto out;
//如果地址指派给隧道接口:
// ifa->ifa_local为隧道的本地地址,ifa->ifa_address为远程地址
//否则:
// ifa->ifa_local, ifa->ifa_address均为本地地址
if (!rta[IFA_ADDRESS - 1])
rta[IFA_ADDRESS - 1] = rta[IFA_LOCAL - 1];
memcpy(&ifa->ifa_local, RTA_DATA(rta[IFA_LOCAL - 1]), 4);
memcpy(&ifa->ifa_address, RTA_DATA(rta[IFA_ADDRESS - 1]), 4);
//网络地址部分
ifa->ifa_prefixlen = ifm->ifa_prefixlen;
//掩码
ifa->ifa_mask = inet_make_mask(ifm->ifa_prefixlen);
//广播地址
if (rta[IFA_BROADCAST - 1])
memcpy(&ifa->ifa_broadcast,
RTA_DATA(rta[IFA_BROADCAST - 1]), 4);
//选播地址
if (rta[IFA_ANYCAST - 1])
memcpy(&ifa->ifa_anycast, RTA_DATA(rta[IFA_ANYCAST - 1]), 4);
//ifa_flags标示主辅地址
ifa->ifa_flags = ifm->ifa_flags;
ifa->ifa_scope = ifm->ifa_scope;
in_dev_hold(in_dev);
ifa->ifa_dev = in_dev;
//设备别名
if (rta[IFA_LABEL - 1])
rtattr_strlcpy(ifa->ifa_label, rta[IFA_LABEL - 1], IFNAMSIZ);
else
memcpy(ifa->ifa_label, dev->name, IFNAMSIZ);
//将新地址插入到in_device中
rc = inet_insert_ifa(ifa);
out:
return rc;
}
网络设备管理层已经为这个机制做好了准备。让我们来看看曾经走过的路。inet_insert_ifa
之前就看到了,只是没有给出它的详细代码,为了要弄清楚IP别名在内核中的实现,那就得看详细代码:
static int inet_insert_ifa(struct in_ifaddr *ifa)
{
struct in_device *in_dev = ifa->ifa_dev;
struct in_ifaddr *ifa1, **ifap, **last_primary;
//辅地址标志
ifa->ifa_flags &= ~IFA_F_SECONDARY;
//主地址列表
last_primary = &in_dev->ifa_list;
//遍历主地址列表
for (ifap = &in_dev->ifa_list; (ifa1 = *ifap) != NULL;
ifap = &ifa1->ifa_next) {
if (!(ifa1->ifa_flags & IFA_F_SECONDARY) &&//主地址
ifa->ifa_scope <= ifa1->ifa_scope)//ifa的scope小于等于主地址的scope
last_primary = &ifa1->ifa_next;
//相同子网:
// 子网掩码长度相同的情况,网络地址相同
if (ifa1->ifa_mask == ifa->ifa_mask &&
inet_ifa_match(ifa1->ifa_address, ifa)) {//网络地址有重叠
if (ifa1->ifa_local == ifa->ifa_local) {//本地地址相同
inet_free_ifa(ifa);//重复添加
return -EEXIST;
}
if (ifa1->ifa_scope != ifa->ifa_scope) {
inet_free_ifa(ifa);
return -EINVAL;
}
//子网掩码长度相同,网络地址有重叠,设置为辅地址
ifa->ifa_flags |= IFA_F_SECONDARY;
}
}
//加入新子网
// 子网掩码或网络地址没有重叠
if (!(ifa->ifa_flags & IFA_F_SECONDARY)) {
net_srandom(ifa->ifa_local);
ifap = last_primary;
}
//上一个scope大于本ip地址的主地址
ifa->ifa_next = *ifap;
*ifap = ifa;
//向netlink发送消息,新添加了ip地址
rtmsg_ifa(RTM_NEWADDR, ifa);
//通知inetaddr_chain,ip地址加入到设备
notifier_call_chain(&inetaddr_chain, NETDEV_UP, ifa);
return 0;
}
这里要注意的是in_ifaddr->local
和in_ifaddr->address
在绝大多数情况下是一样的。
上面那段代码完成的工作就是把该接口上的IP地址组成一条链表,如果我们在一台机器上为某块网卡设置5个IP地址,其配置顺序如下
# ip addr add 192.168.0.1/24 label eth0:1 dev eth0
# ip addr add 192.168.0.2/24 label eth0:2 dev eth0
# ip addr add 192.168.0.3/24 label eth0:3 dev eth0
# ip addr add 192.168.1.1/24 label eth0:4 dev eth0
# ip addr add 192.168.1.2/24 label eth0:5 dev eth0
# ip addr add 192.168.0.4/24 label eth0:6 dev eth0