【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
相关推荐
wdxylb1 小时前
云原生俱乐部-shell知识点归纳(1)
linux·云原生
飞雪20072 小时前
Alibaba Cloud Linux 3 在 Apple M 芯片 Mac 的 VMware Fusion 上部署的完整密码重置教程(二)
linux·macos·阿里云·vmware·虚拟机·aliyun·alibaba cloud
路溪非溪2 小时前
关于Linux内核中头文件问题相关总结
linux
Lovyk5 小时前
Linux 正则表达式
linux·运维
Fireworkitte6 小时前
Ubuntu、CentOS、AlmaLinux 9.5的 rc.local实现 开机启动
linux·ubuntu·centos
sword devil9006 小时前
ubuntu常见问题汇总
linux·ubuntu
ac.char6 小时前
在CentOS系统中查询已删除但仍占用磁盘空间的文件
linux·运维·centos
淮北也生橘128 小时前
Linux的ALSA音频框架学习笔记
linux·笔记·学习
华强笔记11 小时前
Linux内存管理系统性总结
linux·运维·网络
十五年专注C++开发11 小时前
CMake进阶: CMake Modules---简化CMake配置的利器
linux·c++·windows·cmake·自动化构建