继《Java文件的简单读写、随机读写、NIO读写与使用MappedByteBuffer读写》,本篇通过调用LinuxOS文件操作系统函数实现copy命令以加深我们对Java文件读写底层实现的理解。
本篇内容包括:
文件操作系统函数
本篇将介绍的函数有:open、close、write、lseek、read、mmap、msync。使用Linux下系统调用函数来进行对文件的操作须要导出头文件、,安装gcc后这两个头文件位置在/usr/include/目录下。mmap例外,使用mmap须要导出头文件,该文件位置在/usr/include/sys目录下。
open函数
open函数用于打开一个文件获取文件句柄,当文件不存在时支持创建文件再打开文件。
int open(const char* filename,int flag,int mode);
该方式执行成功会返回文件描述符(文件句柄),失败则返回-1,参数说明如下:
flag参数的可取值如下:
(以上这三个参数只能选择其中一个arch linux,但可以与下述多个参数通过or运算组合使用)
上一篇文章笔者有提及,就是我们创建FileOutputStream时,假若不指定append为true,这么会造成文件内容被清空,缘由是FileOutputStream构造函数调用了open函数,open函数判定append函数倘若为false,则flags会组合O_TRUNC(假如文件存在,则文件将被截断,即厚度为0),因而会造成文件被清空linux格式化硬盘,源码如下。
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
jstring path, jboolean append) {
fileOpen(env, this, path, fos_fd,
O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}
model可取的值如下,支持或(or)运算:
close函数
close函数用于关掉已打开的文件。
int close(int fd);
方式执行成功返回0,失败返回-1,参数说明如下:
read函数
read函数用于读取文件数据到显存中。
int read(int fd,void * buff,int count);
方式执行成功返回本次真正读取到的字节数,读取到文件末尾返回0,出现异常返回-1,参数说明如下:
write函数
write函数用于向文件中写入数据。
int write(int fd,void* buff,int count);
方式执行成功返回本次真正写入文件的字节数,出现异步返回-1,参数说明如下:
lseek函数
lseek函数用于将文件读写表针联通到文件的指定偏斜量位置处。
int lseek(int fd,int offset,int seek_flag);
方式执行成功返回文件新的偏斜量,出现异常返回-1,参数说明如下:
seek_flag可取值如下:
mmap函数
void * mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
该方式执行成功返回可用的显存首地址,失败则返回-1,各参数说明如下:
prot可取值如下,可以通过or运算组合在一起:
Java只支持使用PROT_READ或则PROT_READ与PROT_WRITE组合。
flags可取值也好多,可以通过or运算符组合在一起,但因为Java只使用了MAP_SHARED与MAP_PRIVATE,因而这儿只介绍这两种取值:
Java的FileChannel的mmap方式实际调用的navite方式的源码如下:
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
// 获取文件句柄
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
// 解析port和flags取值
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
// 调用mmap方法
mapAddress = mmap64(
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
// 映射失败
if (mapAddress == MAP_FAILED) {
......
}
return ((jlong) (unsigned long) mapAddress);
}
从Java_sun_nio_ch_FileChannelImpl_map0方式源码可以看出,Java仅支持使用的flags为MAP_SHARED或MAP_PRIVATE,支持使用的prot为PROT_READ或则PROT_WRITE"+"PROT_READ。
msync函数
倘若采用显存映射文件的形式读写文件linux mmap 文件,须要调用msync确保更改的内容完全同步到硬碟之上。
int msync(void *addr, size_t length, int flags)
msync须要指定同步的地址区间,这么细细度的控制虽然比fsync愈发高效,由于应用程序一般晓得自己的脏页位置。(但实际上Linuxkernel才能很快地找出文件的脏页,致使fsync只会同步文件的更改内容。)
Java的MappedByteBuffer提供的force方式底层就是调用msync方式,如下源码所示。
JNIEXPORT void JNICALL
Java_java_nio_MappedByteBuffer_force0(JNIEnv *env, jobject obj, jobject fdo,
jlong address, jlong len)
{
void* a = (void *)jlong_to_ptr(address);
int result = msync(a, (size_t)len, MS_SYNC);
if (result == -1) {
JNU_ThrowIOExceptionWithLastError(env, "msync failed");
}
}
实战:实现文件拷贝命令
编撰一个c程序实现对文件的拷贝,指定源文件拷贝输出到指定目标文件,并将程序所在目录加入到系统环境变量中linux mmap 文件,实现自定义文件拷贝命令。
首先是实现打开文件与关掉文件,代码如下。
#include
#include
// 定义每次拷贝的字节长度
#define ONE_LINE_SIZE 100
void closeFile(int fd);
// 以只读方式打开一个文件
// 第一个参数:包含文件路径的文件名,这里应该传源文件名,即被拷贝的文件名
// return返回文件标识符
int openReadFile(char* fileName){
int fd open(fileName, O_RDONLY, S_IRWXU);
if(fd==-1){
printf(" open read file error !n");
}else{
printf("open read file success! n");
}
return fd;
}
// 以只写方式打开一个文件,文件不存在则创建,如果文件存在则写入的内容将会覆盖原有的
// 第一个参数:包含文件路径的文件名,这里应该传自标文件茗
// return返回文件标识符
int openWriteFile(char* fileName){
int fd= open(fileName, o WRONLY IO CREAT, S IRWXU):
if(fd!=-1){
printf ("open write file success! n")
}else{
printf ("open write file error!n");
}
return fd;
}
//关闭文件
//第一个参数:文件描述符
void closeFile(int fd){
if(fd==-1){
return;
}
if(close(fd)==0){
printf("file close success! n");
}else{
printf( "file close fail! n");
}
}
接着实现文件的拷贝逻辑,从原文件读取数据写入目标文件,代码如下。
// 声明拷贝方法
void copy(char* sFILeName, char* dFileName);
// 声明执行读写的方法
void copyFile(int sfd, int dfd);
// 第一个参数:源文件名
// 第二个参数:目标文件名
void copy (char* sFileName, char* dFileName){
int sfd openReadFile(sfileName);
if(sfd==-1){
printf("copy fail!n");
return;
}
int dfd openWriteFile(dFileName);
if(dfd==-1){
printf("copy fail!n");
closeFile(sfd);
return;
}
copyFile(sfd, dfd);
closeFile(sfd);
closeFile(dfd);
}
copy方式主要实现文件的打开和关掉,在文件打开出错的情况下不执行真正的文件拷贝操作,倘若指定的两个文件都打开成功,这么将返回的文件描述符传递给copyFile方式实现真正的拷贝动作。
真正实现拷贝逻辑的copyFile方式代码如下。
// 真正执行拷贝的方法
// 第一个参数:源文件的文件句柄
// 第二个参数:目标文件的文件句柄
void copyFile(int sfd, int dfd) {
char buff[ONE_LINE_SIZE+1];
int sLseekOffset = 0;
int dLseekOffset = 0;
int rLength;
int countSize=0;
char flag = 1;
// 出头开始
(void)lseek(sfd, O, SEEK_SEK);
(void)lseek(dfd, O, SEEK_SEK);
// 循环读文件
while(-1 != (rLength = read(sfd, buff, ONE_LINE_ SIZE))){
if(rLength==0){
flag=0;
break;
}
countSize += rLength;
// 写到另一个文件
int writeSize = write(dfd, buff, rLength);
if(writeSize != rLength){
flag=0;
printf( "copy fail! read or write error !n");
break;
}
dLseekOffset = lseek(dfd, 0, SEEK_CUR);
sLseekOffset = lseek(sfd, 0, SEEK_CUR);
if(sLseekOffset != dLseekOffset) {
flag=0;
printf("copy fail! seek error!n");
break;
}
printf("current copy byte size %d, s_seek is %d, d_seek is %d, count copy byte size is %dn, rLength, sLseekOffset, dLseekOffset, countSize);
}
if(flag == 1) {
printf( "file copy success!n");
}
}
其中rLength用于保存每次调用read方式实际读取的字节数,假若它的值为0,说明读取到了文件末尾,假如值为-1说明读取出错,不管是读取到文件尾还是读取出错都应当停止循环。
lseek(dfd,0,SEEK_CUR)和lseek(sdf,0,SEEK_CUR)并没有改变文件的偏斜量,只是拿来获取当期文件相对文件开始位置的偏斜量,由于read和write执行完后就会将文件偏斜量往前联通读取或写入的字节数,所以不须要自动调用lseek改变文件偏斜量。
buff用于保存读取的文件的内容,之后再将读取的内容写入到新文件中,循环该操作直至读取到的字节数为0就正常完成了拷贝。
程序入口方式的实现代码如下。
// 关于参数说明:
// 例如:filecopy test1.txt test2.txt
// argc=3
// argv[0]:filecopy命令的字符串
// argv[1]:源文件名
// argv[2]:目标文件名
int main(int argc, char* argv[]){
switch(argc)[
case 3:
copy (argv[1], argv[2]);
break;
default:
printf("input parameters error !n");
break;
}
return 0;
}
main方式主要做的事情是判定参数的完整性,参数不完整程序执行失败,当参数完整时调用copy方式进一步判定输入的参数是否有效。假如参数完整这么传入main方式的参数argc等于3,argv的宽度为3,第一个参数为可执行文件的文件名,第二个参数必须是指定要拷贝的文件的文件名(包含路径信息,可以是绝对路径也可以是相对路径),第三个参数是新的文件名即保存拷贝内容的文件的文件名,要求同第二个参数。
程序编撰完成后,首先使用gcc将源程序编译成可执行文件:
gcc filecopy.c -o ./filecopy
接着使用export命令为系统添加一个临时的环境变量:
# 由于笔者将编译后的可执行文件filecopy保存在“/home/wujiuye/桌面/c_projects/linux/file”下
export PATH:$PATH:/home/wujiuye/桌面/c_projects/linux/file
如今我们就可以像使用系统命令一样来使用自己写的文件拷贝程序了。
将终端关掉,重新打开后步入tmp目录,随意找一个文件测试,这儿我找了一个图片文件进行测试,输入:
filecopy qt-trayicon-ns2416.png news.png
运行过程及结果如右图,其中qt-trayicon-ns2416.png是要拷贝的文件名,news.png为通过拷贝生成的新文件的文件名。
此时使用文件管理器步入到tmp目录下可以看见拷贝成功后的news.png文件,如右图。
实战:使用mmap实现文件拷贝命令
拷贝命令须要实现给定一个原文件,创建一个目标文件,通过mmap将原文件数据拷贝到目标文件。程序完整代码如下。
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
if (argc != 3) { return -1; }
// 打开文件获取文件句柄
int sfd, dfd;
if ((sfd = open(argv[1], O_RDONLY, S_IRWXU)) < 0) { return -1; }
if ((dfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, S_IRWXU | S_IRWXG | S_IRWXO)) < 0) { return -1; }
// 获取原文件的大小
int size = lseek(sfd, 0, SEEK_END);
lseek(sfd, 0, SEEK_SET);
// 设置文件大小
truncate(argv[2], size);
// mmap映射
void *src, *dst;
if ((src = mmap(0, size, PROT_READ, MAP_SHARED, sfd, 0)) == MAP_FAILED) { return -1; }
if ((dst = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, dfd, 0)) == MAP_FAILED) { return -1; }
// 关闭文件
close(sfd);
close(dfd);
// 直接内存拷贝
memcpy(dst, src, size);
// 执行同步操作
msync(dst, size, MS_SYNC);
return 0;
}
此例中用到的其它c方式说明:
该程序第一步是使用open系统调用打开文件获取文件句柄;第二步是通过lseek估算获取文件大小(这是笔者参照Java的RandomAccessFile类的length方式获取文件大小的实现);第三步是改变目标文件的大小,让目标文件的大小与原文件大小一样;第四步是关掉文件,映射成功后就早已不须要用到文件句柄了;第五步是直接显存拷贝,将原文件映射的显存拷贝到目标文件映射的显存;最后调用msync同步显存修改到文件中。
文章评论