Java调用Windows API实战:JNA原理与系统级开发指南

📅 2026/8/25 8:38:34
Java调用Windows API实战:JNA原理与系统级开发指南
1. 为什么 Java 程序员需要亲手敲开 Windows 系统的大门Java 的“一次编写到处运行”是教科书里的金句但现实里你总得和 Windows 打交道——不是为了写个跨平台 GUI而是要干点真正“接地气”的事比如让 Java 程序精准获取当前屏幕的 DPI 缩放比例而不是靠GraphicsEnvironment猜比如在用户最小化窗口时真正拦截WM_SYSCOMMAND消息并阻止它执行比如读取 Windows 事件日志里某条特定安全审计记录的原始二进制数据比如调用CryptProtectData对一段密钥做系统级加密再比如把 Java 进程注入到另一个进程的地址空间里做深度调试当然仅限合法授权场景。这些事JDK 自带的 API 不提供入口JNI 写起来又像在刀尖上跳舞——头文件要手写、类型映射要手动校验、内存管理全靠自己扛一个指针越界就是 JVM 崩溃。这时候JNA 就不是“可选工具”而是你手里那把能拧开 Windows 系统保险柜的万能钥匙。我第一次用 JNA 是为了给一个金融交易客户端加“防截屏”功能。客户明确要求当检测到第三方录屏软件如 OBS、Bandicam的窗口句柄活跃时必须立即模糊主交易界面。这事儿用 Swing 或 JavaFX 的Robot类根本做不到——它们只能截图不能监听系统级窗口创建事件。最终方案是用 JNA 调用SetWindowsHookEx(WH_SHELL, ...)注册全局钩子再在回调函数里解析HSHELL_WINDOWCREATED消息的LPARAM提取出窗口类名。整个过程没写一行 C 代码所有 Windows API 的结构体定义、函数声明、回调注册逻辑全在 Java 里完成。后来上线半年没出过一次崩溃而隔壁团队用 JNI 实现同样功能的模块因为JNIEnv*在钩子回调线程里没正确 Attach导致三次 JVM crash dump。核心关键词Java、JNA、Windows API不是三个孤立词而是一条技术链路Java 是你的主战场JNA 是桥梁Windows API 是你要抵达的真实世界。它解决的从来不是“能不能调用”的问题而是“敢不敢在生产环境里稳定调用”的问题。适合谁不是刚学完ArrayList的新手而是已经写过 3 个以上 Spring Boot 项目、遇到过java.awt.Robot抓不到远程桌面画面、被SystemTray在 Win11 上莫名失效折磨过的实战派。你不需要成为 Windows 内核专家但得懂HANDLE和LPVOID的区别得知道stdcall和cdecl调用约定对栈清理的影响得明白为什么WString传参时要加MarshalAs(WSTRING)注解——这些细节才是 JNA 能否从玩具变成生产武器的分水岭。2. JNA 的底层逻辑为什么它比 JNI 更“Java”2.1 JNA 不是 JNI 的简化版而是另一套运行时契约很多人误以为 JNA 是 JNI 的语法糖这是最大的认知陷阱。JNI 的本质是“C 语言主导权”Java 层通过native方法声明接口C 层必须实现对应函数JVM 负责在调用时切换线程上下文、传递参数、处理异常。而 JNA 的契约是“Java 主导权”你完全在 Java 类里用注解定义 Windows DLL 的函数签名JNA 运行时库jna.jar在类加载时动态解析这些注解生成对应的本地调用桩stub并在运行时通过VirtualAlloc分配内存、LoadLibrary加载 DLL、GetProcAddress获取函数地址最后用纯 Java 字节码模拟函数调用过程。整个过程你不用碰 C 编译器不写.h文件甚至不知道javah是什么。举个具体例子调用GetSystemMetrics(SM_CXSCREEN)获取屏幕宽度。用 JNI你需要在 Java 里声明public static native int getScreenWidth();写 C 文件实现JNIEXPORT jint JNICALL Java_MyClass_getScreenWidth(JNIEnv*, jobject)编译成mylib.dll再用System.loadLibrary(mylib)JVM 启动时必须确保 DLL 在PATH或java.library.path中而用 JNA你只需要public interface User32 extends StdCallLibrary { User32 INSTANCE Native.load(user32, User32.class); int GetSystemMetrics(int nIndex); } // 调用int width User32.INSTANCE.GetSystemMetrics(0);JNA 在Native.load()时自动完成 DLL 加载、符号解析、调用桩生成。它甚至帮你处理了stdcall调用约定——Windows API 大部分用stdcall参数从右往左压栈被调用者清理栈而 Java 默认是cdecl调用者清理栈。如果你没指定接口继承StdCallLibraryJNA 会默认用cdecl结果就是栈被错误清理后续函数调用全乱套。这个细节90% 的初学者会在第一次调用MessageBoxA时踩坑。2.2 类型映射Java 和 Windows 的“翻译官”不是免费的JNA 最容易被低估的环节是 Java 基本类型与 Windows 类型的映射规则。这不是简单的int→int32_t而是涉及字节序、内存对齐、指针语义的精密工程。比如HANDLE在 Windows 里是void*但在 JNA 中必须声明为WinDef.HANDLE继承自Pointer否则传参时会被当作普通int处理导致CloseHandle接收一个无效句柄值。再比如LPCWSTR指向宽字符字符串的常量指针在 Java 里必须用WString类型且要加MarshalAs(WSTRING)注解否则 JNA 会按CStringANSI 字符串编码中文全变问号。更隐蔽的是结构体对齐。Windows SDK 的RECT结构体定义是typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT;每个LONG是 4 字节理论上sizeof(RECT) 16。但如果你在 Java 里这样写public static class RECT extends Structure { public int left, top, right, bottom; protected ListString getFieldOrder() { return Arrays.asList(left,top,right,bottom); } }实测size()可能返回 24因为 JVM 默认按 8 字节对齐尤其在 64 位系统int字段间会插入填充字节。解决方案是显式声明ALIGNMENT 4public static class RECT extends Structure { public int left, top, right, bottom; public RECT() { super(Structure.ALIGN_DEFAULT); } protected ListString getFieldOrder() { return Arrays.asList(left,top,right,bottom); } Override protected void setAlignType(int alignType) { super.setAlignType(alignType); } }或者更稳妥地直接继承WinDef.RECTJNA 自带的已验证结构体。这个细节决定了你的GetWindowRect(hwnd, rect)调用能否正确返回坐标——填充值错位right字段可能被写入top的内存位置。2.3 内存模型JNA 的 Pointer 不是 Java 的引用JNA 的Pointer类是理解其内存管理的核心。它本质上是一个内存地址的包装器不持有任何 Java 对象的强引用。当你调用Kernel32.INSTANCE.VirtualAlloc(...)分配一块内存时返回的Pointer指向操作系统分配的物理页但如果你没有在 Java 层保存这个Pointer的引用GC 会认为它“不可达”下次 GC 时就可能回收掉这个Pointer对象——虽然底层内存还在但 Java 里再也找不到它了。更危险的是JNA 提供的Memory类继承自Pointer会自动在finalize()里调用free()但 finalize 时机不可控可能导致内存提前释放。真实案例我曾写过一个模块用CreateFileMapping创建共享内存然后用MapViewOfFile映射到进程地址空间。代码里Memory mem new Memory(size);创建后没把它存在成员变量里而是直接传给MapViewOfFile。结果在高并发下GC 频繁触发mem对象被回收MapViewOfFile返回的指针指向已释放内存后续读写直接引发EXCEPTION_ACCESS_VIOLATION。修复方案很简单把Memory实例作为类的私有字段长期持有并在close()方法里显式调用mem.free()。这提醒我们JNA 的内存必须用 Java 的引用计数逻辑来管理不能依赖“自动释放”。3. 实战拆解用 JNA 实现 Windows 系统级音量控制3.1 需求分析为什么标准 Java Audio API 不够用Java 的javax.sound.sampled包能播放音频、录制麦克风但它无法控制系统的主音量滑块也不能单独调节某个应用程序如 Chrome、微信的音量。这是因为 Windows 的音量控制属于“会话音频策略”Session Audio Policy由 Windows Core Audio APIs 管理这套 API 从 Vista 开始取代了旧的waveOut系列核心是IAudioEndpointVolume和ISimpleAudioVolume接口。它们基于 COMComponent Object Model而 JNA 对 COM 的支持是通过com.sun.jna.platform.win32.COM包实现的本质是用 JNA 封装了CoInitialize、CoCreateInstance等 COM 基础函数。我们的目标写一个 Java 工具能获取当前默认播放设备的总音量0.0~1.0设置总音量为指定值如 0.75获取/设置 Chrome 浏览器进程的独立音量需识别其 Audio Session这个需求直击痛点很多企业内部系统需要根据会议状态自动静音/恢复音量而Runtime.getRuntime().exec(nircmd.exe setsysvolume 32768)这种外部命令调用既不安全需管理员权限又无法精确控制单个应用。3.2 核心接口定义从 Windows SDK 到 Java 的逐行翻译第一步定义 COM 接口。Windows SDK 中IAudioEndpointVolume的 IID 是{1be09788-f645-4fb9-85ea-70a9a8b8d84c}方法列表在IAudioEndpointVolume.h里。JNA 要求我们用 Java 接口继承Com4jObject并用IID注解标注public interface IAudioEndpointVolume extends IUnknown { IID({1be09788-f645-4fb9-85ea-70a9a8b8d84c}) public static final String IID {1be09788-f645-4fb9-85ea-70a9a8b8d84c}; // HRESULT GetMasterVolumeLevelScalar(float *pfLevel); int GetMasterVolumeLevelScalar(FloatByReference pfLevel); // HRESULT SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext); int SetMasterVolumeLevelScalar(float fLevel, GUID pguidEventContext); // HRESULT GetMute(BOOL *pbMute); int GetMute(IntByReference pbMute); // HRESULT SetMute(BOOL bMute, LPCGUID pguidEventContext); int SetMute(int bMute, GUID pguidEventContext); }注意几个关键点FloatByReference是 JNA 提供的包装类对应 C 的float*用于输出参数。IntByReference对应BOOL*Windows 的BOOL是 4 字节整数非 Java 的 boolean。GUID是 JNA 自带的结构体必须用new GUID(...)初始化不能用String。所有方法返回int即 HRESULT 值0 表示成功负数表示错误如0x80004005是 E_FAIL。第二步定义IMMDeviceEnumerator接口用于枚举音频设备public interface IMMDeviceEnumerator extends IUnknown { IID({A95664D2-9614-4F35-A746-DE8DB63108CB}) public static final String IID {A95664D2-9614-4F35-A746-DE8DB63108CB}; int EnumAudioEndpoints(int dataFlow, int dwStateMask, ByReference ppDevices); }这里dataFlow参数是EDataFlow枚举需自己定义public interface EDataFlow { int eRender 0; // playback int eCapture 1; // recording }3.3 完整调用链从初始化 COM 到控制音量完整流程分五步每一步都有陷阱Step 1初始化 COM 库// 必须在主线程调用且每个线程只能调用一次 int hr Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_APARTMENTTHREADED); if (hr ! 0 hr ! S_OK hr ! S_FALSE) { throw new RuntimeException(CoInitializeEx failed: hr); }COINIT_APARTMENTTHREADED是关键Windows Core Audio 要求 STASingle Threaded Apartment线程模型如果用COINIT_MULTITHREADED后续CoCreateInstance会返回CLASS_E_NOAGGREGATION错误。Step 2创建设备枚举器IMMDeviceEnumerator enumerator null; try { enumerator (IMMDeviceEnumerator) Ole32.INSTANCE.CoCreateInstance( new GUID({BCDE0395-E52F-467C-8E3D-C4579291692E}), // __uuidof(MMDeviceEnumerator) null, CLSCTX_INPROC_SERVER, new GUID(IMMDeviceEnumerator.IID), IMMDeviceEnumerator.class ); } catch (Exception e) { throw new RuntimeException(Failed to create IMMDeviceEnumerator, e); }CLSCTX_INPROC_SERVER表示在当前进程内加载 COM 组件这是最常用且最稳定的选项。Step 3获取默认播放设备IMMDevice device null; try { device enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole); } catch (Exception e) { throw new RuntimeException(Failed to get default audio endpoint, e); }ERole.eConsole表示“多媒体”角色对应用户设置的默认播放设备。如果要获取“通信”角色如视频会议专用设备用eCommunications。Step 4激活音量控制接口IAudioEndpointVolume volume null; try { volume (IAudioEndpointVolume) device.Activate( new GUID(IAudioEndpointVolume.IID), CLSCTX_INPROC_SERVER, null ); } catch (Exception e) { throw new RuntimeException(Failed to activate IAudioEndpointVolume, e); }device.Activate()是 COM 的核心方法它根据 IID 创建对应接口实例。这里null表示不传递激活参数。Step 5读写音量值// 获取当前音量 FloatByReference levelRef new FloatByReference(); int hr volume.GetMasterVolumeLevelScalar(levelRef); if (hr ! 0) { throw new RuntimeException(GetMasterVolumeLevelScalar failed: hr); } float currentLevel levelRef.getValue(); // 0.0 ~ 1.0 // 设置新音量 hr volume.SetMasterVolumeLevelScalar(0.75f, new GUID()); // GUID() 生成空 GUID if (hr ! 0) { throw new RuntimeException(SetMasterVolumeLevelScalar failed: hr); }new GUID()是关键pguidEventContext参数用于音量变更事件的上下文标识传null会导致E_POINTER错误必须传一个有效的GUID实例。3.4 进阶控制单个应用程序音量ISimpleAudioVolume要控制 Chrome 的音量需获取其 Audio Session。Windows 用IAudioSessionManager2管理会话流程如下通过IMMDevice获取IAudioSessionManager2实例调用GetSessionEnumerator()得到IAudioSessionEnumerator遍历所有会话用GetSessionControl()获取IAudioSessionControl调用GetDisplayName()或GetIconPath()识别进程名实际中更可靠的是GetProcessId()用QueryInterface()查询ISimpleAudioVolume接口难点在于进程识别GetDisplayName()返回的是会话名称如 “Google Chrome”但可能被用户修改。最稳的方式是int pid sessionControl.GetProcessId(); // 然后用 Kernel32.INSTANCE.OpenProcess(...) 获取进程句柄 // 再用 Psapi.INSTANCE.GetModuleFileNameExA(...) 读取主模块路径 // 最后比对路径是否包含 chrome.exe这段代码需要额外加载Psapi.dll并定义GetModuleFileNameExA函数。这就是 JNA 的威力——你可以在同一个 Java 项目里无缝组合ole32.dll、kernel32.dll、psapi.dll的调用像拼乐高一样构建系统级能力。4. 高频问题排查与避坑指南那些让你加班到凌晨的细节4.1 “No matching function found” 错误签名不匹配的隐形杀手这是 JNA 新手最常遇到的错误表面看是函数找不到根源往往是类型或调用约定不匹配。例如调用FindWindowAUser32.INSTANCE.FindWindowA(null, Notepad);如果报错No matching function found for User32.FindWindowA检查点有三个DLL 名称User32.INSTANCE是Native.load(user32, ...)创建的但FindWindowA在user32.dll中名称没错。参数类型FindWindowA第二个参数是LPCSTRANSI 字符串Java 里必须用StringJNA 默认按CString编码不能用WString那是FindWindowW的参数。调用约定FindWindowA是stdcall所以接口必须继承StdCallLibrary。如果继承Library默认cdecl就会报此错。实操技巧用 Dependency Walkerdepends.exe打开user32.dll查看FindWindowA的导出符号确认它是stdcall符号名以结尾如FindWindowA8而cdecl函数名无修饰如printf。这是快速验证调用约定的土办法。4.2 “Access is denied” 错误UAC 和权限的无声壁垒调用AdjustTokenPrivileges提升进程权限或OpenProcess打开其他进程句柄时常遇到ERROR_ACCESS_DENIED (5)。这不是 JNA 的 bug而是 Windows UACUser Account Control的硬性限制。解决方案分三层基础层确保 Java 进程以管理员身份运行。在 IntelliJ IDEA 里右键菜单选择 “Run as Administrator”在命令行用runas /user:Administrator java -jar myapp.jar。API 层调用OpenProcess时dwDesiredAccess参数不能盲目设PROCESS_ALL_ACCESS0x1FFFFF这需要SeDebugPrivilege权限。应按需申请最小权限如PROCESS_QUERY_INFORMATION | PROCESS_VM_READ0x410。COM 层某些 COM 接口如IAudioSessionManager2在低完整性级别Low IL进程里无法激活。解决方案是调用ShellExecute以中等完整性启动新进程或在 manifest 文件中声明requireAdministrator。经验我在开发一个进程监控工具时发现EnumProcesses总是返回 0 个进程。用 Process Explorer 查看发现 Java 进程的 Integrity Level 是 “Low”而系统进程是 “Medium”。最终在src/main/resources/META-INF/MANIFEST.MF里添加Windows-Application-Model: true Windows-Application-Model-Execution-Level: requireAdministrator并用mt.exe工具嵌入 manifest问题解决。4.3 内存泄漏Pointer 和 Callback 的双重陷阱JNA 的内存泄漏通常有两种模式未释放的 VirtualAlloc 内存调用Kernel32.INSTANCE.VirtualAlloc分配内存后忘记调用VirtualFree。JNA 不会自动回收因为VirtualAlloc分配的是操作系统页不是 JVM 堆内存。未注销的 Windows Hook用SetWindowsHookEx注册WH_KEYBOARD_LL钩子后程序退出时没调用UnhookWindowsHookEx。这会导致钩子句柄泄露系统资源耗尽后新钩子无法注册。避坑技巧用try-with-resources模式封装资源。例如public class AutoCloseablePointer implements AutoCloseable { private final Pointer pointer; private final Runnable freeAction; public AutoCloseablePointer(Pointer p, Runnable freeAction) { this.pointer p; this.freeAction freeAction; } Override public void close() { if (pointer ! null freeAction ! null) { freeAction.run(); } } } // 使用 try (AutoCloseablePointer mem new AutoCloseablePointer( Kernel32.INSTANCE.VirtualAlloc(null, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE), () - Kernel32.INSTANCE.VirtualFree(mem.pointer, 0, MEM_RELEASE) )) { // use mem.pointer }对于 Hook注册后保存HHOOK句柄在shutdownHook里统一注销HHOOK hook User32.INSTANCE.SetWindowsHookEx(WH_KEYBOARD_LL, keyboardProc, hInstance, 0); Runtime.getRuntime().addShutdownHook(new Thread(() - { if (hook ! null) { User32.INSTANCE.UnhookWindowsHookEx(hook); } }));4.4 字符编码乱码ANSI vs Unicode 的千年战争Windows API 有AANSI和WUnicode两个版本如MessageBoxA和MessageBoxW。JNA 默认优先调用W版本但如果 DLL 没导出W版本某些老旧 DLL就会失败。解决方案显式指定函数名User32.INSTANCE.MessageBoxA(hwnd, Hello, Title, MB_OK);强制使用 ANSI在Native.load()时传Collections.singletonMap(Library.OPTION_STRING_ENCODING, GBK)统一用 Unicode所有字符串用WString并确保 DLL 支持W版本现代 Windows 系统都支持真实案例调用ShellExecuteA打开含中文路径的 PDF 文件路径显示为乱码。原因是ShellExecuteA用 ANSI 编码而 Java 字符串是 UTF-16。修复改用ShellExecuteW参数全用WStringShell32.INSTANCE.ShellExecuteW( null, new WString(open), new WString(C:\\文档\\报告.pdf), null, null, SW_SHOW );4.5 JVM CrashJNI 与 JNA 的共存雷区当项目里同时存在 JNI 和 JNA 代码时最容易引发 JVM 崩溃。根本原因是 JNI 的JNIEnv*指针在线程间不通用而 JNA 的回调函数如 Windows Hook 的LowLevelKeyboardProc可能在非 JVM 线程里执行。如果回调里调用了 JNI 函数如env-FindClass就会因JNIEnv*无效而 crash。解决方案只有两个绝对禁止在 JNA 回调里调用任何 JNI 函数。所有 JNI 逻辑必须在 JVM 线程里执行。用 JNA 的Callback机制将任务转回 Java 主线程public interface KeyboardHookCallback extends StdCallCallback { int callback(int nCode, WPARAM wParam, LPARAM lParam); } KeyboardHookCallback callback (nCode, wParam, lParam) - { // 这里只做轻量工作如记录日志 System.out.println(Key pressed); // 重任务提交到 SwingUtilities.invokeLater 或 ExecutorService executor.submit(() - heavyWork()); return User32.INSTANCE.CallNextHookEx(hHook, nCode, wParam, lParam); };5. 工具链与工程化实践让 JNA 项目走出玩具阶段5.1 Maven 依赖与版本锁定别让 JNA 成为版本炸弹JNA 的版本兼容性极差。jna-5.12.1能完美调用user32.dll但升级到jna-5.13.0后SetWindowsHookEx可能返回NULL。原因在于 JNA 内部对Callback的线程模型做了重构。因此工程化第一原则固定 JNA 版本禁用版本范围。正确配置dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.12.1/version !-- 严格锁定 -- /dependency dependency groupIdnet.java.dev.jna/groupId artifactIdjna-platform/artifactId version5.12.1/version !-- 必须与 jna 版本一致 -- /dependencyjna-platform提供了WinDef、WinUser、Ole32等预定义接口省去大量重复劳动。但要注意它的更新滞后于 Windows SDK某些新 API如 Win11 的IAppActivationManager需自行定义。5.2 接口定义工程化用模板生成代替手写手写IAudioEndpointVolume这样的接口效率低下且易错。推荐用 JNAerator 工具它能解析 Windows SDK 的.h文件自动生成 Java 接口。例如下载 Windows SDK 的audioclient.h运行java -jar jnaerator.jar -libraryName CoreAudio -o src/main/java com.microsoft.windows.coreaudio.audioclient.h生成的代码需人工审核重点检查#define常量是否转为public static final intstruct是否正确映射为Structure子类HRESULT返回值是否统一为int我维护的 JNA 接口库已积累 200 个 Windows API 接口定义全部按模块组织win32/,com/,coreaudio/并通过单元测试验证基本调用。这种沉淀让新项目接入 Windows 功能的时间从 2 天缩短到 2 小时。5.3 单元测试用 TestContainers 模拟 Windows 环境JNA 代码无法用纯 Java 单元测试覆盖因为依赖真实 DLL。解决方案是用 TestContainers 启动 Windows Docker 容器Test public void testGetSystemMetrics() { try (GenericContainer? windows new GenericContainer(mcr.microsoft.com/windows/servercore:ltsc2022) .withExposedPorts(22) .withClasspathResourceMapping(test-script.ps1, /test.ps1, BindMode.READ_ONLY) .withCommand(powershell -ExecutionPolicy Bypass -File /test.ps1)) { windows.start(); // 通过 SSH 或 WinRM 执行 PowerShell 脚本验证 JNA 调用结果 } }更轻量的方案是用junit-platform-launcher的EnabledOnOs(OS.WINDOWS)注解只在 Windows CI 环境运行 JNA 测试避免 Linux/macOS 上跳过测试的尴尬。5.4 生产环境加固异常处理与降级策略JNA 调用失败是常态必须设计降级。例如调用GetDpiForWindow获取 DPI 时若 Windows 版本低于 10.0.14393该函数不存在应降级为GetDeviceCaps(HORZRES)public static int getDpiForWindow(HWND hwnd) { try { // 尝试新 API return User32.INSTANCE.GetDpiForWindow(hwnd); } catch (UnsatisfiedLinkError e) { // 降级到旧 API HDC hdc User32.INSTANCE.GetDC(hwnd); int dpi Gdi32.INSTANCE.GetDeviceCaps(hdc, LOGPIXELSX); User32.INSTANCE.ReleaseDC(hwnd, hdc); return dpi; } }所有 JNA 调用必须包裹try-catch捕获UnsatisfiedLinkErrorDLL 未找到、LastErrorExceptionWindows 错误码、RuntimeExceptionJNA 内部错误。日志里记录Native.getLastError()的值方便定位问题。最后分享一个血泪教训某次上线后用户反馈音量控制失效。日志显示CoCreateInstance返回REGDB_E_CLASSNOTREG0x80040154。排查发现目标机器是 Windows Server 2012 R2而IAudioEndpointVolume在 Server 版本默认禁用音频服务。解决方案不是改代码而是写部署文档“请确保 Windows Audio 服务已启动”。技术再牛也得尊重操作系统的基本约束。