网口状态标志位详解(Part 1/2)

常见问题

我们在平常定位问题,经常会通过ip linkifconfig查看网络环境和配置。其中网口的标志位是其中很重要的一个检查环节。在输出结果里,会有两组标志位信息

  • 一组是尖括弧里包含的值<BROADCAST,MULTICAST,UP,LOWER_UP\>,有很多标志位的组合。
  • 另一组是 state UP。还有其他状态如DOWN, LOWERLAYERDOWN等。
    图1:ip link 输出结果
    这些信息有时候看起来是一致的, 有时候是重复的,甚至矛盾的。难免让人产生一些疑惑,这两组标志位分别代表什么?他们是什么关系?下面我们就这两组标志,逐个展开分析下。
    我们先来看几个场景里,网口在不同状态下,输出的标志位有哪些差异。

场景1:网线没插,显示NO-CARRIER

图2: 拔掉网线后的网口输出结果

  • 【Q1】 网口状态标志位里有个明显特征,有个NO-CARRIER标志,但是状态里一个UP,一个DOWN,why? :(

场景2: 正常状态下物理口eth0 的输出

ip link show dev eth0命令输出网口eth0的一些状态标志位。
图3: 正常网口的ip link 命令和ifconfig命令结果
这里有几个疑问:

  • 【Q2】RUNNING标志位:为什么ifconfig命令显示有这个标志位,而ip link 命令没有显示出来。
    或者说,我们通过ip link命令能确认网口是RUNNING吗?还是只能通过ifconfig命令查看。
  • 【Q3】LOWER_UP标志位:UP紧跟个LOWER_UP. 标志位里这两个标记分别代表的意义是什么?有啥区别?是不是重复了?

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

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

  • 通过ip link命令创建一对veth口。默认网口状态down。
    图4: veth1口状态对比
  • 【Q4】M-DOWN:veth0口上第一组状态标志位里是M-DOWN(不是DOWN), 而第二组是state DOWN
  • 设置veth1 网口UP

图5: veth1口状态对比
veth1被UP后,有两个变化:

  1. M-DOWN:veth0口上的M-DOWN消失了, veth1口UP后反而没有消失
  2. LOWERLAYERDOWN: veth1口UP后, state没有变成UP而是LOWERLAYERDOWN
    这里就带来个问题:
  • 【Q5】M-DOWN state LOWERLAYERDOWNUP/DOWN的关联关系是什么,有哪些区别?

标记汇总:

<flags>

  • UP: 可以通过ip link set dev xx up设置,系统管理员启用某个网口,不代表网口可以正常使用。
  • DOWN:与UP 类似。通过ip命里停用某个网口。 停用后,网口无法收发包,设置可以因为节能,将设备断电。
  • NO-CARRIER: 如果是物理网口,网口上网线没有插好。如果是虚拟口,关联网口的状态不正常。如veth情况下,对端网口没有被UP
  • RUNNING: 通过ifconfig命令才能看到。在ip link命令不显示这个标志位。在 iplink 命令里如果网口有UP标志,没有NO-CARRIER,那么ifconfig一定能看到这个标志。反之也成立, 如果网口有UPNO-CARRIER标志两个标志, ifconfig肯定看不到RUNNING
  • LOWER_UP: 本意是指当网口被UP后,网口底层底层载体是正常的,如以太网口的网线是插好的(wifi情况是指信道是好的?待学习)。 对eth口来说,有``LOWER_UP, 等价与有RUNNING`
  • DORMANT:
  • M-DOWN: 当网口有配对端口时,对端网口如果没有UP标记,则本端网口会显示这个标记。 若 veth0/1配对, veth0如果是DOWN, 则 veth1 口的状态里会有这个标记。

operate

  • state UP: 代表网口可以正常使用。 对eth口,等于flag里的UPRUNNING同时存在。
  • state DOWN: 代表网口不可用。有可能是flags的标志位DOWN了, 也有可能是网口被UP但是状态不正常(如NO-CARRIER 或者 wifi 网口的认证不通过)。
  • state LOWERLAYERDOWN:当网口配对使用时候,本端网口被UP, 但是对端网口没有UP时候,会显示这个状态。对veth口来说等同于UPM-DOWN同时存在。
  • state DORMANT: 跟移动网络和节能相关。 工作中没有遇不到这种场景,现在只能贴英文, 待学习。
  • state DORMANT: A state in which the mobile restricts its ability to receive normal IP traffic by reducing monitoring of radio
    channels. This allows the mobile to save power and reduces signaling load on the network.`

以上提到的UPDOWN,都是指 flags 标志位(<>)里的,不要跟下面的 state 标志混了。

标志位分类

IP link标志位分类

根据这些标志位的来源, 我们可以把他们分为三大类:
前两类都是在<XX, ...>里打印的, 第三类是在 state XX里打印的。

  • ip link类:ip link命令自己打印的,不是内核里携带过来的。这一类比较特殊,比如NO_CARRIERM-DOWMN
  • flags类:根据内核里网口的dev->flags对应的标志位转换而来的。 如IFF_UPIFF_PROMISC等。
  • operstate类:与内核里对应网口下的dev->operstate一致。在第二组状态里,以state XXX形式显示。内核里网口的operstate是根据dev->state状态和网口类型等条件转换而来。

内核网口的3个状态变量

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

  • dev->flags:每个位代表一个标志。
  • dev->operstate:
  • dev->state:

ip link命令通过netlink消息跟内核交互,通过sendmsg下发req请求(命令), 通过recvmsg获取内核返回结果(命令结果)。解析结果,以获取内核里网口(netdevice)的状态。根据内核返回网口状态信息,打印输出网口的标志位(falgs)和状态值(operstate)。
可以分为以下几步:

  1. 构建一个netlink socket,并通过它往内核发送一个获取内核网口状态的命令,
  2. 内核根据命令,读取网口状态并发送一个响应消息,通过netlink socket返回给ip link命令。
  3. ip link根据内核返回结果,输出网口flags和状态

netlink命令的实现机制涉及的内容很多,我们这里不展开讲解netlink在内核如何实现。只关注调用链两端的实现。即:

  • ip命令如何解析并打印网口状态
  • 内核如何搜集并发回网口状态
    后续我们按照标志位分类顺序,结合ip link show eth0命令,逐个解析各个标志位在两端(内核和 ip link)的实现。

ip link命令的实现

函数调用栈

当执行命令ip link show eth0时候,具体函数调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main / iplink.c
|--> do_cmd
|--> c->func(...) //等同于do_iplink
do_iplink(...)
|--> brlink_show
|--> rtnl_linkdump_req
|--> rtnl_dump_filter(&rth, print_linkinfo, stdout) //rtnl_dump_filter_nc
rtnl_dump_filter_nc
|--> a.filter = filter, // filter => `print_linkinfo`
|--> rtnl_dump_filter_l(rth, a);
|--> a->filter(h, a->arg1);
print_linkinfo
|--> print_name_and_link("%s: ", name, tb) // 如"eth0:"
|--> print_link_flags(fp, ifi->ifi_flags, m_flag); //如`<..,UP,LOWER_UP\>`
|--> if (flags & IFF_UP && !(flags & IFF_RUNNING))
|--> print_string(flags ? "%s," : "%s", "NO-CARRIER"); //
|--> _PF(UP);
|--> _PF(LOWER_UP);
|--> _PF(DORMANT);
|--> if (mdown)
|--> print_string(PRINT_ANY, NULL, ",%s", "M-DOWN");
|--> print_operstate(fp, rta_getattr_u8(tb[IFLA_OPERSTATE]));//如`state UP`。
|--> color_fprintf(..., "%s ", oper_states[state]);

ip link命令, 因为没有指定具体网口,参数不同, 它的调用栈跟这个略有不同。

1
2
3
4
5
6
7
main / iplink.c
|--> do_cmd
|--> c->func(...) //等同于do_iplink
do_iplink(...)
|--> ipaddr_list_link
|--> ipaddr_list_flush_or_save
|--> print_linkinfo //后续调用同上

但最终都会调用print_linkinfo去处理netlink返回消息,逐个打印返回消息里的网口信息。这里不展开讨论。

主要函数

其中处理状态标志位的两个主要函数:函数print_link_flagsprint_operstate

  1. 函数print_link_flags输出网口的flags。如<BROADCAST,MULTICAST,UP,LOWER_UP\>
  2. 函数print_operstate网卡operstate状态。 如state UP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  83 static void print_link_flags(FILE *fp, unsigned int flags, unsigned int mdown)
84 {
85 open_json_array(PRINT_ANY, is_json_context() ? "flags" : "<");
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);
...
114 if (mdown)
115 print_string(PRINT_ANY, NULL, ",%s", "M-DOWN");
116 close_json_array(PRINT_ANY, "> ");
117 }

这个函数会打印<>中全部的标志位,因此我们后续拆分成几部分解释:

  1. NO_CARRIER标志位:
  2. RUNNING标志位:
  3. UPLOWER_UP DORMANT 标志位
  4. M-DOWN标志位:
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
124 static void print_operstate(FILE *f, __u8 state)
125 {
126 if (state >= ARRAY_SIZE(oper_states)) {
127 if (is_json_context())
128 print_uint(PRINT_JSON, "operstate_index", NULL, state);
129 else
130 print_0xhex(PRINT_FP, NULL, "state %#llx", state);
131 } else if (brief) {
132 print_color_string(PRINT_ANY,
133 oper_state_color(state),
134 "operstate",
135 "%-14s ",
136 oper_states[state]);
137 } else {
138 if (is_json_context())
139 print_string(PRINT_JSON,
140 "operstate",
141 NULL, oper_states[state]);
142 else {
143 fprintf(f, "state ");
144 color_fprintf(f, oper_state_color(state),
145 "%s ", oper_states[state]);
146 }
147 }
148 }

这个函数会打印state XX这个状态信息,具体数值是从内核传递回来的, 使用oper_states做了数值到状态字符串的转换。

1
2
3
4
119 static const char *oper_states[] = {
120 "UNKNOWN", "NOTPRESENT", "DOWN", "LOWERLAYERDOWN",
121 "TESTING", "DORMANT", "UP"
122 };

NO_CARRIER标志位

在函数print_link_flags里,ip link命令根据内核返回的网口属性里的ifi_flags标志进行打印,实现了NO_CARRIER标志位的输出。
这个标志位比较特殊:不是内核里自带的,而是ip link根据返回结果自己追加的。
只有在网口UP并且没有RUNNING时,ip link就会打印NO_CARRIER,这个标志。
或者说:
+ 如果网口被UP了,没有NO_CARRIER,那说明网口是RUNNING状态。此时是CARRIER_OK(现实中没有这个标志位)
+ 如网口被DOWN了(NOT UP),RUNNING标志位肯定就没有了。

1
2
3
86         if (flags & IFF_UP && !(flags & IFF_RUNNING))
87 print_string(PRINT_ANY, NULL,
88 flags ? "%s," : "%s", "NO-CARRIER");

【Q】 这里有个问题解释下:
CARRIER本意是表示网口上网线状态。现实中,网线是否插好,跟网口是否被UP或者DOWN没直接关系。
为什么必须网口被UP后,才能显示?网口被DOWN的时候,为什么不考虑显示?
A:【待完善】有些类型的物理网卡,当网口DOWN了,会做节能断电处理。 此时设备上没有光电信号,不可能去做网口的信号检测,因此不能判断光纤或者网线是否插好。

RUNNING标志位

【Q】为什么ip link命令,输出的网口状态标志位的列表里,看不到标志位RUNNING
接前面NO_CARRIER的处理,print_link_flags 处理完NO_CARRIER标志位的后,
就会把抹掉标志位IFF_RUNNING,不再处理。(见代码第89行)
因此即使内核携带IFF_RUNNING标记位返回给ip命令,在 iplink命令中也不会显示RUNNING标志位。这与ifconfig的处理是不一样的。

1
89         flags &= ~IFF_RUNNING;

UP``LOWER_UP ``DORMANT标志位

这三个标志位处理比较简单, 都是根据内核里返回的标志位,打印对应的标记字符串。
_PF 的解释:
每次打印一个标志位前,先清除掉这个标志位,再打印。
打印时候,如果是最后一个标志(flags为0), 则只打印状态字符串(格式”%s”),否选择要多个,(格式”%s”,)。用,分割多个标志位。

1
2
3
90 #define _PF(f) if (flags&IFF_##f) {                                     \
91 flags &= ~IFF_##f ; \
92 print_string(PRINT_ANY, NULL, flags ? "%s," : "%s", #f); }

M-DOWN标志位:

结论:当前网口的peer端口,没有UP时候,会在当前接口的<>里打印这个标志。

  1. print_name_and_link:根据内核上传的网口的iflink的序号(IFLA_LINK属性),查找到iflink对应的关联网口peer, 并返回peer网口的flags(m_flag)。
  2. print_link_flags: 如果peer网口没有IFF_UP,打印M-DOWN标记。
1
2
3
4
5
 962 int print_linkinfo(struct nlmsghdr *n, void *arg)
963 {
...
1027 m_flag = print_name_and_link("%s: ", name, tb);
1028 print_link_flags(fp, ifi->ifi_flags, m_flag);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1269 unsigned int print_name_and_link(const char *fmt,
1270 const char *name, struct rtattr *tb[])
1271 {
...
1276 if (tb[IFLA_LINK]) {
1277 int iflink = rta_getattr_u32(tb[IFLA_LINK]);
...
1296 m_flag = ll_index_to_flags(iflink);
1297 m_flag = !(m_flag & IFF_UP);
...
1310 }
...
1314 return m_flag;
1315 }
1
2
3
4
5
  83 static void print_link_flags(FILE *fp, unsigned int flags, unsigned int mdown)
84 {
...
114 if (mdown)
115 print_string(PRINT_ANY, NULL, ",%s", "M-DOWN");

内核的网口状态标志位

struct net_device的相关定义

  • 每个网口在内核里有对应的struct net_device结构体。
    1
    2
    3
    4
    5
    6
    2080 struct net_device {
    ...
    2127 unsigned long state;
    2128 unsigned int flags;
    ...
    2232 unsigned int operstate;

标志位netdev->flags

每个netdev设备里有一个上的flags用来存放标志位,我们这里只关注up/down相关的几个标志位

  • IFF_UP
  • IFF_RUNING
  • IFF_LOWER_UP
  • IFF_DORMANT

定义

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 };

状态获取函数

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);

标志位IFF_RUNNING

放到后面跟netif_oper_up一起解释。

标志位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标记
    注意: 内核的netif_running跟用户态ip link命令里用到的RUNNING标志位含义是完全不一样的。netif_running在网口up时候,被设置标志位__LINK_STATE_START

标志位 IFF_DORMANT

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

netdev->statenetdev->operstate

这部分我们放到后面part2里详细讲述。
operstate的小插曲
2006年内核引入operstate特性,在当时协议栈的维护者中也是颇有争议的!!!
引入operstat特性的patch
图4: 2006年协议栈加入operstate特性

==注:更新源代码的内核版本:v6.14==