Try Everything Different In My Life.

「💡探究」使用strace来探究Java I/O在操作系统层面的调用

2020.07.23

我们写的代码最终以一个进程的形式运行在操作系统上,我们的进程要依赖操作系统完成一些工作,此时进程便会进行系统调用,Linux系统下提供strace来查看系统调用信息,以此来查看Java 提供的IO在操作系统层面是什么情况

BIO(使用多线程实现非阻塞)

示例代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author : Jaswine
 * @date : 2020-07-23 13:04
 **/
public class Test {

	public static void main(String[] args) throws IOException {

		ServerSocket socket = new ServerSocket();		
		socket.bind(new InetSocketAddress(8888));
		System.out.println("新建的socket8888");
		while (true){
			Socket client = socket.accept();
			System.out.println("client" + client.getPort());

			new Thread(() -> {
				try {
					BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
					while (true){
						System.out.println(reader.readLine());
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			}).start();
		}
	}
}

代码很简单,开启一个ServerSocket绑定8888端口,然后等待连接。

strace查看系统调用

关于strace的用法参考:

🔗 strace - Linux命令Doc

## 1.编译Test.java
javac Test.java

## 2.运行
strace -ff -o ./test java Test

会在当前目录下生成以test.开头,线程号码结尾的文件

使用JPS查看当前的Java进程,进程号为2592

打开test.2592文件,发现最后使用clone函数开启了新进程2593

现在打开test.2593,发调用了socket函数创建了一个网络socket

socket函数成功成功后悔创建一个指向socket的文件描述符,看到指向的文件描述符为 5

在Linux系统中一切都是文件,到当前进程的目录文件下看一看到该文件

cd /proc/2592/fd

其中0,1,2是Linux系统的标准输出,每一个进程都会有这三个文件描述符

再看系统调用的秒速文件,看到调用了bind函数绑定了8888端口,将当前socket和文件描述符为5的文件进行了绑定,然后socket处于listen状态

使用命令行可以查看 占用 8888 端口的应用程序状态

netstat -anp | grep 8888

此时我们连接这个socket服务,会出现以下的现象

nc localhost 8888
  • 终端会打印出连接进来客户端的端口
  • 在进程的目录下的fd文件中会新出现一个指向socket的文件描述符
  • 在test.2593中显示这个程序执行的系统调用
  • 使用netstat查看网络会发现一个建立连接的TCP

因为我们代码是用多线程来接收客户端输入,所以在主程最后发现将io交给了新的线程

我们打开上面说的线程号为4160的test文件,发现被recvfrom函数阻塞住,等待文件描述符为6的文件

当我们输入jaswine的时候,发现test.4160文件拿到客户端输入之后又进入recvfrom函数,进入阻塞状态

总结

Java的IO就是操作系统的底层封装。

BIO(多线程非阻塞)

实现逻辑

在主线程中使用

ServerSocket serverSocket = new ServerSocket(8888);

在操作系统层面上发生了以下的操作 使用socket函数返回了一个socket套接字,这个套接字指向进程fd目录下的一个具体文件(Linux中一切都是文件),然后使用bind函数绑定对应的端口,使用listen函数进行监听。

当有客户端连接服务端的时候

nc localhost 8888

调用serverSocket的accept()方法之后操作系统执行accept方法返回一个socket,因为是使用多线程生成一个子线程,将信息复制到子线程中然后子线程调用revcfrom方法阻塞住,等待客户端输入。

当客户端输入的时候,调用read方法将数据读出来,然后处理完对应的业务逻辑再次执行revcfrom方法阻塞等待客户端的输入。

优点

代码逻辑简单

缺点

当有很多客户端的时候要生成很多子线程

参考

🔗 strace - Linux命令Doc

🔗 Linux系统中流、管道和重定向 – IBM Developer

🔗 poll(UNIX) – 维基百科

🔗 recvfrom – Linux Man