Hello World是如何被打印的?

(本文章为本人自媒体账号视频的文字版,方便大家阅读)


我们以Java为例,深入背后的JDK,Glibc和Linux内核,浅显地解释一下。打印Hello World涉及的技术细节。


Java


这就是所有学Java的人梦开始地方:

System.out.print("Hello World");


不过可能很少有人在乎,out具体是什么呢?out是一个PrintStream类型的实例:

public static final PrintStream out = null;


它由 initPhase1 这个private 方法初始化,注释里有写这个方法是VM调用的,我们不用关心。在这个方法里,它先使用FileDescriptor.out创建了一个FileOutputStream

FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);


FileDescriptor.out是什么呢?实际上是值为1的一个文件描述符:

public static final FileDescriptor out = new FileDescriptor(1);


1代表的是标准输出流,现在不理解也没有关系,在后面讲Linux的时候还会提到。


拿到FileOutputStream之后,它又在newPrintStream这个方法里面,把它包了一层BufferedOutputStream,然后又包了一层PrintStream:

private static PrintStream newPrintStream(FileOutputStream fos, String enc) {
if (enc != null) {
try {
return new PrintStream(new BufferedOutputStream(fos, 128), true, enc);
} catch (UnsupportedEncodingException uee) {}
}
return new PrintStream(new BufferedOutputStream(fos, 128), true);
}


最后set给了out这个变量:

setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));


PrintStream里面也没闲着,它先用外面传进来的OutputStream和charset,创建了一个OutputStreamWriter,再给writer包了一层BufferedWriter:

public PrintStream(OutputStream out, boolean autoFlush, Charset charset) {
super(out);
this.autoFlush = autoFlush;
this.charOut = new OutputStreamWriter(this, charset);
this.textOut = new BufferedWriter(charOut);
}


BufferedWriter就是一层加Buffer,这个很好理解,我重点想说OutputStreamWriter。它里面干了一个很有意思的事情。它用外面传进来的Charset字符集,创建了一个StreamEncoder:

public OutputStreamWriter(OutputStream out, Charset cs) {
super(out);
if (cs == null)
throw new NullPointerException("charset");
se = StreamEncoder.forOutputStreamWriter(out, this, cs);
}


它在write里面实际上用的是,同样的Charset创建的CharsetEncoder的write。


折腾这么多东西是为了干什么呢?就是为了做encoding编码。编码是个很复杂的话题,首先我们得区分内部编码和外部编码。内部编码就是语言内部String类型存储使用的编码类型,而外部编码则是包括源代码文件输入输出文件等等“外部"文件的编码。


Java和C#这俩语言,内部编码使用的是UTF-16(JDK9中引入latin1),而现在主流的外部编码是UTF-8。因此它们来说打印输出String类型的时候,需要进行一个编码转换然后才能得到正确的字节流。值得一提的是,Swift和Rust这种新势力语言,内部编码直接使用的就是UTF-8,跟上了时代发展的浪潮,也就省去了转换的步骤。


回到正题,最后经过很深的一个调用栈,实际输出编码后的字节流的函数是,FileOutputStream里面的writeBytes:

private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;

它是一个原生方法,并不是Java写的。这代表着我们需要去看JDK源码了。


JDK 中的 C


JDK1的源代码中除了Java代码之外,也有一些代码是由C编写的。我们想找的方法在这儿,FileOutputStream里面采用JNI实现的writeBytes方法:

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
writeBytes(env, this, bytes, off, len, append, fos_fd);
}

在它的内部,经过一些异常处理逻辑之后,调用了IO_Append和IO_Write这两个宏,它在不同平台上的实现是不一样的。


在unix文件夹下,它的实现是这样的:

ssize_t handleWrite(FD fd, const void *buf, jint len)
{
ssize_t result;
RESTARTABLE(write(fd, buf, len), result);
return result;
}

在windows文件夹下,它的实现是这样的:

static jint writeInternal(FD fd, const void *buf, jint len, jboolean append)
{
//...
result = WriteFile(h, /* File handle to write */
buf, /* pointers to the buffers */
len, /* number of bytes to write */
&written, /* receives number of bytes written */
lpOv); /* overlapped struct */
//...
}


看到这儿大家应该也能理解,为什么需要C代码了。当JDK需要和操作系统进行交互的时候,使用C进行调用是更加方便的。


还有个很有意思的问题是,为什么不让JVM来负责这种和操作系统的交互呢?JVM本身就是C/C++写的,为什么还要从JVM里绕出来再用JNI呢?我的理解是JVM是一个纯粹的VM,它只负责JVM底层指令的解析执行。而跟操作系统交互对于它来说是一个太“高层”的任务了,不符合JVM的抽象层级。


回到writeBytes,我们主要关注Linux上的实现,它调用的是 Glibc 提供 write方法,下面我们看一下 Glibc 的源码。


Glibc


Glibc全称GNU C Library,GNU C标准库,是包括Linux在内的GNU系统当中重要的组成部分。几乎所有程序都对Glibc存在直接或者间接的依赖。


在Linux上Glibc里write方法,由__write_nocancel提供,里面调用的就是write这个Linux系统调用:

ssize_t
__write_nocancel (int fd, const void *buf, size_t nbytes)
{
return INLINE_SYSCALL_CALL (write, fd, buf, nbytes);
}
hidden_def (__write_nocancel)

既然就是 Linux 系统调用,那为什么要封装这么一层呢?直接调用write这个系统调用不好吗?在Linux肯定没问题,但是万一它不是Linux呢?Glibc里还提供了Hurd内核的write实现,里面调用的就是Hurd有关的方法:

ssize_t
__write_nocancel (int fd, const void *buf, size_t nbytes)
{
error_t err = HURD_FD_USE (fd, _hurd_fd_write (descriptor, buf, &nbytes, -1));
return err ? __hurd_dfail (fd, err) : nbytes;
}
libc_hidden_weak (__write_nocancel)

现在大家应该能理解了,面向Glibc而不是直接面向Linux内核,提供了更强的可移植性 。因此除了一些特殊情况,很少直接使用Linux的系统调用,而是使用Glibc提供的对应方法。


回到打印Hello World的正题,Linux的write系统调用是怎么实现的?下面我们看一下Linux的源码。


Linux 内核


在Linux内核里,write这个系统调用的实现,最终由 ksys_write 这个函数承接:

// read_write.c
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}

ret = vfs_write(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}

它先根据传进来的FD,调用fdget_pos拿到了一个FD结构体。然后在计算了一下位置之后,调用了vfs_write这个函数写入数据。


有这么几个问题,首先什么是VFS?VFS全称Virtual File System 虚拟文件系统。Linux里贯彻了Unix”一切皆文件“的思想,VFS就是对各种文件系统进行的抽象。不管是往磁盘写,还是往命令行输出写,都可以用write来实现。


第二个问题,大家还记得FD是什么吗?FD是1,真的就是一个普通的int1,而且对所有程序来说都是1。为什么能都是1呢?你是1我也是1大家都是1,Linux怎么知道往哪儿写呢?我们追踪一下fdget_pos这个函数,最后会找到__fget_light这个函数:

static unsigned long __fget_light(unsigned int fd, fmode_t mask)
{
struct files_struct *files = current->files;
struct file *file;
//...

它里面第一句有个很重要的东西叫current,它是个宏,在不同的平台上由不同的实现,在x86上它的实现是这样的:

static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(pcpu_hot.current_task);
}
#define current get_current()

它的作用是拿到当前正在运行的,也就是正在调用内核的进程的信息,返回的是 task_struct 这个结构体。然后__fget_light再从里面拿到 files_struct,也就是进程里面打开的文件信息。这样就很清晰了,虽然你是1我也是1,但是我们的1只是看起来一样,背后的”文件“并不是一个。


然后就是第三个问题了,这个1对应的”文件“是什么?它有文件系统吗?它还真有,在Linux里它由一个叫做Devpts的虚拟文件系统承接。PTS的全称是Pseduo Terminal Device伪终端设备,它的作用就是模拟终端设备提供命令行的输入输出。PTS具体的实现原理就不在本视频的讨论范围了,我们只需要关注它的效果。每open一次 /dev/ptmx,它就会返回 /dev/pts/0 /dev/pts/1这样的负责输入输出的”文件“。使用 lsof 可以看到当前有哪些程序占用了输入输出:

$ lsof /dev/pts/0
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 28128 skyline 0u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 1u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 2u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 255u CHR 136,0 0t0 3 /dev/pts/0
lsof 28157 skyline 0u CHR 136,0 0t0 3 /dev/pts/0
lsof 28157 skyline 1u CHR 136,0 0t0 3 /dev/pts/0
lsof 28157 skyline 2u CHR 136,0 0t0 3 /dev/pts/0

当我们打开新的程序比方python,你会发现它继承了bash的输入输出,也占用了相同的 /dev/pts/0:

$ lsof /dev/pts/0
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 28128 skyline 0u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 1u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 2u CHR 136,0 0t0 3 /dev/pts/0
bash 28128 skyline 255u CHR 136,0 0t0 3 /dev/pts/0
python 28174 skyline 0u CHR 136,0 0t0 3 /dev/pts/0
python 28174 skyline 1u CHR 136,0 0t0 3 /dev/pts/0
python 28174 skyline 2u CHR 136,0 0t0 3 /dev/pts/0

如果我们直接用echo往 /dev/pts/0 打印,是可以直接打到命令行的。


结尾


终于浅显地把打印Hello World的流程说完了,限于篇幅,这篇文章内容比较粗糙,包括Linux内核里面的细节,以及终端模拟器的实现,也就是字符具体是怎么被打印到屏幕上的,都没有涉及。感兴趣的大家把这篇文章当成引子,自己去探索。希望对大家有用吧。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
小刘
作者分享
对象是如何被创建的?
23
对象是如何被创建的?
23
入职不是上岸,学习没有终点 看到最近阿里又在裁员的消息,让我也想起了自己当年被阿里“拥抱变化”的故事。我人生中第一份实习来自阿里,踩的第一个“坑”就来自这份实习。 15年暑假我通过面试拿到了杭州天猫的iOS实习岗位,从哈尔滨飞到杭州,开启了人生中第一段实习。当时人也比较年轻,又进入了所谓的大厂,说实话是有一点飘的,当时已经决定以后留在阿里,留在杭州发展。 顺利入职之后,我开始了正常的实习工作,在天猫也确实学到了很多。美好的未来几乎快要在眼前慢慢展开了。结果实习三个月后,一封发给所有实习生的邮件,给了我当头一棒。马总这封题为“拥抱变化”的邮件,通知大家当年的实习生裁撤比例会超过往年,大批的实习生将不会留下。我成为了没有被留下的大多数人。 现在想想,实习留不下其实是一个非常正常的事。但对于当时涉世不深的我来说,这事儿仿佛是一个天大的打击。我非常慌张地在杭州又待了一个月,期间找到了网易在内的一些公司,参加了它们的面试看看有没有机会。不过由于一直在实习工作,事发突然,也没有什么准备时间,面试状态不好,各种面试都没有通过。眼看在杭州没有什么机会了,我只能灰溜溜地回到了河北老家。 当时在家里,整个人其实都是颓废的。说白了之前的路太顺了,没受过这种挫折。幸亏有爸妈的鼓励,让我重新找到了动力。经过一些面试,我找到了北京一家中小公司的iOS岗位,然后起身去北京实习。 小公司虽然比不上阿里,但是也能学到不少东西,在这家小公司里,我认识了后来帮了我很多的前辈,也学到了不少有用的技术知识。虽然小公司给的工资不如阿里,也没有所谓大厂“光环”,但是我心想着,起码是个工作,留在北京也不错。结果两个月之后,现实又给了我当头第二棒。这家小公司也没有让我留下,给出的理由到现在我都印象深刻:“我没有足够的正能量”。 怎么回事呢?后来我反思了一下自己,在技术层面大家对我都是认可的,但是在职场层面,当时的我年少轻狂,经常直接指出别人的代码和设计上的问题,语气上非常不留情面,有时候还会带着情绪。这样的表现,在小团队里或许确实是难以接受的。大家也可以自查一下,有没有类似的问题,如果有的话还是要尽快调整自己。 又没有留下,当时我已经开始怀疑自己了,为什么自己这么差呢?怎么办呢?没有办法,只能重新准备面试,后来经过几番辗转,最后我通过了猿题库(猿辅导)的面试。当时HR还是想让我实习一段,我以时间不充足为例拒绝了。而实际上,我是因为对实习这事有一些阴影了。毕业之后我入职了猿辅导,工作能力也没有让领导失望,承接了若干重要工作,我的职场之路终于算是顺利了一些。 说了这么多我的故事,我想有这么几点是可以对大家有用的: 1.挫折教育是我们的教育体系内缺失的一环。对没有背景的普通人来说,人生中十有八九是不如意的,越早认清这点,越早能避免挫折带来的负面影响,轻装上路。 2.否认自己不如提升自己。陷入到否认自己的情绪中,只会让自己更加焦虑。行动起来,才是缓解焦虑的最佳方法。 3.入职不是上岸。不管是实习还是正式,都有可能面临裁撤的风险,提升自己的能力,才能让自己有更宽广的选择空间。 4.学习没有终点。星球内很多同学都会提到在学校感觉很卷,而职场的卷想必大家都感受到了,强如阿里也会有被裁的风险。这种卷带来的影响就是,不学习就会落后。因此对未来保持适当的警惕感,能更加激励自己学习。 5.社会比职场还卷。像我们的鱼总已经不满足于在程序员这个群体卷了,先是进入自媒体领域和一帮博主卷,现在又开始创业,和更多对手展开商业上的竞争。鱼总能做到这点,背后付出的是很多人想象不到的努力。从这个角度看,鱼总确实是我们每个人的学习榜样。 最后送给大家一句忘了在哪儿看到的话: 当你对未来完全没有焦虑的时候,你应该有所担心。当你对未来过于焦虑的时候,你应该放宽心态。听起来很矛盾,但是很多事情就是要在黑和白中,找到那个灰。在焦虑和躺平中,找到每个人自己觉得最舒服的平衡点。 共勉。 #求职# #职场# #经验#
220
入职不是上岸,学习没有终点 看到最近阿里又在裁员的消息,让我也想起了自己当年被阿里“拥抱变化”的故事。我人生中第一份实习来自阿里,踩的第一个“坑”就来自这份实习。 15年暑假我通过面试拿到了杭州天猫的iOS实习岗位,从哈尔滨飞到杭州,开启了人生中第一段实习。当时人也比较年轻,又进入了所谓的大厂,说实话是有一点飘的,当时已经决定以后留在阿里,留在杭州发展。 顺利入职之后,我开始了正常的实习工作,在天猫也确实学到了很多。美好的未来几乎快要在眼前慢慢展开了。结果实习三个月后,一封发给所有实习生的邮件,给了我当头一棒。马总这封题为“拥抱变化”的邮件,通知大家当年的实习生裁撤比例会超过往年,大批的实习生将不会留下。我成为了没有被留下的大多数人。 现在想想,实习留不下其实是一个非常正常的事。但对于当时涉世不深的我来说,这事儿仿佛是一个天大的打击。我非常慌张地在杭州又待了一个月,期间找到了网易在内的一些公司,参加了它们的面试看看有没有机会。不过由于一直在实习工作,事发突然,也没有什么准备时间,面试状态不好,各种面试都没有通过。眼看在杭州没有什么机会了,我只能灰溜溜地回到了河北老家。 当时在家里,整个人其实都是颓废的。说白了之前的路太顺了,没受过这种挫折。幸亏有爸妈的鼓励,让我重新找到了动力。经过一些面试,我找到了北京一家中小公司的iOS岗位,然后起身去北京实习。 小公司虽然比不上阿里,但是也能学到不少东西,在这家小公司里,我认识了后来帮了我很多的前辈,也学到了不少有用的技术知识。虽然小公司给的工资不如阿里,也没有所谓大厂“光环”,但是我心想着,起码是个工作,留在北京也不错。结果两个月之后,现实又给了我当头第二棒。这家小公司也没有让我留下,给出的理由到现在我都印象深刻:“我没有足够的正能量”。 怎么回事呢?后来我反思了一下自己,在技术层面大家对我都是认可的,但是在职场层面,当时的我年少轻狂,经常直接指出别人的代码和设计上的问题,语气上非常不留情面,有时候还会带着情绪。这样的表现,在小团队里或许确实是难以接受的。大家也可以自查一下,有没有类似的问题,如果有的话还是要尽快调整自己。 又没有留下,当时我已经开始怀疑自己了,为什么自己这么差呢?怎么办呢?没有办法,只能重新准备面试,后来经过几番辗转,最后我通过了猿题库(猿辅导)的面试。当时HR还是想让我实习一段,我以时间不充足为例拒绝了。而实际上,我是因为对实习这事有一些阴影了。毕业之后我入职了猿辅导,工作能力也没有让领导失望,承接了若干重要工作,我的职场之路终于算是顺利了一些。 说了这么多我的故事,我想有这么几点是可以对大家有用的: 1.挫折教育是我们的教育体系内缺失的一环。对没有背景的普通人来说,人生中十有八九是不如意的,越早认清这点,越早能避免挫折带来的负面影响,轻装上路。 2.否认自己不如提升自己。陷入到否认自己的情绪中,只会让自己更加焦虑。行动起来,才是缓解焦虑的最佳方法。 3.入职不是上岸。不管是实习还是正式,都有可能面临裁撤的风险,提升自己的能力,才能让自己有更宽广的选择空间。 4.学习没有终点。星球内很多同学都会提到在学校感觉很卷,而职场的卷想必大家都感受到了,强如阿里也会有被裁的风险。这种卷带来的影响就是,不学习就会落后。因此对未来保持适当的警惕感,能更加激励自己学习。 5.社会比职场还卷。像我们的鱼总已经不满足于在程序员这个群体卷了,先是进入自媒体领域和一帮博主卷,现在又开始创业,和更多对手展开商业上的竞争。鱼总能做到这点,背后付出的是很多人想象不到的努力。从这个角度看,鱼总确实是我们每个人的学习榜样。 最后送给大家一句忘了在哪儿看到的话: 当你对未来完全没有焦虑的时候,你应该有所担心。当你对未来过于焦虑的时候,你应该放宽心态。听起来很矛盾,但是很多事情就是要在黑和白中,找到那个灰。在焦虑和躺平中,找到每个人自己觉得最舒服的平衡点。 共勉。 #求职# #职场# #经验#
215
如何处理和领导的关系? 看到星球里面有人发了刚入职不久被领导骂的帖子,也看到了小林兄的文章,有感而发,趁这个机会也想说两句。 首先一个很残酷的事实是,和进入社会之后的大部分选择一样,能不能碰到一个好领导这事儿,具有相当大的随机性。你可能在校招的时候选择了一家大厂,但是却分到了一个喜欢PUA的领导。或者反过来进入了一个中小厂,但是领导本身人还不错,愿意帮助你成长。 对于刚毕业的同学,这种随机性带来的影响是比较大的,因为刚进入一家公司,不管是技术经验还是话语权上,都处于弱势。再加上现在就业环境确实不好,也很难有人因为被领导骂两句就拍拍屁股走人,放弃来之不易的岗位。顺便提一句,和人生中可能面临的各种曲折来说,被骂两句真的不是什么特别重大的事儿。 我能给大家的建议有两个,第一是抽离自己的主观感受和情绪,从客观的角度思考一下,为什么会被骂?是自己能力确实没有不够,还是领导本身的要求就是过分的?或者两者都有?思考清楚这一点,才能矫正自己的心态,找到以后努力的方向。 如果是自己能力确实没有达到领导的预期,那就要快速地做出反应,提升自己的技术积累。我记得我刚入职的时候,第一个做的任务完成的也并不出色,来回来去改了很多遍,最后还是没有达到领导的要求。所幸领导很有耐心,最后他自己上手写完了那个模块,那件事之后我深刻意识到了自己的水平欠缺,学习的时候紧迫感更强了,也更有动力了,提升的速度也相当快。抗压能力,以及压力之下的学习能力,本身也是一种非常重要的软技能。大家在工作几年之后,对这点理解会更加深刻。 如果领导本身的要求就是过分的,我给出的第二个建议就是,勇敢地去和领导沟通。大家都是打工人,职级上有高低,人格上并无贵贱。领导可能会对某个人有不满情绪,但是本质上他的终极目标也只是完成工作任务。只要你的沟通内容是合理的,正向的,有助于公司发展的,就可以大胆地表达自己的意见,坦诚地提出自己的需求。如果领导愿意采纳,当然是最好的,没有采纳也没关系,就当是锻炼自己的沟通能力了。沟通和表达能力,也是职场中非常重要的软技能,表达能力强了之后,你甚至可以像我们这些嘉宾一样,搞个副业做自媒体[机智] 这篇文章结尾,为了呼应一下开头,我想再聊聊职场的残酷。做自媒体之后,有很多大学生朋友咨询我,我都会劝他们,好好珍惜大学时光,因为那可能是一生中时间最自由,精力最充沛,学东西最快的阶段了。工作之后不管是生计带来的压力,选择带来的焦虑,以及生活的各种琐事,都会让我们更难找到时间提升自己。没有办法,人总是要长大的,希望看到这篇文章的朋友,都能着眼未来,珍惜现在。 共勉。 #职场# #求职# #经验#
50
下载 APP