任何一个学过JAVA的人应该都对这段代码非常熟悉。空闲时间翻了下代码,看看它的底层是怎么实现的
public class HelloWorld {
public static void main(String[] args) {
System.out.print("Hello, World!");
}
}
首先点开out
,发现它是System
类中的一个public static final
变量,类型为PrintStream
。为了找到它是怎么初始化的,一直往前翻到System
类的构造函数
![](https://i-blog.csdnimg.cn/blog_migrate/bcec7474cdee679b8a702e14f19905ec.png)
从System
类的注释中发现,VM会调用initPhase1
这个方法来初始化这个类。先不管VM,先看下initPhase1
方法做了什么
![](https://i-blog.csdnimg.cn/blog_migrate/3caf1a16406176109317370a16526387.png)
发现它用FileDescriptor.out
创建了FileOutputStream
对象,再用这个对象创建了PrintStream
对象,最后调用native的setOut0
![](https://i-blog.csdnimg.cn/blog_migrate/1a775899dbb3c25b37c2e0336934502b.png)
在创建PrintStream
对象时,先将FileOutputStream
封装成BufferedOutputStream
,然后把BufferedOutputStream
封装成OutputStreamWriter
。这一步中会根据传入的字符集创建OutputStreamWriter
中的编码器StreamEncoder
![](https://i-blog.csdnimg.cn/blog_migrate/2f947f5348081066fde18d59ac88f9ca.png)
这样OutputStreamWriter
就创建好了,在print的时候会调用这个类的方法,最后根据调用栈发现调用了FileOutputStream
中的writeBytes
方法
![](https://i-blog.csdnimg.cn/blog_migrate/7d3b4c3aaeaa38e5c8d889cf46bb1ac2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7080feaf9558c48d42dc7548bbe35b91.png)
发现这个方法是native的,也就是说不在JAVA中实现,打开openjdk,checkout到tag jdk18
![](https://i-blog.csdnimg.cn/blog_migrate/b0bb5b43014fc6a0ca977a5e2ece4b77.png)
找到在jdk中的实现,发现调用了IO_Append
和IO_Write
,而这两个是宏定义,指向了handleWrite
方法
![](https://i-blog.csdnimg.cn/blog_migrate/2d6fc74281717d10da555f0e6a92dcfb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e387c7ef74118db1fb8de1d0bf1cc87f.png)
在不同的平台下,这个方法有不同的实现
在windows下调用了WriteFile
这个Win32 API
![](https://i-blog.csdnimg.cn/blog_migrate/405ed474bc669265b83997f58b2d1603.png)
在linux下调用了unistd.h
中定义的write
方法
![](https://i-blog.csdnimg.cn/blog_migrate/c389e657b63d70857eebd471071e26cc.png)
打开glibc,在write_nocancel.c
下看到提供的write方法实现,通过一堆的宏定义最终是一个系统调用,调用了linux的write
方法
![](https://i-blog.csdnimg.cn/blog_migrate/b5e829e8c79f67e6d1a1bbaf91630411.png)
在linux内核源代码中,找到write
的SYSCALL,其中调用了ksys_write
方法
![](https://i-blog.csdnimg.cn/blog_migrate/e10d7c90358c4e5c9e7f1bc017e2f8c1.png)
这个方法中会获取fd,然后再通过vfs_write
写入,顺着调用链一路找到了下面这个write
方法
![](https://i-blog.csdnimg.cn/blog_migrate/0f7c31346dcf0784f9c1774316bc2f35.png)
后面的两个参数很好理解,第一个tty_struct
是什么?
In many computing contexts, “TTY” has become the name for any text terminal, such as an external console device, a user dialing into the system on a modem on a serial port device, a printing or graphical computer terminal on a computer’s serial port or the RS-232 port on a USB-to-RS-232 converter attached to a computer’s USB port, or even a terminal emulator application in the window system using a pseudoterminal device.
简单来说,就是一个文本终端。它也是一个虚拟文件系统,模拟了终端设备
回头看ksys_write
方法的第一行,打开了一个文件描述符,其中调用了__fget_light
方法
![](https://i-blog.csdnimg.cn/blog_migrate/c155da4c10f5286b4f5577c905a8aed9.png)
从第一行就能看到,从current
中找到了files_struct
。current
是一个宏定义,获取当前正在运行的任务current_task
,而current_task->files
是这个当前正在运行的任务所打开的文件信息
![](https://i-blog.csdnimg.cn/blog_migrate/fb3e63d18a883526fc2dcf927aa96ea3.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a295ecf09bd6d92be0bea57c1ce39ecc.png)
也就是说,进程打开了一个由虚拟文件系统管理的虚拟文件,它是一个伪终端PTY,然后向其中写入数据
在linux中输入tty,就可以看到对应的伪终端设备文件路径。往里写入数据,就可以实现向另一个终端中打印东西
![](https://i-blog.csdnimg.cn/blog_migrate/faf75d8ad7cf7bce1780fedd8abc7591.png)
参考:
- https://github.com/openjdk/jdk
- https://sourceware.org/git/glibc.git
- https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
- https://www.linusakesson.net/programming/tty/index.php
- https://en.wikipedia.org/wiki/Devpts
- https://man7.org/linux/man-pages/man7/pty.7.html
- https://github.com/GNOME/vte