Linux-高级IO函数
# Linux-高级IO函数
2024年,打算新开一个专栏,专门写清楚Linux服务器编程,包括TCP/IP协议、socket原理、进程间通信、内核系统调用(sendfile、mmap等)
# 1、pipe
pipe函数可用于创建一个管道,以实现进程间通信
pipe函数的定义如下:
#include<unistd.h>
int pipe(int fd[2]);
// pipe函数的参数是一个包含两个int型整数的数组指针。
// 该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。
// 如果失败,则返回-1并设置errno
2
3
4
5
C可以很简单的调用pipe函数来进行进程间通信,但是对于Java来说并不能直接调用pipe函数,Java封装了更高级别的API来实现进程间通信,比如:java.io.PipedInputStream
和java.io.PipedOutputStream
或者java.nio.channels.Pipe
实际的例子如下:
使用PipedInputStream
import java.io.*;
public class PipeExample {
public static void main(String[] args) throws IOException {
PipedInputStream pipedInputStream = new PipedInputStream();
PipedOutputStream pipedOutputStream = new PipedOutputStream();
// 将输入流和输出流连接起来
pipedInputStream.connect(pipedOutputStream);
// 创建线程写入数据
Thread writerThread = new Thread(() -> {
try {
String message = "Hello, world!";
pipedOutputStream.write(message.getBytes());
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
});
// 创建线程读取数据
Thread readerThread = new Thread(() -> {
try {
int data;
while ((data = pipedInputStream.read()) != -1) {
System.out.print((char) data);
}
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
});
writerThread.start();
readerThread.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
使用java.nio.channels.Pipe
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
public class PipeExample {
public static void main(String[] args) throws IOException {
Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();
// 创建线程写入数据
Thread writerThread = new Thread(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put("Hello, world!".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
sinkChannel.write(buffer);
}
sinkChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
});
// 创建线程读取数据
Thread readerThread = new Thread(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = sourceChannel.read(buffer);
}
sourceChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
});
writerThread.start();
readerThread.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 2、dup/dup2
有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如CGI编程)。这可以通过下面的用于复制文件描述符的dup或dup2函数来实现
#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);
2
3
dup函数创建新的文件描述符,该新文件描述符和原有文件描述符file_descriptor指向相同的文件、管道或者网络连接,并且dup返回的文件描述符总是取系统当前可用的最小整数值。
dup2和dup类似,不过它将返回第一个不小于file_descriptor_two的整数值
两个函数调用失败都返回-1并设置errno
在Java中并没有提供类似于dup的调用,如果非要走dup调用可以使用JNI来实现,通过编写本地代码来调用底层的系统调用
1、首先编写一个本地方法,调用dup
#include <unistd.h>
#include <jni.h>
JNIEXPORT jint JNICALL Java_com_example_FileDescriptorDup_dup(JNIEnv *env, jobject obj, jint oldfd) {
return dup(oldfd);
}
2
3
4
5
6
2、编译这个C文件生成共享库
gcc -shared -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -o libFileDescriptorDup.so FileDescriptorDup.c
3、在Java代码中加载这个共享库,并声明本地方法
public class FileDescriptorDup {
static {
System.loadLibrary("FileDescriptorDup");
}
public static native int dup(int oldfd);
public static void main(String[] args) {
// 假设需要复制的文件描述符
int oldfd = 0;
int newfd = dup(oldfd);
System.out.println("New file descriptor: " + newfd);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3、readv/writev
readv函数将数据从文件描述符读到分散的内存块中,即分散读
writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写
#include<sys/uio.h>
ssize_t readv(int fd,const struct iovec*vector,int count);
ssize_t writev(int fd,const struct iovec*vector,int count);
// fd参数是被操作的目标文件描述符
// vector参数的类型是iovec结构数组
// count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd
2
3
4
5
6
readv和writev在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno
在Java中并不能直接调用这两个系统函数,Java提供了更高级别的API,使用缓冲区进行读取和写入操作,就是java.nio.channels.FileChannel
通过read(ByteBuffer[] dsts)
和write(ByteBuffer[] srcs)
方法来进行批量读取和批量写入操作
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ReadvExample {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("example.txt");
FileChannel channel = fis.getChannel();
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.allocate(10);
buffers[1] = ByteBuffer.allocate(20);
buffers[2] = ByteBuffer.allocate(30);
// 从文件通道中批量读取数据到多个缓冲区中
long bytesRead = channel.read(buffers);
for (ByteBuffer buffer : buffers) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
}
channel.close();
fis.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class WritevExample {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("example.txt");
FileChannel channel = fos.getChannel();
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.wrap("Hello, ".getBytes());
buffers[1] = ByteBuffer.wrap("world!".getBytes());
buffers[2] = ByteBuffer.wrap("\n".getBytes());
// 从多个缓冲区中批量写入数据到文件通道中
long bytesWritten = channel.write(buffers);
channel.close();
fos.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4、sendfile
零拷贝这个概念应该都比较熟悉了,针对于零拷贝的系统调用就是sendfile函数
sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count);
// in_fd参数是待读出内容的文件描述符
// out_fd参数是待写入内容的文件描述符
// offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置
// count参数指定在文件描述符in_fd和out_fd之间传输的字节数
// sendfile成功时返回传输的字节数,失败则返回-1并设置errno
2
3
4
5
6
7
该函数的man手册明确指出,in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是 socket和管道;而out_fd则必须是一个socket。由此可见,sendfile几乎是专门为在网络上传输文件而设计的
Java并没有提供sendfile的直接调用,但是可以使用Java的java.net.Socket
和java.nio.channels.FileChanne
包,使用FileChannel.transferTo()
或FileChannel.transferFrom()
方法来实现类似sendfile
的功能,这两个方法允许你将数据从文件通道直接传输到套接字通道或者将数据从套接字通道直接传输到文件通道
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class SendFileExample {
public static void main(String[] args) throws IOException {
String filename = "example.txt"; // 要发送的文件名
// 创建文件输入流和文件通道
FileInputStream fis = new FileInputStream(filename);
FileChannel fileChannel = fis.getChannel();
// 创建套接字通道并连接到目标地址
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("destination_ip", destination_port));
// 从文件通道直接传输数据到套接字通道
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
// 关闭通道和流
fileChannel.close();
fis.close();
socketChannel.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 5、mmap/munmap
mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。
munmap函数则释放由mmap创建的这段内存空间
#include<sys/mman.h>
void*mmap(void*start,size_t length,int prot,int flags,int fd,off_toffset);
int munmap(void*start,size_t length);
// start参数允许用户使用某个特定的地址作为这段内存的起始地址,如果它被设置成NULL,则系统自动分配一个地址
// length参数指定内存段的长度
// prot参数用来设置内存段的访问权限(PROT_READ内存段可读 | PROT_WRITE内存段可写 | PROT_EXEC内存段可执行 | PROT_NONE内存段不能被访问)
// flags参数控制内存段内容被修改后程序的行为
// fd参数是被映射文件对应的文件描述符
// offset参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)
// mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno
// munmap函数成功时返回0,失败则返回-1并设置errno
2
3
4
5
6
7
8
9
10
11
12
Java并没有直接调用mmap的API,除了使用JNI可以调用mmap外,Java提供了类似mmap的功能,例如java.nio包里面的MappedByteBuffer,它提供了一种将文件直接映射到内存中的方式,从而允许直接对文件进行读取和写入,而无需通过常规的IO操作。下面是一个例子:
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MmapExample {
public static void main(String[] args) throws Exception {
// 打开文件并创建一个只读的MappedByteBuffer
RandomAccessFile file = new RandomAccessFile("example.txt", "r");
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
// 从MappedByteBuffer中读取数据
byte[] data = new byte[(int) channel.size()];
buffer.get(data);
// 打印数据
System.out.println(new String(data));
// 关闭文件和通道
channel.close();
file.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Java由于提供了自动GC,不需要手动解除内存映射,所以并不需要munmap的能力。
# 6、splice
作用是在两个文件描述符之间移动数据,也是零拷贝操作
#include<fcntl.h>
ssize_t splice(int fd_in, loff_t*off_in, int fd_out, loff*off_out, size_t len, unsigned int flags);
// fd_in参数是待输入数据的文件描述符,如果fd_in是一个管道描述符,那么off_in必须设置为null;如果fd_in不是一个管道描述符(比如socket),那么off_in表示从输入数据流的何处开始读取数据,此时,若off_in被设置为null,则表示从输入数据流的当前偏移量位置读入,若不为null,则它将指出具体的偏移位置
// fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流
// len参数指定移动数据的长度
// flags参数则控制数据如何移动
// 使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符;splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而该管道没有被写入任何数据时。splice函数失败时返回-1并设置errno
2
3
4
5
6
7
8
# 7、tee
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作
#include<fcntl.h>
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags);
// 该函数的参数的含义与splice相同(但fd_in和fd_out必须都是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任何数据。tee失败时返回-1并设置errno
2
3
# 8、fcntl
提供了对文件描述符的各种控制操作
#include<fcntl.h>
int fcntl(int fd,int cmd,…);
// fd参数是被操作的文件描述符
// cmd参数指定执行何种类型的操作
2
3
4
在网络编程中,fcntl函数通常用来将一个文件描述符设置为非阻塞的
int setnonblocking(int fd){
int old_option=fcntl(fd,F_GETFL);/*获取文件描述符旧的状态标志*/
int new_option=old_option|O_NONBLOCK;/*设置非阻塞标志*/
fcntl(fd,F_SETFL,new_option);
return old_option;/*返回文件描述符旧的状态标志,以便*//*日后恢复该状态标志*/
}
2
3
4
5
6