浅析Go汇编语法和MatrixOne使用介绍

目录
  • MatrixOne数据库是什么?
  • Go汇编介绍
  • 为什么使用Go汇编?
    • 为什么不用CGO?
  • Go汇编语法特点
    • 操作数顺序
    • 寄存器宽度标识
    • 函数调用约定
  • 对写Go汇编代码有帮助的工具
    • avo
    • text/template
    • 在Go汇编代码中使用宏
  • 在MatrixOne数据库中的Go语言汇编应用
    • 基本向量运算加速
    • Go语言无法直接调用的指令
    • 编译器无法达到的特殊优化效果

MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP、AP、流计算等多种负载的极简大数据引擎。MatrixOne由Go语言所开发,并已于2021年10月开源,目前已经release到0.3版本。在MatrixOne已发布的性能报告中,与业界领先的OLAP数据库Clickhouse相比也不落下风。作为一款Go语言实现的数据库,可以达到C++实现的数据库一样的性能,其中一个很重要的优化就是利用Go语言自带的汇编能力,来通过调用SIMD指令进行硬件加速。本文就将对Go汇编及在MatrixOne的应用做详细介绍。

MatrixOne数据库是什么?

MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP、AP、流计算等多种负载的极简大数据引擎。MatrixOne由Go语言所开发,并已于2021年10月开源,目前已经release到0.3版本。在MatrixOne已发布的性能报告中,与业界领先的OLAP数据库Clickhouse相比也不落下风。作为一款Go语言实现的数据库,可以达到C++实现的数据库一样的性能,其中一个很重要的优化就是利用Go语言自带的汇编能力,来通过调用SIMD指令进行硬件加速。本文就将对Go汇编及在MatrixOne的应用做详细介绍。

Github地址:https://github.com/matrixorigin/matrixone 有兴趣的读者欢迎star和fork。

Go汇编介绍

Go是一种较新的高级语言,提供诸如协程、快速编译等激动人心的特性。但是在数据库引擎中,使用纯粹的Go语言会有力所未逮的时候。例如,向量化是数据库计算引擎常用的加速手段,而Go语言无法通过调用SIMD指令来使向量化代码的性能最大化。又例如,在安全相关代码中,Go语言无法调用CPU提供的密码学相关指令。在C/C++/Rust的世界中,解决这类问题可通过调用CPU架构相关的intrinsics函数。而Go语言提供的解决方案是Go汇编。本文将介绍Go汇编的语法特点,并通过几个具体场景展示其使用方法。

本文假定读者已经对计算机体系架构和汇编语言有基本的了解,因此常用的名词(比如“寄存器”)不做解释。如缺乏相关预备知识,可以寻求网络资源进行学习,例如这里

如无特殊说明,本文所指的汇编语言皆针对x86(amd64)架构。关于x86指令集,IntelAMD官方都提供了完整的指令集参考文档。想快速查阅,也可以使用这个列表。Intel的intrinsics文档也可以作为一个参考。

为什么使用Go汇编?

维基百科把使用汇编语言的理由概括成3类:

  • 直接操作硬件
  • 使用特殊的CPU指令
  • 解决性能问题

Go程序员使用汇编的理由,也不外乎这3类。如果你面对的问题在这3个类别里面,并且没有现成的库可用,就可以考虑使用Go汇编。

为什么不用CGO?

  • 巨大的函数调用开销
  • 内存管理问题
  • 打破goroutine语义 若协程里运行CGO函数,会占据单独线程,无法被Go运行时正常调度。
  • 可移植性差 交叉编译需要目的平台的全套工具链。在不同平台部署需要安装更多依赖库。

倘若在你的场景中以上几点无法接受,不妨尝试一下Go汇编。

Go汇编语法特点

根据Rob Pike的The Design of the Go Assembler,Go使用的汇编语言并不严格与CPU指令一一对应,而是一种被称作Plan 9 assembly的“伪汇编”。

The most important thing to know about Go's assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load. Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.

我们不用关心Plan 9 assembly与机器指令的对应关系,只需要了解Plan 9 assembly的语法特点。网络上有一些可获得的文档,如这里这里

一例胜千言,下面我们以最简单的64位整数加法为例,从不同方面来看Go汇编语法的特点。

// add.go
func Add(x, y int64) int64
//add_amd64.s
#include "textflag.h"
TEXT ·Add(SB), NOSPLIT, $0-24
	MOVQ x+0(FP), AX
	MOVQ y+8(FP), CX
    ADDQ AX, CX
    MOVQ CX, ret+16(FP)
	RET

这四条汇编代码所做的依次是:

  • 第一个操作数x放入寄存器AX
  • 第二个操作数y放入寄存器
  • CXCX加上AX,结果放回CX
  • CX放入返回值所在栈地址

操作数顺序

x86汇编最常用的语法有两种,AT&T语法和Intel语法。AT&T语法结果数放在最后,其他操作数放在前面。Intel语法结果数放最前面,其他操作数在后面。

Go的汇编在这方面接近AT&T语法,结果数放最后。

一个容易写错的例子是CMP指令。从效果上来看,CMP类似于SUB指令只修改EFLAGS标志位,不修改操作数。而在Go汇编中,CMP是以第一个操作数减去第二个操作数(与SUB相反)的结果来设置标志位。

寄存器宽度标识

部分指令支持不同的寄存器宽度。以64位操作数的ADD为例,按AT&T语法,指令名要加上宽度后缀变成ADDQ,寄存器也要加上宽度前缀变成RAX和RCX。按Intel语法,指令名不变,只给寄存器加上前缀。

上面例子可以看出,Go汇编跟两者都不同:指令名需要加宽度后缀,寄存器不变。

函数调用约定

编程语言在函数调用中传递参数的方式,称做函数调用约定(function calling convention)。x86-64架构上的主流C/C++编译器,都默认使用基于寄存器的方式:调用者把参数放进特定的寄存器传给被调用函数。而Go的调用约定,简单地讲,在最新的Go 1.18上,Go自己的runtime库在amd64与arm64与ppc64架构上使用基于寄存器的方式,其余地方(其他的CPU架构,以及非runtime库和用户写的库)使用基于栈的方式:调用者把参数依次压栈,被调用者通过传递的偏移量去栈中访问,执行结束后再把返回值压栈。

在上面代码中,FP是一个虚拟寄存器,指向第一个参数在栈中的地址。多个参数和返回值会按顺序对齐存放,因此x,y,返回值在栈中地址分别是FP加上偏移量0,8,16。

对写Go汇编代码有帮助的工具

avo

熟悉汇编语言的读者应该知道,手写汇编语言,会有选择寄存器、计算偏移量等繁琐且易出错的步骤。avo库就是为解决此类问题而生。如欲了解avo的具体用法,请参见其repo中给出的样例

text/template

这是Go语言自带的一个库。在写大量重复代码时会有帮助,例如在向量化代码中为不同类型实现相同基本算子。具体用法参见官方文档,这里不占用篇幅。

在Go汇编代码中使用宏

Go汇编代码支持跟C语言类似的宏,也可以用在代码大量重复的场景。内部库中就有很多例子,比如这里

在MatrixOne数据库中的Go语言汇编应用

基本向量运算加速

在OLAP数据库计算引擎中,向量化是必不可少的加速手段。通过向量化,消除了大量简单函数调用带来的不必要开销。而为了达到最大的向量化性能,使用SIMD指令是十分自然的选择。

我们以8位整数向量化加法为例。将两个数组的元素两两相加,把结果放入第三个数组。这样的操作在某些C/C++编译器中,可以自动优化成使用SIMD指令的版本。而以编译速度见长的Go编译器,不会做这样的优化。这也是Go语言为了保证编译速度所做的主动选择。在这个例子中,我们介绍如何使用Go汇编以AVX2指令集实现int8类型向量加法(假设数组已经按32字节填充)。

由于AVX2一共有16个256位寄存器,我们希望在循环展开中把它们全部使用上。如果完全手写的话,重复罗列寄存器非常繁琐且容易出错。因此我们使用avo来简化一些工作。avo的向量加法代码如下:

package main

import (
	. "github.com/mmcloughlin/avo/build"
	. "github.com/mmcloughlin/avo/operand"
	. "github.com/mmcloughlin/avo/reg"
)
var unroll = 16
var regWidth = 32
func main() {
    TEXT("int8AddAvx2Asm", NOSPLIT, "func(x []int8, y []int8, r []int8)")
    x := Mem{Base: Load(Param("x").Base(), GP64())}
    y := Mem{Base: Load(Param("y").Base(), GP64())}
    r := Mem{Base: Load(Param("r").Base(), GP64())}
    n := Load(Param("x").Len(), GP64())
    blocksize := regWidth * unroll
    blockitems := blocksize / 1
    regitems := regWidth / 1
    Label("int8AddBlockLoop")
    CMPQ(n, U32(blockitems))
    JL(LabelRef("int8AddTailLoop"))
    xs := make([]VecVirtual, unroll)
    for i := 0; i < unroll; i++ {
        xs[i] = YMM()
        VMOVDQU(x.Offset(regWidth*i), xs[i])
    }
        VPADDB(y.Offset(regWidth*i), xs[i], xs[i])
        VMOVDQU(xs[i], r.Offset(regWidth*i))
    ADDQ(U32(blocksize), x.Base)
    ADDQ(U32(blocksize), y.Base)
    ADDQ(U32(blocksize), r.Base)
    SUBQ(U32(blockitems), n)
    JMP(LabelRef("int8AddBlockLoop"))
    Label("int8AddTailLoop")
    CMPQ(n, U32(regitems))
    JL(LabelRef("int8AddDone"))
    VMOVDQU(x, xs[0])
    VPADDB(y, xs[0], xs[0])
    VMOVDQU(xs[0], r)
    ADDQ(U32(regWidth), x.Base)
    ADDQ(U32(regWidth), y.Base)
    ADDQ(U32(regWidth), r.Base)
    SUBQ(U32(regitems), n)
    JMP(LabelRef("int8AddTailLoop"))
    Label("int8AddDone")
    RET()
}

运行命令

go run int8add.go -out int8add.s

之后生成的汇编代码如下:

// Code generated by command: go run int8add.go -out int8add.s. DO NOT EDIT.

#include "textflag.h"
// func int8AddAvx2Asm(x []int8, y []int8, r []int8)
// Requires: AVX, AVX2
TEXT ·int8AddAvx2Asm(SB), NOSPLIT, $0-72
	MOVQ x_base+0(FP), AX
	MOVQ y_base+24(FP), CX
	MOVQ r_base+48(FP), DX
	MOVQ x_len+8(FP), BX
int8AddBlockLoop:
	CMPQ    BX, $0x00000200
	JL      int8AddTailLoop
	VMOVDQU (AX), Y0
	VMOVDQU 32(AX), Y1
	VMOVDQU 64(AX), Y2
	VMOVDQU 96(AX), Y3
	VMOVDQU 128(AX), Y4
	VMOVDQU 160(AX), Y5
	VMOVDQU 192(AX), Y6
	VMOVDQU 224(AX), Y7
	VMOVDQU 256(AX), Y8
	VMOVDQU 288(AX), Y9
	VMOVDQU 320(AX), Y10
	VMOVDQU 352(AX), Y11
	VMOVDQU 384(AX), Y12
	VMOVDQU 416(AX), Y13
	VMOVDQU 448(AX), Y14
	VMOVDQU 480(AX), Y15
	VPADDB  (CX), Y0, Y0
	VPADDB  32(CX), Y1, Y1
	VPADDB  64(CX), Y2, Y2
	VPADDB  96(CX), Y3, Y3
	VPADDB  128(CX), Y4, Y4
	VPADDB  160(CX), Y5, Y5
	VPADDB  192(CX), Y6, Y6
	VPADDB  224(CX), Y7, Y7
	VPADDB  256(CX), Y8, Y8
	VPADDB  288(CX), Y9, Y9
	VPADDB  320(CX), Y10, Y10
	VPADDB  352(CX), Y11, Y11
	VPADDB  384(CX), Y12, Y12
	VPADDB  416(CX), Y13, Y13
	VPADDB  448(CX), Y14, Y14
	VPADDB  480(CX), Y15, Y15
	VMOVDQU Y0, (DX)
	VMOVDQU Y1, 32(DX)
	VMOVDQU Y2, 64(DX)
	VMOVDQU Y3, 96(DX)
	VMOVDQU Y4, 128(DX)
	VMOVDQU Y5, 160(DX)
	VMOVDQU Y6, 192(DX)
	VMOVDQU Y7, 224(DX)
	VMOVDQU Y8, 256(DX)
	VMOVDQU Y9, 288(DX)
	VMOVDQU Y10, 320(DX)
	VMOVDQU Y11, 352(DX)
	VMOVDQU Y12, 384(DX)
	VMOVDQU Y13, 416(DX)
	VMOVDQU Y14, 448(DX)
	VMOVDQU Y15, 480(DX)
	ADDQ    $0x00000200, AX
	ADDQ    $0x00000200, CX
	ADDQ    $0x00000200, DX
	SUBQ    $0x00000200, BX
	JMP     int8AddBlockLoop
int8AddTailLoop:
	CMPQ    BX, $0x00000020
	JL      int8AddDone
	ADDQ    $0x00000020, AX
	ADDQ    $0x00000020, CX
	ADDQ    $0x00000020, DX
	SUBQ    $0x00000020, BX
	JMP     int8AddTailLoop
int8AddDone:
	RET

可以看到,在avo代码中,我们只需要给变量指定寄存器类型,生成汇编的时候会自动帮我们绑定相应类型的可用寄存器。在很多场景下这确实能够带来方便。不过avo目前只支持x86架构,给arm CPU写汇编无法使用。

Go语言无法直接调用的指令

除了SIMD,还有很多Go语言本身无法使用到的CPU指令,比如密码学相关指令。如果是用C/C++,可以使用编译器内置的intrinsics函数(gcc和clang皆提供)来调用,还算方便。遗憾的是Go语言并不提供intrinsics函数。遇到这样的场景,汇编是唯一的解决办法。Go语言自己的crypto官方库里就有大量的汇编代码。

这里我们以CRC32C指令作为例子。在MatrixOne的哈希表实现中,整数key的哈希函数只使用一条CRC32指令,达到了理论上的最高性能。代码如下:

TEXT ·Crc32Int64Hash(SB), NOSPLIT, $0-16
	MOVQ   -1, SI
	CRC32Q data+0(FP), SI
	MOVQ   SI, ret+8(FP)
	RET

实际代码中,为了消除汇编函数调用带来的指令跳转开销,以及参数进出栈开销,使用的是批量化的版本。这里为了节约篇幅,我们用简化版举例。

编译器无法达到的特殊优化效果

下面是MatrixOne使用的两个有序64位整数数组求交集的算法的一部分:

...
loop:
	CMPQ  DX, DI
	JE    done
	CMPQ  R11, R8
	JE    done
	MOVQ  (DX), R10
	MOVQ  R10, (SI)
	CMPQ  R10, (R11)
	SETLE AL
	SETGE BL
	SETEQ CL
	SHLB  $0x03, AL
	SHLB  $0x03, BL
	SHLB  $0x03, CL
	ADDQ  AX, DX
	ADDQ  BX, R11
	ADDQ  CX, SI
	JMP   loop

done:
...

CMPQ R10, (R11)这一行,是比较两个数组当前指针位置的元素。后面几行根据这个比较的结果,来移动对应操作数数组及结果数组的指针。文字解释不如对比下面等价的C语言代码来得清楚:

while (true) {
    if (a == a_end) break;
    if (b == b_end) break;
    *c = *a;
    if (*a <= *b) ++a;
    if (*a >= *b) ++b;
    if (*a == *b) ++c;
}

汇编代码中,循环体内只做了一次比较运算,并且没有任何的分支跳转。高级语言编译器达不到这样的优化效果,原因是任何高级语言都不提供“根据一个比较运算的3种不同结果,分别修改3个不同的数”这样直接跟CPU指令集相关的语义。

这个例子算是对汇编语言威力的一个展示。编程语言不断发展,抽象层次越来越高,但是在性能最大化的场景下,仍然需要直接与CPU指令打交道的汇编语言。

到此这篇关于浅析Go汇编语法和MatrixOne使用介绍的文章就介绍到这了,更多相关Go汇编MatrixOne使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 从Go汇编角度解读for循环的问题

    Go常用的遍历方式有两种:for和for-range.实际上,for-range也只是for的语法糖,本文试图从汇编代码入手解释for循环是如何工作的. 问题 首先来看看几个令人迷惑的地方. 问题1:遍历过程中取值 func main() { arr := [5]int{1, 2, 3, 4, 5} for _, v := range arr { println(&v) } } 上面这段代码里,会打印出什么? 问题2:遍历过程中修改 arr := []int{1, 2, 3, 4, 5} for

  • 汇编分析 Golang 循环(推荐)

    女主宣言 今天小编为大家分享一篇关于Golang循环汇编分析的文章,文章中介绍了golang循环的汇编层面的处理,通过分析,我们可以更了解循环的实现.希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在" 3 60云计算 ",点关注哦! 循环是编程中很强大的一个概念,而且非常容易处理. 但是,必须将其翻译成机器可理解的基本指令. 它的编译方式也可能影响标准库中的其他组件. 让我们开始分析一下范围循环 . 1循环汇编 范围循环可以迭代数组,切片或通道.下面函数展示了,对分

  • Golang汇编命令解读及使用

    我们可以很容易将一个golang程序转变成汇编语言. 比如我写了一个main.go: package main func g(p int) int { return p+1; } func main() { c := g(4) + 1 _ = c } 使用命令: GOOS=linux GOARCH=386 go tool compile -S main.go >> main.S 我们就获取了main.S是main.go的汇编版本. "".g t=1 size=16 valu

  • 通过汇编看golang函数的多返回值问题

    golang这门语言,有个比较好的特性,就是支持函数的多返回值.想C,C++,Java等这些语言,是不支持函数多返回的.但是C,C++可以使用传递指针,实现函数多返回.但是,你有没有想过,golang是怎样实现函数多返回值的呢? 我们知道,C,C++是通过寄存器实现函数返回值的,也就是先把返回值写入到一个寄存器中,然后再从寄存器中,读到函数的返回值.golang也是这样实现的吗? 伟大的思想家孔子曾说过,在源码面前一切都如同裸奔.后来,鲁迅先生,总结了孔子的思想,说出了,在汇编面前,一切语法都是

  • Go 中的循环是如何转为汇编的(方法详解)

    本文基于 Go 1.13 版本 循环在编程中是一个重要的概念,且易于上手.但是,循环必须被翻译成计算机能理解的底层指令.它的编译方式也会在一定程度上影响到标准库中的其他组件.让我们开始分析循环吧. 循环的汇编代码 使用循坏迭代 array , slice , channel ,以下是一个使用循环对 slice 计算总和的例子. func main() { l := []int{9, 45, 23, 67, 78} t := 0 for _, v := range l { t += v } pri

  • 浅析Go汇编语法和MatrixOne使用介绍

    目录 MatrixOne数据库是什么? Go汇编介绍 为什么使用Go汇编? 为什么不用CGO? Go汇编语法特点 操作数顺序 寄存器宽度标识 函数调用约定 对写Go汇编代码有帮助的工具 avo text/template 在Go汇编代码中使用宏 在MatrixOne数据库中的Go语言汇编应用 基本向量运算加速 Go语言无法直接调用的指令 编译器无法达到的特殊优化效果 MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP.AP.流计算等多种负载的极简大数据引擎.MatrixO

  • 汇编语言中mov和lea指令的区别详解

    指令(instruction)是一种语句,它在程序汇编编译时变得可执行.汇编器将指令翻译为机器语言字节,并且在运行时由 CPU 加载和执行. 一条指令有四个组成部分: 标号(可选) 指令助记符(必需) 操作数(通常是必需的) 注释(可选) 最近在学习汇编语言,过程中遇到很多问题,对此在以后的随笔会逐渐更新,这次谈谈mov,lea指令的区别   一,关于有没有加上[]的问题 1,对于mov指令来说: 有没有[]对于变量是无所谓的,其结果都是取值 如: num dw 2 mov bx,num mov

  • 汇编语言中test和cmp有什么区别

    汇编语言(assembly language)是一种用于电子计算机.微处理器.微控制器或其他可编程器件的低级语言,亦称为符号语言.在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址.在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令.特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植.下面看下汇编语言中test和cmp有什么区别, 汇编test和cmp区别 看过破解教程,都知道test,cmp是比较关键,可是我一

  • Vue3中的模板语法和vue指令

    目录 1 模板插值语法 2 指令 1 模板插值语法 在script 声明一个变量可以直接在template 使用用法为{{变量名称}} 模板语法是可以编写条件运算的 运算也是支持的 操作API 也是支持的 <template> {{ message }} {{ message2==0 ? '我是老大' : '我笑的' }} {{ message2 + 1 }} {{ message.split('').map(v => `4546$v`) }} </template> <

  • 详解Webstorm 新建.vue文件支持高亮vue语法和es6语法

    Webstorm 添加新建.vue文件功能并支持高亮vue语法和es6语法,分享给大家,具体如下: 添加新建.vue文件功能 ①Webstorm 右上角File-Plugins 搜索vue如果没有就去下载 点击serch in repositories ②点击安装vue.js ③安装成功后点击右下角Apply 提示重启webstorm 重启完成后 Setting-Editor-File and Code Templates 点击右上角的加号 添加vue文件 Name为vue File, Exte

  • 汇编语言中cmp指令用法笔记与总结

    本文实例讲述了汇编语言中cmp指令用法.分享给大家供大家参考,具体如下: cmp是比较指令,cmp的功能是相当于减法指令,只是不保存结果.cmp指令执行后,将对标志寄存器产生影响.其他相关指令通过识别这些被影响的标志寄存器来得知比较结果. cmp指令格式: cmp  操作对象1,操作对象2 功能: 计算操作对象1 - 操作对象2 但不保存结果,仅仅根据计算结果对标志寄存器进行设置.比如cmp ax,ax  是做ax - ax 的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位. 指令

  • 浅析Bean Searcher 与 MyBatis Plus 区别介绍

    目录 区别一(基本) 区别二(高级查询) 1)使用 MyBatis Plus 查询: 2)使用 Bean Searcher 查询: 区别三(逻辑分组) 区别四(多表联查) 区别五(使用场景) 疑问 1)这貌似开放很大的检索能力,风险可控吗? 条件约束 排序约束 2)使用 Bean Searcher 后 Controller 的入参必须是 Map 类型? 3)前端乱传参数的话,存在 SQL 注入风险吗? 4)可以随意传参,会让用户获取本不该看到的数据吗? 总结 Bean Searcher 号称 任

  • Python tuple方法和string常量介绍

    目录 前言 1 tuple.count(value) 2 tuple.index(value[, start[, end]]) 1 string.ascii_letters 2 string.ascii_lowercase 3 string.ascii_uppercase 4 string.digits 5 string.hexdigits 6 string.octdigits 7 string.punctuation 8 string.printable 9 string.whitespace

  • 浅析ARMv8汇编指令adrp和adr

    目录 1.概述 2.adrp 2.1.定义 2.2.测试 3.adr 3.1.定义 3.2.测试 参考资料 1.概述 在阅读Linux内核代码时,经常能碰到汇编代码,网上能查的资料千篇一律,大多都描述的很模糊.俗话说,实践是检验真理的唯一标准,我们就参考官方文档,自己写汇编代码并反汇编,探寻其中的奥妙. 2.adrp 在Linux内核启动代码primary_entry中,使用adrp指令获取Linux内核在内存中的起始页地址,页大小为4KB,由于内核启动的时候MMU还未打开,此时获取的Linux

  • 深入浅析WinForm 进程、线程及区别介绍

    一.进程 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动. 它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体. Process 类,用来操作进程. 命名空间:using System.Diagnostics; Process.Start("calc"); //打开计算器 Process.Start("mspaint"); //打开画图 Process.Start("iexplore" , "http://www.

随机推荐