网口状态标志位解析: part1

IP link命令结果里的网口状态标志位

我们先来看几个场景里,不同状态的网口,输出的标志位有哪些不一样。

正常状态下物理网口eth0 的输出

命令行ip link show dev eth0会输出网口eth0的一些状态标志位。
这里注意一个现象, ‘ifconfig’命令显示的结果里有’RUNNING’标志位, 但是’ip link’命令的结果里没有这个标志位。
图1: 正常网口的ip link 命令和ifconfig命令结果

场景2:拔掉物理口的网线后, 网口的状态变化。

网口状态标志位里有个明显变化,多了一个’NO-CARRIER’
图2: 拔掉网线后的网口输出结果

场景3:veth口 UP/DOWN的状态变化

veth口在对端veth0口down状态下,veth1 down/up状态下的对比
图2: veth1口状态对比

通过结果,我们注意到,’ip link’命令输出的结果里,跟状态有关系的有两个地方,在图中用红色标记出来。
一部分是刚开始尖括弧里的值,一部分是 ‘state XX’,

这里有几个疑问?

  1. NO_CARRIER标志位:为什么拔掉网线时候,ip link会显示这个标志位,正常状态下不会显示CARRIER——OK之类消息?
  2. RUNNING标志位:为什么ifconfig命令显示有这个标志位,而ip link 命令没有显示出来。
  3. UP,LOWER_UP: 标志位里这两个标记分别代表的意义是什么?有啥区别?
  4. state UP: 这个’UP’标记跟前面的UP标是不是重复的?

结论:
这两组状态都是从内核里读取过来的,分别对应内核里的两个状态标记,dev->flagsdev->operate
在具体实现实现时候,还要涉及另外一个状态标记dev->state

网口状态 网线 ip 命令 flag标记 state(内核 operate 标记)
普通以太网口 正常 link up UP,LOWER_UP,有 RUNNING不展示 UP(内核 IF_OPER_UP)
普通以太网口 拔线 link up NO-CARRIER,UP DOWN(IP_OPER_DOWN)
普通以太网口 正常/拔线 link down DOWN DOWN(IP_OPER_DOWN)
veth1 veth0 up veth1 up UP UP(内核 IF_OPER_UP)
veth1 veth0 down veth1 up NO-CARRIER, UP, M-DOWN LOWERLAYERDOWN
veth1 veth0 down veth1 down M-DOWN DOWN(内核 IF_OPER_DOWN)

下面我们详细讲述下, ip link命令如何把展示这些标志位从内核里读出来,以及这些标志位在内核里的相关关系.

dev->flags标志位的定义

我们这里关注几个标志位

  • NO-CARRIER
  • IFF_UP
  • IFF_RUNING
  • IFF_LOWER_UP
  • IFF_DORMANT

根据这些标志位的来源和产生方式,又可以把这些标志位分为3类

  • ‘IP link’类:根据内核返回的设备状态,’ip link’命里自己打印的,内核里并没有这一类标志。 如’NO-CARRIER’.。
  • ‘netdev->flags’类: 直接读取的内核里,对应网口的netdev结构体里的flags标志位。如IFF_UP, ``IFF_PROMISC’等。`
  • 内核合成类:根据内核网口对应的netdev下的,operate和state 状态合成的。
IPF_XXX flags定义
1
2
3
4
5
6
7
8
9
10
11
12
13
 80 enum net_device_flags {
81 /* for compatibility with glibc net/if.h */
82 #if __UAPI_DEF_IF_NET_DEVICE_FLAGS
83 IFF_UP = 1<<0, /* sysfs */
...
89 IFF_RUNNING = 1<<6, /* __volatile__ */
...
100 #if __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO
101 IFF_LOWER_UP = 1<<16, /* __volatile__ */
102 IFF_DORMANT = 1<<17, /* __volatile__ */
103 IFF_ECHO = 1<<18, /* __volatile__ */
104 #endif /* __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO */
105 };

ip link命里如何打印这些标记的?

ip link命令通过netlink 消息跟内核交互,以获取内核里 netdevice 的状态,然后跟进内核返回状态,打印输出网口标志位和状态值。
可以分为以下几部:

  1. 构建一个netlink scoekt 并发送命令,发送给内核,
  2. 内核根据命令,读取网口状态并发送相应消息,返回给ip link命令。
  3. ip link根据内核返回结果,输出网口flags和状态
    具体函数调用关系如下,最终函数print_link_flags输出网口的 flags,
    print_operstate输出网卡operate状态state UP
1
2
3
4
5
6
7
8
9
10
=> main / iplink.c
=> => do_cmd
=> => => do_iplink
=> => => => ipaddr_list_link
=> => => => => ipaddr_list_flush_or_save(argc, argv, IPADD_LIST)
=> => => => => => ip_link_list(iplink_filter_req, &linfo)
=> => => => => => print_linkinfo
=> => => => => => => m_flag = print_name_and_link("%s: ", name, tb);
=> => => => => => => print_link_flags(fp, ifi->ifi_flags, m_flag);
=> => => => => => => print_operstate(fp, rta_getattr_u8(tb[IFLA_OPERSTATE]));

标志位NO_CARRIER

这个标志位是 ip link 特殊处理的,跟IFF_RUNNING一起讲。

标志位IFF_UP

标志位IFF_UP: ip link命令 up/down 网口

ip link set eth0 up 会分别设置内核里 eth0 口对应的 netdevice 设备上的 flags 标志位IFF_UP

  • 代码实现
    在 ip link 命令实现里, 是通过ioctol获取eth0 口对应的 flags,然后将IFF_UP标志位设置到 flags 上,再通过ioctol 命令SIOCSIFFLAGS下发会内核。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static int do_chflags(const char *dev, __u32 flags, __u32 mask)
    ...
    err = ioctl(fd, SIOCGIFFLAGS, &ifr);
    ...
    if ((ifr.ifr_flags^flags)&mask) {
    ifr.ifr_flags &= ~mask;
    ifr.ifr_flags |= mask&flags;
    err = ioctl(fd, SIOCSIFFLAGS, &ifr);
    ...
  • 注意: ioctol命令参数里,获取和设置的命令名字, 只有一个字母GS的差别。

    1
    2
    #define SIOCGIFFLAGS    0x8913          /* get flags                    */
    #define SIOCSIFFLAGS 0x8914 /* set flags */

标志位IFF_RUNNINGNO-CARRIER

总结: IFF_UP+ !IFF_RUNNING <==> NO-CARRIER 两者是等价的。
ip link 命令根据内核返回的网口属性里的ifi_flags标志进行打印。

  1. NO-CARRIER:如果网口处于IFF_UP状态, 但是没有IFF_RUNNING 标志,ip link命令首先就会打印一个NO-CARRIER标志。
  2. IFF_RUNNINGip link后续的处理中,不再考虑IFF_RUNNING标志位。因此即使内核携带IFF_RUNNING标记位,在 iproute2 工具中不再显示的输出它。这与ifconfig的处理是不一样的。

反向推导一下ip link输出了IFF_UP,但没有NO-CARRIER,则网口 eth0 其实处于IFF_RUNNING

1
2
3
4
5
6
7
8
9
10
1003
1004 int print_linkinfo(struct nlmsghdr *n, void *arg)
1005 {
...
1011 unsigned int m_flag = 0;
...
1027 parse_rtattr_flags(tb, IFLA_MAX, IFLA_RTA(ifi), len, NLA_F_NESTED);
...
1065 m_flag = print_name_and_link("%s: ", name, tb);
1066 print_link_flags(fp, ifi->ifi_flags, m_flag);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  83 static void print_link_flags(FILE *fp, unsigned int flags, unsigned int mdown)
84 {
86 if (flags & IFF_UP && !(flags & IFF_RUNNING))
87 print_string(PRINT_ANY, NULL,
88 flags ? "%s," : "%s", "NO-CARRIER");
89 flags &= ~IFF_RUNNING;
90 #define _PF(f) if (flags&IFF_##f) { \
91 flags &= ~IFF_##f ; \
92 print_string(PRINT_ANY, NULL, flags ? "%s," : "%s", #f); }
...
107 _PF(UP);
108 _PF(LOWER_UP);
109 _PF(DORMANT);
110 _PF(ECHO);
111 #undef _PF
...
117 }

标志位 IFF_LOWER_UP

在内核里,IFF_LOWER_UP标志位被置位的两个前提条件:

  • 网口处于UP状态: 即前面解释的IFF_UP标志位被设置
  • 网口的载质(网线) OK: 内核判断函数netif_carrier_ok为 TRUE,即dev->state上没有__LINK_STATE_NOCARRIER标志位

对应场景:以太网网口eth0,被管理员通过 ip link 命令设置UP后,并且网线正常接入对端交换机(交换机状态正常加电,且交换机上对应网口也被 UP)
当内核网口flags上有这个标志位时候,ip link打印网口标志位时候就会打印LOWER_UP标记。
这个等同于ifconfig命令里的RUNNING标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
9191 unsigned int dev_get_flags(const struct net_device *dev)
9192 {
9193 unsigned int flags;
9194
9195 flags = (READ_ONCE(dev->flags) & ~(IFF_PROMISC |
9196 IFF_ALLMULTI |
9197 IFF_RUNNING |
9198 IFF_LOWER_UP |
9199 IFF_DORMANT)) |
9200 (READ_ONCE(dev->gflags) & (IFF_PROMISC |
9201 IFF_ALLMULTI));
9202
9203 if (netif_running(dev)) {
9204 if (netif_oper_up(dev))
9205 flags |= IFF_RUNNING;
9206 if (netif_carrier_ok(dev))
9207 flags |= IFF_LOWER_UP;
9208 if (netif_dormant(dev))
9209 flags |= IFF_DORMANT;
9210 }
9211
9212 return flags;
9213 }
9214 EXPORT_SYMBOL(dev_get_flags);

+注意:内核的’netif_running’跟用户态’ip link’命令里用到的’RUNNING’标志位含义是完全不一样的。

标志位 IFF_DORMANT

DORMANT在我工作中没有接触到,看文档是 wifi 类的网口会使用,这里没有具体验证,只描述理解。
应用场景:wifi 网口, 当 wifi 网口连接到路由器后,记为 T1(carrier OK),跟以太网网口不一样的是,在物理载质检测通过后, wifi 口有时候还需要做一些安全认证, 也就是咱们平时配置 wifi 密码的哪些上层身份验证。DORMANT感觉翻译为匿名比较合适,因为没有还没有通过身份校验。
具体代码有机会在展开分析。对以太网网口没有意义。

operater 标志位 IF_OPER_LOWERLAYERDOWN

veth1口的对端网口是veth0。 veth0没有UP时候,veth1口的operater就会显示为更IF_OPER_LOWERLAYERDOWN.
这样可以更精确的知道 vet1 口是因为底层网口(对端网口,没有carrier_ok导致,veth 口只要UP就carrier_ok)

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
31
32
33
34
35
36
37
36 static unsigned int default_operstate(const struct net_device *dev)
37 {
38 if (netif_testing(dev))
39 return IF_OPER_TESTING;
40
41 /* Some uppers (DSA) have additional sources for being down, so
42 * first check whether lower is indeed the source of its down state.
43 */
44 if (!netif_carrier_ok(dev)) {
45 struct net_device *peer;
46 int iflink;
47
48 /* If called from netdev_run_todo()/linkwatch_sync_dev(),
49 * dev_net(dev) can be already freed, and RTNL is not held.
50 */
51 if (dev->reg_state <= NETREG_REGISTERED)
52 iflink = dev_get_iflink(dev);
53 else
54 iflink = dev->ifindex;
55
56 if (iflink == dev->ifindex)
57 return IF_OPER_DOWN;
58
59 ASSERT_RTNL();
60 peer = __dev_get_by_index(dev_net(dev), iflink);
61 if (!peer)
62 return IF_OPER_DOWN;
63
64 return netif_carrier_ok(peer) ? IF_OPER_DOWN :
65 IF_OPER_LOWERLAYERDOWN;
66 }
67
68 if (netif_dormant(dev))
69 return IF_OPER_DORMANT;
70
71 return IF_OPER_UP;
72 }
TODO M-DOWN

==备注:源代码引用的内核版本:v6.14==