Быстрая замена WMI-класса Win32_NetworkAdapter для получения MAC-адреса локального компьютера
TL; версия DR этого вопроса: WMI класс Win32_NetworkAdapter содержит информацию, которая мне нужна, но она слишком медленная. Какой более быстрый способ получения информации для столбцов MACAddress, ConfigManagerErrorCode и PNPDeviceID в Windows?
Мне нужно получить информацию для подключенных сетевых адаптеров, чтобы я мог получить MAC-адрес для уникальной идентификации локального компьютера с Microsoft Windows. Класс WMI Win32_NetworkAdapter, похоже, содержит информацию, которую я ищу. Столбцы MACAddress, ConfigManagerErrorCode и PNPDeviceID - единственные, которые мне действительно нужны:
- MACAddress: MAC-адрес (цель этой операции)
- ConfigManagerErrorCode: позволяет определить, включен ли адаптер и работает ли он. (Если он отключен, я должен использовать MAC-адрес, ранее кэшированный моим приложением, если он доступен).
- PNPDeviceID: проверяя префикс "PCI" (и, возможно, другие интерфейсы, если необходимо), я могу отфильтровать нефизические адаптеры, которых на моем Windows 7 несколько (включая виртуальные адаптеры, например, для VMware / VirtualBox).,
Мой план состоял в том, чтобы отфильтровать нефизические устройства, используя PNPDeviceID. Тогда я бы использовал столбец MACAddress для всех оставшихся записей таблицы (сохранение адреса в кеше). Когда устройство отключено (как, возможно, указано ненулевым ConfigManagerErrorCode), а MAC-адрес равен нулю, я могу использовать ранее увиденный MAC-адрес для этого устройства из своего кэша.
Вы можете увидеть содержимое этой таблицы на моем компьютере с Windows 7. Вы можете видеть там тонны мусора, но только одну запись с PNPDeviceID "PCI".
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
(Если я отключаю свой физический адаптер, столбец MACAddress становится равным нулю, а ConfigManagerErrorCode изменяется на ненулевое значение).
К сожалению, этот класс просто слишком медленный. Любой запрос в Win32_NetworkAdapter последовательно занимает 0,3 секунды на моем относительно современном компьютере под управлением Windows 7 Core i7. Таким образом, использование этого добавит еще 0,3 секунды к запуску приложения (или хуже), что я считаю недопустимым. Это связано с тем, что я не могу придумать единственной уважительной причины, по которой выясняется, какие MAC-адреса и идентификаторы устройств plug-and-play находятся на локальном компьютере.
В результате поиска других методов для получения MAC-адреса были получены функции GetAdaptersInfo и более новые функции GetAdaptersAddresses. У них нет 0,3-секундного штрафа, который накладывает WMI. Эти функции используются классом NetworkInterface платформы.NET Framework (как определено при изучении исходного кода.NET) и инструментом командной строки "ipconfig" (как определено с помощью Dependency Walker).
Я сделал простой пример в C#, который перечисляет все сетевые адаптеры, используя класс NetworkInterface. К сожалению, использование этих API имеет два недостатка:
- Эти API даже не перечисляют отключенные сетевые адаптеры для начала. Это означает, что я не могу найти MAC-адрес для отключенного адаптера из моего кэша.
- Я не вижу, как я могу получить PNPDeviceID для фильтрации нефизических адаптеров.
Мой вопрос: какой метод я могу использовать, чтобы получить MAC-адрес физических адаптеров локального компьютера (независимо от того, включен он или нет) всего за несколько десятков миллисекунд?
(У меня есть опыт работы как на C#, так и на C++, и я умею читать на других языках, поэтому мне действительно все равно, какой язык можно использовать в ответах).
РЕДАКТИРОВАТЬ: В ответ на предложение Алекса К использовать только немедленный возврат и пересылку, а также предоставить пример кода 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);
}
Я вызывал эту функцию 3 раза, и каждый раз в последней строке печаталось около 0,36 секунды. Таким образом, предложенные флаги, кажется, не имеют никакого эффекта: положительный или отрицательный. Это не так уж удивительно, как ответ на вопрос Как сделать WMI-запросы только для чтения и только для чтения в C#? похоже, указывает на то, что никаких изменений в производительности не будет наблюдаться, если не будет большого количества записей (например, от сотен до тысяч), что не относится к таблице Win32_NetworkAdapter.
РЕДАКТИРОВАТЬ 2: Несколько ответов было предложено использовать SendARP из API-помощника IP (это тот же API, который имеет функцию GetAdaptersInfo). Какие преимущества это дает по сравнению с GetAdaptersInfo для поиска локального MAC-адреса? Я не могу думать ни о чем - на первый взгляд, GetAdaptersInfo, похоже, возвращает более полный набор информации, чем SendARP для локальных адаптеров. Теперь, когда я думаю об этом, я думаю, что большая часть моего вопроса сосредоточена на концепции перечисления: какие адаптеры существуют на компьютере в первую очередь? SendARP не выполняет перечисление: предполагается, что вы уже знаете IP-адрес адаптера, для которого вы хотите MAC. Мне нужно выяснить, какие адаптеры существуют в системе. Некоторые проблемы, которые это поднимает:
- Что произойдет, если сетевой кабель отключен? Например, это очень часто встречается на ноутбуке (отключенный Ethernet, отключенная карта WiFi). Я попытался использовать NetworkInterface.GetAllNetworkInterfaces() и перечислить все адреса одноадресной рассылки с помощью GetIPProperties().UnicastAddresses, когда носитель отключен. В Windows нет адресов, поэтому я не могу вспомнить ни одного адреса, который можно было бы передать SendARP. Интуитивно понятно, что у отключенного адаптера все равно будет физический адрес, но нет IP-адреса (так как он не находится в сети с сервером DHCP).
- Что подводит меня к следующему: как получить список локальных IP-адресов для тестирования с SendARP?
- Как получить PNPDeviceID (или аналогичный идентификатор, который можно использовать для фильтрации нефизических адаптеров) для каждого адаптера?
- Как мне вывести список отключенных адаптеров, чтобы я мог найти MAC-адрес из своего кэша (то есть MAC-адрес, который я нашел, когда он последний раз был включен)?
Похоже, что эти проблемы не решаются с помощью SendARP, и это основная причина, по которой я задал этот вопрос (в противном случае я бы использовал GetAdaptersInfo и продолжал с вещами...).
2 ответа
Я полностью исключил WMI из уравнения и добился значительного улучшения, все еще получая нужную информацию. Как отмечалось в WMI, получение результатов заняло> 0,30 секунды. С моей версией я могу получить ту же информацию за 0,01 секунды.
Я использовал API настройки, API диспетчера конфигурации, а затем сделал запросы OID непосредственно на сетевой драйвер NDIS, чтобы получить MAC-адрес. API настройки кажется отвратительно медленным, особенно при получении таких вещей, как значения свойств. Обязательно, чтобы настройки API-вызовов были минимальными. (На самом деле вы можете увидеть, насколько это плохо, посмотрев, сколько времени занимает загрузка вкладки "Подробности" устройства в диспетчере устройств).
Предположение о том, почему WMI был таким медленным: я заметил, что Win32_NetworkAdapter WMI всегда занимал одинаковое количество времени, независимо от того, какое подмножество свойств я запрашивал. Похоже, что программисты класса WMI Win32_NetworkAdapter были ленивы и не оптимизировали свой класс для сбора только запрошенной информации, как это делают другие классы WMI. Они, вероятно, собирают всю информацию, запрошенную или нет. Вероятно, они в значительной степени полагаются на API настройки, чтобы сделать это, и чрезмерные вызовы медленного API установки для получения нежелательной информации - вот что делает его таким медленным.
Обзор высокого уровня того, что я сделал:
- Используйте SetupDiGetClassDevs, чтобы получить все сетевые устройства, присутствующие в системе.
- Я отфильтровываю все результаты, у которых нет перечислителя "PCI" (используйте SetupDiGetDeviceRegistryProperty с SPDRP_ENUMERATOR_NAME, чтобы получить перечислитель).
- В остальном я могу использовать CM_Get_DevNode_Status, чтобы получить статус устройства и код ошибки. Все устройства со сменными кодами состояния устройства отфильтровываются.
- Если DN_HAS_PROBLEM установлен так, что существует ненулевой код ошибки, то устройство, вероятно, отключено (или имеет какую-то другую проблему). Драйвер не загружен, поэтому мы не можем сделать запрос к драйверу. Поэтому в этом случае я загружаю MAC-адрес для сетевой карты из кеша, который я поддерживаю.
- Родительское устройство может быть съемным, поэтому я тоже отфильтрую их, рекурсивно исследуя дерево устройств, используя CM_Get_Parent и CM_Get_DevNode_Status для поиска родительских съемных устройств.
- Все остальные устройства являются несъемными сетевыми картами на шине PCI.
- Для каждого сетевого устройства я использую SetupDiGetClassDevs с GUID_NDIS_LAN_CLASS GUID и флагом DIGCF_DEVICEINTERFACE, чтобы получить его интерфейсы (это работает только в том случае, если устройство включено / не имеет проблем).
- Используйте IOCTL_NDIS_QUERY_GLOBAL_STATS с OID_802_3_PERMANENT_ADDRESS на интерфейсе драйвера, чтобы получить постоянный MAC-адрес. Сохраните его в кеше, а затем верните.
Результатом является надежное указание MAC-адресов на ПК, которые должны быть защищены от "поддельных" сетевых карт, изготовленных VMware, VirtualBox, в значительной степени защищены от временных карт, которые временно отключены, и защищены от временных сетевых карт, подключенных через USB, ExpressCard, PC Card или любой будущий съемный интерфейс.
РЕДАКТИРОВАТЬ: IOCTL_NDIS_QUERY_GLOBAL_STATS не поддерживается всеми сетевыми картами. Подавляющее большинство работает, но некоторые карты Intel нет. См. Как надежно и быстро получить MAC-адрес сетевой карты по идентификатору экземпляра устройства.
Вы должны быть в состоянии получить все, что вам нужно от System.Net
Пространство имен. Например, следующий пример взят из MSDN и выполняет то, что вы просили в оригинальной версии вопроса. Он отображает физические адреса всех интерфейсов на локальном компьютере.
public static void ShowNetworkInterfaces()
{
IPGlobalProperties computerProperties = IPGlobalProperties.GetIPGlobalProperties();
NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
Console.WriteLine("Interface information for {0}.{1} ",
computerProperties.HostName, computerProperties.DomainName);
if (nics == null || nics.Length < 1)
{
Console.WriteLine(" No network interfaces found.");
return;
}
Console.WriteLine(" Number of interfaces .................... : {0}", nics.Length);
foreach (NetworkInterface adapter in nics)
{
IPInterfaceProperties properties = adapter.GetIPProperties(); // .GetIPInterfaceProperties();
Console.WriteLine();
Console.WriteLine(adapter.Description);
Console.WriteLine(String.Empty.PadLeft(adapter.Description.Length,'='));
Console.WriteLine(" Interface type .......................... : {0}", adapter.NetworkInterfaceType);
Console.Write(" Physical address ........................ : ");
PhysicalAddress address = adapter.GetPhysicalAddress();
byte[] bytes = address.GetAddressBytes();
for(int i = 0; i< bytes.Length; i++)
{
// Display the physical address in hexadecimal.
Console.Write("{0}", bytes[i].ToString("X2"));
// Insert a hyphen after each byte, unless we are at the end of the
// address.
if (i != bytes.Length -1)
{
Console.Write("-");
}
}
Console.WriteLine();
}
}