四大主流编译语言深度解析:C、C++、Go、Rust技术特性全景比对

编译语言

四大主流编译语言深度解析:C、C++、Go、Rust技术特性全景比对

在编程领域,编译语言凭借高效的执行性能、严谨的内存控制,长期占据系统开发、底层架构、高性能服务等核心场景。C、C++ 作为经典老牌编译语言,奠定了现代编程的基础;Go、Rust 则作为后起之秀,针对新时代开发痛点(如并发安全、内存安全)进行了革新性设计。本文将从语言定位、核心特性、性能效率、内存管理、并发模型、生态场景等核心维度,对这四大主流编译语言进行全方位对比,帮你清晰认知各语言的优势与适用场景,为技术选型提供参考。

一、语言定位:各自的核心使命与设计初衷

维度 C C++ Go Rust
设计年代 1972 1985 2009 2010
核心哲学 极致简洁、直接控制硬件 零成本抽象、向后兼容 简洁高效、快速编译 内存安全、零成本抽象
定位 系统编程基石 高性能通用系统编程 云原生、高并发服务 安全关键型系统编程
适用层级 操作系统、驱动、嵌入式 游戏引擎、高频交易、大型软件 微服务、DevOps工具、云基础设施 区块链、浏览

每一门语言的诞生,都对应着特定的时代需求和开发场景,定位的差异决定了它们的技术侧重和适用边界。

– C语言:诞生于1972年,核心定位是“系统级编程语言”,初衷是为了编写UNIX操作系统,追求 极致简洁、高效、可移植。它摒弃了高级语言的冗余特性,贴近硬件底层,能直接操作内存和CPU指令,是连接硬件与软件的“桥梁”,也是后续众多语言(包括C++、Go)的设计基础。

– C++:在C语言基础上于1983年诞生,定位是“兼容C的通用型编译语言”,核心目标是 在保持C语言高效性的同时,引入面向对象编程(OOP)特性,解决C语言在大型项目中代码复用、模块化不足的问题。它兼容C语言的所有语法,同时新增类、继承、多态等特性,兼顾底层控制与高层抽象。

– Go语言:由Google于2009年推出,定位是“云原生时代的高性能并发编程语言”,初衷是解决大型分布式系统中“高并发、低延迟、易维护”的痛点。它简化了语法,摒弃了复杂的OOP特性(如继承),内置并发模型,主打“简单、高效、易部署”,适配云计算、微服务等场景。

– Rust语言:由Mozilla于2010年稳定发布,定位是“安全、高效的系统级编程语言”,核心使命是 解决C/C++的内存安全问题,同时保持与C/C++相当的性能。它通过独特的所有权机制、借用规则,在编译期杜绝内存泄漏、空指针、数据竞争等问题,兼顾底层控制与安全,适配嵌入式、操作系统、区块链等对安全和性能要求极高的场景。

二、核心特性:语法与设计的关键差异

特性 C C++ Go Rust
模块系统 头文件包含 头文件/模块(C++20) package module(2018 edition)
可见性控制 static关键字 public/private等 首字母大小写 pub关键字
接口抽象 函数指针 抽象类、虚函数 interface trait
包管理 无标准 无标准(多种方案) 内置go mod 内置Cargo
编译时检查 基本类型检查,无内存安全检查 类型检查强于C,模板元编程可在编译期计算 类型检查强,但1.18之前无泛型,表达能力受限 最强编译时检查,包括生命周期、所有权、并发安全

四大语言的核心特性,反映了它们的设计哲学——C追求简洁可控,C++追求兼容与灵活,Go追求简单高效,Rust追求安全与性能的平衡。

2.1 语法特性

– C语言:语法极简,无面向对象、无泛型、无垃圾回收,仅包含基本数据类型(int、char、float等)、指针、数组、函数和结构体。代码简洁紧凑,学习门槛低,但编写大型项目时需手动管理所有细节,代码复用性差。

– C++:兼容C语法,新增面向对象三大特性(封装、继承、多态),支持泛型(模板)、异常处理、命名空间、STL标准库等。语法灵活度极高,可根据需求选择“面向过程”或“面向对象”编程,但灵活性也带来了复杂度,学习门槛高,容易写出难以维护的代码。

– Go语言:语法极简,摒弃了继承、多态、泛型(早期不支持,后期新增基础泛型)、异常处理等复杂特性,采用“结构体+接口”实现面向对象思想,支持函数多返回值、defer延迟执行、切片(Slice)、映射(Map)等实用特性。代码可读性强,上手快,注重“约定优于配置”。

– Rust语言:语法借鉴了C++和Go,支持泛型、 traits(类似接口)、模式匹配、错误处理(Result/Option类型)等特性,核心是“所有权机制”(每个值有且仅有一个所有者,所有者生命周期结束后自动释放内存)。语法严谨,编译检查严格,上手门槛较高,但一旦掌握,能写出安全且高效的代码。

2.2 关键设计亮点

– C语言:指针操作灵活,能直接访问内存地址,可移植性强(几乎支持所有硬件平台),代码编译后体积小、执行速度快,是底层开发的“基石”。

– C++:支持“零成本抽象”——引入的面向对象、泛型等特性不会带来额外的性能开销,兼顾底层控制与高层抽象,STL标准库提供了丰富的数据结构和算法,大幅提升开发效率。

– Go语言:内置goroutine(轻量级线程,占用内存少、切换成本低)和channel(管道),实现“基于通信的并发模型”,解决了传统多线程的锁竞争问题,能轻松支撑高并发场景;编译速度快,生成单一可执行文件,部署简单(无需依赖运行时)。

– Rust语言:所有权机制+借用规则,在编译期解决内存安全问题,无需垃圾回收,也无需手动管理内存(避免了C/C++的内存泄漏、野指针);支持“零成本抽象”,性能与C/C++相当,同时支持并发安全(编译期检查数据竞争)。

三、类型系统与安全性:从灵活到严谨的演进

特性 C C++ Go Rust
类型安全 弱类型 强类型(可显式绕过) 强类型 强类型(编译时强制)
类型推断 有限(C++11 auto) 强(:=声明) 强(局部变量)
泛型支持 模板(编译时多态) 1.18+ 泛型 泛型 + trait约束
空安全 无(NULL) 无(nullptr, 仍可能空) 接口可nil Option(编译时检查)
默认不可变性
代数数据类型 无(可模拟) 有(enum模式匹配)
特性 C C++ Go Rust
主要机制 错误码/返回值 异常 多返回值(err模式) Result<T,E>枚举
优点 简单、明确 非侵入式错误传播 显式处理、简单 编译时强制处理、无开销
缺点 易忽略、无强制 性能开销、控制流模糊 冗长、易忽略错误检查 代码略显冗长

类型系统是编译语言的核心骨架,它决定了语言的表达能力、安全性和编译期的错误检测能力。四种语言在类型系统方面呈现出从弱到强的演进趋势,同时也各具特色。

3.1 C语言:弱类型与信任程序员的哲学

C语言以其“弱类型”特性著称,提供了高度的灵活性但缺乏足够的编译期保护。C语言允许各种隐式类型转换,允许指针的自由转换,允许数组退化为指针等行为。这些特性使得C语言能够高效地操作底层内存,但也为bug的滋生提供了温床。空指针解引用、缓冲区溢出、未初始化变量使用等常见错误在C语言中屡见不鲜。

C语言的类型检查主要依赖编译器的警告机制,而许多警告在默认配置下是不显示的。这意味着C程序员需要具备高度的风险意识,主动启用编译器的高级警告选项(如gcc的-Wall -Wextra),并严格遵守编码规范。静态分析工具(如Clang Static Analyzer、Cppcheck)可以在一定程度上弥补C语言类型系统的不足,但无法从根本上解决问题。

3.2 C++:强类型与复杂的模板元编程

C++在类型系统方面比C更为严格,引入了更丰富的类型修饰符和更完善的类型检查机制。C++还支持模板元编程,使得类型本身可以作为编译期的计算对象。然而,C++也继承了C的许多“灰色地带”,如隐式类型转换规则、拷贝构造函数的自动生成等,这些特性在不经意间可能导致性能问题或微妙的bug。

现代C++(C++11以后)引入了enum class、std::optional、std::variant等更安全的类型构造,显著提升了类型系统的表达能力。模板别名、变参模板、概念(Concepts,C++20)等特性使得泛型编程更加直观和类型安全。但与此同时,C++的复杂性也在不断增长,学习C++意味着需要持续跟进语言特性的演进,这是一项终身的事业。

3.3 Go语言:简洁强类型与接口的鸭子类型

Go语言采用简洁的强类型系统,变量必须有明确的类型声明(尽管可以使用类型推断)。Go的类型系统设计遵循“简单即美”的原则,刻意排除了一些复杂的特性——如传统的类继承体系。Go的接口(Interface)采用鸭子类型(Duck Typing)的语义:只要一个类型实现了接口定义的所有方法,它就自动满足该接口,无需显式声明。

Go 1.18引入了泛型支持,这是Go语言历史上最重要的特性更新之一。在此之前,Go程序员不得不用空接口(interface{})和类型断言来处理通用编程场景,这既不类型安全也不高效。Go的泛型实现采用了类型参数和类型约束的设计,在保持语言简洁性的同时提供了必要的泛型能力。然而,Go的泛型实现被认为过于保守,与C++的模板元编程相比,在表达能力和性能优化空间上仍有差距。

Go语言的另一个独特之处是对错误处理的设计。Go没有异常机制,而是通过返回error类型来处理错误。这种显式的错误处理方式虽然代码冗长,但使得错误流清晰可控,开发者无法忽略错误处理。defer、panic和recover机制则用于处理真正的异常情况。

3.4 Rust:极致类型安全与代数数据类型

Rust拥有四种语言中最强大的类型系统。Rust的类型系统基于代数数据类型(Algebraic Data Types),enum可以包含数据变体,Option和Result类型强制开发者处理可能为空或可能失败的情况。模式匹配(Pattern Matching)配合枚举使用,使得处理复杂状态逻辑既类型安全又表达力丰富。

Rust的借用检查器是其类型系统的核心组成部分,它不仅检查内存安全,还检查数据竞争。生命周期标注(’a、’static等)使得Rust能够精确管理引用有效期,这是Rust能够在没有GC的情况下保证内存安全的根本原因。Rust还提供了不安全代码(unsafe)块,允许在受控范围内绕过某些安全检查,以换取与C/C++相当的底层操作能力。

Rust的特质(Trait)系统提供了类似于接口的功能,但更加强大。特质可以包含默认实现、关联类型、泛型约束等高级特性。Rust 2018 edition引入的impl Trait和dyn Trait进一步丰富了类型系统的表达能力。总体而言,Rust的类型系统在安全性和表达力之间达到了新的平衡点。

四、性能效率:执行速度与编译速度对比

指标 C C++ Go Rust
执行速度 100% (基准) 100-130% 150-200% 100-105%
内存占用 极低 中等(GC 开销)
编译速度 极快 中等(模板膨胀问题) 极快 较慢(借用检查分析)
启动时间 极快
并发性能 需手动优化 需手动优化 优秀(goroutine) 优秀(零成本抽象)

编译语言的核心优势之一是高性能,四大语言的性能差异主要体现在执行速度、编译速度两个维度,具体表现与语言设计、内存管理方式密切相关。

4.1 执行速度

执行速度的核心影响因素是“内存管理方式”“是否有运行时开销”“代码优化程度”,四大语言的执行速度排序大致为:C ≈ C++ ≈ Rust > Go。

– C/C++/Rust:三者均无垃圾回收(Rust虽无需手动管理内存,但无GC运行时),能直接操作内存,编译期优化充分,执行速度几乎处于同一水平。其中,C语言因语法极简,无额外抽象开销,在极端场景下略占优势;Rust通过编译器优化,能达到与C/C++完全持平的性能;C++在开启O2/O3优化后,性能与C基本一致。

– Go语言:执行速度略低于前三者,核心原因是内置了垃圾回收(GC),GC运行时会带来轻微的性能开销(尤其是在高并发、大内存场景下)。但Go的GC经过多代优化,延迟已大幅降低,在大多数场景下(如微服务、API服务),性能完全能满足需求,且开发效率远高于C/C++/Rust。

4.2 编译速度

编译速度主要受“语法复杂度”“依赖管理”“编译器优化”影响,排序大致为:Go > C > C++ > Rust。

– Go语言:编译速度极快,这是其核心优势之一。原因是语法简单、无复杂模板、依赖管理简洁(采用模块机制),编译器优化针对性强,即使是大型项目,编译也能在几秒内完成。

– C语言:语法简单,无额外抽象,编译过程简单,编译速度较快,但随着项目规模增大、依赖增多,编译速度会有所下降。

– C++:编译速度较慢,核心原因是支持模板(模板实例化会增加编译开销)、语法复杂、头文件依赖繁琐,大型项目(如Chrome、Qt)编译可能需要几十分钟甚至几小时。

– Rust语言:编译速度最慢,因为编译器需要进行严格的安全检查(所有权、借用、数据竞争等),且泛型、traits等特性会增加编译复杂度,即使是小型项目,编译时间也可能比Go长几倍。

五、内存管理:安全与可控的平衡艺术

特性 C C++ Go Rust
管理方式 纯手动(malloc/free) 手动 + 智能指针 自动垃圾回收(GC) 所有权系统 + 生命周期检查
内存安全 无保障 依赖程序员经验 GC 保障,但存在 STW 停顿 编译期强制保证
悬空指针 常见 Bug 可能(野指针) GC 避免 编译期禁止
数据竞争 无保护 无保护 运行时检测 编译期禁止
运行时开销 零开销 零开销(raw ptr) GC 开销 零开销
确定性释放 完全确定 确定(RAII) 不确定 确定(Drop trait)
数据竞争预防 无编译时保护 无编译时保护(依赖规范) 无编译时保护(race detector) 编译时防止数据竞争
主要并发原语 手动同步(锁、信号量) 原子操作、互斥锁、future goroutine、channel、sync包 基于所有权的线程安全保证

内存管理是编译语言的核心痛点,也是四大语言差异最大的维度之一——不同的内存管理方式,决定了语言的安全性、开发效率和性能。

– C语言:手动内存管理,通过malloc/free函数手动分配和释放内存。优点是完全可控,无额外开销;缺点是极易出现内存泄漏(忘记free)、野指针(使用已释放的内存)、双重释放等问题,调试难度大,尤其是在大型项目中。

– C++:兼容C的手动内存管理(malloc/free),同时引入了“智能指针”(auto_ptr、shared_ptr、unique_ptr等),可实现半自动内存管理,减少内存安全问题。但智能指针仍存在使用门槛(如循环引用导致内存泄漏),且手动管理的部分依然可能出现安全隐患,整体内存安全性优于C,但远不如Rust。

– Go语言:自动内存管理(垃圾回收,GC),无需手动分配和释放内存,编译器自动跟踪内存使用情况,在合适的时机回收无用内存。优点是开发效率高,无需关注内存细节,减少内存安全问题;缺点是GC会带来轻微的性能开销,且无法完全避免内存泄漏(如循环引用)。

– Rust语言:编译期内存管理(所有权+借用规则),既无需手动管理内存,也无需垃圾回收。通过编译器检查所有权和借用规则,确保内存使用安全,当所有者生命周期结束时,内存自动释放。优点是内存安全(编译期杜绝内存泄漏、野指针),无GC开销,性能优异;缺点是学习门槛高,需要理解所有权、借用、生命周期等概念,编写代码时需遵循严格的规则。

六、并发模型:高并发场景的适配能力

维度 C/C++ Go Rust
并发原语 线程 + 锁(pthread/std::thread) Goroutine + Channel 线程 + 异步(async/await)
内存模型 宽松,需手动同步 CSP 模型,内存共享通过通信 所有权模型自动避免数据竞争
线程安全 无编译期保证 运行时保证 编译期保证(Send/Sync trait)
开发难度 高(易死锁、数据竞争) 低(语言级支持) 中(学习曲线陡峭但安全)
适用场景 细粒度控制 高并发服务 高性能并发系统

随着分布式系统、云原生的发展,并发能力成为编译语言的核心竞争力。四大语言的并发模型差异显著,适配不同的并发场景。

– C语言:无内置并发支持,需依赖操作系统的多线程(如POSIX线程pthread)或多进程实现并发。并发控制需手动使用互斥锁(mutex)、条件变量等,容易出现锁竞争、死锁等问题,开发难度大,适配高并发场景的成本高。

– C++:在C的基础上,通过STL提供了线程库(std::thread)、互斥锁(std::mutex)、条件变量等,支持多线程并发。但本质上仍是“基于共享内存的并发模型”,需手动管理锁,同样存在锁竞争、死锁等问题,并发开发复杂度高,适合对性能要求极高但并发量不极端的场景(如游戏引擎、高性能计算)。

– Go语言:内置“基于通信的并发模型”,核心是goroutine和channel。goroutine是轻量级线程(每个goroutine占用约2KB内存,可同时创建数十万甚至数百万个),切换成本远低于操作系统线程;channel用于goroutine之间的通信,实现“无锁并发”,避免了锁竞争问题。开发难度低,能轻松支撑高并发场景(如微服务、消息队列、Web服务器),是Go语言最核心的优势之一。

– Rust语言:支持多种并发模型,包括多线程、异步编程(async/await),核心优势是“并发安全”。通过所有权机制和借用规则,编译期检查数据竞争,确保多线程并发时的内存安全,无需手动管理锁(但仍可手动使用锁实现更灵活的并发控制)。同时,Rust的异步编程无运行时开销,性能优于Go的异步,适合对并发安全和性能要求极高的场景(如区块链、高性能服务器)。

七、生态与适用场景:各有所长,精准选型

维度 C C++ Go Rust
包管理器 无标准(Makefile/CMake) 无标准(Conan/vcpkg 尝试统一) 内置(go modules) 内置(Cargo)
构建系统 Make/CMake CMake/Bazel go build Cargo
编译器 GCC/Clang/MSVC GCC/Clang/MSVC GC rustc(LLVM 后端)
标准库 极小(libc) 庞大(STL + Boost) 丰富(网络、并发内置) 丰富(零成本抽象)
IDE 支持 基础 优秀(CLion/VS) 优秀(VS Code/GoLand) 优秀(rust-analyzer)
学习曲线 中(指针难) 陡峭(模板、元编程) 平缓 陡峭(所有权系统)

语言的生态成熟度和适用场景,决定了它在实际开发中的落地能力。四大语言的生态各有侧重,适配不同的行业和项目类型。

7.1 生态成熟度

– C语言:生态极其成熟,诞生几十年,拥有大量的开源库和工具(如OpenSSL、MySQL底层),几乎支持所有硬件平台,是底层开发的“标配”。但生态相对老旧,缺乏现代开发所需的便捷工具(如包管理工具)。

– C++:生态同样成熟,STL标准库功能强大,拥有大量开源框架(如Qt、Boost、Chrome内核),覆盖游戏、桌面应用、高性能计算等多个领域。但生态复杂度高,不同版本的编译器、库之间兼容性较差。

– Go语言:生态发展迅速,由Google主导,拥有丰富的官方库和第三方库(如Gin、Echo、Kubernetes),主打云原生、微服务、Web开发,工具链完善(如go mod包管理、go test测试工具),社区活跃。

– Rust语言:生态处于快速发展阶段,拥有 Cargo 包管理工具、Rustup 版本管理工具,第三方库数量不断增加(如Tokio异步框架、Actix Web服务器),社区活跃,但整体生态规模仍不及C/C++/Go,部分领域(如桌面应用)的库相对薄弱。

7.2 适用场景

– C语言:适合底层开发,如操作系统内核(Linux、Windows内核部分)、嵌入式系统(单片机、物联网设备)、驱动程序、数据库底层(MySQL、PostgreSQL内核)等,追求极致性能和内存可控的场景。

– C++:适合对性能和灵活性要求高的场景,如游戏引擎(Unreal Engine、Unity底层)、桌面应用(Qt开发)、高性能计算(科学计算、人工智能训练框架底层)、浏览器内核等,可兼顾底层控制与高层抽象。

– Go语言:适合云原生、高并发场景,如微服务(Kubernetes、Docker)、Web服务器(Gin、Echo)、消息队列(RabbitMQ客户端)、分布式系统等,追求开发效率和并发能力的平衡。

– Rust语言:适合对安全和性能要求极高的场景,如操作系统(Redox OS)、嵌入式系统(安全物联网设备)、区块链(Solana、Polkadot)、高性能服务器、加密货币等,解决C/C++的内存安全问题。

八、总结:如何选择适合自己的编译语言?

评估维度 推荐排序(降序)
极致性能 C ≈ Rust ≈ C++ > Go
开发效率 Go > Rust > C++ > C
内存安全 Rust > Go > C++ > C
系统控制 C > C++ ≈ Rust > Go
并发安全 Rust > Go > C++ > C
生态成熟度 C++ > Go > C > Rust
长期可维护性 Rust > Go > C++ > C

四大主流编译语言没有绝对的“优劣之分”,只有“适配与否”,结合自身需求和场景,才能做出最优选择:

1. 如果做底层开发、嵌入式、操作系统,追求极致性能和内存可控,选 C语言;若需要兼顾面向对象和代码复用,选 C++。

2. 如果做云原生、微服务、Web开发、高并发服务,追求开发效率和并发能力,选 Go语言,上手快、部署简单,能快速落地项目。

3. 如果做安全敏感、高性能的场景(如区块链、嵌入式安全、高性能服务器),需要杜绝内存安全问题,选 Rust语言,虽然学习门槛高,但能大幅降低后期维护成本。

从发展趋势来看,Go语言凭借其简单高效的特性,在云原生领域的地位持续提升;Rust语言则凭借内存安全和高性能,逐渐替代C/C++在部分安全敏感场景的应用;而C/C++作为经典语言,仍将在底层开发、高性能计算等领域长期占据主导地位。

无论选择哪门语言,核心都是“用合适的工具解决合适的问题”,掌握其核心设计哲学和技术特性,才能真正发挥语言的优势。

一文看懂JVM核心架构:拆解 “搬运工、仓库、加工厂、对外窗口”

JVM核心功能:
JVM核心功能


一文看懂JVM核心架构:拆解 “搬运工、仓库、加工厂、对外窗口”

想搞懂 Java 为什么能 “一次编写,到处运行”?核心就在 JVM这个 “隐形容器” 里。
为了让复杂的架构更易理解,咱们把 JVM 拆解成 “搬运工”、“仓库”、“加工厂”、“对外窗口” 四大核心模块,带你完整看懂 JVM 的工作逻辑。

1. 类加载器(搬运工:Class Loader)
JVM 要运行代码,首先得把硬盘上的 .class 字节码文件 “搬” 进内存 —— 这就是类加载器的核心任务。
加载策略:不搞 “一次性搬运”,而是按需动态加载,用到哪个类才加载哪个,减少启动时的内存占用。
核心机制:严格遵循 “双亲委派模型”—— 搬运前先问 “上级”(父类加载器)有没有搬过,避免 String 这类核心类被自定义同名类冒充,保证类的唯一性和安全性。
完整流程:加载→验证→准备→解析→初始化,每一步都有严格校验,比如字节码验证会杜绝非法指令,防止恶意代码入侵。
加载器分类:启动类加载器(加载系统核心类)、扩展类加载器(加载扩展库)、应用类加载器(加载项目代码)、自定义类加载器(满足特殊需求),各司其职。

2. 运行时数据区(仓库:Runtime Data Areas)
这是 JVM 存储数据的 “核心仓库”,所有程序运行时的数据都在这里流转,按归属分为 “线程共享” 和 “线程私有” 两类,避免数据混乱。

区域 归属 核心作用 关键特性
堆 (Heap) 线程共享 存放所有 new 出来的对象实例,是最大的内存区域 GC(垃圾回收)的主要战场,所有对象存活与回收都在这里发生
方法区 (Method Area) 线程共享 存储类元数据(类结构、属性、方法信息)、常量池、静态变量 相当于 “类的图纸仓库”,提供对象创建的模板
虚拟机栈 (Stack) 线程私有 存放局部变量,每个方法执行对应一个 “栈帧”(包含参数、返回值、局部变量) 方法执行时入栈,执行完毕后出栈,自动释放内存,不会产生垃圾
程序计数器 (PC) 线程私有 记录当前线程执行的指令位置 像 “导航指针”,CPU 切换线程后能快速恢复执行,避免 “迷路”
本地方法栈 线程私有 为 JNI 调用的本地方法(如 C/C++ 编写的方法)提供内存支持 与虚拟机栈功能类似,专门服务本地方法调用

3. 执行引擎(加工厂:Execution Engine)
内存里的字节码是 “中间指令”,CPU 看不懂 —— 执行引擎就是把字节码翻译成机器码的 “加工厂”,同时负责内存清理,保障运行效率。

双引擎协作:
解释器:逐行翻译字节码,翻译一句执行一句,启动快但执行慢,适合低频代码;
JIT 编译器(即时编译):识别 “热点代码”(频繁执行的代码),一次性整块编译成机器码并缓存,后续直接复用,大幅提升执行速度。
垃圾回收器 (GC):“仓库清洁工”,自动识别堆内存中不再被引用的对象,通过分代收集、标记 – 清除、标记 – 复制、标记 – 整理等算法回收内存,支持 SerialGC、ParallelGC、CMS、G1、ZGC 等多种回收器,适配不同性能需求。
同步与锁机制:为多线程并发保驾护航,提供偏向锁、轻量级锁、重量级锁、自旋锁等多级锁优化,结合 monitor 监视器与 synchronized 底层实现,平衡并发安全与执行效率。

4. 本地接口与跨平台支持(对外窗口:Native & Cross-Platform)
Java 无法直接操作底层硬件和系统,“对外窗口” 负责打通 Java 与外部的连接,同时实现跨平台特性。
JNI(本地方法接口):Java 与底层系统的 “翻译官”,通过调用本地库方法,实现 I/O 操作、硬件交互等 Java 本身无法完成的功能。
I/O 优化机制:支持堆内缓冲区与直接缓冲区(Direct Buffer),搭配 I/O 多路复用、内存映射(mmap)技术,减少数据拷贝,提升读写效率。
跨平台核心:抽象虚拟运行环境,隔离字节码与底层硬件 / 操作系统差异,不管是 Windows、Linux 还是 macOS,都能通过对应的 JVM 解析执行,实现 “一次编写,到处运行”。

5. 异常处理与安全机制(防护盾:Protection)
JVM 内置 “防护盾”,保障程序稳健运行,抵御恶意攻击。
异常处理:通过 athrow 字节码指令触发异常,依托 Throwable 及其子类(Error、Exception)构建异常链,异常表存储捕获 / 处理信息,让程序在出错时能优雅响应,而非直接崩溃。
安全沙箱:通过安全管理器限制系统资源(文件、网络、内存)访问,结合类加载验证、字节码校验,防止核心 API 被篡改,抵御恶意代码入侵。

6. 性能监控与调优(优化器:Optimization)
JVM 提供丰富的监控工具和调优参数,帮你排查性能问题,让程序跑得更快更稳。
监控工具接口:JVM TI(JVM Tool Interface)替代早期的 JVMPI,支持第三方监控工具(如 JConsole、VisualVM)接入,实时采集运行数据。
核心监控指标:GC 日志(回收次数、耗时)、线程状态(运行、阻塞、等待)、内存使用量(堆 / 方法区占用)、类加载统计(加载 / 卸载数量),全面掌握 JVM 运行状态。
常用调优参数:配置堆大小(-Xms 初始堆、-Xmx 最大堆)、选择 GC 收集器(-XX:+UseG1GC)、调整 JIT 编译阈值(-XX:CompileThreshold)、设置直接缓冲区大小(-XX:MaxDirectMemorySize)等,按需优化性能。

总结 JVM 完整工作流程:
类加载器按 “双亲委派模型”,按需加载 .class 文件到方法区;
执行引擎通过解释器 / JIT 编译器,将方法区的字节码翻译成机器码;
程序运行时,虚拟机栈存储局部变量与栈帧,堆创建对象实例,程序计数器维护执行位置;
多线程并发时,锁机制保障数据安全,GC 实时清理堆内无用对象;
需底层操作时,通过 JNI 调用本地方法,I/O 优化机制提升数据传输效率;
异常发生时,异常链与异常表处理错误,安全机制抵御恶意攻击;
借助监控工具与调优参数,持续优化 JVM 运行性能。

GoLang实现跨平台的一些技巧03

以新建文件为例,对比一下几个常见平台的区别。

继续看下MacOS平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/zsyscall_darwin_amd64.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 调用syscall
	r0, _, e1 := syscall(abi.FuncPCABI0(libc_open_trampoline), uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(perm))
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_darwin.go
func syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

// internal/abi/funcpc.go
func FuncPCABI0(f interface{}) uintptr

// syscall/zsyscall_darwin_amd64.go
func libc_open_trampoline()
//go:cgo_import_dynamic libc_open open "/usr/lib/libSystem.B.dylib"

// 先通过abi.FuncPCABI0(libc_open_trampoline)先获取到open函数的地址
// 然后通过syscall调用open函数
// open函数是libc标准库中的函数,C语言定义为
int open(const char *pathname, int flags, mode_t mode);
//syscall在这里实现

//runtime/sys_darwin.go
//go:linkname syscall_syscall syscall.syscall
//go:nosplit
func syscall_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
	args := struct{ fn, a1, a2, a3, r1, r2, err uintptr }{fn, a1, a2, a3, r1, r2, err}
	entersyscall()
	//跳转到libcCall
	libcCall(unsafe.Pointer(abi.FuncPCABI0(syscall)), unsafe.Pointer(&args))
	exitsyscall()
	return args.r1, args.r2, args.err
}
func syscall()

// runtime/sys_libc.go
func libcCall(fn, arg unsafe.Pointer) int32 {
	// Leave caller's PC/SP/G around for traceback.
	gp := getg()
	var mp *m
	if gp != nil {
		mp = gp.m
	}
	if mp != nil && mp.libcallsp == 0 {
		mp.libcallg.set(gp)
		mp.libcallpc = getcallerpc()
		// sp must be the last, because once async cpu profiler finds
		// all three values to be non-zero, it will use them
		mp.libcallsp = getcallersp()
	} else {
		// Make sure we don't reset libcallsp. This makes
		// libcCall reentrant; We remember the g/pc/sp for the
		// first call on an M, until that libcCall instance
		// returns.  Reentrance only matters for signals, as
		// libc never calls back into Go.  The tricky case is
		// where we call libcX from an M and record g/pc/sp.
		// Before that call returns, a signal arrives on the
		// same M and the signal handling code calls another
		// libc function.  We don't want that second libcCall
		// from within the handler to be recorded, and we
		// don't want that call's completion to zero
		// libcallsp.
		// We don't need to set libcall* while we're in a sighandler
		// (even if we're not currently in libc) because we block all
		// signals while we're handling a signal. That includes the
		// profile signal, which is the one that uses the libcall* info.
		mp = nil
	}
	// 跳转到asmcgocall
	res := asmcgocall(fn, arg)
	if mp != nil {
		mp.libcallsp = 0
	}
	return res
}

// 硬件平台相关代码
// runtime/asm_arm64.s
// func asmcgocall(fn, arg unsafe.Pointer) int32
// Call fn(arg) on the scheduler stack,
// aligned appropriately for the gcc ABI.
// See cgocall.go for more details.
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
	MOVD	fn+0(FP), R1
	MOVD	arg+8(FP), R0

	MOVD	RSP, R2		// save original stack pointer
	CBZ	g, nosave
	MOVD	g, R4

	// Figure out if we need to switch to m->g0 stack.
	// We get called to create new OS threads too, and those
	// come in on the m->g0 stack already. Or we might already
	// be on the m->gsignal stack.
	MOVD	g_m(g), R8
	MOVD	m_gsignal(R8), R3
	CMP	R3, g
	BEQ	nosave
	MOVD	m_g0(R8), R3
	CMP	R3, g
	BEQ	nosave

	// Switch to system stack.
	MOVD	R0, R9	// gosave_systemstack_switch<> and save_g might clobber R0
	BL	gosave_systemstack_switch<>(SB)
	MOVD	R3, g
	BL	runtime·save_g(SB)
	MOVD	(g_sched+gobuf_sp)(g), R0
	MOVD	R0, RSP
	MOVD	(g_sched+gobuf_bp)(g), R29
	MOVD	R9, R0

	// Now on a scheduling stack (a pthread-created stack).
	// Save room for two of our pointers /*, plus 32 bytes of callee
	// save area that lives on the caller stack. */
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	R4, 0(RSP)	// save old g on stack
	MOVD	(g_stack+stack_hi)(R4), R4
	SUB	R2, R4
	MOVD	R4, 8(RSP)	// save depth in old g stack (can't just save SP, as stack might be copied during a callback)
	BL	(R1)
	MOVD	R0, R9

	// Restore g, stack pointer. R0 is errno, so don't touch it
	MOVD	0(RSP), g
	BL	runtime·save_g(SB)
	MOVD	(g_stack+stack_hi)(g), R5
	MOVD	8(RSP), R6
	SUB	R6, R5
	MOVD	R9, R0
	MOVD	R5, RSP

	MOVW	R0, ret+16(FP)
	RET

nosave:
	// Running on a system stack, perhaps even without a g.
	// Having no g can happen during thread creation or thread teardown
	// (see needm/dropm on Solaris, for example).
	// This code is like the above sequence but without saving/restoring g
	// and without worrying about the stack moving out from under us
	// (because we're on a system stack, not a goroutine stack).
	// The above code could be used directly if already on a system stack,
	// but then the only path through this code would be a rare case on Solaris.
	// Using this code for all "already on system stack" calls exercises it more,
	// which should help keep it correct.
	MOVD	RSP, R13
	SUB	$16, R13
	MOVD	R13, RSP
	MOVD	$0, R4
	MOVD	R4, 0(RSP)	// Where above code stores g, in case someone looks during debugging.
	MOVD	R2, 8(RSP)	// Save original stack pointer.
	BL	(R1)
	// Restore stack pointer.
	MOVD	8(RSP), R2
	MOVD	R2, RSP
	MOVD	R0, ret+16(FP)
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧02

以新建文件为例,对比一下几个常见平台的区别。

继续看下Linux平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_unix.go

// openFileNolog的unix实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	setSticky := false
	if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
		if _, err := Stat(name); IsNotExist(err) {
			setSticky = true
		}
	}

	var r int
	var s poll.SysFile
	for {
		var e error
		//跳转到open
		r, s, e = open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
		if e == nil {
			break
		}

		// We have to check EINTR here, per issues 11180 and 39237.
		if e == syscall.EINTR {
			continue
		}

		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// open(2) itself won't handle the sticky bit on *BSD and Solaris
	if setSticky {
		setStickyBit(name)
	}

	// There's a race here with fork/exec, which we are
	// content to live with. See ../syscall/exec_unix.go.
	if !supportsCloseOnExec {
		syscall.CloseOnExec(r)
	}

	kind := kindOpenFile
	if unix.HasNonblockFlag(flag) {
		kind = kindNonBlock
	}

	// 封装为File结构
	f := newFile(r, name, kind)
	f.pfd.SysFile = s
	return f, nil
}
// os/file_open_unix.go

func open(path string, flag int, perm uint32) (int, poll.SysFile, error) {
	// 跳转到syscall.Open
	fd, err := syscall.Open(path, flag, perm)
	return fd, poll.SysFile{}, err
}
// syscall/syscall_linux.go

func Open(path string, mode int, perm uint32) (fd int, err error) {
	// 跳转到openat
	return openat(AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

//sys	openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)

// syscall/zsyscall_linux_amd64.go

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
	var _p0 *byte
	_p0, err = BytePtrFromString(path)
	if err != nil {
		return
	}
	// 跳转到Syscall6
	r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}
// syscall/syscall_linux.go

func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) {
	runtime_entersyscall()
	// 跳转到RawSyscall6
	r1, r2, err = RawSyscall6(trap, a1, a2, a3, a4, a5, a6)
	runtime_exitsyscall()
	return
}

// N.B. RawSyscall6 is provided via linkname by runtime/internal/syscall.
//
// Errno is uintptr and thus compatible with the runtime/internal/syscall
// definition.
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsysnum_linux_amd64.go
	SYS_OPENAT                 = 257

// RawSyscall6是通过汇编实现的,传入SYS_OPENAT,最终调用openat函数
// openat函数是libc标准库中的函数,C语言定义为
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
// runtime/internal/syscall/asm_linux_amd64.s

// Syscall6 的实现在这里
// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
//
// We need to convert to the syscall ABI.
//
// arg | ABIInternal | Syscall
// ---------------------------
// num | AX          | AX
// a1  | BX          | DI
// a2  | CX          | SI
// a3  | DI          | DX
// a4  | SI          | R10
// a5  | R8          | R8
// a6  | R9          | R9
//
// r1  | AX          | AX
// r2  | BX          | DX
// err | CX          | part of AX
//
// Note that this differs from "standard" ABI convention, which would pass 4th
// arg in CX, not R10.
TEXT ·Syscall6<ABIInternal>(SB),NOSPLIT,$0
	// a6 already in R9.
	// a5 already in R8.
	MOVQ	SI, R10 // a4
	MOVQ	DI, DX  // a3
	MOVQ	CX, SI  // a2
	MOVQ	BX, DI  // a1
	// num already in AX.
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok
	NEGQ	AX
	MOVQ	AX, CX  // errno
	MOVQ	$-1, AX // r1
	MOVQ	$0, BX  // r2
	RET
ok:
	// r1 already in AX.
	MOVQ	DX, BX // r2
	MOVQ	$0, CX // errno
	RET

// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(fd int, name string, kind newFileKind) *File {
	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         fd,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name:        name,
		stdoutOrErr: fd == 1 || fd == 2,
	}}

	pollable := kind == kindOpenFile || kind == kindPipe || kind == kindNonBlock

	// If the caller passed a non-blocking filedes (kindNonBlock),
	// we assume they know what they are doing so we allow it to be
	// used with kqueue.
	if kind == kindOpenFile {
		switch runtime.GOOS {
		case "darwin", "ios", "dragonfly", "freebsd", "netbsd", "openbsd":
			var st syscall.Stat_t
			err := ignoringEINTR(func() error {
				return syscall.Fstat(fd, &st)
			})
			typ := st.Mode & syscall.S_IFMT
			// Don't try to use kqueue with regular files on *BSDs.
			// On FreeBSD a regular file is always
			// reported as ready for writing.
			// On Dragonfly, NetBSD and OpenBSD the fd is signaled
			// only once as ready (both read and write).
			// Issue 19093.
			// Also don't add directories to the netpoller.
			if err == nil && (typ == syscall.S_IFREG || typ == syscall.S_IFDIR) {
				pollable = false
			}

			// In addition to the behavior described above for regular files,
			// on Darwin, kqueue does not work properly with fifos:
			// closing the last writer does not cause a kqueue event
			// for any readers. See issue #24164.
			if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && typ == syscall.S_IFIFO {
				pollable = false
			}
		}
	}

	clearNonBlock := false
	if pollable {
		if kind == kindNonBlock {
			// The descriptor is already in non-blocking mode.
			// We only set f.nonblock if we put the file into
			// non-blocking mode.
		} else if err := syscall.SetNonblock(fd, true); err == nil {
			f.nonblock = true
			clearNonBlock = true
		} else {
			pollable = false
		}
	}

	// An error here indicates a failure to register
	// with the netpoll system. That can happen for
	// a file descriptor that is not supported by
	// epoll/kqueue; for example, disk files on
	// Linux systems. We assume that any real error
	// will show up in later I/O.
	// We do restore the blocking behavior if it was set by us.
	if pollErr := f.pfd.Init("file", pollable); pollErr != nil && clearNonBlock {
		if err := syscall.SetNonblock(fd, false); err == nil {
			f.nonblock = false
		}
	}

	runtime.SetFinalizer(f.file, (*file).close)
	return f
}

GoLang实现跨平台的一些技巧01

最近在读GoLang的源码,源码中有一些跨平台的操作,Go处理的很有意思,在这整理一下。

以新建文件为例,对比一下几个常见平台的区别。

首先看下Windows平台的代码:

// os/file.go

// 新建文件
func Create(name string) (*File, error) {
	// 跳转到下面的OpenFile
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// OpenFile在这里还是平台无关的代码
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	// 从openFileNolog开始,不同平台代码会有不同
	f, err := openFileNolog(name, flag, perm)
	if err != nil {
		return nil, err
	}
	f.appendMode = flag&O_APPEND != 0

	return f, nil
}
// os/file_windows.go

// openFileNolog的windows实现
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
	if name == "" {
		return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
	}
	path := fixLongPath(name)
	// 跳转到了syscall.Open
	r, e := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm))
	if e != nil {
		// We should return EISDIR when we are trying to open a directory with write access.
		if e == syscall.ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) {
			pathp, e1 := syscall.UTF16PtrFromString(path)
			if e1 == nil {
				var fa syscall.Win32FileAttributeData
				e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
				if e1 == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
					e = syscall.EISDIR
				}
			}
		}
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}

	// 封装为File结构
	f, e := newFile(r, name, "file"), nil
	if e != nil {
		return nil, &PathError{Op: "open", Path: name, Err: e}
	}
	return f, nil
}
// syscall/syscall_windows.go

func Open(path string, mode int, perm uint32) (fd Handle, err error) {
	if len(path) == 0 {
		return InvalidHandle, ERROR_FILE_NOT_FOUND
	}
	pathp, err := UTF16PtrFromString(path)
	if err != nil {
		return InvalidHandle, err
	}
	var access uint32
	switch mode & (O_RDONLY | O_WRONLY | O_RDWR) {
	case O_RDONLY:
		access = GENERIC_READ
	case O_WRONLY:
		access = GENERIC_WRITE
	case O_RDWR:
		access = GENERIC_READ | GENERIC_WRITE
	}
	if mode&O_CREAT != 0 {
		access |= GENERIC_WRITE
	}
	if mode&O_APPEND != 0 {
		access &^= GENERIC_WRITE
		access |= FILE_APPEND_DATA
	}
	sharemode := uint32(FILE_SHARE_READ | FILE_SHARE_WRITE)
	var sa *SecurityAttributes
	if mode&O_CLOEXEC == 0 {
		sa = makeInheritSa()
	}
	var createmode uint32
	switch {
	case mode&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL):
		createmode = CREATE_NEW
	case mode&(O_CREAT|O_TRUNC) == (O_CREAT | O_TRUNC):
		createmode = CREATE_ALWAYS
	case mode&O_CREAT == O_CREAT:
		createmode = OPEN_ALWAYS
	case mode&O_TRUNC == O_TRUNC:
		createmode = TRUNCATE_EXISTING
	default:
		createmode = OPEN_EXISTING
	}
	var attrs uint32 = FILE_ATTRIBUTE_NORMAL
	if perm&S_IWRITE == 0 {
		attrs = FILE_ATTRIBUTE_READONLY
		if createmode == CREATE_ALWAYS {
			// We have been asked to create a read-only file.
			// If the file already exists, the semantics of
			// the Unix open system call is to preserve the
			// existing permissions. If we pass CREATE_ALWAYS
			// and FILE_ATTRIBUTE_READONLY to CreateFile,
			// and the file already exists, CreateFile will
			// change the file permissions.
			// Avoid that to preserve the Unix semantics.
			h, e := CreateFile(pathp, access, sharemode, sa, TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
			switch e {
			case ERROR_FILE_NOT_FOUND, _ERROR_BAD_NETPATH, ERROR_PATH_NOT_FOUND:
				// File does not exist. These are the same
				// errors as Errno.Is checks for ErrNotExist.
				// Carry on to create the file.
			default:
				// Success or some different error.
				return h, e
			}
		}
	}
	if createmode == OPEN_EXISTING && access == GENERIC_READ {
		// Necessary for opening directory handles.
		attrs |= FILE_FLAG_BACKUP_SEMANTICS
	}
	if mode&O_SYNC != 0 {
		const _FILE_FLAG_WRITE_THROUGH = 0x80000000
		attrs |= _FILE_FLAG_WRITE_THROUGH
	}

	// 跳转CreateFile
	return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
}


func CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile int32) (handle Handle, err error) {
	// 跳转Syscall9
	r0, _, e1 := Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
	handle = Handle(r0)
	if handle == InvalidHandle {
		err = errnoErr(e1)
	}
	return
}
// syscall/dll_windows.go
// 封装了Syscall9
func Syscall9(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)

// syscall/zsyscall_windows.go
// Syscall9中传入的API名为procCreateFileW 
procCreateFileW                        = modkernel32.NewProc("CreateFileW")

// 实际上最终调用了windows API CreateFileW,下面是CPP版本的API定义
// 到这里,也可以看到,通过Syscall的定义,比较巧妙的做了一定程度上的解耦
HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
// runtime/syscall_windows.go

// Syscall9是在这里实现的
//go:linkname syscall_Syscall9 syscall.Syscall9
//go:nosplit
func syscall_Syscall9(fn, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) {
	return syscall_SyscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9)
}

//go:linkname syscall_SyscallN syscall.SyscallN
//go:nosplit
func syscall_SyscallN(trap uintptr, args ...uintptr) (r1, r2, err uintptr) {
	nargs := len(args)

	// asmstdcall expects it can access the first 4 arguments
	// to load them into registers.
	var tmp [4]uintptr
	switch {
	case nargs < 4:
		copy(tmp[:], args)
		args = tmp[:]
	case nargs > maxArgs:
		panic("runtime: SyscallN has too many arguments")
	}

	lockOSThread()
	defer unlockOSThread()
	c := &getg().m.syscall
	c.fn = trap
	c.n = uintptr(nargs)
	c.args = uintptr(noescape(unsafe.Pointer(&args[0])))
	cgocall(asmstdcallAddr, unsafe.Pointer(c))
	return c.r1, c.r2, c.err
}

// 最后,通过cgocall,将go的调用,转换为c的调用
// 然后回到openFileNolog中
// 在openFileNolog中,继续调用newFile,整体封装为File结构,原路返回
func newFile(h syscall.Handle, name string, kind string) *File {
	if kind == "file" {
		var m uint32
		if syscall.GetConsoleMode(h, &m) == nil {
			kind = "console"
		}
		if t, err := syscall.GetFileType(h); err == nil && t == syscall.FILE_TYPE_PIPE {
			kind = "pipe"
		}
	}

	f := &File{&file{
		pfd: poll.FD{
			Sysfd:         h,
			IsStream:      true,
			ZeroReadIsEOF: true,
		},
		name: name,
	}}
	runtime.SetFinalizer(f.file, (*file).close)

	// Ignore initialization errors.
	// Assume any problems will show up in later I/O.
	f.pfd.Init(kind, false)

	return f
}

换行符引发的惨案

最近在读go源码。

本来环境都搭建好了,源码也上传git了。
但从另一台电脑下载源码后,报了一堆神奇的错误。
最后发现是go.env文件中,回车换行是按windows系统设定上传到git的,改为linux系统设定就好了。

想起入行以来,因为字符集、换行符、正斜杠反斜杠、tab还是空格,遇到的那堆坑,唏嘘不已。
希望UTF-8早日一统天下,希望各大平台别再特立独行。
非标准化害死人,多套标准更是害死人啊。

跨进程通讯的种种方式

内核模块 内核模块
驱动
内核对象 各类锁【互斥量、信号量、读写锁】
Event【Windows事件对象、Java的wait,可以等待一个或多个】
句柄【进程句柄、对象句柄】
注意只能用于多线程的情况【原子变量、关键区、条件变量是不可以跨进程使用的】
系统层面 系统服务
环境变量
协议调用【URL Scheme】
命令行调用
定时器调用
系统脚本调用
信号类 信号
匿名管道
命名管道
UNIX域
邮件槽
事件类 消息【Windows消息、QT信号槽】
粘贴板
钩子函数
系统内置消息队列
基于内存 共享内存
文件映射
DLL全局数据
DLL注入
基于文件 虚拟文件
实体文件【本地磁盘文件、SAN文件、NAS文件、FTP文件、对象存储文件】
基于网络 Web【WebSocket、SSE、HTTP、REST、SOAP等】
邮件
NetBios函数
数据广播
各类其他Socket通信【TCP、UDP、RAW等】
基于中间件 RPC协议及框架:SAMBA、SOAP、EJB、ICE、Swift、Dubbo等
服务发现:etcd、zk、consol
消息队列【队列、广播、订阅发布】:Kafka、RabbitMQ
数据库:关系【MySQL】、非关系【Redis】
ESB【企业服务总线】
Win系统组件 动态数据交换(DDE)
OLE技术
COM
ALT
DCOM

容器相关

几个问题
1,不同发行版的docker容器,尤其是glibc这些底层库版本不一致的情况下,可以在同一个宿主下运行,是因为glibc这些库与系统内核提供的ABI一直都保持不变吗?万一内核升级,有ABI变动了,docker是如何处理的呢?
处理办法是镜像自己带着基础库,事实上大多数发行版docker镜像都带着glibc,alpine用的则是更轻量的musl。镜像封装的应用,就只需保证兼容自带的glibc/musl即可。

2,现在虚拟机,都支持将某个虚拟机的窗体,直接投射到宿主机上,让宿主机像操作本地应用来操作虚拟机的应用,这个算那一层的虚拟化呢?也是通过ABI模拟来实现的吗?
说的是vmware unity mode这类功能么?具体如何实现的我没有研究过,无责任猜测只是vmware tool这样的工具提供的屏幕映射。但可以肯定的是,并不影响虚拟化层次,即还是硬件抽象层虚拟化。

3,在windows的vmware或virtualbox运行macos的虚拟机超级慢,但运行ubuntu就还蛮快的,是因为mac到win的ABI很难模拟吗?
“超级缓慢”主要是GUI慢,这个与显示方式和驱动都有关。如果你在SSH到虚拟的ubuntu和macos上,跑个console下的跑分软件的话,得到的分数相差并不大。

Windows 系统中是否有文件、访问、资源的隔离手段?是否存在 Windows 版本的容器运行时呢?
Windows的容器还是太重了,镜像超级大,而且优先支持微软自己的技术栈。这么重,加之不成熟,现在对上服务器虚拟化的各类成熟方案,实在没啥优势可言。

WindowsServer从设计上来讲,模块化做的比linux要差一些,内核做的事情也太多,早期类库变化也过于剧烈,类库前后兼容性也不算好。加之之前主推的技术,生命周期都太短,自家技术代与技术代之间没有传承,闭源得不到社区支持,开源服务端软件,在linux上性能,往往吊打windows。

而且即使是windows的虚拟化软件,微软自家做的也不是最好的。vmware和virtualbox多香。

微软唯一让我感到惊艳了一次的,还是wsl,但总归功能差了些,性能差了些。我倒是觉得,把wsl做好,可以同时支持win和linux容器,windows可能更有机会一些。

再后面,就是微软家生态的事情咯。其实微软做的很多理念,都挺超前的,而且也开始与社区合作。但微软,啥都要自己造轮子,技术延续性差又不断要大家换轮子,又没有强援,做生态挺难的。

看下linux和java,一个轮子能用多少年。看一下苹果,也是闭源,也是自己造轮子,还要交保护费,但苹果也不要你三五年就把轮子换了啊,而且保护费不是白交啊。远了远了。。。

docker
我觉得docker和其他虚拟化技术比,仍是最成功的,从docker到containerd,是一种技术上的成功,但是商业上的失败。

k8s应当与swarm对比。k8s成功,源自于google需要这样的一套工具,用来管理自己的容器,开发来先自己用,积累了很多经验。swarm则没有这个过程。云厂商们,自然选择对自己更好用,更有利的工具咯。

google至此,已占有web流量制高点,浏览器制高点,移动操作系统制高点,云原生制高点。厉害

网络虚拟化
N年前,在大学用虚拟机的时候,就遇到了HostOnly,Bridge,NAT等联网模式,当时对Bridge和NAT的区别也是搞了挺久才弄清楚的。

系统对既有旧方案和旧功能的兼容
这种例子蛮多的,比如:
1、系统升级时,如果API版本升级到了2.0,1.0版本也要长期保留。
2、界面风格变化了,还能允许用户切换回之前的风格
3、做系统替换时,一些用户用惯了的工具、报表什么的,经常会被要求在新系统上增加对应功能
4、系统升级时,尽量去兼容原有数据库设计,而不是推翻重来
5、对于部分政企用户,被迫去兼容IE,全是泪啊

存储
存储小文件:用过本地存储、SSD、SAN、NAS、SFTP、Ceph【对象存储】,云存储【云盘、对象存储】
存储大文件:用过SAN、NAS、Ceph【块存储】、HDFS
存储XML用过existdb、Oracle XML DB
存储JSON用过mongodb、es
备份数据,用过磁带和蓝光盘
试过IPFS,但并没有实际投入使用

从底层介质来看:可以分为SSD、磁盘、磁盘阵列、磁带、光盘等
块存储,相当于划分了一块存储空间或一块逻辑盘,给了操作系统
文件存储,相当于操作系统在磁盘上创建了文件系统,可以作为本地磁盘使用,加上网络访问功能,可以封装成为NAS、SFTP等
HDFS,可以看作分布式的文件存储
对象存储,可以看作分布式的NAS
IPFS,可以看作把一个BT网络,封装成了一个NAS

在我们行业里,使用云存储最大的障碍是两个:
1、使用公有云,担心数据安全、用户隐私的问题;少量上云的数据,也因种种限制,只能用移动联通电信的云;
2、使用私有云,很多机构不愿花大价钱购买服务,又没有能力自己运维,最终很难推进;
但总的来讲,这几年,云存储的使用面还是越来越广的,需要有个渐进的过程。

网络透明
服务网格可以实现透明,很大程度上是服务网格的整个网络环境是相对可控的。
远程通讯,如果在网络可控的环境下,其实完全可以和服务网格采用同样的方式。
但在互联网环境下,无法实现网络的可控,运维工程师、网络工程师是无法把程序员的大部分工作都做掉的,也就是程序员不能只关心数据,不关心网络。

何时能实现这种透明呢?个人认为,需要网络设备更新换代才行,要华为、思科支持这种透明,并能将透明能力,一定程度上开放给开发应用的厂商,才有可能实现一定程度上的透明。

SDN
个人感觉,SDN与服务网格感觉从思想上很相似,实现途径却不一样。

SDN本身十分依赖于网络提供商,其实按我理解,需要一次网络设备更新换代才行,而且网络提供商也需要进行一次大升级,用户也要跟着改造,这一方面需要技术沉淀,另一方面需要很多资金投入,一方面需要等用户升级。SDN概念也比较早,在网络提供商向用户推销SDN概念时,多数网络用户第一反映很可能是一脸懵逼。很多用户根本听不明白,有啥用,现在感觉挺好的啊。

而服务网格,是在可控网络下进行的,不需要网络设备的更替,也不需要以来网络供应商,更不存在跨越网络供应商的问题。大厂支持,再加上K8S和Istio的加持,所以生态就起来了,有滋有味的。服务网格用户是技术人员,服务网格出来推广的时候,微服务已大行其道了。技术人员们被服务治理烦的不要不要的,一听有方法直接把自己解放出来,学习应用热情高涨。加上并不需要夸张的资金投入,方案也就更容易落地了。

分布式相关

共识机制
Basic Paxos在全部节点可信任的时候,主要还是效率问题。所以zk、etcd都要用改进的算法。
Basic Paxos在部分节点不可信任的时候,是不适用的。所以公共区块链项目需要用其他的共识机制。

公共区块链中,常用的共识方法有PoW、PoS、DPoS等,这些方法,一方面是要达成共识进行记账,另一方面是要防止网络被恶意攻击;
联盟链中,一般采用PBFT、PoET等方法,由于不需要面向整个互联网,所以共识机制效率比公共链高不少;同理RAFT、ZAB,默认所有节点是可信任的,效率也是比较高的。
其实大家在日常生活中,默认会通过中心化的方式来达成共识,比如转账成功与否,大家一般会通过银行或支付宝的记录来判断,而不是线下协商。而这种方式,反而是交易效率最高的。
还是那句话,根据业务场景,选择足够用的架构和算法就好了。适合团队,能高效低成本解决问题的方案,就是最好的方案,哪怕这个技术方十分十分的朴素。

为何有了 DNS,还会出现 Eureka、Consul 这些专有的服务发现框架?
感觉其实要解决的问题并不一样:
1、基于DNS,其实要解决的问题是流量的流向,可以做到流量监控,但无法管理到具体业务,功能简单,效率更高;正好符合K8S的需要;
2、服务发现框架,其实是深入到业务层面,在应用框架集成、业务监听支持、健康检查、服务保护等功能。而且基于业务需求,进一步提供配置管理、环境管理等、多数据中心管理、业务紧密结合的功能;
有点像前面说的四层负载和七层负载的意思。
一个功能简单,效率高花头少;一个功能复杂,效率低可发挥空间更大;

那么除了 BFF 之外,你还用网关来做什么?
在API网关之前,会把移动APP的后端服务单独独立出来,APP访问APP后端服务,APP后端服务访问Web后端服务,相当于做了一个完全定制化的API网关。
当时也尝试过直接用Nginx来完成,但当时也不知道OpenResty这类扩展,复杂些的功能没能实现,没能实际应用。
后面从Zuul开始就不造轮子了。

客户端的负载均衡
客户端的负载均衡,在传统的CS解决方案中也有涉及,比如:
1、直连数据库时,统计类SQL直连只读库就好了
2、而且有些其实也变相做了优化,比如大家都熟悉的游戏开新服,登陆时就把流量分了

客户端负载均衡方案,其实和nginx的均衡策略基本一致的。但还遇到过一种中间情况,就是多了一个调度器,客户端定期获取服务IP列表,也可以从调度器获知每个IP的繁忙程度,然后决定要访问那个IP。

此外,云厂商对于移动、联通、电信的网络,有时也会用不同的链路,并会交一定的“保护费”啦。

负载均衡的设计,最佳人选,应该是懂业务的架构师(默认架构懂开发、懂运维)。
但同时,开发、运维团队要参与及评审:
1、比如开发要与架构沟通,团队熟悉哪些技术栈,哪些服务有状态哪些服务无状态等,中间件如何取舍
2、比如运维要与架构沟通,跨机房数据同步要如何做,容灾备份要如何处理,动态扩展是靠脚本还是靠云平台机制等