Go源码--channel源码解读

news2024/11/25 14:55:50

简介

channel顾名思义就是channel的意思,主要用来在协程之间传递数据,所以是并发安全的。其实现原理,其实就是一个共享内存再加上锁,底层阻塞机制使用的是GMP模型。可见 GMP模型就是那个道,道生一,一生二,二生三,三生万物。
一个简单的例子 如下:

func main() {
	c := make(chan int32, 1)

	go func() {
		c <- 1
	}()

	go func() {
		fmt.Println(<-c)
	}()

	time.Sleep(2 * time.Second)
	close(c)
}

运行结果是:

1

其汇编部分代码如下:

 CALL    runtime.makechan(SB)  // 对应 make(chan int32,1)
 ...
 CALL    runtime.chanrecv1(SB) // 对应 <-c 
 ...
 CALL    runtime.chansend1(SB) // 对应 c<-1 其中 <- 编译后就代表运行时 chanrecv1 函数 
 ...
 CALL    runtime.closechan(SB) // 对应 close(c)

CALL runtime.closechan(SB)
协程部分汇编代码,因为不是重点,略过 感兴趣的自己可以编译看看。

chan的所有内容都存放在runtime.hchan这个结构体中,makechan,chanrecv1和chansend1函数都是操作hchan这个结构体来实现chan的功能的。下面我们来看下 hchan结构体。

两种重要结构体

hchan 结构体

hchan结构体 如下

type hchan struct {
	qcount   uint           // total data in the queue  环形队列里面的总的数据量 
	dataqsiz uint           // size of the circular queue // 环形队列大小 就是 make chan时 申请的大小
	buf      unsafe.Pointer // points to an array of dataqsiz elements // 指向环形队列的指针
	elemsize uint16         // 储存的元素类型占空间大小
	closed   uint32         // chan 状态 1 关闭 0 未关闭
	elemtype *_type         // element type // 元素类型   // make chan是 指定的类型 不过这个类型要进行运行时转换 但对应关系是这样的 
	sendx    uint           // send index // 发送索引
	recvx    uint           // receive index //  获取索引
	recvq    waitq          // list of recv waiters // 获取协程等待队列
	sendq    waitq          // list of send waiters // 发送携程等待队列

	lock mutex // 锁
}
waitq 结构体

waitq结构体用来存储阻塞在chan上的协程状态sudog结构体(内部包含了 协程信息)
其结构如下:

// 等待队列
type waitq struct {
	first *sudog // 双向链表 头
	last  *sudog // 双向链表 尾
}

这个结构体有两个函数 enqueuq 和dequeue 插入链表和删除链表 就是双向链表的基础操作

hchan的结构丑图如下:
在这里插入图片描述
接下来我们按照 执行流程来梳理下部分源码 源码位置在 runtime/chan.go中,首先是 make(chan int32,1)函数,我们都知道 make函数可以初始化,map,slice和chan。编译时根据不同类型,会调用makechan,makeslcie和makemap函数。我们来看下 makechan函数。

makechan

其源码如下:
请结合比较丑流程图来看,比较好理解。


func makechan(t *chantype, size int) *hchan {
	elem := t.Elem

	// compiler checks this but be safe.
	if elem.Size_ >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {
		throw("makechan: bad alignment")
	}
	// 计算内存
	mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

	// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
	// buf points into the same allocation, elemtype is persistent.
	// SudoG's are referenced from their owning thread so they can't be collected.
	// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
	var c *hchan
	switch {
	case mem == 0:
		// Queue or element size is zero.
		// 如果chan是空的 则需要申请hchanSize空间 以满足 内存对齐要求
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.PtrBytes == 0:
		// Elements do not contain pointers.
		// Allocate hchan and buf in one call.
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		// 分配内存
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}
	// 初始化hchan
	c.elemsize = uint16(elem.Size_)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")
	}
	return c
}

其实 就是 根据 chan 中元素类型和 大小 来计算需要的存储空间。逻辑还是比较清晰的。初始化了空间后,接下来就应该向chan存数据了,上例中是c <- 1,在编译时会转译成 chansend1函数。其源码如下:

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

它只是调用了 chansend 函数 且 阻塞 状态是 true(阻塞状态true代表 如果 chan 的 buf满员,则写协程放入sendq,如果 chan 为空,则读协程会放入 recvq。 如果阻塞状态是 false 如果chan buf,满员 则返回错误。如果chan 为空,则读协程读取chan返回错误。阻塞状态为false 主要是select函数用)。接下来我们来看下其源码:

chansend

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 省略部分代码

	// chan没关闭 且 缓存buf已满 且 是非阻塞模式(select方法使用)  则不会写入 sendq里 直接返回错误
	if !block && c.closed == 0 && full(c) {
		return false
	}
	// todo
	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}
	// 加锁
	lock(&c.lock)
	
	// 如果chan已经关闭 直接panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

	// 有阻塞的取协程 送的数据 直接交给取协程 并将取协程唤醒
	if sg := c.recvq.dequeue(); sg != nil {
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	// 如果没有阻塞的获取协程 且 channel容量没满 则需要插入channel中
	if c.qcount < c.dataqsiz {
		// 获取要放的位置的指针
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		// 将元素 放入其中
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		// ring buffer 循环数组 到尾 就 从头开始
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		// 解锁
		unlock(&c.lock)
		return true
	}
	// buf满了 但是没有 阻塞 则直接返回失败
	if !block {
		unlock(&c.lock)
		return false
	}

	// 如果channel满了 则所有send协程就需要加入 sendq 里排队,创建一个 等待状态的 sudog 包装当前 协程和数据 ep 放入 sendq中
	// Block on the channel. Some receiver will complete our operation for us.
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	// 初始化 sudog
	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
	// 放入 发送等待队列
	c.sendq.enqueue(mysg)
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	gp.parkingOnChan.Store(true)
	// 调用GMP模型 阻塞协程 并释放 chan的 lock 锁,释放后别的协程可以进来
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
	// Ensure the value being sent is kept alive until the
	// receiver copies it out. The sudog has a pointer to the
	// stack object, but sudogs aren't considered as roots of the
	// stack tracer.
	// ep保持活着 不被垃圾回收
	KeepAlive(ep)

	// 从等待队列唤醒 
	// someone woke us up.
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}

	gp.waiting = nil
	gp.activeStackChans = false
	closed := !mysg.success
	gp.param = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	mysg.c = nil
	// 释放sudog
	releaseSudog(mysg)
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

其执行主流程如下:
首先加锁

  1. 如果recvq里有数据,则将recvq链表头节点中的sudog拿出来,将send的数据直接交给sudog的recv协程,然后唤醒这个协程,最后释放当前send协程拥有的锁,返回。
  2. 否则 如果 hchan buf没满,则将数据存入其中,更新 qcount的值,释放锁,返回。
  3. 否则 初始化一个sudog并将 当前 send协程放入其中,并阻塞(调用GMP模型),然后释放当前send协程拥有的锁(别的send协程可以执行 chansend)
  4. 当前阻塞的send协程被待被recv协程唤醒。
  5. 唤醒后 将 当前协程状态变为非等待,释放当前协程对应的sudog 返回

这里有注意的点,sendq里的协程只能被 recv协程唤醒,反之亦然。这里就带来一个有趣的问题,sendq和recvq能都有数据吗?大神们可以思考下。

send讲完了,接下来改recv了,要不不就阻塞了吗,
例子中的 < - c 是 编译时会转译成 chanrecv1,我们来看下源码:

func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

其block参数也是 true ,这阻塞跟 send一样。我们来看下 chanrecv方法吧

chanrecv

其源码如下:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 非主逻辑代码 跳过

	lock(&c.lock)

	// 如果chan已关闭 且qcount==0 则 证明chan中已没有数据 返回
	if c.closed != 0 {
		if c.qcount == 0 {
			if raceenabled {
				raceacquire(c.raceaddr())
			}
			unlock(&c.lock)
			if ep != nil {
				typedmemclr(c.elemtype, ep)
			}
			return true, false
		}
		// The channel has been closed, but the channel's buffer have data.
	} else {
		// 如果chan没关闭 先看 sendq里 有没有阻塞获取sudog  如果有 取出来 直接将 数据给其中的协程 并唤醒 阻塞的读操作 并解锁
		// Just found waiting sender with not closed.
		if sg := c.sendq.dequeue(); sg != nil {
			// Found a waiting sender. If buffer is size 0, receive value
			// directly from sender. Otherwise, receive from head of queue
			// and add sender's value to the tail of the queue (both map to
			// the same buffer slot because the queue is full).
			recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
			return true, true
		}
	}

	// 走到这里 证明 chan没关闭 且 sendq没数据 则从缓存区取数据
	if c.qcount > 0 {
		// Receive directly from queue
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
		}
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

	// 非阻塞(waitq不能有数据), 返回
	if !block {
		unlock(&c.lock)
		return false, false
	}

	// 将 协程 构造sudog 存放到 recvq 中 阻塞协程, 下面代码跟 sendq类似 
	// no sender available: block on this channel.
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
	// 插入 recvq 链表尾部
	c.recvq.enqueue(mysg)
	// Signal to anyone trying to shrink our stack that we're about
	// to park on a channel. The window between when this G's status
	// changes and when we set gp.activeStackChans is not safe for
	// stack shrinking.
	gp.parkingOnChan.Store(true)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)

	// someone woke us up
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	gp.activeStackChans = false
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	success := mysg.success
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, success
}

其执行主流程如下:
首先加锁

  1. 如果协程已关闭 且 buf中没有数据 则 返回
  2. 如果协程没关闭 则从 sendq里取sudog数据,如果取到了 就 将 数据 传递给 sudog的send协程 当前recv协程释放锁,唤醒send协程 返回。
  3. 如果 没有从sendq里取到数据,则从 buf 里取数据,更新qcount 然后 解锁,返回
  4. 如果 buf 为空 ,则初始化一个sudog 将 当前协程放入其中,阻塞当前recv协程,释放当前recv协程拥有的锁。
  5. 等待其他send协程唤醒 当前recv协程。
  6. 唤醒后 将 当前协程状态变为非等待,释放当前协程对应的sudog 返回

send和recv后,接下来就该 close了
close( c)编译后 函数 closechan函数 我们来看下

closechan

源码如下:


func closechan(c *hchan) {
	if c == nil {
		panic(plainError("close of nil channel"))
	}
	
	// 加锁
	lock(&c.lock)
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

	if raceenabled {
		callerpc := getcallerpc()
		racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
		racerelease(c.raceaddr())
	}
 	
	// 将锁状态变为1 
	c.closed = 1

	var glist gList
	
	// 释放所有 读协程 
	// release all readers
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem)
			sg.elem = nil
		}
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	
	// 释放所有写协程 包括其数据 所以 我们从关闭的协程里读的数据 就是 buf 中的 不会有 sendq里的数据
	// release all writers (they will panic)
	for {
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}
		sg.elem = nil
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false
		if raceenabled {
			raceacquireg(gp, c.raceaddr())
		}
		glist.push(gp)
	}
	unlock(&c.lock)
	
	// 将所有协程唤醒 
	// Ready all Gs now that we've dropped the channel lock.
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

close 主要是将阻塞的 其读和写协程 携带的 元素 释放,(但是缓存buf里的数据并没释放)可以使得GC捕获,然后将阻塞的协程唤醒。这时 在 chansend函数 lock()处 阻塞的协程会panic,被goready 唤醒的协程会正常退出;在 chanrecv 函数 lock()处 阻塞的协程(或者继续执行<-c的协程)会继续从 buf 拿数据,当数据获取完后,会退出。

总结

channel 其实就是用了共享内存加锁这种机制来处理协程之间的共享数据的,这次阅读源码还是有些细节没整明白,整体理解的也不够透彻,还望大神指正。

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

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

相关文章

【Spring Boot】统一数据返回

目录 统一数据返回一. 概念二.实现统一数据返回2.1 重写responseAdvice方法2.2 重写beforeBodyWriter方法 三. 特殊类型-String的处理四. 全部代码 统一数据返回 一. 概念 其实统一数据返回是运用了AOP&#xff08;对某一类事情的集中处理&#xff09;的思维&#xff0c;简单…

UML2.0-系统架构师(二十四)

1、&#xff08;重点&#xff09;系统&#xff08;&#xff09;在规定时间内和规定条件下能有效实现规定功能的能力。它不仅取决于规定的使用条件等因素&#xff0c;还与设计技术有关。 A可靠性 B可用性 C可测试性 D可理解性 解析&#xff1a; 可靠性&#xff1a;规定时间…

在Linux操作系统中关于逻辑卷的案例

1.如何去创建一个逻辑卷 1.1先去创建物理卷 如上图所示&#xff0c;physical volume 物理卷 被成功创建。 如上图所示&#xff0c;可以使用pvscan来去查看当前Linux操作系统的物理卷/ 1.2使用创建好的物理卷去创建一个卷组。 如上图所示&#xff0c;可以使用第一步创建的两个…

点亮led灯——Arduino uno R3 学习之旅

​ 常识: 一般智能手机的额定工作电流大约为200mA Arduino Uno板上I/0(输入/输出)引脚最大输出电流为40 mA Uno板控制器总的输出电流为200 mA 发光二极管介绍 发光二极管(Light Emitting Diode&#xff0c;简称LED)是一种能够将电能转化为光能的固态的半导体器件。 LED的极…

鸿蒙开发:Universal Keystore Kit(密钥管理服务)【密钥导入介绍及算法规格】

密钥导入介绍及算法规格 如果业务在HUKS外部生成密钥&#xff08;比如应用间协商生成、服务器端生成&#xff09;&#xff0c;业务可以将密钥导入到HUKS中由HUKS进行管理。密钥一旦导入到HUKS中&#xff0c;在密钥的生命周期内&#xff0c;其明文仅在安全环境中进行访问操作&a…

输入框输入值之后,检索表格中是否存在输入框中的值,存在就让当前文字为红色

this.searchValue为输入框的值 createKeywordHtml_content(data) { if (data undefined) { return data; } if (typeof data ! string) { data String(data) } let value data.replace(this.searchValue, <span style"color:#FF5555">$&</span>…

Vue 性能革命:揭秘前端优化的终极技巧;Vue优化技巧,解决Vue项目卡顿问题

目录 Vue优化路径 一、使用key 二、使用冻结对象 三、使用函数式组件 四、使用计算属性 五、使用非实时绑定的表单项 六、保持对象引用稳定 6.1、保持对象引用稳定定义 6.2、保持对象引用稳定与不稳定的例子 6.3、vue2判断数据是否变化是通过hasChanged函数实现的 ①…

2024年软件测试岗必问的100+个面试题【含答案】

一、基础理论 1、开场介绍 介绍要领&#xff1a;个人基本信息、工作经历、之前所做过的工作及个人专长或者技能优势。扬长避短&#xff0c;一定要口语化&#xff0c;语速适中。沟通好的就多说几句&#xff0c;沟通不好的话就尽量少说两句。举例如下&#xff1a; 面试官你好&…

ONLYOFFICE 8.1版本震撼来袭,让办公更高效、更智能

官网链接&#xff1a; 在线PDF查看器和转换器 | ONLYOFFICE 在线办公套件 | ONLYOFFICE 随着科技的不断发展&#xff0c;办公软件已经成为现代企业提高工作效率、实现信息共享的重要工具。在我国&#xff0c;一款名为ONLYOFFICE的在线办公套件受到了越来越多企业的青睐。今天…

3086.力扣每日一题7/4 Java

博客主页&#xff1a;音符犹如代码系列专栏&#xff1a;算法练习关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 目录 思路 解题方法 时间复杂度 空间复杂度 Code 思路 首先通过循环计…

AI大模型对话(上下文)缓存能力

互联网应用中&#xff0c;为了提高数据获取的即时性&#xff0c;产生了各种分布式缓存组件&#xff0c;比如Redis、Memcached等等。 大模型时代&#xff0c;除非是免费模型&#xff0c;否则每次对话都会花费金钱来进行对话&#xff0c;对话是不是也可以参照缓存的做法来提高命…

ASUS/华硕飞行堡垒8 FX506L FX706L系列 原厂win10系统 工厂文件 带F12 ASUS Recovery恢复

华硕工厂文件恢复系统 &#xff0c;安装结束后带隐藏分区&#xff0c;一键恢复&#xff0c;以及机器所有驱动软件。 系统版本&#xff1a;Windows10 原厂系统下载网址&#xff1a;http://www.bioxt.cn 需准备一个20G以上u盘进行恢复 请注意&#xff1a;仅支持以上型号专用…

大量设备如何集中远程运维?用好向日葵的这几个功能很重要

当企业的业务不断发展&#xff0c;不同系统、不同用途的IT设备数量也会不断上升&#xff0c;面对不断扩张的设备规模&#xff0c;IT运维的压力自然也会陡然上升。 面对这种情况&#xff0c;传统的线下运维方式已经不再合适&#xff0c;我们需要引入一个专业的&#xff0c;可以…

Spring Boot集成jacoco实现单元测试覆盖统计

1.什么是jacoco&#xff1f; JaCoCo&#xff0c;即 Java Code Coverage&#xff0c;是一款开源的 Java 代码覆盖率统计工具。支持 Ant 、Maven、Gradle 等构建工具&#xff0c;支持 Jenkins、Sonar 等持续集成工具&#xff0c;支持 Java Agent 技术远程监控 Java 程序运行情况…

如何利用好用便签提高工作效率?

在忙碌的工作中&#xff0c;我们经常需要记住许多琐碎的任务。如果这些任务被遗忘&#xff0c;可能会对我们的工作产生影响。这时&#xff0c;便签就成为了我们的得力助手。通过合理的使用和管理&#xff0c;便签不仅能帮助我们记住重要的事项&#xff0c;还能提高我们的工作效…

计算机图形学入门25:BRDF的测量

1.前言 BRDF(双向反射分布函数)可以用各种各样的材质去描述&#xff0c;但是这只是一种基于物理的描述或者近似&#xff0c;那什么是真正的BRDF&#xff1f;只有测出来的才是真正的。 为什么要测出BRDF&#xff1f;因为之前所描述的BRDF并不准确。如下图所示&#xff0c;以菲涅…

MySQL 9.0 创新版发布,大失所望。。

大家好&#xff0c;我是程序员鱼皮。2024 年 7 月 1 日&#xff0c;MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS&#xff08;Long-Term Support&#xff09;长期支持版本&#xff0c;创新版本的发布会更频繁、会更快地推出新的特性和变更&#xff0c;可以理解…

鸿蒙开发设备管理:【@ohos.account.appAccount (应用帐号管理)】

应用帐号管理 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 导入模…

wsl安装Linux系统到指定位置

默认情况下,wsl安装的系统,会安装到系统C盘,长期下去,很容易把C盘的空间消耗完,从而影响系统的正常运行,所以我建议是将wsl所有的系统都安装到其它磁盘中,便于维护。 1、导出镜像 通过wsl -l -v 查看当前已安装的系统版本。 导出到当前目录位置,也可以指定目录位置。 w…

C语言 -- 操作符详解​

C语言 -- 操作符详解​ 1. 操作符的分类2. 二进制和进制转换​2.1 2进制转10进制​2.1.1 10进制转2进制数字​ 2.2 2进制转8进制和16进制​2.2.1 2进制转8进制​2.2.2 2进制转16进制​ 3. 原码、反码、补码​4. 移位操作符​4.1 左移操作符​ 4.2 右移操作符​5. 位操作符&…