深入剖析 Golang 程序启动原理 - 从 ELF 入口点到GMP初始化到执行 main!

news2024/11/20 20:43:35

大家好,我是飞哥!

在过去的开发工作中,大家都是通过创建进程或者线程来工作的。Linux进程是如何创建出来的? 、聊聊Linux中线程和进程的联系与区别! 和你的新进程是如何被内核调度执行到的? 这几篇文章就是帮大家深入理解进程线程原理的。

但是,时至今日光了解进程和线程已经不够了。因为现在协程编程模型大行其道。很多同学知道进程和线程,但就是不理解协程是如何工作的。虽然能写出来代码,但不理解底层运行原理。

今天就让我以 golang 版本的 hello world 程序为例,给大家拆解一下协程编程模型的工作过程。 

在本文中我会从 ELF 可执行文件的入口讲起,讲到 GMP 调度器的初始化,到主协程的创建,到主协程进入 runtime.main 最后执行到用户定义的 main 函数。

一、 hello world 程序的运行入口

golang 的 hello world 写起来非常的简单。

package main
import "fmt"
func main() {
 fmt.Println("Hello World!")
}

运行起来也是一样非常的简单。

# go build main.go
# ./main
Hello World!

程序是跑起来了,但是问题来了。传说中的协程究竟长什么样子,是何时被创建的,又是如何被加载运行并打印出 “Hello World!” 的。

不管是啥语言编译出来的可执行文件,都有一个执行入口点。shell 在将程序加载完后会跳转到程序入口点开始执行。

但值得提前说的一点是一般编程语言的入口点都不会是我们在代码中写的那个 main。c 语言中如此,golang 中更是这样。这是因为各个语言都需要在进程启动过程中做一些启动逻辑的。在 golang 中,其底层运行的 GMP、垃圾回收等机制都需要在进入用户的 main 函数之前启动起来。

接下来我们需要借助 readelf 和 nm 命令来找到上述编译出来的可执行文件 main 的执行入口。首先使用 readelf 找到 main 的入口点是在 0x45c220 位置处,如下图所示。

$ readelf --file-header main
ELF Header:
 ......
 Entry point address:               0x45c220

那么 0x45c220 这个位置对应的是哪个函数呢?借助 nm 命令我们可以看到它是 _rt0_amd64_linux。

nm -n main | grep 45c220
000000000045c220 T _rt0_amd64_linux

这其实是一个汇编函数。

// file:asm_amd64.s
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)

这个函数的开头也有明确的注释 “_rt0_amd64 is common startup code for most amd64 systems when using internal linking”。这说明我们找对了。

接下来的 golang 运行就是顺着这个汇编函数开始执行,最后一步步地运行到我们所熟悉的 main 函数的。

二、入口执行分析

这一小节我们来看看 golang 程序在启动的时候都做了哪些事情。相信理解这些底层的工作机制对从事 golang 开发的同学会非常的大有裨益。

在上一小节我们看到了的 golang 入口函数 _rt0_amd64。要注意的是,当代码运行到这里的时候,操作系统已经为当前可执行文件创建好了一个主线程了。_rt0_amd64 只是将参数简单地保存一下后就 JMP (汇编中的函数调用)到 runtime·rt0_go 中了。

这个函数很长,我们只挑有重要的讲!

// file:runtime/asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
	......

	// 2.1 Golang 核心初始化过程
	CALL	runtime·osinit(SB)
	CALL	runtime·schedinit(SB)


	//2.2 调用 runtime·newproc 创建一个协程
	//    并将 runtime.main 函数作为入口
	MOVQ	$runtime·mainPC(SB), AX		// entry
	PUSHQ	AX
	CALL	runtime·newproc(SB)
	POPQ	AX

	//2.3 启动线程,启动调度系统
	CALL	runtime·mstart(SB)

洋洋洒洒好几百行汇编代码,其实缩略完后,关键的核心逻辑就是上面几个关键点。

第一、通过 runtime 中的 osinit、schedinit 等函数对 golang 运行时进行关键的初始化。在这里我们将看到 GMP 的初始化,与调度逻辑。

第二、创建一个主协程,并指明 runtime.main 函数是其入口函数。因为操作系统加载的时候只创建好了主线程,协程这种东西还是得用户态的 golang 自己来管理。golang 在这里创建出了自己的第一个协程。

第三、调用 runtime·mstart 真正开启运行。

接下来我们分三个小节来详细了解下这三块的逻辑。

2.1 golang 核心初始化

golang 的核心初始化包括 runtime·osinit 和 runtime·schedinit 这两个函数。

在 runtime·osinit 中主要是获取CPU数量,页大小和 操作系统初始化工作。

// file:os_linux.go
func osinit() {
 ncpu = getproccount()
 physHugePageSize = getHugePageSize()
 osArchInit()
}

接下来是 runtime.schedinit 的初始化,这里主要是对调度系统的初始化。

在这个函数的注释中,也贴心地告诉了我们,golang 的 bootstrap(启动)流程步骤分别是 call osinit、call schedinit、make & queue new G 和 call runtime·mstart 四个步骤。这和我们前面说的一致。

Golang 中调度的核心就是 GMP 原理。这里我们不展开对 GMP 进行过多的说明,留着将来再细说。这里只提一下,在 runtime.schedinit 这个函数中,会将所有的 P 都给初始化好,并用一个 allp slice 维护管理起来。

// file:runtime/proc.go
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
 ......

 // 默认情况下 procs 等于 cpu 个数
 // 如果设置了 GOMAXPROCS 则以这个为准
 procs := ncpu
 if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
  procs = n
 }

 // 分配 procs 个 P
 if procresize(procs) != nil {
  throw("unknown runnable goroutine during bootstrap")
 }

 ......
}

从上述源码中我们可以看到,P 的数量取决于当前 cpu 的数量,或者是 runtime.GOMAXPROCS 的配置。

不少 golang 的同学都有一种错误的认知,认为 runtime.GOMAXPROCS 限制的是 golang 中的线程数。这个认知是错误的。runtime.GOMAXPROCS 真正制约的是 GMP 中的 P,而不是 M。

再来简单看下 procresize,这个函数其实就是在维护 allp 变量,在这里保存着所有的 P。

// file:runtime/proc.go
// Change number of processors
// Returns list of Ps with local work, they need to be scheduled by the caller
func procresize(nprocs int32) *p {

 // 申请存储 P 的数组
 if nprocs > int32(len(allp)) {
  allp = ...
 }

 // 对新 P 进行内存分配和初始化,并保存到 allp 数组中
 for i := old; i < nprocs; i++ {
  pp := allp[i]
  if pp == nil {
   pp = new(p)
  }
  pp.init(i)
  atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
 }
 ...
}

2.2 golang 主协程的创建

汇编代码调用 runtime·newproc 创建一个协程,并将 runtime.main 函数作为入口。我们来看下第一个主协程是如何创建出来的。

//file:runtime/proc.go
func newproc(fn *funcval) {
  ...
  systemstack(func() {
  newg := newproc1(fn, gp, pc)

  _p_ := getg().m.p.ptr()
  runqput(_p_, newg, true)

  if mainStarted {
   wakep()
  }
 })
}

systemstack 这个函数是 golang 内部经常使用的,runtime 代码经常通过调用 systemstack 临时性的切换到系统栈去执行一些特殊的任务。这里所谓的系统栈,就是操作系统视角创建出来的线程和线程栈。如果不理解,先不管这个也问题不大。

接着调用 newproc1 来创建一个协程出来,runqput 达标的是将协程添加到运行队列。最后的 wakep 是去唤醒一个线程去执行运行队列中的协程。

协程创建

我们一个一个分别来看。先看 newproc1 是如何创建协程的。

// file:runtime/proc.go
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
 ...
 //从缓存中获取或者创建 G 对象
 newg := gfget(_p_)
 if newg == nil {
  newg = malg(_StackMin)
  ...
 }

 newg.sched.sp = sp
 newg.stktopsp = sp
 ...
 newg.startpc = fn.fn
 ...
 return newg
}

在 gfget 中是尝试从缓存中获取一个 G 对象出来。我们忽略这个逻辑,直接看 malg,因为它是创建一个 G。对我们理解更有帮助。在 malg 创建完后,对新的 gorutine 对象进行一些设置后就返回了。

在调用 malg 时传入了一个 _StackMin,这表示默认的栈大小,在 Golang 中的默认值是 2048。

这也就是很多人所说的 Golang 中协程很轻量,只需要消耗 2 KB 内存的缘由。但其实这个说法并不是很准确。首先这里分配的并不是 2KB,下面我们会看到还有有一些预留。另外当发生缺页中断的时候,Linux 是以 4 KB为单位分配的。

// file:runtime/proc.go
func malg(stacksize int32) *g {
 newg := new(g)
 if stacksize >= 0 {
  //这里会在 stacksize 的基础上为每个栈预留系统调用所需的内存大小 \_StackSystem
  //在 Linux/Darwin 上( \_StackSystem == 0 )本行不改变 stacksize 的大小
  stacksize = round2(_StackSystem + stacksize)
 }
 // 切换到 G0 为 newg 初始化栈内存
 systemstack(func() {
   newg.stack = stackalloc(uint32(stacksize))
  })

 // 设置 stackguard0 ,用来判断是否要进行栈扩容 
 newg.stackguard0 = newg.stack.lo + _StackGuard
 newg.stackguard1 = ^uintptr(0)
}

在调用 malg 的时候会将传入的内存大小加上一个 _StackSystem 值预留给系统调用使用,round2 函数会将传入的值舍入为 2 的指数。然后会切换到 G0 执行 stackalloc 函数进行栈内存分配。分配完毕之后会设置 stackguard0 为 stack.lo + _StackGuard,作为将来判断是否需要进行栈扩容使用。

//file:runtime/stack.go
func stackalloc(n uint32) stack {
 thisg := getg()
 ...
 //对齐到整数页
 n = uint32(alignUp(uintptr(n), physPageSize))
 v := sysAlloc(uintptr(n), &memstats.stacks_sys)
 return stack{uintptr(v), uintptr(v) + uintptr(n)}
}

其中栈是这样一个结构体

//file:runtime/runtime2.go
type stack struct {
 lo uintptr
 hi uintptr
}

sysAlloc 使用 mmap 系统调用来真正为协程栈申请指定大小的地址空间。

// file:runtime/mem_darwin.go
func sysAlloc(n uintptr, sysStat *sysMemStat) unsafe.Pointer {
 v, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
 if err != 0 {
  return nil
 }
 sysStat.add(int64(n))
 return v
}

将协程添加到运行队列

在协程创建出来后,会调用 runqput 将它添加到运行队列中。在讲这块的逻辑之前,我们得首先讲讲 Golang 中的运行队列。

Golang 为什么会抽象出一个 P 来呢。这是因为 Golang 在 1.0 版本的多线程调度器的实现中,调度器和锁都是全局资源,锁的竞争和开销非常的大,导致性能比较差。

其实这个问题在 Linux 中早已很好地解决了。Golang 就把它学来了。在 Linux 中每个 CPU 核都有一个 runqueue,来保存着将来要在该核上调度运行的进程或线程。这样调度的时候,只需要看当前的 CPU 上的资源就行,把锁的开销就砍掉了。

所以,Golang 中的 P 可以认为是对 Linux 中 CPU 的一个虚拟,目的是和 Linux 一样,找一个无竞争地保管运行队列资源的方法。在 Golang 中,每个 P 都有它的运行队列。

3de42bdd5e3810d645cda5788ba0a971.png

理解了这个背景,我们再来看 Golang 中的 runqput 是如何将协程添加到 P 的运行队列中的。

// file:runtime/proc.go
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(_p_ *p, gp *g, next bool) {
 ...

 //将新 goroutine 添加到 P 的 runnext 中
 if next {
 retryNext:
  oldnext := _p_.runnext
  if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
   goto retryNext
  }
  if oldnext == 0 {
   return
  }
  // 将原来的 runnext 添加到运行队列中
  gp = oldnext.ptr()
 }

 //将新协程或者被从 runnext 上踢下来的协程添加到运行队列中
retry:
 h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
 t := _p_.runqtail

 //如果 P 的运行队列没满,那就添加到尾部
 if t-h < uint32(len(_p_.runq)) {
  _p_.runq[t%uint32(len(_p_.runq))].set(gp)
  atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
  return
 }

 //如果满了,就添加到全局运行队列中
 if runqputslow(_p_, gp, h, t) {
  return
 }
}

在 runqput 中首先尝试将新协程放到 runnext 中,这个有优先执行权。然后会将新协程,或者被新协程从 runnext 上踢下来的协程加入到当前 P(运行队列)的尾部去。但还有可能当前这个运行队列已经任务过多了,那就需要调用 runqputslow 分一部分运行队列中的协程到全局队列中去。以便于减轻当前运行队列的执行压力。

唤醒一个线程去

前面只是将新创建的 goroutine 添加到了 P 的运行队列中。现在 GMP 中的 G 有了,P 也有了,就还差 M 了。真正的运行还是需要操作系统的线程去执行的。

// file:runtime/proc.go
func wakep() {
 ...
 startm(nil, true)
}

wakep 核心是调用 startm。这个函数将会调度线程去运行 P 中的运行队列。如果有必要的话,可能也需要创建新线程出来。

// file:runtime/proc.go
// Schedules some M to run the p (creates an M if necessary).
func startm(_p_ *p, spinning bool) {
 mp := acquirem()

 //如果没有传入 p,就获取一个 idel p
 if _p_ == nil {
  _p_ = pidleget()
 }

 //再获取一个空闲的 m
 nmp := mget()
 if nmp == nil {
  //如果获取不到,就创建一个出来
  newm(fn, _p_, id)
  ...
  return
 }

 ...
}

2.3 启动调度系统

现在 GMP 中的三元素全具备了,而且主协程中的运行函数 fn 也指定为了 runtime.main。接下来就是调用 mstart 来启动线程,启动调度系统。

汇编中的 mstart 函数调用的是 golang 源码中的 mstart0

// file:runtime/proc.go
func mstart0() {
 ...
 mstart1()
}
// file:runtime/proc.go
func mstart1() {
 ...
 // 进入调度循环
 schedule()
}

其中,schedule 是整个 golang 程序的运行核心。所有的协程都是通过它来开始运行的。

schedule 的主要工作逻辑有这么几点

  1. 每隔 61 次调度轮回从全局队列找,避免全局队列中的g被饿死。

  2. 从 p.runnext 获取 g,从 p 的本地队列中获取。

  3. 调用 findrunnable 找 g,找不到的话就将 m 休眠,等待唤醒。

  4. 当找到一个 g 后,就会调用 execute 去执行 g

然后再来看源码就很容易理解了。

// file:runtime/proc.go
func schedule() {
 _g_ := getg()
 ...
top:
 pp := _g_.m.p.ptr()

 //每 61 次从全局运行队列中获取可运行的协程
 if gp == nil {
  if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
   lock(&sched.lock)
   gp = globrunqget(_g_.m.p.ptr(), 1)
   unlock(&sched.lock)
  }
 }

 if gp == nil {
  //从当前 P 的运行队列中获取可运行
  gp, inheritTime = runqget(_g_.m.p.ptr())
 }

 if gp == nil {
  //当前P或者全局队列中获取可运行协程
  //尝试从其它P中steal任务来处理
  //如果获取不到,就阻塞
  gp, inheritTime = findrunnable() // blocks until work is available
 }

//执行协程
 execute(gp, inheritTime) 
}

其中 findrunnable 如果从当前 P 的运行队列和全局运行队列获取 G 都没有任务后,还会尝试从其它的 P 中获取一些任务过来运行。代码就不过多展示了。

三、main 函数的真正运行

至此,整个 golang 的调度系统就算是跑起来了。因为前面我们创建了主协程,而且还给它设置了 runtime.main 函数作为入口。所以对于主协程的调度,就会进入这个入口进行执行。终于,能看到 runtime 快运行到我们自己写的 main 函数中了。

runtime.main 在执行 main 包中的 main 之前,还是做了一些不少其他工作,包括:

  1. 新建一个线程来执行 sysmon。sysmon的工作是系统后台监控(定期垃圾回收和调度抢占)。

  2. 执行 runtime init 函数。runtime 包中也有不少的 init 函数,会在这个时机运行

  3. 启动 gc 清扫的 goroutine。

  4. 执行 main init 函数。包括用户定义的所有的 init 函数。

  5. 执行用户 main 函数。

// file:runtime/proc.go
// The main goroutine.
func main() {
 g := getg()

 // 在系统栈上运行 sysmon
 systemstack(func() {
  newm(sysmon, nil, -1)
 })

 // runtime 内部 init 函数的执行,编译器动态生成的。
 doInit(&runtime_inittask) // Must be before defer.

 // gc 启动一个goroutine进行gc清扫
 gcenable()

 // 执行main init
 doInit(&main_inittask)

 // 执行用户main
 fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
 fn()

 // 退出程序
 exit(0)
}

好了,终于,我们定义的 main 函数能被执行到。可以输出 “Hello World!”了。

四、总结

Golang 程序的运行入口是 runtime 定义的一个汇编函数。这个函数核心有三个逻辑:

第一、通过 runtime 中的 osinit、schedinit 等函数对 golang 运行时进行关键的初始化。在这里我们将看到 GMP 的初始化,与调度逻辑。

第二、创建一个主协程,并指明 runtime.main 函数是其入口函数。因为操作系统加载的时候只创建好了主线程,协程这种东西还是得用户态的 golang 自己来管理。golang 在这里创建出了自己的第一个协程。

第三、调用 runtime·mstart 真正开启调度器进行运行。

当调度器开始执行后,其中主协程会进入 runtime.main 函数中运行。在这个函数中进行几件初始化后,最后后真正进入用户的 main 中运行。

第一、新建一个线程来执行 sysmon。sysmon的工作是系统后台监控(定期垃圾回收和调度抢占)。
第二、启动 gc 清扫的 goroutine。
第三、执行 runtime init,用户 init。
第四、执行用户 main 函数。

看似简简单单的一个 Golang 的 Hello World 程序,只要你愿意深挖,里面真的有极其丰富的营养的!

如果觉得有用,期待和给你的朋友一起分享~

e9b2f9bec5ac14497d74dde450095d2e.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/961538.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一题(链表中倒数第k个节点)

每日一题&#xff08;链表中倒数第k个节点&#xff09; 链表中倒数第k个结点_牛客网 (nowcoder.com) 思路: 如下图所示&#xff1a;此题仍然定义两个指针&#xff0c;fast指针和slow指针&#xff0c;假设链表的长度是5&#xff0c;k是3&#xff0c;那么倒数第3个节点就是值为…

解决WebSocket通信:前端拿不到最后一条数据的问题

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

最新智能AI系统ChatGPT网站程序源码+详细图文搭建教程/支持GPT4/WEB-H5端+微信公众号版源码

一、AI系统 如何搭建部署AI创作ChatGPT系统呢&#xff1f;小编这里写一个详细图文教程吧&#xff01;SparkAi使用Nestjs和Vue3框架技术&#xff0c;持续集成AI能力到AIGC系统&#xff01; 1.1 程序核心功能 程序已支持ChatGPT3.5/GPT-4提问、AI绘画、Midjourney绘画&#xf…

MySQL高阶语句(三)

一、NULL值 在 SQL 语句使用过程中&#xff0c;经常会碰到 NULL 这几个字符。通常使用 NULL 来表示缺失 的值&#xff0c;也就是在表中该字段是没有值的。如果在创建表时&#xff0c;限制某些字段不为空&#xff0c;则可以使用 NOT NULL 关键字&#xff0c;不使用则默认可以为空…

Vue中过滤器如何使用?

过滤器是对即将显示的数据做进⼀步的筛选处理&#xff0c;然后进⾏显示&#xff0c;值得注意的是过滤器并没有改变原来 的数据&#xff0c;只是在原数据的基础上产⽣新的数据。过滤器分全局过滤器和本地过滤器&#xff08;局部过滤器&#xff09;。 目录 全局过滤器 本地过滤器…

Python之父加入微软三年后,Python嵌入Excel!

近日&#xff0c;微软传发布消息&#xff0c;Python被嵌入Excel&#xff0c;从此Excel里可以平民化地进行机器学习了。只要直接在单元格里输入“PY”&#xff0c;回车&#xff0c;调出Python&#xff0c;马上可以轻松实现数据清理、预测分析、可视化等等等等任务&#xff0c;甚…

好马配好鞍:Linux Kernel 4.12 正式发布

Linus Torvalds 在内核邮件列表上宣布释出 Linux 4.12&#xff0c;Linux 4.12 的主要特性包括&#xff1a; BFQ 和 Kyber block I/O 调度器&#xff0c;livepatch 改用混合一致性模型&#xff0c;信任的执行环境框架&#xff0c;epoll 加入 busy poll 支持等等&#xff0c;其它…

从零开始,探索C语言中的字符串

字符串 1. 前言2. 预备知识2.1 字符2.2 字符数组 3. 什么是字符串4. \04.1 \0是什么4.2 \0的作用4.2.1 打印字符串4.2.2 求字符串长度 1. 前言 大家好&#xff0c;我是努力学习游泳的鱼。你已经学会了如何使用变量和常量&#xff0c;也知道了字符的概念。但是你可能还不了解由…

2023_Spark_实验四:SCALA基础

一、在IDEA中执行以下语句 或者用windows徽标R 输入cmd 进入命令提示符 输入scala直接进入编写界面 1、Scala的常用数据类型 注意&#xff1a;在Scala中&#xff0c;任何数据都是对象。例如&#xff1a; scala> 1 res0: Int 1scala> 1.toString res1: String 1scala…

11 模型选择 + 过拟合和欠拟合

训练集&#xff1a;用于训练权重参数 验证集&#xff1a;用来调参&#xff0c;评价模型的好坏&#xff0c;选择合适的超参数 测试集&#xff1a;只用一次&#xff0c;检验泛化性能&#xff0c;实际场景下的数据 非大数据集通常使用K-折交叉验证 K-折交叉验证 一个数据集分成…

云原生Kubernetes:二进制部署K8S多Master架构(三)

目录 一、理论 1.K8S多Master架构 2.配置master02 3.master02 节点部署 4.负载均衡部署 二、实验 1.环境 2.配置master02 3.master02 节点部署 4.负载均衡部署 三、总结 一、理论 1.K8S多Master架构 (1) 架构 2.配置master02 &#xff08;1&#xff09;环境 关闭防…

Docker:自定义镜像

&#xff08;总结自b站黑马程序员课程&#xff09; 环环相扣&#xff0c;跳过部分章节和知识点是不可取的。 一、镜像结构 镜像是分层结构&#xff0c;每一层称为一个Layer。 ①BaseImage层&#xff1a;包含基本的系统函数库、环境变量、文件系统。 ②Entrypoint&#xff1…

Vue在表格中拿到该行信息的方式(作用域插槽-#default-scope-解决按钮与行点击的顺序问题)

遇到的问题 在做表格的时候&#xff0c;表格是封装好了的&#xff0c;用于展示数据。如果想给单行增加按钮&#xff0c;可以单独写一列存放按钮&#xff0c;最基本的需求是&#xff0c;点击按钮后要拿到数据然后发起请求。 且Vue的element-plus&#xff0c;当我们点击按钮之后…

python二级例题

请编写程序&#xff0c;生成随机密码。具体要求如下&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬ &#xff08;1&#xff09;使用 rand…

XSS漏洞及分析

目录 1.什么是xss漏洞 1&#xff09;存储型XSS漏洞 2&#xff09;反射型XSS漏洞 3&#xff09;DOM型XSS漏洞 2.什么是domcobble破环 3.案例一 1&#xff09;例题链接 2&#xff09;代码展示 3&#xff09;例题分析 4.案例二 1&#xff09;例题链接 2&#xff09;代…

jvm-堆

1.堆的核心概念 一个jvm实例只存在一个堆内存&#xff0c;堆也是java内存管理核心区域 java堆区在jvm启动的时候即被创建&#xff0c;其空间大小就确定了&#xff0c;是jvm管理最大的一块内存空间&#xff1b; 堆可以处于物理上不连续的内存空间&#xff0c;但在逻辑上它应该被…

Linux gdb调式的原理

文章目录 一、原理分析二、dmoe测试2.1 hello.s2.2 demo演示 参考资料 一、原理分析 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <…

使用VBA快速比对数据

实例需求&#xff1a;第一行是全系列数据集合&#xff0c;现在需要对比第一行数据&#xff0c;查找第2行数据中缺失的数字&#xff0c;保存在第3行中。 具备VBA初步使用经验的同学&#xff0c;都可以使用双重循环实现这个需求&#xff0c;这里给大家提供另一种实现思路&#x…

写的一款简易的热点词汇记录工具

项目需要对用户提交的附件、文章、搜索框内容等做热词分析。如下图&#xff1a; 公司有大数据团队。本着不麻烦别人就不麻烦别人的原则&#xff0c;写了一款简易的记录工具&#xff0c;原理也简单&#xff0c;手工在业务插入锚点&#xff0c;用分词器分好词&#xff0c;排掉字…

阿晨的运维笔记 | CentOS部署Docker

使用yum安装 # step 1: 安装必要的一些系统工具 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 # Step 2: 添加软件源信息 sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 3: 更新并安装 …