0.0.1. 总结
内核如何管理 网口上的IPv4 地址:
- 每个网口下面有多个IPv4地址,每个IP地址存放到一个
struct in_ifaddr的结构体里。这个结构体里存放别名、ip地址和掩码信息。
- 每个网口下面有一个单链表,每个ifa 节点通过
ifa_next指向下一个 ifa 节点。通过这个链表把多个 ifa 地址链接在一起。
- 每个 net_namespace下有多个网口,通过一个 hash 数据链表(
net->ipv4.inet_addr_lst),把各个网口下的全部ifa节点,放到这个hash链里面。hash值相同的 ifa 节点,通过addr_list 挂在到一个hash 桶下。
- 别名并不会创建一个独立的网口,只是在同一个网口下增加一个对应的 ifa结构体并挂载到上面的两个链表中。跟通过ip link 命令给一个网口增加一个地址相同,唯一不同在于这个地址起了一个别名或者叫label。

0.0.2. 命令行操作
有时候我们需要给一个网口配置的多个 IP 地址,这时候我们有两种配置方法:
- 使用 ifconfig 配置到网口别名上。
- 使用 ip addr 命令直接配置到网口上。
两种方法最终在内核实现是一样的,存储位置也一样,并且可以相互读写配置结果。
比如将9.9.9.199/24配置到 lo 口上,并起个别名lo:9
1 2 3 4 5 6
| [root@VM-0-12-centos ~]# ifconfig lo:9 9.9.9.199/24 [root@VM-0-12-centos ~]# ifconfig lo:9 lo:9: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 9.9.9.199 netmask 255.255.255.0 loop txqueuelen 1000 (Local Loopback) [root@VM-0-12-centos ~]#
|
ifconfig命令的配置结果,也可以通用ip link命令来查看
1 2 3 4 5 6 7 8 9
| [root@VM-0-12-centos ~] 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet 9.9.9.199/24 scope global lo:9 valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever
|
网口的别名在ip命令里被当做label输出,放在scope字段后面
Q: ifconfig和ip命令在功能上有什么差别呢?
A: 从功能上没有什么不一样的地方,只是使用了内核的两套不同的接口,一般推荐使用ip命令来配置网口。
- ifconfig:使用
ioctl接口配置/读取网口上的IP地址
- ip addr:使用
netlink消息接口配置/读取网口上的IP地址
这里只要写ifconfig和ioctl相关的处理逻辑。ip命令如何实现对网口IP地址的配置,另外一篇文章总结。
0.0.3. ifcofig代码实现
源代码: https://github.com/giftnuss/net-tools/blob/master/ifconfig.c
ifconfig 首先创建一个AF_INET的socket,借助这个socket 通过,通过一个 ioctl 命令SIOCSIFADDR下发给内核。
配置IP 地址时候,参数被存放到struct ifreq结构体里,用来存放网口名字和 ip 地址.
1 2 3 4 5 6 7
| int main(int argc, char **argv) {... memcpy((char *) &ifr.ifr_addr, (char *) &sa, sizeof(struct sockaddr)) ... fd = get_socket_for_af(AF_INET) ... r = ioctl(fd, SIOCSIFADDR, &ifr)
|
0.0.4. 内核侧代码实现
内核实现是围绕着一个ifa结构体struct in_ifaddr展开的。
- 函数调用:内核的 ioctl系统调用函数,接收用户空间传回来的命令
SIOCSIFADDR和对应的struct ifreq变量, 里面存放着别名及 IP 地址。
- ifa结构体
struct in_ifaddr:ioctl通过几层抽象及 ops 的调用,最终在devinet_ioctl中, 创建一个struct in_ifaddr的结构体,存放别名(ifa_label)及IP 地址(ifa_address)。
- 同一个网口下多个IP地址:只要地址不冲突,可以配置多个 IP 地址。 并通过 ifa 结构体下面的
ifa_next组成一个单向链表。
- 同一个 ns(netnamespace)下,ifa 按照 hash 分散到一个 hash 桶链表里。
0.0.4.1. 数据结构
0.0.4.1.1. ifa结构体in_ifaddr
1 2 3 4 5 6 7
| 143 struct in_ifaddr { 144 struct hlist_node addr_lst; 145 struct in_ifaddr __rcu *ifa_next; 146 struct in_device *ifa_dev; ... 157 char ifa_label[IFNAMSIZ]; ... };
|
0.0.4.2. 函数调用栈
1 2 3 4 5 6 7 8 9 10 11
| SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) => vfs_ioctl(fd_file(f), cmd, arg); => => filp->f_op->unlocked_ioctl //相当于sock_ioctl => => => sock_do_ioctl => => => => ops->ioctl(sock, cmd, arg);// 等效与packet_ioctl => => => => => packet_ioctl => => => => => => inet_ioctl => => => => => => => devinet_ioctl => => => => => => => => __inet_insert_ifa => => => => => => => => => ifa插入到in_dev->ifa_list => => => => => => => => => inet_hash_insert(dev_net(in_dev->dev), ifa);
|
0.0.4.3. 变量 packet_ops_spkt
内核基于文件的 ioctl的实现,最终会调用到packet_ioctl
1 2 3
| 4684 static const struct proto_ops packet_ops_spkt = { ... 4694 .ioctl = packet_ioctl,
|
0.0.4.4. 函数packet_ioctl
1 2 3 4 5
| 4285 static int packet_ioctl(struct socket *sock, unsigned int cmd, 4286 unsigned long arg) 4316 case SIOCSIFADDR: ... 4324 return inet_dgram_ops.ioctl(sock, cmd, arg);
|
1 2 3 4
| 1087 const struct proto_ops inet_dgram_ops = { ... 1097 .ioctl = inet_ioctl, ...
|
0.0.4.5. 函数inet_ioctl
1 2 3 4 5 6 7 8 9
| 957 int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg) 958 { ... 993 case SIOCSIFADDR: ... 998 case SIOCSIFFLAGS: 999 if (get_user_ifreq(&ifr, NULL, p)) 1000 return -EFAULT; 1001 err = devinet_ioctl(net, cmd, &ifr);
|
0.0.4.6. 函数 devinet_ioctl
别名(label)有:前后两部分, 前半部分是网口名字,后半部分是用户自定义的字符串。
- 根据前半部分网口名字找到对应的网口 netdev。
- 调用
inet_set_ifa,把 ifa 加入到对应的网口 netdev 下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 1071 int devinet_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr) 1072 { 1073 struct sockaddr_in sin_orig; 1074 struct sockaddr_in *sin = (struct sockaddr_in *)&ifr->ifr_addr; ... 1088 colon = strchr(ifr->ifr_name, ':'); 1089 if (colon) 1090 *colon = 0; ... 1132 dev = __dev_get_by_name(net, ifr->ifr_name); 1133 if (!dev) 1134 goto done; 1135 1136 if (colon) 1137 *colon = ':'; ... 1211 case SIOCSIFADDR: ... 1224 if (colon) 1225 memcpy(ifa->ifa_label, ifr->ifr_name, IFNAMSIZ); 1226 else 1227 memcpy(ifa->ifa_label, dev->name, IFNAMSIZ); ... 1236 1237 ifa->ifa_address = ifa->ifa_local = sin->sin_addr.s_addr; ... 1250 set_ifa_lifetime(ifa, INFINITY_LIFE_TIME, INFINITY_LIFE_TIME); 1251 ret = inet_set_ifa(dev, ifa); 1252 break; 1253
|
inet_set_ifa:主要函数功能是调用inet_insert_ifa,把 ifa 插入到两个链表里。
1 2 3 4 5
| 590 static int inet_set_ifa(struct net_device *dev, struct in_ifaddr *ifa) 591 { ... 599 return inet_insert_ifa(ifa); 600 }
|
0.0.4.7. 函数 inet_insert_ifa
把ifa节点插入到两个list 里。
ifa_list单向链表: 同一个网口下的 IP 地址,插入到对应的ifa_list下的单向链表中。
- hash桶链表:同一个ns(netnamespace)下的所有ifa 分散到一个hash链表中。根据计算出的 hash,挂载到
net->ipv4.inet_addr_lst[hash]这个桶下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 499 static int __inet_insert_ifa(struct in_ifaddr *ifa, struct nlmsghdr *nlh, 500 u32 portid, struct netlink_ext_ack *extack) 501 { ... 517 ifap = &in_dev->ifa_list 518 ifa1 = rtnl_dereference(*ifap) 519 520 while (ifa1) { ... 538 ifap = &ifa1->ifa_next 539 ifa1 = rtnl_dereference(*ifap) 540 } ... 563 rcu_assign_pointer(ifa->ifa_next, *ifap) 564 rcu_assign_pointer(*ifap, ifa) 565 566 inet_hash_insert(dev_net(in_dev->dev), ifa)
|
1 2 3 4 5 6 7
| 129 static void inet_hash_insert(struct net *net, struct in_ifaddr *ifa) 130 { 131 u32 hash = inet_addr_hash(net, ifa->ifa_local); 132 133 ASSERT_RTNL(); 134 hlist_add_head_rcu(&ifa->addr_lst, &net->ipv4.inet_addr_lst[hash]); 135 }
|
- 备注:2025更新到内核代码版本v6.14(v6.14-rc6-22-gb7f94fcf5546)