快速替换Win32_NetworkAdapter WMI类以获取本地计算机的MAC地址

Jam*_*ton 4 c# c++ windows wmi networking

TL; DR版本的问题:WMI Win32_NetworkAdapter类包含我需要的信息,但是太慢了。在Windows上获取MACAddress,ConfigManagerErrorCode和PNPDeviceID列的信息的更快方法是什么?

我需要检索连接的网络适配器的信息,以便可以获取MAC地址来唯一标识本地Microsoft Windows计算机。WMI Win32_NetworkAdapter类似乎具有我在寻找的信息。MACAddress,ConfigManagerErrorCode和PNPDeviceID列是我真正需要的唯一列:

  • MACAddress:MAC地址(此操作的目标)
  • ConfigManagerErrorCode:允许我确定适配器是否已启用并正在运行。(如果已禁用,则应使用我的应用先前缓存的MAC地址(如果有))。
  • PNPDeviceID:通过检查“ PCI”(如果需要,可能还有其他接口)的前缀,我可以过滤掉非物理适配器,在我的Windows 7盒中有几个(包括虚拟适配器,例如VMware / VirtualBox) 。

我的计划是使用PNPDeviceID过滤掉非物理设备。然后,我将在所有剩余的表条目上使用MACAddress列(将地址保存到缓存中)。当设备被禁用(可能由非零的ConfigManagerErrorCode指示)并且MACAddress为空时,我可以从缓存中对该设备使用以前看到的MACAddress。

您可以在Windows 7计算机上看到此表的内容。您可以看到其中有大量垃圾,但是只有一个带有“ PCI” PNPDeviceID的条目。

wmic:root\cli>NIC GET Caption, ConfigManagerErrorCode, MACAddress, PNPDeviceID
Caption                                                   ConfigManagerErrorCode  MACAddress         PNPDeviceID
[00000000] WAN Miniport (SSTP)                            0                                          ROOT\MS_SSTPMINIPORT\0000
[00000001] WAN Miniport (IKEv2)                           0                                          ROOT\MS_AGILEVPNMINIPORT\0000
[00000002] WAN Miniport (L2TP)                            0                                          ROOT\MS_L2TPMINIPORT\0000
[00000003] WAN Miniport (PPTP)                            0                                          ROOT\MS_PPTPMINIPORT\0000
[00000004] WAN Miniport (PPPOE)                           0                                          ROOT\MS_PPPOEMINIPORT\0000
[00000005] WAN Miniport (IPv6)                            0                                          ROOT\MS_NDISWANIPV6\0000
[00000006] WAN Miniport (Network Monitor)                 0                                          ROOT\MS_NDISWANBH\0000
[00000007] Intel(R) 82567LM-2 Gigabit Network Connection  0                       00:1C:C0:B0:C4:89  PCI\VEN_8086&DEV_10CC&SUBSYS_00008086&REV_00\3&33FD14CA&0&C8
[00000008] WAN Miniport (IP)                              0                                          ROOT\MS_NDISWANIP\0000
[00000009] Microsoft ISATAP Adapter                       0                                          ROOT\*ISATAP\0000
[00000010] RAS Async Adapter                              0                       20:41:53:59:4E:FF  SW\{EEAB7790-C514-11D1-B42B-00805FC1270E}\ASYNCMAC
[00000011] Microsoft Teredo Tunneling Adapter             0                                          ROOT\*TEREDO\0000
[00000012] VirtualBox Bridged Networking Driver Miniport  0                       00:1C:C0:B0:C4:89  ROOT\SUN_VBOXNETFLTMP\0000
[00000013] VirtualBox Host-Only Ethernet Adapter          0                       08:00:27:00:C4:A1  ROOT\NET\0000
[00000014] Microsoft ISATAP Adapter                       0                                          ROOT\*ISATAP\0001
[00000015] VMware Virtual Ethernet Adapter for VMnet1     0                       00:50:56:C0:00:01  ROOT\VMWARE\0000
[00000016] Microsoft ISATAP Adapter                       0                                          ROOT\*ISATAP\0002
[00000017] VMware Virtual Ethernet Adapter for VMnet8     0                       00:50:56:C0:00:08  ROOT\VMWARE\0001
[00000018] Microsoft ISATAP Adapter                       0                                          ROOT\*ISATAP\0003
Run Code Online (Sandbox Code Playgroud)

(如果禁用物理适配器,则MACAddress列将变为null,并且ConfigManagerErrorCode更改为非零)。

不幸的是,这堂课太慢了。在我相对较新的基于Windows 7 Core i7的计算机上,Win32_NetworkAdapter上的任何查询始终需要0.3秒。因此,使用此方法将使应用程序启动又增加0.3秒(或更糟的时间),我认为这是不可接受的。尤其是因为我想不出一个唯一的正当理由,即弄清楚本地计算机上的MAC地址和即插即用设备ID需要花这么长时间。

搜索其他方法以获取MAC地址产生了GetAdaptersInfo和较新的GetAdaptersAddresses函数。他们没有WMI施加的0.3秒惩罚。这些功能是.NET Framework的NetworkInterface类(通过检查.NET源代码确定)和“ ipconfig”命令行工具(通过使用Dependency Walker确定)所使用的功能。

我在C#中做了一个简单的示例,其中列出了使用NetworkInterface类的所有网络适配器。不幸的是,使用这些API似乎有两个缺点:

  • 这些API甚至没有列出禁用的网络适配器。这意味着我无法从缓存中查找已禁用适配器的MAC地址。
  • 我看不到如何获取PNPDeviceID来过滤掉非物理适配器。

我的问题是:最多可以在几十毫秒内使用什么方法来获取本地计算机的物理适配器的MAC地址(无论是否启用)?

(我在C#和C ++上都有丰富的经验,并且可以阅读其他语言,所以我真的不在乎答案中可能使用哪种语言)。

编辑: 作为对Alex K关于仅使用立即返回和正向返回的建议的回应,并且还为我在做什么提供了一些示例WMI代码-这是一些列出感兴趣的列的C#代码:

    public static void NetTest() {
        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        EnumerationOptions opt = new EnumerationOptions();
        // WMI flag suggestions from Alex K:
        opt.ReturnImmediately = true;
        opt.Rewindable = false;
        ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\cimv2", "select MACAddress, PNPDeviceID, ConfigManagerErrorCode from Win32_NetworkAdapter", opt);
        foreach (ManagementObject obj in searcher.Get()) {
            Console.WriteLine("=========================================");
            foreach (PropertyData pd in obj.Properties) {
                Console.WriteLine("{0} = {1}", pd.Name, pd.Value);
            }
        }
        Console.WriteLine(sw.Elapsed.TotalSeconds);
    }
Run Code Online (Sandbox Code Playgroud)

我调用了此函数3次,每次都在最后一行打印了0.36秒。因此,建议的标志似乎没有任何作用:正或负。这并不奇怪,因为如何在C#中进行仅转发,只读WMI查询?似乎表明除非有大量记录(例如几百到几千条),否则不会观察到性能的变化,而Win32_NetworkAdapter表不是这种情况。

编辑2: 已提出多个答案,以使用IP帮助程序API中的SendARP(这是具有GetAdaptersInfo函数的相同API)。与GetAdaptersInfo相比,这对于查找本地MAC地址有什么优势?我想不出什么-从表面上看,GetAdaptersInfo返回的信息集比SendARP对本地适配器的返回的信息更为详尽。现在,我考虑了一下,我认为我的问题很大一部分集中在枚举的概念上:计算机上首先存在哪些适配器?SendARP不执行枚举:它假定您已经知道想要MAC的适配器的IP地址。我需要弄清楚系统上存在哪些适配器。这引起了一些问题:

  • 如果拔下网络电缆会怎样?例如,这在笔记本电脑上会很常见(拔掉以太网,断开WiFi卡)。我尝试使用NetworkInterface.GetAllNetworkInterfaces()并在拔出媒体时使用GetIPProperties()。UnicastAddresses列出了所有单播地址。Windows没有列出地址,因此我无法想到可以传递给SendARP的任何地址。直观地讲,拔出的适配器仍将具有物理地址,但没有IP地址(因为它不在具有DHCP服务器的网络上),这是有道理的。
  • 这使我想到:如何获取本地IP地址列表以使用SendARP进行测试?
  • 如何为每个适配器获取PNPDeviceID(或可用于过滤非物理适配器的类似ID)?
  • 如何列出禁用的适配器,以便可以从缓存中查找MAC地址(即上次启用该地址时找到的MAC地址)?

这些问题似乎无法由SendARP解决,这是我提出此问题的主要原因(否则,我将使用GetAdaptersInfo并继续进行操作...)。

Jam*_*ton 5

我彻底放弃了WMI,在获得所需信息的同时进行了重大改进。正如WMI所指出的,要花费大于0.30秒才能获得结果。使用我的版本,我可以在约0.01秒内获得相同的信息。

我使用了设置API,配置管理器API,然后直接在NDIS网络驱动程序上发出OID请求以获取MAC地址。设置API似乎很慢,特别是在获取诸如属性值之类的东西时。必须将安装程序API调用降到最低。(通过查看在设备管理器中加载设备的“详细信息”选项卡需要多长时间,您实际上可以看到它有多严重)。

关于WMI为什么这么慢的猜测:我注意到WMI的Win32_NetworkAdapter总是花费相同的时间,无论我查询的是哪个属性子集。似乎WMI Win32_NetworkAdapter类的程序员很懒,并且没有像其他WMI类那样优化其类以仅收集所请求的信息。他们可能会收集所有信息,无论是否要求。他们可能极大地依赖Setup API来执行此操作,而对慢速Setup API的过多调用以获取不需要的信息是使它如此缓慢的原因。

我所做工作的高级概述:

  1. 使用SetupDiGetClassDevs获取系统上存在的所有网络设备。
  2. 我筛选出了没有“ PCI”枚举数的所有结果(使用带有SPDRP_ENUMERATOR_NAME的SetupDiGetDeviceRegistryProperty来获取枚举数)。
  3. 对于其余的内容,我可以使用CM_Get_DevNode_Status获取设备状态和错误代码。具有可移动设备状态代码的所有设备均被过滤掉。
  4. 如果设置DN_HAS_PROBLEM使得错误代码为非零,则设备可能已禁用(或存在其他问题)。未加载驱动程序,因此我们无法向驱动程序发出请求。因此,在这种情况下,我将从维护的缓存中加载网卡的MAC地址。
  5. 父设备可以是可移动的,因此我也可以通过使用CM_Get_Parent和CM_Get_DevNode_Status递归检查设备树来查找父可移动设备,从而过滤掉那些父设备。
  6. 其余所有设备都是PCI总线上的不可移动网卡。
  7. 对于每个网络设备,我将SetupDiGetClassDevs与GUID_NDIS_LAN_CLASS GUID和DIGCF_DEVICEINTERFACE标志一起使用以获取其接口(仅在启用了设备/没有问题的情况下才有效)。
  8. 在驱动程序接口上将IOCTL_NDIS_QUERY_GLOBAL_STATS与OID_802_3_PERMANENT_ADDRESS结合使用以获取永久MAC地址。将其保存在缓存中,然后返回。

结果是可以可靠地指示PC上的MAC地址,这些地址应不受VMware,VirtualBox制造的“假”网卡的影响,而不受临时禁用的网卡的影响,并不受通过USB,ExpressCard, PC卡或任何将来的可移动接口。

编辑: 并非所有网卡都支持IOCTL_NDIS_QUERY_GLOBAL_STATS。绝大多数都可以工作,但是有些英特尔卡却不能。请参阅如何根据给定的设备实例ID快速可靠地获取网卡的MAC地址。