APP下载

Java Native Interface技术研究、设计与实践

2024-06-03刘玉龙朱文松

数字通信世界 2024年4期

刘玉龙 朱文松

摘要:文章研究Java程序与本机代码交互的机制,它通过在Java虚拟机(JVM)和本机代码之间提供一组接口来实现。JNI可用于访问本机库、函数和数据结构。通过具体实例的实现,验证了在保持Java平台无关性的同时又充分发挥了本地平台的优势。

关键词:Java;JNI;Win32

doi:10.3969/J.ISSN.1672-7274.2024.04.026

中图分类号:TP 309;TP 311.52          文献标志码:B           文章编码:1672-7274(2024)04-00-03

Research, Design, and Practice of Java Native Interface Technology

LIU Yulong1, ZHU Wensong2

(1. Graduate School of Anhui Jianzhu University, Hefei 230022, China;

2. Xianheng International Technology Co., Ltd., Hangzhou 310000, China)

Abstract: This article investigates the mechanism of interaction between Java programs and native code, which is achieved by providing a set of interfaces between the Java Virtual Machine (JVM) and native code. JNI can be used to access native libraries, functions, and data structures. Through the implementation of specific examples, it has been verified that the advantages of the local platform are fully utilized while maintaining Java platform independence.

Keywords: Java; JNI; Win32

在工程開发过程中,使用“纯Java”代码的解决方案是非常好的,但是有时候某些功能必须引入其他语言的支持。例如,在中小型企业级项目中经常会有一个监测系统性能的模块,这个模块需要使用和操作系统相关度较强的功能,如监控磁盘剩余空间、内存使用、处理器占用等。由于这些功能与平台关联性较强,Java没有对这些功能提供支持。不过好在Java提供了JNI(Java Native Interface),它允许在Java虚拟机中运行的Java代码与用其他编程语言(如C、C++和汇编)编写的应用程序和库进行互操作。

1   JNI的开发步骤与动态链接库

1.1 JNI的开发步骤

在Java项目中合适的位置加入本地方法,本地方法类似于抽象方法,只有方法签名没有方法实现。不同于抽象类和接口,拥有本地方法的类是一个完整的能够实例化的类,只是它的方法由本地代码实现。经过编译器编译,带有本地方法的类会生成一个字节码文件。使用javah可以从这个字节码文件产生一个C语言的头文件,javah可以在jdk/bin目录下找到。创建一个C/C++的工程用于创建动态链接库,将上一步得到的头文件加入到此工程中,根据头文件编写相应方法的实现代码。通常动态链接库只在本地方法执行时才需要加载,因此在本地方法所在的类中加入static代码块去加载动态链接库即可[1]。

1.2 为什么要使用动态链接库

在应用程序开发过程中,一个用户源程序变为一个可在内存中执行的程序的步骤,需要经历编译、链接、装入三个主要过程。编译是将源文件编译为一个或多个目标模块,链接是将标准代码同使用的函数的目标代码以及一些标准的启动代码组合起来生成程序的运行阶段版本,装入是将可执行文件载入内存中。

首先C/C++的产物是源文件编译产生的目标文件,由于这些文件没有经过链接,不能使用任何外部的函数或功能,这类文件对于Java程序也就没有任何作用。这些文件在经过链接之后,会变成可执行文件、静态库或动态库。由于可执行文件并不对外提供函数访问入口,因此它也不是JNI使用的目标。静态库的使用需要将库中的二进制码和客户端代码合并到一起,但是字节码和二进制码有天然的差异性,难以合并到一起。经过一系列排除,JNI只能通过调用动态链接库中的函数完成本地调用[2]。

2   接口设计与JNI对象访问

2.1 接口设计

在运维环境中,磁盘空间、CPU占用和内存占用是系统运行中最重要的三个指标,本文就针对这三项指标进行接口设计。其中磁盘信息主要包括盘符、驱动器类型、总驱动器空间和可用空间。进程信息主要包括进程ID、内存占用和CPU使用率。其中getDiskInfo用于一次性获取所有的驱动器信息,getProcessInfo根据进程ID获取进程的运行信息。因为进程在每次启动之后,通常会产生进程ID,所以通过进程ID获取进程信息对外提供的功能依然有限。虽然进程ID经常变化,但是进程名称通常不会变化,因此设计getProcessId方法用于根据进程名称获取进程ID。

public class SystemInfoUtils {

public static native DriveInfo[] getDiskInfo(); // 获取本地磁盘信息

public static native int getProcessId(String processName); // 根据名称获取进程ID

public static native ProcessInfo getProcessInfo(int processId); // 根据进程ID获取进程信息

}

2.2 JNI对象访问

虽然在C/C++代码中,主要是按照C/C++的语言特性进行代码开发的,因为需要实现Java的方法,那就避免不了C/C++变量和Java类型变量的转换和操作。Java中的类型分为基本类型和对象类型两大类,前者Java提供了对C/C++的直接类型映射,后者则需要通过一系列对象操作完成访问。其中,Java对象访问可以分为对象类型访问、对象域访问、静态域访问、静态方法访问和对象方法访问。在使用javah生成的头文件中,本地方法映射到的C/C++函数声明的第一个参数都是JNIEnv类型的指针变量,这个数据结构的元素指向JVM产生的矩阵的指针,矩阵中的每一个元素指向一个Java预定义的函数。使用这些函数就可以完成对对象的各种访问。

3   DDL开发

3.1 磁盘信息读取

磁盘信息的读取主要就是WindowsAPI和JNI的使用过程,使用GetLogicalDriveStringsW函数可以获取一个NULL结尾的字符串,这个字符串中每一个字符对应系统中的一个有效驱动器。该函数的第二个参数需要先开辟一个字符数组作为缓冲区用于接收“NULL结尾的字符串”,为了防止数组访问越界,还需要使用第一个参数告知该字符数组的长度。如果函数访问成功,返回值就是复制到缓冲区的字符串的长度。这里获取到的有效驱动器是指Windows支持的各种驱动,不仅包括磁盘驱动器,还包括光盘,软盘等驱动器。可以通过GetDriveType函数获取驱动器的类型,其中输入参数是驱动器的根目录,返回值是驱动器的类型。其中最常见的光盘驱动器DRIVE_CDROM和磁盘驱动器类型DRIVE_FIXED,更多类型可以参考Win32API的官方文档[3]。

使用GetDiskFreeSpace函数可以检索有关指定磁盘的信息,包括磁盘上的可用空间量。其中第一个输入参数是驱动器的根目录,其余四个分别用于接收簇上的扇区数、扇区中的字节数、磁盘上空闲的簇数和磁盘上总的簇数。根据这些指标就可以计算出这块磁盘总的空间大小和可用空间大小。其中,块(Block)/簇(Cluster)是磁盘管理中的逻辑概念,扇区是磁盘最小的物理存储单元,但由于操作系统无法对数目众多的扇区进行寻址,所以操作系统就将相邻的扇区组合在一起,形成一个簇,然后再对簇进行管理。

3.2 进程管理

使用OpenProcess函数可以根据进程ID打开进程的句柄。句柄是Windows编程的一个基础,可以用来标志应用程序中的不同对象和同类对象中的不同的实例。该函数的第一个参数是进程访问权限,第二个参数是所得到的进程句柄是否可以被继承,第三个参数是被打开进程的ID。在取得进程的句柄之后,可以通过GetProcessMemoryInfo检索有关指定进程的内存使用情况的信息。参数1是被访问的目标进程的句柄,来自于之前OpenProcess函数的返回值。另外两个参数分别是输出结构和输出结构变量的大小,输出结构接收了进程内存使用情况的信息,其中当前工作集大小WorkingSetSize就是目标进程占用的内存大小,用字节表示。

CPU作为计算机中最重要的计算资源,其资源分配取决于操作系统使用的调度算法。对于不同的系统和系统目标,通常采用不同的调度算法,在Windows中主要采用基于时间片的轮转式进程调度算法。这种调度算法,在早期采用的是简单的时间片轮转法,进入20世纪90年代后,开始采用多级反馈队列调度算法。在Windows系统中,CPU的使用率类似于车辆的速度,是一段时间内进程使用CPU时间在这段时间内的占比。WindowsAPI没有直接提供CPU使用率的获取函数,但是提供了GetProcessTimes函数用于獲取进程,从运行开始后,在内核模式下执行的时间量和在用户模式下执行的时间量[4]。

在Windows操作系统下有用户模式和内核模式两种模式,根据处理器上运行的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。进程在两种模式的执行时间总量就是进程CPU的使用时间。通过在不同的时间点,两次获取进程CPU使用时间的差就是进程在这段时间内使用的CPU时间,这个CPU时间在这段时间内的占比就可以被认为是CPU的使用率。此外,多核处理器在现代计算机中基本上是标配,GetProcessTimes函数获取的结果是进程在所有核心上的使用时间总量,所以最终结果还需要按核心数做平均值[5]。

最后根据名称获取进程的ID,使用EnumProcesses函数可以遍历当前系统中每个进程的进程标识符,然后再通过EnumProcessModules获取进程的名称,最后通过wcscmp比对程序名称就可以获取进程ID[6]。

int getCpuUsageRate(HANDLE hProcess) {

int cpu_num = get_processor_number();

FILETIME now;

GetSystemTimeAsFileTime(&now);

DWORD start = FileTimeToInt64(now);

DWORD used = (FileTimeToInt64(ker) + FileTimeToInt64(user));

Sleep(200);

GetSystemTimeAsFileTime(&now);

DWORD end = FileTimeToInt64(now);

DWORD used1 = (FileTimeToInt64(ker1) + FileTimeToInt64(user1));

return (used1 - used) * 100 / cpu_num / (end - start);

}

4   结束语

本文围绕在Java中使用本地方法这一课题进行研究和思考,结合工程中常见的问题与相关理论进行技术设计和实现。现在社会的发展方向就是信息化和智能化,随着移动互联网的兴起,手机系统在人们生活中扮演着越来越重要的角色。作为现代社会最具时代特点的智能终端,想要充分发挥其硬件性能,不可避免地需要本地代码驱动硬件,因此在安卓系统中JNI同样扮演着重要角色。JAVA的应用范围虽然在不断扩大,但是很多用户仅仅从程序设计的语言方面了解JAVA。所以现在加强用户对JAVA技术的认知是扩大JAVA的適用范围的手段之一,也可为人们提供一个更加安全、更加简便的计算机程序,为我国的计算机信息产业发展做出贡献。■

参考文献

[1] Cay S.Horstmann.Java核心技术[M].北京:机械工业出版社,2017.

[2] 汤小丹.计算机操作系统[M].西安:西安电子科技大学出版社,2007.

[3] Charles Petzold.Windows程序设计[M].北京:清华大学出版社,2010.

[4] 杨文超.Java虚拟机内存管理与优化策略[J].电子测试,2013(10):43-44,62.

[5]李卓恒.JAVA虚拟机相关技术研究与实践[J].科技创新导报,2018(1):156,158.

[6] 许晓宁.Java_Native_Interface应用研究[J].计算机科学,2006(10):291-292.