ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux内核网络设备状态全解:从UP标志到载波检测

Linux内核网络设备状态全解:从UP标志到载波检测

带团队时,我经常遇到一种情况:网管报障说服务器断网,等我登录上去一看,网卡状态明明显示UP,可ping网关就是不通。后来查了整整半天,发现是内核里网络设备状态这套体系没吃透——嘴上说的"UP"根本不是同一个UP。这篇就把内核网络设备状态这件事讲透,从状态标志位、生命周期、载波检测到实际排查,全是我摸过的坑。

1. 一次诡异的丢网故障:状态全对,数据就是不通

1.1 故障现象与第一轮误判

先说那次故障。某机房一台机器,业务侧反应网络时断时续,SSH偶尔能连上,但连上之后延迟高得离谱,没过多久就彻底断连。我远程登录到机器上,第一条命令就是看网卡状态:

ip link show eth0

输出很"健康":

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000

BRD、MULTICAST、UP、LOWER_UP 都有,state也是UP。按这个状态,物理链路应该是通的,协议栈也该正常。我当时第一反应是查路由、查ARP、查iptables,折腾了一圈,啥毛病都没有。后来实在没招,跑到机房现场用笔记本直连这台机器,竟然能ping通。再顺着网线往上摸,发现接入交换机的端口指示灯不亮,但服务器端网口灯是亮的。

问题就出在这里:服务器和交换机之间有一段光模块转电口的跳线,服务器侧光模块发光正常,内核检测到"有光",认为链路存在,但实际数据的通路早就断了。换句话说,内核的carrier状态判断依据,和你以为的"物理链路连通"之间,隔着一层现实世界的复杂性。

1.2 从ip link输出反向追踪状态来源

那个时刻我才意识到,ip link输出的那几行字符,背后是内核里一套非常严谨又容易被忽视的状态机。state UP不是某个单一布尔值,而是内核综合了管理员配置、驱动反馈、物理层检测三层信息之后推算出来的"结论"。每层都有各自的状态字段,任何一层出了问题,用户态看到的表象都可能和真实情况对不上。

顺着这个思路往下挖,就还原出了这样一张状态地图:

  • 管理员层:dev->flags里的IFF_UP,表示"系统管理员要不要让这块网卡工作"。
  • 驱动层:dev->state里的__LINK_STATE_START、__LINK_STATE_RUNNING等,表示"驱动是否已经准备好收发数据"。
  • 物理/链路层:dev->state里的__LINK_STATE_LOWER_UP,对应我们常说的 carrier,表示"底层链路是否真的存在"。
  • 操作状态:dev->operstate,这是内核根据上面几层状态算给用户态看的"操作状态",也就是ip link里显示的state UP/DOWN/UNKNOWN。

你看,一个简单的UP,背后至少四层信息。排查网络问题如果只看最后那个操作状态,就很容易被表象欺骗。那次故障,光模块发光但数据不通,驱动层的carrier认为链路存在,操作状态自然是UP,可数据真正走不通。从那以后,我再也不迷信state UP这四个字了。

2. net_device的状态标志位:管理层、驱动层、操作层三层分离

2.1 dev->flags 与 IFF_UP:管理员想不想让设备工作

Linux内核里,每个网络设备都对应一个struct net_device结构体。这里面有一堆标志位,最常用的就是dev->flags。dev->flags是一个整数型位图,每个bit代表一个开关。比如:

#define IFF_UP 0x1 #define IFF_BROADCAST 0x2 #define IFF_LOOPBACK 0x8 #define IFF_RUNNING 0x40 #define IFF_NOARP 0x80 #define IFF_PROMISC 0x100 #define IFF_ALLMULTI 0x200 #define IFF_MULTICAST 0x1000

注意,IFF_RUNNING虽然在dev->flags里存在,但它的维护方式和IFF_UP完全不同。IFF_UP是管理员执行ip link set eth0 up时,由内核的dev_open()函数设置的,代表"这台设备被打开了"。IFF_RUNNING则更多由驱动或底层状态机制来更新,代表"这台设备当前真的能跑"。

这两者的区别是本质性的。一辆车,钥匙拧到ON(IFF_UP),不代表发动机在转(IFF_RUNNING),更不代表轮子在动(LOWER_UP)。曾经有人只盯着IFF_UP,以为设备状态正常,结果驱动根本没完成初始化,数据包发不出去。

还有一个很容易被忽略的点:IFF_UP是会被ip link set eth0 down清除的。一旦清除,内核会立即停止该设备的发送队列,丢掉路由相关状态,通知上层协议栈设备已停止。很多排障的人没意识到,这里有个"管理性关闭"和"物理性关闭"的概念差异。state DOWN和state LOWERLAYERDOWN,在ip link里都是显示DOWN,但一个是你主动关的,一个是底层链路没了,处理方式完全不一样。

2.2 dev->state 与 IFF_RUNNING:驱动眼里设备有没有准备好

dev->flags里虽然有IFF_RUNNING位,但内核内部更多使用dev->state这个独立字段来维护链路状态。dev->state同样是一堆bit位,其中有几个比较关键:

enum netdev_state_t { __LINK_STATE_START, __LINK_STATE_PRESENT, __LINK_STATE_RUNNING, __LINK_STATE_LOWER_UP, __LINK_STATE_DORMANT, __LINK_STATE_TX_SCHED, };

__LINK_STATE_START对应设备是否已启动(ndo_open成功),__LINK_STATE_RUNNING表示设备运行中,__LINK_STATE_LOWER_UP则和物理链路状态直接挂钩。你可能很奇怪,为什么不直接用dev->flags里的IFF_RUNNING?原因很简单:dev->flags是用户态可见的、历史遗留下来的AIP,很多用户态工具(比如老牌的网络配置命令)直接读它。但内核内部需要更细的状态管理,dev->state就是这套内部状态机。

驱动的ndo_open回调成功返回后,内核调用netif_start_queue等函数,把队列打开,这时候设备才算真正进入可收发状态。如果驱动初始化失败,ndo_open返回非零值,内核就不会设置__LINK_STATE_START,IFF_UP标志也会被回滚。这块逻辑在排障时尤其重要:ip link set eth0 up命令本身可能成功了,但驱动背后的ndo_open可能报错,需要去dmesg里找驱动打印的错误日志。

我遇到过不少案例,驱动初始化依赖固件加载,固件文件损坏之后ndo_open反复失败。ip link眼看是UP了——因为dev_open之前其实会先设标志——但IFF_RUNNING和__LINK_STATE_START根本没起来,数据链路完全瘫痪。这种时候如果只看ip link,会被那个UP骗得团团转。

2.3 operstate六种取值:内核如何向用户态"说人话"

中间状态再复杂,最终都要展示给用户态。内核定义了一套operstate操作状态,枚举值如下:

枚举值ip link显示含义
IF_OPER_UNKNOWNunknown驱动没有提供链路状态信息,无法判断
IF_OPER_NOTPRESENTnotpresent设备对应的硬件不存在(常见于热插拔网卡)
IF_OPER_DOWNdown管理性关闭(IFF_UP未设置)或初始化未完成
IF_OPER_LOWERLAYERDOWNlowerlayerdown设备已打开,但底层链路不存在(例如网线没插)
IF_OPER_TESTINGtesting设备处于测试模式,一般不常见
IF_OPER_DORMANTdormant设备已打开且底层链路存在,但等待外部事件激活
IF_OPER_UPup一切正常,可以收发数据

内核计算operstate的逻辑,大致是这样的:先看IFF_UP有没有设置,没设置就直接返回IF_OPER_DOWN;设置了再看__LINK_STATE_LOWER_UP有没有设置,没有就返回IF_OPER_LOWERLAYERDOWN;再往下看驱动是否主动设置了链路状态,如果驱动完全没管carrier,就返回IF_OPER_UNKNOWN;只有链路状态正常时才返回IF_OPER_UP。这是个层层递进的判定链。

这里有一个非常经典的经验:ip link显示为state DOWN时,先别急着怪网线。你执行ethtool eth0看一下,如果Link detected: yes,那大概率是IFF_UP没设,纯管理性关闭;如果Link detected: no,那才是物理链路问题。这两种情况虽然显示一样,但处理路径完全相反。很多时候网管上来就把网线重插一遍,费了半天劲发现机架上的线根本没松,回头一看是配置被改了。

3. 状态怎么变:设备注册、打开、关闭全生命周期

3.1 register_netdevice 之后的初始状态

网络设备不是生来就有的,它的状态从内核检测到硬件、注册设备那一刻就开始演变。驱动探测到硬件后,会调用alloc_netdev分配net_device结构体,再调用register_netdevice把设备注册到内核网络子系统。注册完成后,设备以eth0、eth1这样的名字出现在/sys/class/net/目录下。

设备刚注册时,默认处于一个"半初始化"状态:设备名存在,但没有IFF_UP,也没有IFF_RUNNING,operstate通常是IF_OPER_UNKNOWN或IF_OPER_DOWN。为什么不是直接DOWN?因为对某些虚拟设备或特殊驱动来说,管理员尚未配置,内核不敢贸然宣称链路断开,所以UNKNOWN反而是更安全的初始值。

对驱动开发者来说,注册阶段有几件事必须做对:

  • 设置正确的设备类型标志,ARPHRD_ETHER、ARPHRD_LOOPBACK等;
  • 设置netdev_ops,把ndo_open、ndo_stop、ndo_start_xmit等回调绑定好;
  • 设置ethtool_ops,否则用户态无法查询链路状态。

任何一个环节缺失,设备虽然注册成功,但后续的状态切换一定会出问题。比如ndo_open为空,内核调用dev_open时会直接返回-EINVAL,IFF_UP根本设不上。我见过某款虚拟网卡驱动就是这个毛病,注册一切正常,一执行ip link set up就报"Operation not supported",排查半天才发现是回调没绑全。

3.2 dev_open 与 ndo_open 回调链

当管理员执行ip link set eth0 up时,内核走向dev_open()函数,随后调用__dev_open(),这里做了几件关键的事:

  1. 检查设备是否有ndo_open回调,没有就直接报错;
  2. 调用ndo_open,让驱动做硬件初始化,比如申请DMA缓冲区、开启中断、配置PHY;
  3. ndo_open返回0之后,设置IFF_UP标志;
  4. 调用dev_activate(),把发送队列挂到调度器上,设备才算真正开始工作。

这不是一个"先设置标志再干活"的过程,而是"先干活,干成了才设置标志"。驱动类型不同,ndo_open里做的事也完全不同。物理网卡要初始化硬件寄存器、启动PHY;虚拟网卡可能只是把内核数据结构准备好;bonding设备的ndo_open会遍历所有slave,逐个打开底层设备。所以bonding的触发逻辑比单网卡复杂得多——bond0的IFF_UP设置成功,不代表它下面每一个slave都真的起来了。

驱动在ndo_open成功之后,还有一个经常会遗漏的动作:主动上报carrier状态。对纯软件设备(比如loopback、veth),驱动可以直接调用netif_carrier_on()告诉内核链路OK;对物理设备,则要等待PHY中断或轮询结果。如果驱动忘了在打开后更新carrier,operstate会长时间停留在UNKNOWN或DOWN,明明能通却显示异常,反而引发更多误判。

3.3 注销与清理:状态回收的次序讲究

设备销毁时,状态回收的顺序也很讲究。unregister_netdevice()被调用后,内核会先把设备从系统中摘除,发送NETDEV_UNREGISTER事件通知所有订阅者,然后逐步清理引用。一个常见的问题是:设备还没完全注销,名字就被复用了。老设备刚注销,新设备马上注册成同样的eth0,用户态看到的名字一样,但内核里的net_device对象已经换了个全新地址。

这对排查问题是个大坑:/proc/net/dev里同一个eth0名称,前后可能是两个完全不同的设备。某些网卡驱动支持热插拔,拔卡后eth0消失,再插卡后又出现。如果你恰好在拔卡和插卡之间去读状态,拿到的是旧的缓存还是新的数据,完全取决于你读的是哪个层级的接口。

正常注销流程大概是:

  • dev_close()或ndo_stop被调用,设备停止收发;
  • 标志位逐位清除,依次清掉IFF_UP、__LINK_STATE_START、IFF_RUNNING;
  • 发送NETDEV_DOWN事件;
  • unregister_netdevice()完成结构体注销和资源释放。

驱动开发者还要注意一点:注销前一定要把硬件中断、定时器等异步资源清理干净,否则设备已经注销,中断处理函数还在跑,就会导致内核态空指针崩溃。这类崩溃排查起来特别费劲,因为崩溃栈往往会指向一些无关的函数,只有在dmesg里看到"BUG: unable to handle kernel NULL pointer"配合设备名线索,才能定位到是状态回收顺序问题。

4. carrier检测机制:内核怎么知道网线真的插好了

4.1 物理层中断、PHY状态机与轮询

carrier这个词,最早是从"载波检测"来的。传统以太网里,网卡通过检测对端发来的链路脉冲来判断线缆是否连接。现代网卡绝大多数都内置了PHY芯片,PHY芯片通过MDIO总线与MAC通信,实时上报链路状态。

PHY的状态机很有意思,里面有PHY_UP、PHY_RUNNING、PHY_AN(自动协商中)等状态。自动协商没有完成之前,PHY不知道对端速率和双工模式,此时carrier不可能设为up。所以新插上网线那一刻,ip link不会立刻显示state UP,通常要等几秒甚至十几秒,这个延迟就是PHY自动协商的时间。

PHY状态变化时,驱动通过两种方式得知:

  • 中断方式:PHY芯片的Link状态引脚变化会触发网卡中断,驱动中断处理函数里调用netif_carrier_on/off;
  • 轮询方式:驱动定时读取PHY状态寄存器,发现链路变化后主动更新carrier。

很多老驱动默认用轮询,间隔时间长短直接决定链路状态感知的实时性。举个例子,某驱动用mii_check_link()定时查询,间隔2秒钟,那么拔掉网线后,用户态可能最多滞后2秒才看到状态变化。这在配置bonding主备切换时尤其致命,切换延迟完全取决于载波检测的反馈速度。

4.2 软件设备的carrier语义与veth的特殊性

物理网卡的carrier好理解,就是网线信号。但虚拟设备呢?veth、bridge、bond、tap这些纯软件设备,根本没有物理载体,carrier的语义就得靠驱动自己定义。

以veth为例,它是一对虚拟网卡,两个端点成对出现。一个端点像"网线插头",另一个端点像"网线插座"。内核里veth的carrier是跟随对端状态的:对端设备存在且处于启用状态,本端carrier就是1;对端被移到别的网络命名空间且被down掉,本端carrier就是0。这个机制的设计初衷很合理——veth的"物理链路"就是它和peer之间的连接关系,peer没了,链路就断了。

但容器场景里,这个机制经常引发误判。比如容器重启时,veth端点在容器侧会被临时down再up,宿主侧对应的veth端点如果恰好被网络管理服务重新配置,carrier会短暂抖动。如果上层监控脚本检测到carrier为0就报警"物理链路中断",实际上这只是容器生命周期事件,并非网络故障。所以对虚拟设备做监控时,不要用物理网卡的判断标准来套,需要结合容器生命周期事件一起分析。

bridge和bond的carrier逻辑又不同。bridge的carrier取决于它下面有没有处于forwarding状态的端口;bond的carrier取决于活动slave的carrier。这些语义都是内核在驱动代码里明确实现的,读状态之前一定要先搞清楚设备类型。

4.3 linkwatch与NETDEV_CHANGE的传播链路

驱动调用netif_carrier_on/off之后,内核并不会立刻更新所有用户态可见的状态。中间还隔着一个名叫linkwatch的机制。

netif_carrier_on会触发linkwatch_fire_event,把这个设备放进一个全局的linkwatch工作队列。链路事件被延后处理,由内核的linkwatch_event工作函数统一执行。之所以要延后,是为了合并短时间内多次发生的链路跳变——拔一次网线可能触发中断、状态查询、PHY驱动更新等多个事件,如果每个事件都立刻广播,内核和用户态会被消息风暴淹没。

真正对外传播的是一次NETDEV_CHANGE事件。这个事件通过内核的netdev_chain通知链发给所有订阅者,IPv4协议栈、IPv6协议栈、路由表、bonding、bridge、用户态netlink监听程序都会收到。ip monitor link命令干的事,就是监听netlink链路事件,实时打印设备状态变化。

排障时这个机制很有用。执行ip monitor link all,同时拔掉网线,你能看到事件发生的先后顺序和延迟,进而判断到底是驱动没有上报carrier,还是内核事件传播出了问题。我见过一种异常:驱动频繁调用netif_carrier_on/off,导致linkwatch工作队列里积压了大量事件,CPU占用飙升,业务流量反而被饿死。这种"状态抖动风暴"在物理链路不稳定时非常常见,也让故障表象变得异常诡异——cat /proc/net/dev显示包量稀疏,但软中断占用高得离谱。

5. 实战排查方法论:从状态标志到定位问题

5.1 三分钟状态体检:命令与文件对照表

踩过那么多坑之后,我自己总结了一套"状态体检"三板斧,任何网络设备状态问题,先跑这一套,基本能定位到是哪一层出了问题。

第一板斧:看用户态操作状态。

ip link show eth0

重点看state字段,同时看标志位里有没有UP、LOWER_UP、RUNNING。这三个关键词对应三层状态,哪里缺了就优先查哪层。

第二板斧:看ethtool链路检测结果。

ethtool eth0

看输出里的Link detected:yes表示物理链路OK,no表示驱动认为链路不存在,unknown或干脆没有这行输出,说明驱动没实现链路检测,需要去内核日志里再挖。

第三板斧:看内核状态文件和日志。

cat /sys/class/net/eth0/operstate cat /sys/class/net/eth0/carrier cat /sys/class/net/eth0/flags dmesg | grep eth0

/sys/class/net/eth0/operstate直接给操作状态;carrier给0或1;flags给出的是dev->flags的十进制值,需要自己换算成十六进制核对IFF_UP等标志;dmesg里则能看到驱动上报的链路up/down事件。

这几个信息凑到一起,基本就能区分:是配置问题、驱动问题,还是物理链路问题。

5.2 一个bonding主备切换的真实复盘

讲一个我实际经历的主备切换问题。某台机器配置了bond0,两个slave,eth0和eth1,mode=1(active-backup)。某天机房调整网络,拔掉eth0的网线,预期bond0自动切到eth1,业务不中断。结果业务断了,登录上去一看:

bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 state UP eth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 state UP eth1: <BROADCAST,MULTICAST,SLAVE,UP> mtu 1500 state DOWN

eth0还是UP,eth1还是DOWN。我当时第一反应是bonding的miimon没生效。进/proc/net/bonding/bond0一看:

MII Status: up MII Poll Interval (ms): 100 Active Slave: eth0

MII Status显示up,说明bond0认为当前活动链路正常。但eth0的物理链路明明断了,为什么MII Status还是up?继续翻配置,发现队友为了省事,在bond0上配了arp_interval而不是miimon。arp_interval依赖发送ARP请求并收到回应来判断链路状态,如果arp_ip_target指向的网关不可达,或者防火墙上过滤了ARP,bonding就永远认为链路是好的。

这个案例的教训是:miimon直接读slave的carrier寄存器,依赖于PHY链路检测;而arp_interval依赖网络层的双向可达性,两者语义完全不同。你用arp_interval,就等于绕过了carrier状态,物理链路断了根本不感知。所以排查bonding切换问题时,不要只看bond0的MII Status,一定要确认用的监测机制是什么。ip link show eth0里显示UP,是因为IFF_UP还在;真正决定是否切换的是bonding自己的监测结果。

后来我们把配置改成miimon=100,再去反复插拔网线,切换就正常了。这个改动背后的逻辑很简单:链路层的事交给链路层去判断,不要跨界到网络层。

5.3 状态类问题的通用排查顺序

基于这些经验,我把状态类问题的排查顺序固定成了这样,每次遇到都按这个顺序走一遍,基本不会遗漏。

第一步,明确"设备类型"再谈状态。物理网卡、veth、bond、bridge各自的carrier语义不同,不能拿一套标准硬套。先搞清楚设备是谁创建的、上层依赖是什么,再往下查。

第二步,区分"管理状态"和"操作状态"。执行ip link show,看UP标志位对应管理状态,看state字段对应操作状态。管理状态DOWN,就是人配的;操作状态DOWN,大概率是链路或驱动问题。

第三步,查驱动上报链路状态的通道。ethtool、/sys/class/net/*/carrier、dmesg三个来源互相印证。如果ethtool能显示Link detected,但sysfs里carrier为0,可能存在驱动上报延迟;如果dmesg里有"link up/down"事件,但用户态状态没变,可能是netlink事件传播被阻塞。

第四步,看协议栈层面的反应。设备状态变更会触发路由表更新、邻居表刷新,这些动作如果出错,也会表现为状态都正常但网络不通。比如IFF_UP设置后,IPv4地址是否自动分配了,arp_ignore配置是否导致ARP不回应,这些虽然不是设备状态直接的问题,但经常和设备状态绑定发生。

第五步,善用事件监听。ip monitor link能看到状态变更的实时事件和顺序,对判断"先拔线还是先改配置"这类问题非常有效。我曾经通过ip monitor link的日志时间戳,确认了某次故障是运维先改了网卡配置,后重启了网络服务,导致状态变更顺序错乱,跟物理链路完全无关。

这套顺序看起来简单,但真正执行时,每一步都能挖出不同类型的坑。设备的flags、operstate、carrier这些字段,在它自己所属的层里都很合理,但一旦跨界判断,就会产生误判。状态判断的本质,不是找到一个"全知全能"的字段,而是知道每个字段的局限,然后组合起来逼近真相。

最后再分享一个习惯:我写自动化监控或排查脚本时,从来不只取一个状态字段,而是同时读operstate、carrier、flags,并且记录读取时间戳。只要时间戳对上,一层层对照,大部分状态问题都能在几分钟内定位。内核网络设备状态这套东西,说复杂也复杂,说简单也简单,摸清每一层谁在负责什么,遇到问题时自然知道该问谁。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进