golang源码分析-启动过程概述

golang语言作为根据CSP模型实现的一种强类型的语言,本文主要就是通过简单的实例来分析一下golang语言的启动流程,为深入了解与学习做铺垫。

golang代码示例

package mainimport "fmt"func main(){fmt.Println("hello,world")
}

编写完示例代码之后,进行编译;

go build test.go

调试程序的方式有多种方式,可以使用gdb或者golang调试推荐使用的Devle工具。本文采用gdb调试方式;

gdb ./test
(gdb) info files
Symbols from "/root/test/test".
Local exec file:`/root/test/test', file type elf64-x86-64.Entry point: 0x454ae00x0000000000401000 - 0x000000000048cba9 is .text0x000000000048d000 - 0x00000000004dc24c is .rodata0x00000000004dc420 - 0x00000000004dd084 is .typelink0x00000000004dd088 - 0x00000000004dd0d8 is .itablink0x00000000004dd0d8 - 0x00000000004dd0d8 is .gosymtab0x00000000004dd0e0 - 0x0000000000548426 is .gopclntab0x0000000000549000 - 0x0000000000549020 is .go.buildinfo0x0000000000549020 - 0x00000000005560f8 is .noptrdata0x0000000000556100 - 0x000000000055d0f0 is .data0x000000000055d100 - 0x0000000000578950 is .bss0x0000000000578960 - 0x000000000057b0b8 is .noptrbss0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x454ae0
Breakpoint 1 at 0x454ae0: file /usr/lib/golang/src/runtime/rt0_linux_amd64.s, line 8.

此时我们查看位于rt0_linux_amd64.s中的的内容查看;

#include "textflag.h"TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8JMP  _rt0_amd64(SB)                  # 跳转到_rt0_amd64处执行TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0JMP   _rt0_amd64_lib(SB)

此时_rt0_amd64的代码位于runtime/asm_amd64.s中执行。此时就进入了整个的启动与初始化过程。

runtime中的启动与初始化

在位于runtime/asm_amd64.s中;

TEXT _rt0_amd64(SB),NOSPLIT,$-8MOVQ  0(SP), DI   // argcLEAQ 8(SP), SI   // argvJMP  runtime·rt0_go(SB)   // 跳转到rt0_go处执行

真正的初始化与执行的流程都是包含在了rt0_go的流程中。

rt0_go的执行流程
TEXT runtime·rt0_go(SB),NOSPLIT,$0// copy arguments forward on an even stackMOVQ DI, AX      // argc           输入参数MOVQ  SI, BX      // argv        SUBQ $(4*8+7), SP       // 2args 2autoANDQ  $~15, SPMOVQ    AX, 16(SP)MOVQ  BX, 24(SP)// create istack out of the given (operating system) stack.// _cgo_init may update stackguard.MOVQ    $runtime·g0(SB), DI             // 设置g0信息 并设置栈信息LEAQ    (-64*1024+104)(SP), BXMOVQ BX, g_stackguard0(DI)MOVQ   BX, g_stackguard1(DI)MOVQ   BX, (g_stack+stack_lo)(DI)MOVQ SP, (g_stack+stack_hi)(DI)// find out information about the processor we're onMOVL    $0, AXCPUIDMOVL AX, SICMPL  AX, $0JE    nocpuinfo// Figure out how to serialize RDTSC.// On Intel processors LFENCE is enough. AMD requires MFENCE.// Don't know about the rest, so let's do MFENCE.  根据平台不同进行跳转CMPL  BX, $0x756E6547  // "Genu"JNE notintelCMPL    DX, $0x49656E69  // "ineI"JNE notintelCMPL    CX, $0x6C65746E  // "ntel"JNE notintelMOVB    $1, runtime·isIntel(SB)MOVB $1, runtime·lfenceBeforeRdtsc(SB)
notintel:// Load EAX=1 cpuid flagsMOVL $1, AXCPUIDMOVL AX, runtime·processorVersionInfo(SB)nocpuinfo:// if there is an _cgo_init, call it.MOVQ _cgo_init(SB), AXTESTQ  AX, AXJZ    needtls// g0 already in DIMOVQ  DI, CX  // Win64 uses CX for first parameterMOVQ    $setg_gcc<>(SB), SICALL   AX// update stackguard after _cgo_initMOVQ  $runtime·g0(SB), CXMOVQ (g_stack+stack_lo)(CX), AXADDQ $const__StackGuard, AXMOVQ  AX, g_stackguard0(CX)MOVQ   AX, g_stackguard1(CX)#ifndef GOOS_windowsJMP ok
#endif
needtls:
#ifdef GOOS_plan9// skip TLS setup on Plan 9JMP ok
#endif
#ifdef GOOS_solaris// skip TLS setup on SolarisJMP ok
#endif
#ifdef GOOS_darwin// skip TLS setup on DarwinJMP ok
#endifLEAQ  runtime·m0+m_tls(SB), DICALL   runtime·settls(SB)// store through it, to make sure it worksget_tls(BX)MOVQ $0x123, g(BX)MOVQ   runtime·m0+m_tls(SB), AX     CMPQ  AX, $0x123JEQ 2(PC)CALL runtime·abort(SB)
ok:// set the per-goroutine and per-mach "registers"get_tls(BX)LEAQ   runtime·g0(SB), CX      // 设置g0信息MOVQ   CX, g(BX)LEAQ   runtime·m0(SB), AX      // 设置m0信息// save m->g0 = g0MOVQ CX, m_g0(AX)// save m0 to g0->mMOVQ  AX, g_m(CX)CLD              // convention is D is always left clearedCALL   runtime·check(SB)                // 进行检查MOVL    16(SP), AX      // copy argc      拷贝标准输入数据MOVL  AX, 0(SP)MOVQ   24(SP), AX      // copy argv   MOVQ AX, 8(SP)CALL   runtime·args(SB)                // 初始化传入数据CALL  runtime·osinit(SB)              // 初始化核数和页大小CALL    runtime·schedinit(SB)           // 初始化调度器并初始化运行环境// create a new goroutine to start programMOVQ $runtime·mainPC(SB), AX     // entry    设置执行入口PUSHQ AXPUSHQ $0          // arg sizeCALL runtime·newproc(SB)           // 创建协程并绑定运行POPQ  AXPOPQ  AX// start this MCALL   runtime·mstart(SB)              // 开始运行CALL runtime·abort(SB)   // mstart should never returnRET// Prevent dead-code elimination of debugCallV1, which is// intended to be called by debuggers.MOVQ $runtime·debugCallV1(SB), AXRETDATA runtime·mainPC+0(SB)/8,$runtime·main(SB)       // 设置mainPC为runtime.main的地址
GLOBL   runtime·mainPC(SB),RODATA,$8

此时通过该流程可以看出主要的流程首先设置g0的相关环境,接着就初始化输入参数(args)、初始化运行核数与页大小(osinit)接着再初始化运行环境(schedinit),然后调用main函数进行绑定最后调用mstart方法开始执行。

schedinit调度相关初始化
func schedinit() {// raceinit must be the first call to race detector.// In particular, it must be done before mallocinit below calls racemapshadow._g_ := getg()                   // 获取g实例if raceenabled {_g_.racectx, raceprocctx0 = raceinit()}sched.maxmcount = 10000        // 设置系统线程M的最大数量tracebackinit()                // 初始化计数器等内容moduledataverify()stackinit()                    // 栈相关初始化mallocinit()                   // 内存相关初始化mcommoninit(_g_.m)             // 初始化当前的m 即m0的初始化cpuinit()       // must run before alginitalginit()       // maps must not be used before this callmodulesinit()   // provides activeModulestypelinksinit() // uses maps, activeModulesitabsinit()     // uses activeModulesmsigsave(_g_.m)initSigmask = _g_.m.sigmaskgoargs()          // 获取命令行参数goenvs()                   // 获取环境变量parsedebugvars()       gcinit()                // 内存回收Gc的初始化sched.lastpoll = uint64(nanotime())procs := ncpu         // 运行p的个数检查if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n         // 如果设置了最大p个数,检查p个数合法后就设置为该值}if procresize(procs) != nil {      // 初始化对应procs个数的pthrow("unknown runnable goroutine during bootstrap")}// For cgocheck > 1, we turn on the write barrier at all times// and check all pointer writes. We can't do this until after// procresize because the write barrier needs a P.if debug.cgocheck > 1 {writeBarrier.cgo = truewriteBarrier.enabled = truefor _, p := range allp {p.wbBuf.reset()}}if buildVersion == "" {// Condition should never trigger. This code just serves// to ensure runtime·buildVersion is kept in the resulting binary.buildVersion = "unknown"}
}

该函数主要就是初始化了命令行参数,环境变量,gc和p的初始化过程等操作,都是为了后续执行做准备。

newproc函数
//go:nosplit
func newproc(siz int32, fn *funcval) {argp := add(unsafe.Pointer(&fn), sys.PtrSize)gp := getg()                                // 获取gpc := getcallerpc()                         // 获取当前pcsystemstack(func() {newproc1(fn, (*uint8)(argp), siz, gp, pc)   // 添加到栈中 此时的入口函数就是main函数})
}// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {_g_ := getg()          // 获取gif fn == nil {_g_.m.throwing = -1 // do not dump full stacksthrow("go of nil func value")}_g_.m.locks++ // disable preemption because it can be holding p in a local varsiz := narg                           // 设置大小siz = (siz + 7) &^ 7// We could allocate a larger initial stack if necessary.// Not worth it: this is almost always an error.// 4*sizeof(uintreg): extra space added below// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).if siz >= _StackMin-4*sys.RegSize-sys.RegSize {throw("newproc: function arguments too large for new goroutine")}_p_ := _g_.m.p.ptr()              // 获取当前的mnewg := gfget(_p_)                // 生成一个新的gif newg == nil {newg = malg(_StackMin)casgstatus(newg, _Gidle, _Gdead)allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.}if newg.stack.hi == 0 {throw("newproc1: newg missing stack")}if readgstatus(newg) != _Gdead {throw("newproc1: new g is not Gdead")}totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame    设置栈大小totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlignsp := newg.stack.hi - totalSize      // 设置可用的spspArg := spif usesLR {// caller's LR*(*uintptr)(unsafe.Pointer(sp)) = 0prepGoExitFrame(sp)spArg += sys.MinFrameSize}if narg > 0 {          // 如果输入参数大于0memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))// This is a stack-to-stack copy. If write barriers// are enabled and the source stack is grey (the// destination is always black), then perform a// barrier copy. We do this *after* the memmove// because the destination stack may have garbage on// it.if writeBarrier.needed && !_g_.m.curg.gcscandone {f := findfunc(fn.fn)                              // 保存输入参数stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))if stkmap.nbit > 0 {// We're in the prologue, so it's always stack map index 0.bv := stackmapdata(stkmap, 0)bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)}}}memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))newg.sched.sp = sp                             // 设置当前的spnewg.stktopsp = spnewg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function    设置g执行完成后退出的函数地址  指向了goexitnewg.sched.g = guintptr(unsafe.Pointer(newg))   // 设置当前的g的指针gostartcallfn(&newg.sched, fn)                  // 设置当前g的入口函数即该g被调度时执行的入口newg.gopc = callerpcnewg.ancestors = saveAncestors(callergp)newg.startpc = fn.fn                                                    // 保存执行的func地址if _g_.m.curg != nil {newg.labels = _g_.m.curg.labels}if isSystemGoroutine(newg, false) {           atomic.Xadd(&sched.ngsys, +1)}newg.gcscanvalid = false                        // 设置该g不被gc收集回收casgstatus(newg, _Gdead, _Grunnable)            // 设置当前的g的状态为可运行状态if _p_.goidcache == _p_.goidcacheend {// Sched.goidgen is the last allocated id,// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].// At startup sched.goidgen=0, so main goroutine receives goid=1._p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)_p_.goidcache -= _GoidCacheBatch - 1_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch}newg.goid = int64(_p_.goidcache)                            // 获取当前g的id_p_.goidcache++if raceenabled {newg.racectx = racegostart(callerpc)}if trace.enabled {traceGoCreate(newg, newg.startpc)}runqput(_p_, newg, true)                     // 把当前g加入队列中并设置下一个就可被唤起运行if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted { // 将当前g加入到可调度的队列中去 如果是启动阶段不会调用wakeup  如果是运行中则会在队列中重新唤起可运行的wakep()}_g_.m.locks--if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack_g_.stackguard0 = stackPreempt}
}

主要就是新生成一个g来运行,并将该g设置执行函数的入口,栈的初始化并设置g可运行状态,加入到队列中可被调用执行,在启动阶段的第一个g传入的函数其实就是main函数,接着就会调用mstart来调用该新生成的g来执行被包裹的函数main。

mstart函数
//go:nosplit
//go:nowritebarrierrec
func mstart() {_g_ := getg()                           // 获取当前的gosStack := _g_.stack.lo == 0if osStack {// Initialize stack bounds from system stack.// Cgo may have left stack size in stack.hi.// minit may update the stack bounds.size := _g_.stack.hiif size == 0 {size = 8192 * sys.StackGuardMultiplier}_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))_g_.stack.lo = _g_.stack.hi - size + 1024}// Initialize stack guards so that we can start calling// both Go and C functions with stack growth prologues._g_.stackguard0 = _g_.stack.lo + _StackGuard_g_.stackguard1 = _g_.stackguard0mstart1()              // 调用mastart1执行// Exit this thread.if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" || GOOS == "aix" {// Window, Solaris, Darwin, AIX and Plan 9 always system-allocate// the stack, but put it in _g_.stack before mstart,// so the logic above hasn't set osStack yet.osStack = true}mexit(osStack)       // 退出
}func mstart1() {_g_ := getg()                                 // 获取当前的gif _g_ != _g_.m.g0 {throw("bad runtime·mstart")}// Record the caller for use as the top of stack in mcall and// for terminating the thread.// We're never coming back to mstart1 after we call schedule,// so other calls can reuse the current frame.save(getcallerpc(), getcallersp())asminit()minit()       // 初始化信号量// Install signal handlers; after minit so that minit can// prepare the thread to be able to handle the signals.if _g_.m == &m0 {mstartm0()}if fn := _g_.m.mstartfn; fn != nil {fn()}if _g_.m != &m0 {acquirep(_g_.m.nextp.ptr())_g_.m.nextp = 0}schedule()          // 调度可执行的g 本文先不讨论该函数的流程
}

mstart函数主要就是开始调度可以运行的g来执行,在启动阶段可执行的g就是被包裹的main函数,此时继续了解main函数

main函数
func main() {g := getg()// Racectx of m0->g0 is used only as the parent of the main goroutine.// It must not be used for anything else.g.m.g0.racectx = 0// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.// Using decimal instead of binary GB and MB because// they look nicer in the stack overflow failure message.   设置栈的大小if sys.PtrSize == 8 {maxstacksize = 1000000000     } else {maxstacksize = 250000000}// Allow newproc to start new Ms.mainStarted = true                                       // 设置标志位可以允许其他newporc开始生成新的mif GOARCH != "wasm" { // no threads on wasm yet, so no sysmonsystemstack(func() {                                        // 开启一个后台协程来执行垃圾回收等操作newm(sysmon, nil)})}// Lock the main goroutine onto this, the main OS thread,// during initialization. Most programs won't care, but a few// do require certain calls to be made by the main thread.// Those can arrange for main.main to run in the main thread// by calling runtime.LockOSThread during initialization// to preserve the lock.lockOSThread()if g.m != &m0 {                                                // 检查是否是m0协程执行throw("runtime.main not on m0")}runtime_init() // must be before defer     各个包的init函数执行,即init的加载if nanotime() == 0 {throw("nanotime returning zero")}// Defer unlock so that runtime.Goexit during init does the unlock too.needUnlock := truedefer func() {if needUnlock {unlockOSThread()}}()// Record when the world started.runtimeInitTime = nanotime()      // 记录当前执行时间gcenable()                                            // 开启垃圾回收main_init_done = make(chan bool)if iscgo {if _cgo_thread_start == nil {throw("_cgo_thread_start missing")}if GOOS != "windows" {if _cgo_setenv == nil {throw("_cgo_setenv missing")}if _cgo_unsetenv == nil {throw("_cgo_unsetenv missing")}}if _cgo_notify_runtime_init_done == nil {throw("_cgo_notify_runtime_init_done missing")}// Start the template thread in case we enter Go from// a C-created thread and need to create a new thread.startTemplateThread()cgocall(_cgo_notify_runtime_init_done, nil)}fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtimefn()               // 执行main的init函数close(main_init_done)needUnlock = falseunlockOSThread()if isarchive || islibrary {// A program compiled with -buildmode=c-archive or c-shared// has a main, but it is not executed.return}fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtimefn()                          // 执行程序定义的main入口函数if raceenabled {racefini()}// Make racy client program work: if panicking on// another goroutine at the same time as main returns,// let the other goroutine finish printing the panic trace.// Once it does, it will exit. See issues 3934 and 20018.if atomic.Load(&runningPanicDefers) != 0 {// Running deferred functions should not take long.for c := 0; c < 1000; c++ {if atomic.Load(&runningPanicDefers) == 0 {break}Gosched()}}if atomic.Load(&panicking) != 0 {          // 如果当前还有正在执行的状态则调用gopark重新调度让其他协程执行gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)}exit(0)for {var x *int32*x = 0}
}

main函数主要就是最后对应于go程序中的main函数执行,在执行的过程中首先会先执行其他包中的init函数的执行,然后再执行main函数中的init函数,最后执行main函数,至此启动过程中的基本执行流程就完成。

总结

本文主要就是简单查看了一下go程序的启动过程,go中涉及到部分汇编知识,在汇编代码中一步步查找到runtime中的相关的go的源码的实现,本文也参考了大量网上已有的内容,大家有兴趣课自行查看。由于本人才疏学浅,如有错误请批评指正。

golang源码分析-启动过程概述相关推荐

  1. Kubernetes Scheduler源码分析--启动过程与多队列缓存(续)

    继续上文对Scheduler的分析,分析在Scheduler主循环处理过程中,podQueue,Queue和assumePod 三个队列的处理. Scheduler中SchedulerOne为主要的处 ...

  2. golang源码分析-调度概述

    golang源码分析-调度过程概述 本文主要概述一下golang的调度器的大概工作的流程,众所周知golang是基于用户态的协程的调度来完成多任务的执行.在Linux操作系统中,以往的多线程执行都是通 ...

  3. springboot集成mybatis源码分析-启动加载mybatis过程(二)

    springboot集成mybatis源码分析-启动加载mybatis过程(二) 1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplicati ...

  4. 【我的架构师之路】- golang源码分析之协程调度器底层实现( G、M、P)

    本人的源码是基于go 1.9.7 版本的哦! 紧接着之前写的 [我的区块链之路]- golang源码分析之select的底层实现 和 [我的区块链之路]- golang源码分析之channel的底层实 ...

  5. 嵌入式之uboot源码分析-启动第二阶段学习笔记(下篇)

    接上部分---->嵌入式之uboot源码分析-启动第二阶段学习笔记(上篇) 注:如下内容来自朱老师物联网大讲堂uboot课件 3.2.14 CFG_NO_FLASH (1)虽然NandFlash ...

  6. 一文给你解决linux内存源码分析- SLUB分配器概述(超详细)

    SLUB和SLAB的区别 首先为什么要说slub分配器,内核里小内存分配一共有三种,SLAB/SLUB/SLOB,slub分配器是slab分配器的进化版,而slob是一种精简的小内存分配算法,主要用于 ...

  7. v57.02 鸿蒙内核源码分析(编译过程) | 简单案例说透中间过程 | 百篇博客分析HarmonyOS源码

    子畏于匡,颜渊后.子曰:"吾以女为死矣."曰:"子在,回何敢死?" <论语>:先进篇 百篇博客系列篇.本篇为: v57.xx 鸿蒙内核源码分析(编译 ...

  8. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  9. python3.5源码分析-启动与虚拟机

    Python3源码分析 本文环境python3.5.2. 参考书籍<<Python源码剖析>> python官网 Python3启动流程概述 本文基于python3分析其基本的 ...

最新文章

  1. python交互式和文件式区别_Python中的交互式数据可视化与Bokeh(系列五)
  2. 太阳能充电调节代码_太阳能LED路灯控制器有什么作用
  3. 37款机型升级鸿蒙系统,华为终于想通,为鸿蒙系统敞开大门,37款机型将同步升级...
  4. windows找不到文件gpedit.msc_电脑文件搜索神器,没有找不到的东西
  5. LinearLayout和RelativeLayout
  6. 【python基础知识】-引入文件失败问题(同一文件夹和不同文件夹)
  7. UI设计还在为聊天界面苦恼?好的案例,打开任通二脉
  8. 什么是Github?
  9. 新SQL Server 2016示例数据库
  10. 代理服务器反向代理varnish配置文件解析
  11. Opencv单目标定flag的设定
  12. 介绍一个日志记录函数
  13. 开源跨平台计算机视觉库OpenCV 4.0正式发布
  14. 关于namecheap 域名运营商,域名赎回详细步骤
  15. 个性化推荐系统设计(3.1)——如何评价个性化推荐系统的效果
  16. 计算机二级access上机,计算机二级Access上机考点
  17. 2020-01-08 Oracle 数据库储存生僻字
  18. Linux——用户的特殊shell与PAM模块
  19. 区块链相关术语(中英对照)
  20. 谷歌大脑DeepMind合并,Google DeepMind新成立

热门文章

  1. 腾讯云TDSQL-A发布公有云版本 支持第七次全国人口普查等海量数据场景
  2. 「软件」2.0时代已经到来,你需要这样的开发工具
  3. Python实战 | 送亲戚,送长辈,月饼可视化大屏来帮忙!
  4. “不会数学,干啥都不行!”骨灰级程序员:你方向不对,努力也白费!
  5. 8.3折特惠票仅剩3天!「2019 嵌入式智能国际大会」全日程大公开!
  6. 吴恩达:AI未来将呈现四大发展趋势
  7. 50行代码教AI实现动作平衡 | 附完整代码
  8. 你知道“啥是佩奇”,却不一定了解佩奇排名算法
  9. AI一分钟 | 程维成立滴滴股权投资公司;特斯拉董事会决定放弃私有化
  10. 四图,读懂 BIO、NIO、AIO、多路复用 IO 的区别