【linux】NIO中的FileChannel与mmap

FileChannel是Java NIO库中的一个类,用于对文件进行读写操作。它提供了一种高效的方式来读取、写入和操作文件。

使用FileChannel,你可以执行以下操作:

  1. 从文件读取数据到缓冲区(Buffer):你可以使用FileChannel的read()方法将数据从文件读取到缓冲区中。
  2. 将数据从缓冲区写入到文件:你可以使用FileChannel的write()方法将数据从缓冲区写入到文件中。
  3. 文件位置操作:你可以使用FileChannel的position()方法获取或设置文件的当前位置。
  4. 文件截取操作:你可以使用FileChannel的truncate()方法截取文件的大小。
  5. 强制数据同步到磁盘:你可以使用FileChannel的force()方法将数据强制刷新到磁盘上。

文件的顺序读写

要使用FileChannel,首先需要通过FileInputStream或FileOutputStream获取一个FileChannel实例,然后可以使用该实例进行文件的读写操作。

下面是一个简单的示例代码,展示了如何使用FileChannel读取文件内容并写入到另一个文件中:

java 复制代码
package com.morris.io;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

/**
 * FileChannel实现随机读取文件
 */
public class RandomAccessDemo {
    public static void main(String[] args) throws IOException {
        // 创建文件对象和FileChannel对象
        File file = new File("RandomAccessDemo.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 设置文件的当前位置
        long readPosition = 10;
        fileChannel.position(readPosition);

        // 创建缓冲区
        ByteBuffer readBuffer = ByteBuffer.allocate(5);
        // 从当前位置读取数据到缓冲区
        int bytesRead = fileChannel.read(readBuffer);
        System.out.println(new String(readBuffer.array()));

        long writePosition = 20; // 这个位置文件中没数据
        fileChannel.position(writePosition);
        // 创建缓冲区,一个char两个字节
        ByteBuffer writeBuffer = ByteBuffer.allocate(6);
        writeBuffer.put("xyz".getBytes(StandardCharsets.UTF_8));
        writeBuffer.flip();
        fileChannel.write(writeBuffer);

        // 关闭通道和文件
        fileChannel.close();
        randomAccessFile.close();
    }
}

产生的系统调用如下:

shell 复制代码
openat(AT_FDCWD, "input.txt", O_RDONLY) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=13, ...}) = 0
openat(AT_FDCWD, "output.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
fstat(7, {st_mode=S_IFREG|0777, st_size=0, ...}) = 0
read(4, "abc\nooxx\nefg\n", 1024)       = 13
write(7, "abc\nooxx\nefg\n", 13)        = 13
read(4, "", 1024)                       = 0
close(4)
close(7)

文件的随机读写

FileChannel提供了随机读写文件的功能,可以通过position()方法来设置文件的当前位置,然后使用read()方法从该位置开始读取数据,使用write()方法从该位置开始写入数据。

下面是一个示例代码,展示了如何使用FileChannel进行随机读写文件的操作:

java 复制代码
package com.morris.io;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

/**
 * FileChannel实现随机读取文件
 */
public class RandomAccessDemo {
    public static void main(String[] args) throws IOException {
        // 创建文件对象和FileChannel对象
        File file = new File("RandomAccessDemo.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 设置文件的当前位置
        long readPosition = 10;
        fileChannel.position(readPosition);

        // 创建缓冲区
        ByteBuffer readBuffer = ByteBuffer.allocate(5);
        // 从当前位置读取数据到缓冲区
        int bytesRead = fileChannel.read(readBuffer);
        System.out.println(new String(readBuffer.array()));

        long writePosition = 20;
        fileChannel.position(writePosition);
        // 创建缓冲区,一个char两个字节
        ByteBuffer writeBuffer = ByteBuffer.allocate(6);
        writeBuffer.put("xyz".getBytes(StandardCharsets.UTF_8));
        writeBuffer.flip();
        fileChannel.write(writeBuffer);

        // 关闭通道和文件
        fileChannel.close();
        randomAccessFile.close();
    }
}

产生的系统调用如下:

shell 复制代码
openat(AT_FDCWD, "RandomAccessFileDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0644, st_size=18, ...}) = 0
lseek(4, 10, SEEK_SET)                  = 10
read(4, "j\nklm", 5)                    = 5
write(1, "j\nklm\n", 6)                 = 6
lseek(4, 20, SEEK_SET)                  = 20
write(4, "xyz", 3)                      = 3
close(4)

系统调用使用lseek来移动定位position在文件中的位置。

如果访问文件的position大于文件的长度会怎么样?

程序执行前的文件内容:

txt 复制代码
abcde
fghij
klmno

程序执行后的文件内容:

txt 复制代码
abcde
fghij
klmno
^@^@xyz

可以看到访问文件的position大于文件的长度后,中间会用空来填充。

内存映射文件mmap

FileChannel还提供了内存映射文件的功能,通过使用map()方法,可以将文件映射到内存中的一个ByteBuffer对象,从而实现对文件的高效读写操作。

下面是一个示例代码,展示了如何使用内存映射文件的方式读取和写入文件:

java 复制代码
package com.morris.io;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * mmap,将文件映射到内存中
 */
public class MemoryMappedFileDemo {
    public static void main(String[] args) throws IOException {
        // 创建文件对象和FileChannel对象
        File file = new File("MemoryMappedFileDemo.txt");
        FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();

        // 将文件映射到内存中
        MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());

        // 读取文件内容
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get()); // 输出数据
        }

        System.in.read();

        // 修改文件内容
        buffer.put(0, (byte) 'H');
        buffer.put(1, (byte) 'e');
        buffer.put(2, (byte) 'l');
        buffer.put(3, (byte) 'l');
        buffer.put(4, (byte) 'o');

        // 刷新缓冲区到磁盘
        buffer.force();

        // 关闭通道和文件
        fileChannel.close();
    }
}

产生的系统调用如下:

shell 复制代码
openat(AT_FDCWD, "MemoryMappedFileDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=18, ...}) = 0
mmap(NULL, 18, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x7f4fa4004000
write(1, "a", 1)                        = 1
write(1, "b", 1)                        = 1
write(1, "c", 1)                        = 1
write(1, "d", 1)                        = 1
write(1, "e", 1)                        = 1
write(1, "\n", 1)                       = 1
write(1, "f", 1)                        = 1
write(1, "g", 1)                        = 1
write(1, "h", 1)                        = 1
write(1, "i", 1)                        = 1
write(1, "j", 1)                        = 1
write(1, "\n", 1)                       = 1
write(1, "k", 1)                        = 1
write(1, "l", 1)                        = 1
write(1, "m", 1)                        = 1
write(1, "n", 1)                        = 1
write(1, "o", 1)                        = 1
write(1, "\n", 1)                       = 1

read(0, "\n", 8192)
msync(0x7f4fa4004000, 18, MS_SYNC)      = 0
close(4)

中间的代码System.in.read()可以让程序暂停,这时可以查看文件打开的描述符:

shell 复制代码
$ lsof -p 8964
COMMAND  PID USER   FD   TYPE             DEVICE  SIZE/OFF              NODE NAME
java    8964 root  mem    REG               0,50        18 10977524091816630 /io-demo/target/classes/MemoryMappedFileDemo.txt

java    8964 root    4u   REG               0,50        18 10977524091816630 /io-demo/target/classes/MemoryMappedFileDemo.txt

将文件映射到内存中可以指定长度,这样可以做到只映射文件的部分内容,如果映射的长度大于文件本身的长度,就会扩大文件的长度。这样就限制了文件无法完成拓展,因为mmap到内存的时候,所能操作的范围就确定了,无法增加文件的长度。

mmap的优点:

  • 高效访问:mmap使得文件的读写操作像访问内存一样高效,避免了频繁的系统调用和数据拷贝。
  • 文件共享:多个进程可以将同一个文件映射到各自的地址空间,实现文件共享,方便进程间通信和数据共享。
  • 零拷贝:与零拷贝技术结合,可以在网络传输中减少数据拷贝,提高传输性能。
  • 内存管理:支持用户空间的内存管理,例如内存映射和私有化。

FileChannel锁定文件

FileChannel还提供了文件锁定的功能,可以通过lock()方法来对文件进行加锁,以防止其他进程对文件的读写操作。

下面是一个示例代码,展示了如何使用文件锁定的方式对文件进行操作:

java 复制代码
package com.morris.io;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;

/**
 * FileChannel可以对文件加锁
 */
public class FileLockDemo {

    public static void main(String[] args) throws IOException {
        // 创建文件对象和FileChannel对象
        File file = new File("FileLockDemo.txt");
        FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();

        // 对文件进行加锁
        FileLock lock = fileChannel.lock();

        new Thread(() -> {
            try {
                main(args);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        // 暂停下,让其他线程访问下文件,演示锁的效果
        System.in.read();

        // 执行文件操作(读取、写入等)
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("file lock".getBytes(StandardCharsets.UTF_8));
        byteBuffer.flip();
        fileChannel.write(byteBuffer);

        // 解锁文件
        lock.release();

        // 关闭通道和文件
        fileChannel.close();
    }
}

产生的系统调用如下:

shell 复制代码
openat(AT_FDCWD, "FileLockDemo.txt", O_RDWR|O_CREAT, 0666) = 4
fstat(4, {st_mode=S_IFREG|0777, st_size=0, ...}) = 0
fcntl(4, F_SETLKW, {l_type=F_WRLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
read(0, "\n", 8192)
write(4, "file lock", 9)                = 9
fcntl(4, F_SETLK, {l_type=F_UNLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = 0
close(4)

中间的代码System.in.read()可以让程序暂停,这时可以查看文件打开的描述符:

shell 复制代码
java    9355 root    4uW  REG               0,50        12 14073748835656455 /io-demo/target/classes/FileLockDemo.txt

可以看到文件描述符4后面多了个W

其他线程访问已加锁的文件会抛出异常:

shell 复制代码
Exception in thread "Thread-0" java.nio.channels.OverlappingFileLockException
        at java.base/sun.nio.ch.FileLockTable.checkList(FileLockTable.java:229)
        at java.base/sun.nio.ch.FileLockTable.add(FileLockTable.java:123)
        at java.base/sun.nio.ch.FileChannelImpl.lock(FileChannelImpl.java:1276)
        at java.base/java.nio.channels.FileChannel.lock(FileChannel.java:1089)
        at com.morris.io.FileLockDemo.main(FileLockDemo.java:22)
        at com.morris.io.FileLockDemo.lambda$main$0(FileLockDemo.java:26)
        at java.base/java.lang.Thread.run(Thread.java:833)

但是其他进程访问这个已经加锁的文件,可以成功,执行如下的命令可以成功,why???

shell 复制代码
$ echo xxx > FileLockDemo.txt
相关推荐
远游客07131 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<1 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟1 小时前
centos-stream9系统安装docker
linux·docker·centos
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
cominglately4 小时前
centos单机部署seata
linux·运维·centos
魏 无羡4 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse4 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
木子Linux5 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.8245 小时前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维5 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs