macOS 內核之從 I/O Kit 電量管理開始
來源 http://www.cocoachina.com/articles/87071
來源 http://justinyan.me/post/3961
在上一篇macOS 內核之 hw.epoch 是個什么東西?我們提到 XNU 內核包含了 BSD 和 Mach,其中 Mach Kernel 提供了 I/O Kit 給硬件廠商寫驅動用的。這個部分在 NeXT 時期是用 Objective-C 提供的 API,叫做 Driver Kit,后來喬布斯回到蘋果之后,升級了 BSD 和 Mach 的代碼,於是在 OS X 中提供了 C++ 接口的 I/O Kit。
根據官方的這份文檔,以下系統支持 I/O Kit:
- iOS 2.0+
- macOS 10.0+
- Mac Catalyst 13.0+
I/O Kit 里我們可以通過三種不同的方式獲取電池信息,位於 IOKit/pwr_mgt
的 Power Mangement 接口,位於 IOKit/ps
的 Power Sources 接口,以及通過 IOServiceGetMatchingService
獲取 AppleSmartBattery
Service 接口。
1. IOPM (Power Management) API
IOPM 接口需要使用 Mach Port 跟 IOKit 進行 IPC 通信,所以我們先來了解一點 Mach Port 的背景。
1.1 Mach Port
XNU 是一個混合內核,既有 BSD 又有 Mach Kernel,上層還有各種各樣的技術,所以在 macOS 系統中,IPC (跨進程通信)的技術也多種多樣。Mattt 在 NSHipster 上寫過一篇 IPC 的文章: Inter-Process Communication - NSHipster 對此有過詳解。
Mach Port 是在系統內核實現和維護的一種 IPC 消息隊列,持有用於 IPC 通信的 mach messages。只有一個進程可以從對應的 port 里 dequeue 一條消息,這個進程被持有接收權利(receive-right)。可以有多個進程往某個 port 里 enqueue 消息,這些進程持有該 port 的發送權利(send-rights)。
如上圖,PID 123 的進程往一個 port 里發送了一條消息,只有對應的接收端 PID 456 才能從 port 里取出這條消息。
我們可以簡單把 mach port 看做是一個單向的數據發送渠道,構建一個消息結構體后通過mach_msg()
方法發出去。因為只能單向發送,所以當 B 進程收到了 A 進程發來的消息之后要自己創建一個新的 Port 然后又發回去 A 進程。
手動構建 mach message 發送是比較復雜的,大概長這個樣子(代碼來自 Mattt 的那篇文章):
natural_t data;
mach_port_t port;
struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_type_descriptor_t type;
} message;
message.header = (mach_msg_header_t) {
.msgh_remote_port = port,
.msgh_local_port = MACH_PORT_NULL,
.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
.msgh_size = sizeof(message)
};
message.body = (mach_msg_body_t) {
.msgh_descriptor_count = 1
};
message.type = (mach_msg_type_descriptor_t) {
.pad1 = data,
.pad2 = sizeof(data)
};
mach_msg_return_t error = mach_msg_send(&message.header);
if (error == MACH_MSG_SUCCESS) {
// ...
}
其中最關鍵的是 msgh_remote_port
和 msgh_local_port
。上述代碼是發送消息,所以 msgh_remote_port
就是要接收這條消息的那個進程的 port
。我們得先知道這個 port
信息我們才能往里面發消息。另外例子中使用的是 mach_msg_send()
函數。
port name
留意到在上圖中,PID 123 往一個名為 0xabc
的 port
發消息,PID 456 則從名為 0xdef
的 port
里取消息。這里 port name 只對當前進程有意義,並不需要全局一致,內核會自動根據進程 ID 和名字信息找到對應的進程。
Out-of-line memory
我們的代碼在用戶層調用,需要進出內核層,這是一進一出如果消息體里帶上大量的信息就會非常慢。所以如果需要使用 mach message 來發送體積較大的信息,可以使用 “out-of-line memory” descriptor。
我們看到上面 Mattt 的代碼使用 mach_msg_send()
函數來發送消息,message.body
帶了一個 msgh_descriptor_count
為 1。這個 descriptor
是一個 natural_t
。我看到這里的時候並沒有搞懂系統是怎么做 OOL 的 copy-on-write 的。於是照例翻一下 XNU 的源碼,我發現 Mattt 的例子並沒有使用 OOL descriptor,而是使用了 type descriptor。
typedef struct
{
natural_t pad1;
mach_msg_size_t pad2;
unsigned int pad3 : 24;
mach_msg_descriptor_type_t type : 8;
} mach_msg_type_descriptor_t;
ool descriptor 的結構如下:
typedef struct
{
uint64_t address;
boolean_t deallocate: 8;
mach_msg_copy_options_t copy: 8;
unsigned int pad1: 8;
mach_msg_descriptor_type_t type: 8;
mach_msg_size_t size;
} mach_msg_ool_descriptor64_t;
使用時我們需要把內存地址發過去,內核只負責傳遞地址指針,等到進程接受到了這條消息之后才會從內存里 copy buffer。
1.2 使用 Master Port 和 IOKit 通信
在 IOKit 里面,所有的通信都通過 IOKit Master Port
來進行,使用以下函數可以獲取 master port。
kern_return_t
IOMasterPort( mach_port_t bootstrapPort,
mach_port_t * masterPort );
實際使用時如下:
mach_port_t masterPort;
IOMasterPort(MACH_PORT_NULL, &masterPort)
默認把 bootstrapPort
置空。如果返回值是 kIOReturnSuccess
就成功構建了一個 mach_port_t
用於跟 IOKit 通信。
bootstrapPort
不過在這個 API 里面,獲取單一 master port 好理解,那 bootstrapPort 這個參數又是用來干啥的呢?
在上面的例子中 PID 123 和 PID 456 是在已經獲知對方的 port name 的前提下才有辦法互相通信的。但是如果你不知道對方的 port name 呢?於是 XNU 系統提供了 bootstrap port 這個東西,由系統提供查詢服務,這樣所有的進程都可以去廣播自己的 mach port 接收端的名字,也可以查詢其他人的名字。
查詢接口大概是這樣:
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "me.justinyan.example", &port);
注冊接口大概是這樣:
bootstrap_register(bootstrap_port, "me.justinyan.example", port);
同時 bootstrap port 是一個特殊的 port。其他的 mach port 在父進程被 fork()
的時候,子進程是不會繼承 port 的,只有 bootstrap port 可以被繼承。
但是,自從 OS X 10.5 開始,蘋果引入了 Launchd
這么一個服務,同時棄用了 bootstrap_register()
接口。關於這件事情當時 darwin 開發團隊有個長長的郵件列表做了激烈的討論: Apple - Lists.apple.com
新的接口可以參考 CFMessagePortCreateLocal()
和這篇文章: Damien DeVille | Interprocess communication on iOS with Mach messages
IOPM 獲取電池信息接口
上面羅里吧嗦一大堆全是 mach port 的事情,現在終於到正題了。代碼非常簡單:
NSDictionary* get_iopm_battery_info() {
mach_port_t masterPort;
CFArrayRef batteryInfo;
if (kIOReturnSuccess == IOMasterPort(MACH_PORT_NULL, &masterPort) &&
kIOReturnSuccess == IOPMCopyBatteryInfo(masterPort, &batteryInfo) &&
CFArrayGetCount(batteryInfo))
{
CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, CFArrayGetValueAtIndex(batteryInfo, 0));
CFRelease(batteryInfo);
return (__bridge_transfer NSDictionary*) battery;
}
return NULL;
}
NSDictionary *dict = get_iopm_battery_info();
NSLog(@"iopm dict: %@", dict);
輸出:
iopm dict: {
Amperage = 0;
Capacity = 6360;
Current = 6360;
"Cycle Count" = 113;
Flags = 5;
Voltage = 12968;
}
可以看到電池循環次數、容量之類的信息,但是不多。IOPMLib.h
的注釋說 不建議大家使用這個接口,可以考慮用 IOPowerSources API 代替。
2. IOPowerSources API
IOPowerSources 的接口比較簡單,先用 IOPSCopyPowerSourcesInfo()
取到 info, 然后取 IOPSCopyPowerSourcesList()
,最后再 copy 一下就完事了。
NSDictionary* get_iops_battery_info() {
CFTypeRef info = IOPSCopyPowerSourcesInfo();
if (info == NULL)
return NULL;
CFArrayRef list = IOPSCopyPowerSourcesList(info);
// Nothing we care about here...
if (list == NULL || !CFArrayGetCount(list)) {
if (list)
CFRelease(list);
CFRelease(info);
return NULL;
}
CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(list, 0)));
// Battery is released by ARC transfer.
CFRelease(list);
CFRelease(info);
return (__bridge_transfer NSDictionary* ) battery;
}
NSDictionary *iopsDict = get_iops_battery_info();
NSLog(@"iops dict: %@", iopsDict);
輸出:
iops dict: {
"Battery Provides Time Remaining" = 1;
BatteryHealth = Good;
Current = 0;
"Current Capacity" = 100;
DesignCycleCount = 1000;
"Hardware Serial Number" = D**********;
"Is Charged" = 1;
"Is Charging" = 0;
"Is Present" = 1;
"Max Capacity" = 100;
Name = "InternalBattery-0";
"Power Source ID" = 9764963;
"Power Source State" = "AC Power";
"Time to Empty" = 0;
"Time to Full Charge" = 0;
"Transport Type" = Internal;
Type = InternalBattery;
}
可以看到信息多了很多,還有 BatteryHealth
等信息,我們看到我的 MacBook 的電池設計循環次數是 DesignCycleCount = 1000
,然后我已經循環 113 次了。
但是,這批信息里面沒有帶電池的設計容量。
3. IOPMPS Apple Smart Battery API
IOKit 里提供了一套 IOService 相關的接口,你可以往里面注冊 IOService 服務,帶個名字,一樣是通過 IOMasterPort()
來通信。IOKit 主要是面向硬件驅動開發者的,所以如果你的硬件依賴另外一個硬件,但是另外一個硬件還沒有接入,這時候你可以往 IOService 注冊一個通知。使用 IOServiceAddMatchingNotification
,等到你觀察的硬件接入后調用了 registerService()
你就會收到對應的通知了。
這里我們直接用 IOServiceGetMatchingService()
來獲取系統提供的 AppleSmartBattery
service。
NSDictionary* get_iopmps_battery_info() {
io_registry_entry_t entry = 0;
entry = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceNameMatching("AppleSmartBattery"));
if (entry == IO_OBJECT_NULL)
return nil;
CFMutableDictionaryRef battery;
IORegistryEntryCreateCFProperties(entry, &battery, NULL, 0);
return (__bridge_transfer NSDictionary *) battery;
}
NSDictionary *iopmsDict = get_iopmps_battery_info();
NSLog(@"iopmsDict: %@", iopmsDict);
輸出:
iopmsDict: {
AdapterDetails = {
Current = 4300;
PMUConfiguration = 2092;
Voltage = 20000;
Watts = 86;
};
AdapterInfo = 0;
Amperage = 0;
AppleRawAdapterDetails = (
{
Current = 4300;
PMUConfiguration = 2092;
Voltage = 20000;
Watts = 86;
}
);
AppleRawCurrentCapacity = 6360;
AppleRawMaxCapacity = 6360;
AvgTimeToEmpty = 65535;
AvgTimeToFull = 65535;
BatteryData = {
AdapterPower = 1106486026;
CycleCount = 113;
DesignCapacity = 6669;
PMUConfigured = 0;
QmaxCell0 = 6812;
QmaxCell1 = 6859;
QmaxCell2 = 6784;
ResScale = 200;
StateOfCharge = 100;
SystemPower = 4625;
Voltage = 12968;
};
BatteryFCCData = {
DOD0 = 128;
DOD1 = 144;
DOD2 = 128;
PassedCharge = 0;
ResScale = 200;
};
BatteryInstalled = 1;
BatteryInvalidWakeSeconds = 30;
BatterySerialNumber = D**********;
BestAdapterIndex = 3;
BootPathUpdated = 1571194014;
CellVoltage = (
4323,
4322,
4323,
0
);
ChargerData = {
ChargingCurrent = 0;
ChargingVoltage = 13020;
NotChargingReason = 4;
};
CurrentCapacity = 6360;
CycleCount = 113;
DesignCapacity = 6669;
DesignCycleCount70 = 0;
DesignCycleCount9C = 1000;
DeviceName = bq20z451;
ExternalChargeCapable = 1;
ExternalConnected = 1;
FirmwareSerialNumber = 1;
FullPathUpdated = 1571290629;
FullyCharged = 1;
IOGeneralInterest = "IOCommand is not serializable";
IOReportLegend = (
{
IOReportChannelInfo = {
IOReportChannelUnit = 0;
};
IOReportChannels = (
(
7167869599145487988,
6460407809,
BatteryCycleCount
)
);
IOReportGroupName = Battery;
}
);
IOReportLegendPublic = 1;
InstantAmperage = 0;
InstantTimeToEmpty = 65535;
IsCharging = 0;
LegacyBatteryInfo = {
Amperage = 0;
Capacity = 6360;
Current = 6360;
"Cycle Count" = 113;
Flags = 5;
Voltage = 12968;
};
Location = 0;
ManufactureDate = 19722;
Manufacturer = SMP;
ManufacturerData = {length = 27, bytes = 0x00000000 *** };
MaxCapacity = 6360;
MaxErr = 1;
OperationStatus = 58433;
PackReserve = 200;
PermanentFailureStatus = 0;
PostChargeWaitSeconds = 120;
PostDischargeWaitSeconds = 120;
Temperature = 3067;
TimeRemaining = 0;
UserVisiblePathUpdated = 1571291169;
Voltage = 12968;
}
可以看到比前面的兩次輸出多了很多。
CurrentCapacity = 6360;
DesignCapacity = 6669;
有了當前電池容量和設計容量,就可以得到我的電池還剩 95%
的容量。
4. 列出所有 IOService
以上三種方法我都是從 Hammerspoon 的源碼中習得。通過閱讀這部分接口學習了相關的一些內核層 API 的概念,很有意思。那么在 #3 中 Hammerspoon 的作者是怎么知道系統有一個 IOService 叫做 "AppleSmartBattery" 的呢?我們不妨把系統所有的 IOService 打印出來,然后 grep 看看里面有沒有帶 battery
或者 energy
關鍵字的。
IOKitLib.h
里有一個接口 IORegistryCreateIterator()
可以創建一個迭代器,把所有已注冊的 IOService 取出來。
核心代碼如下:
const char *plane = "IOService";
io_iterator_t it = MACH_PORT_NULL;
IORegistryCreateIterator(kIOMasterPortDefault, plane, kIORegistryIterateRecursively, &it)
有一個開源庫實現了這個功能,有興趣的讀者朋友可以看看這里: Siguza/iokit-utils: Dev tools for probing IOKit
➜ iokit-utils ./ioprint| grep -i battery
AppleSmartBatteryManager(AppleSmartBatteryManager)
AppleSmartBattery(AppleSmartBattery)
結果出來兩個 battery
相關的,AppleSmartBattery
就是上述例子用到的,AppleSmartBatteryManager
則打印出如下結果:
iopmsDict: {
CFBundleIdentifier = "com.apple.driver.AppleSmartBatteryManager";
CFBundleIdentifierKernel = "com.apple.driver.AppleSmartBatteryManager";
IOClass = AppleSmartBatteryManager;
IOMatchCategory = IODefaultMatchCategory;
IOPowerManagement = {
CapabilityFlags = 2;
CurrentPowerState = 1;
MaxPowerState = 1;
};
IOProbeScore = 0;
IOPropertyMatch = {
IOSMBusSmartBatteryManager = 1;
};
IOProviderClass = IOSMBusController;
IOUserClientClass = AppleSmartBatteryManagerUserClient;
}
只是一堆蘋果自家驅動的信息而已。
5. 用於 iOS 系統
我在運行了 iOS 13.1.2 的 iPhone Xs Max 機器上進行了測試。iOS 工程引入 IOKit 會比較麻煩,因為這個 Framework 是不公開的,所以你得把所有的頭文件導出來,並且把 #import <IOKit/xxx.h>
的地方都改掉。可以參考此文: [Tutorial] Import IOKit framework into Xcode project | Gary's ...Lasamia
實測 IOPMCopyBatteryInfo
在 iOS 上無效,估計是 iOS 直接不給 mach port 權限到上層。 IOPSCopyPowerSourcesList
和 IOServiceNameMatching
能用。
iops dict: {
"Battery Provides Time Remaining" = 1;
"Current Capacity" = 100;
"Is Charged" = 1;
"Is Charging" = 0;
"Is Present" = 1;
"Max Capacity" = 100;
Name = "InternalBattery-0";
"Play Charging Chime" = 1;
"Power Source ID" = 2490467;
"Power Source State" = "AC Power";
"Raw External Connected" = 1;
"Show Charging UI" = 1;
"Time to Empty" = 0;
"Time to Full Charge" = 0;
"Transport Type" = Internal;
Type = InternalBattery;
}
iopmsDict: {
BatteryInstalled = 1;
ExternalConnected = 1;
}
可以看到信息比 macOS 的少了很多,並且沒有包含 cycleCount
這個信息。
5.1 奇技淫巧 hack 之
但是畢竟 iOS 是有 IOKit 框架的,那么有沒有什么奇技淫巧可以拿到 IOKit 的信息呢?eldade/UIDeviceListener: Obtain power information (battery health, charger details) for iOS without any private APIs.這個庫可以在 iOS 7 - iOS 9.3 上捕獲這部分信息。
所使用之操作也是非常有趣。從 iOS 3.0 開始,UIDevice 增加了 batteryState
和 batteryLevel
這兩個參數,並且允許開啟電池監控 batteryMonitoringEnabled
。通過上文我們已經知道,這些操作最終都是通過 IOKit 來進行的。
IOKit 會從 IORegistry
獲取一份最新的電池信息,就像我們的 get_iopmps_battery_info()
方法一樣。留意到從 IORegistry
取數據的接口長這樣:
IORegistryEntryCreateCFProperties(
io_registry_entry_t entry,
CFMutableDictionaryRef * properties,
CFAllocatorRef allocator,
IOOptionBits options );
重點在第三個參數 CFAllocatorRef
,通常情況下系統會用默認的 CFAllocatorGetDefault()
。我們看看這個 allocator 長啥樣CoreFoundation/CFBase.c:
typedef const struct CF_BRIDGED_TYPE(id) __CFAllocator * CFAllocatorRef;
// CFAllocator structure must match struct _malloc_zone_t!
// The first two reserved fields in struct _malloc_zone_t are for us with CFRuntimeBase
struct __CFAllocator {
CFRuntimeBase _base;
CFAllocatorRef _allocator;
CFAllocatorContext _context;
};
以及 CoreFoundation 提供了不少操作:
CFAllocatorGetDefault();
CFAllocatorGetContext();
CFAllocatorCreate();
CFAllocatorSetDefault();
如果能把系統的默認 allocator 替換成自己的實現,那么當我們打開 batteryMonitoringEnabled
然后電池發生變更的時候,系統就回去用 IORegistry
取一份電池信息,就會掉進我們替換掉的 allocator。這時候就能截取 allocator 剛剛 allocate 的內存信息了。真的佩服作者的腦洞。詳細的實現大家可以看原來的庫: eldade/UIDeviceListener,我們只看關鍵代碼:
// 獲取默認 allocator
_defaultAllocator = CFAllocatorGetDefault();
CFAllocatorContext context;
// 獲取默認 allocator 的 context
CFAllocatorGetContext(_defaultAllocator, &context);
// 全部改成自己的實現, myAlloc/myRealloc/myFree 都是 C 函數
context.allocate = myAlloc;
context.reallocate = myRealloc;
context.deallocate = myFree;
// 用修改后的 context 創建新的 allocator
_myAllocator = CFAllocatorCreate(NULL, &context);
// 把自己創建的 allocator 替換掉系統的默認 allocator
CFAllocatorSetDefault(_myAllocator);
接下來看看 myAlloc
的實現:
void * myAlloc (CFIndex allocSize, CFOptionFlags hint, void *info)
{
// 做一下線程檢查
VERIFY_LISTENER_THREAD();
// 實現一個新的 allocation
void *newAllocation = CFAllocatorAllocate([UIDeviceListener sharedUIDeviceListener].defaultAllocator, allocSize, hint);
// 失敗就放過
if (newAllocation == NULL)
return newAllocation;
// 有東西了,趕緊把新的內容塞進准備好的 allocations 變量里,這是個 C++ 的 std::set<void *>
if (hint & __kCFAllocatorGCObjectMemory)
{
[UIDeviceListener sharedUIDeviceListener].allocations->insert(newAllocation);
}
return newAllocation;
}
與此同時,通過 KVO 觀察 UIDevice
公開的 batteryLevel
屬性,接收 KVO 回調:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([change objectForKey: NSKeyValueChangeNewKey] != nil)
{
std::set<void *>::iterator it;
for (it=_allocations->begin(); it!=_allocations->end(); ++it)
{
CFAllocatorRef *ptr = (CFAllocatorRef *) (NSUInteger)*it;
void * ptrToObject = (void *) ((NSUInteger)*it + sizeof(CFAllocatorRef));
if (*ptr == _myAllocator && // Just a sanity check to make sure the first field is a pointer to our allocator
[self isValidCFDictionary: ptrToObject]) // Check for valid CFDictionary
{
CFDictionaryRef dict = (CFDictionaryRef) ptrToObject;
if ([self isChargerDictionary: dict]) // Check if this is the charger dictionary
{
// Found our dictionary. Let's clear the allocations array:
_allocations->clear();
// We make a deep copy of the dictionary using the default allocator so we don't
// get callbacks when this object and any of its descendents get freed from the
// wrong thread:
CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);
if (latestDictionary != nil)
{
// Notify that new data is available, but that has to happen on the main thread.
// Because of the CFAllocator replacement, we generally shouldn't
// do ANYTHING on this thread other than stealing this dictionary from UIDevice...
dispatch_sync(dispatch_get_main_queue(), ^{
// Pass ownership of the CFDictionary to the main thread (using ARC):
NSDictionary *newPowerDataDictionary = CFBridgingRelease(latestDictionary);
[[NSNotificationCenter defaultCenter] postNotificationName:kUIDeviceListenerNewDataNotification object:self userInfo:newPowerDataDictionary];
});
}
return;
}
}
}
}
}
上面一堆嵌套代碼判斷了一層又一層,最后做了一個 CFPropertyListCreateDeepCopy
然后通過通知轉發出去。
CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);
嚴格來說這種寫法並沒有用到私有 API,但是非常取巧。如果內核實現代碼不用 default allocator 來取 IORegistry 的信息這里就失效了。事實上從 iOS 10 開始這個做法確實也失效了。但是整個思路非常有趣,值得觀摩。
5.2 遍歷所有的 IOService
上面我們在 macOS 上通過取 AppleSmartBattery
這個 IOService 可以獲得更多電池信息,但是在 iOS 上沒有。那么我們還能不能尋找其他的 IOService 看看是否有攜帶了電池信息的呢?
此文iOS IOKit Browser - Christopher Lyon Anderson 使用私有 API 遍歷了 iOS 上所有的 IOService,並且在他的截屏中是包含了電池信息的。我 clone 下來發現已經沒有 cycleCount
信息了,但是這個項目有個地方挺有意思:
NSString *bundlePath = [[NSBundle bundleWithPath:@"/System/Library/Frameworks/IOKit.framework"] bundlePath];
NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
CFBundleRef cfBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)bundleURL);
self.IORegistryGetRootEntryShim = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("IORegistryGetRootEntry"));
先取系統的 IOKit.framework
,然后用 CoreFoundation 的接口來取函數指針,然后就可以使用這批 IOKit 的私有函數了。可惜此方法亦已無效。
6. 小結
iOS 方面暫時還未找到能展示 cycleCount
信息的方法,想必 Battery Health App 應該用了更加厲害的黑科技。可能只有越獄逆向一下才知道它是怎么做到的了。
之前因為 sysctl()
的緣故看了一下 XNU 的源碼,結果發現內核層還是有不少有意思的東西。IOKit 作為驅動層的 API,除了獲取電池信息之外還能干很多事情。
本文通過 IOKit 的簡單接口,擴展學習了 XNU 的 IPC 通信機制 mach port。希望后續能通過這些工具做出點有意思的東西來。
內核系列文章
- macOS 內核之一個 App 如何運行起來
- macOS 內核之網絡信息抓包(三)
- macOS 內核之網絡信息抓包(二)
- macOS 內核之網絡信息抓包(一)
- macOS 內核之系統如何啟動?
- macOS 內核之內存占用信息
- macOS 內核之 CPU 占用率信息
- macOS 內核之 hw.epoch 是個什么東西?
- macOS 內核之從 I/O Kit 電量管理開始
參考資料
============= End