学习Linux必备的硬件基础一网打尽

Linux阅码场原创精华文章汇总 持续更新,敬请期待!(最后更新日期2018.10.27)Linux学习方法:宋宝华: 迭代螺旋法——关于Linux学习方法的血泪建议Linux任督二脉之进程管理郭健: L... 阅读详情

内容简介:

出来混,迟早要还的.....

本文详细论述大学时候的基本功,MMU,CACHE, TLB,  Page Fault, 进程切换........

1.      案情回溯

某一个夜黑风高的晚上,宋宝华老师组织的微信群一片祥和宁静。我刷着手机,心血来潮往微信群中提出一个疑惑:

" 宋老师,cache是一个程序维护一份还是所有程序维护一份?你昨天说进程调度损耗还包括cache命中的问题,那如果一个进程一份cache不就好了? "

正所谓一石激起千层浪,这个傻逼竟然问这么弱智的问题?!宋老师急怒攻心,就差扯着我的衣领大吼:

”别问这么傻的问题了看书!!你这样真吐血!“

在宋老师的淫威之下瑟瑟发抖,于是狠下心来,翻箱倒柜找出了封印多年的《计算机操作系统》,于是便有了此文。

本文尽可能从基础部分讲起,适合完全没学过计算机操作系统的小白,也适合乱七八糟学过一些但没理清脉络的读者;不适合完全掌握计算机操作系统原理的大牛。

本人水平有限,若存在理解错的地方,欢迎提出。

 

2.      存储结构

2.1.    经典存储结构图

结构图要点解析:

  • 从L0到L6的7个层次,都是硬件设备,并不是软件的概念。尤其注意图中的高速缓存,常称为CPU cache,与我们常说的Page cache是不一样的,前者是实在硬件设备,后者是软件上的概念
  • 金字塔越往上,速度越快,价格也越高。出于成本考虑,实际产品中价格越高的存储器实际使用容量越小,例如寄存器以个计数,1级缓存单位是KB,3级缓存单位是MB,内存几GB,最底层的磁盘却可达到TB级别
  • 磁盘是非易失性存储器,而磁盘以上的都是易失性存储器。这也意味着掉电后,磁盘数据依然存在,。于此相反,内存是易失性存储器,掉电则数据丢失。
  • 上层存储器的数据来自下层,且是直接下层,例如L1高速缓存的数据直接来自于L2高速缓存

结合b、c和d三点,我们初步形成一个认识:

“每次从上电开始,只有磁盘有数据。CPU都是从磁盘读取原始数据,然后根据一些算法挑选数据往上传递,例如从磁盘选部分数据放到内存,从内存提取部分数据到高速缓存。”

 

2.2.    本章总结图

 

Ps. 后面章节逐渐完善此图,让章节之间,知识之间构建联系。

3.      什么是页?什么是页表?什么是快表?什么又是MMU

3.1.    “页”是什么?

在了解"页"之前,务必初步了解"虚拟存储器"。

3.1.1.       虚拟存储器

那么,什么又是虚拟存储器呢?《计算机操作系统 第三版》有这么一句话:

“(虚拟存储器是)具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储系统”

本文关注的重点是:“从逻辑上对内存容量加以扩充”。什么是逻辑上扩充内存容量呢?

例如,在旧一点的32位cpu的手机中有且只有2G内存,但是每一个进程在执行过程中都仿佛手机有4G内存,且每个进程都能独占4G内存。注意了,是每一个进程都独占4G内存,每一个!每一个!

这就是从逻辑上对内存进行扩充,但这也是赤裸裸的欺骗。

实际物理内存明明只有2G,页表的机制让每个进程都仿佛有4G。进程被欺骗了,但进程可爽了。为什么爽呢?对应用来说,你告诉我有4G内存啊,我不管你实际有多少,我需要用到4G的内存时,你要给到我!于是内核就苦逼了。

我们把APP访问到的4G虚拟内存地址叫做:虚拟地址

我们把内核实际管理的2G物理内存地址叫做:物理地址

先形成这样一个初步认识:CPU访问虚拟地址,MMU把虚拟地址转为物理地址,CPU再通过物理地址访问物理内存。(Ps. MMU是CPU中的一个硬件模块,硬件模块!硬件模块!专门负责虚拟地址到物理地址的转换,下文会有更详细的介绍)

3.1.2.       页

清楚了什么是虚拟地址,什么是虚拟存储器,我们再来看看,“页”是什么?

“把APP的虚拟地址空间切分为若干个大小相等的片,每一片,就是我们说的页(Page)”

注意,这里说的是虚拟地址空间,虚拟地址空间!虚拟地址空间!在Linux上,页大小通常为4K。每一个页都有编号,从0开始,如第0页,第1页等。

”把物理内存也按"页"的大小切分为若干大小相等的片,每一片,就是我们说的页框(frame)“

页框就是用来装页的呀,就像相框用来装相片,务必大小一致!

页框也叫页帧、物理块(≠I/O概念上的块),由于契合相框的形象,本文统一称为页框。

因此,我们说:

“页是内存管理的基本单位”

内存管理就是把物理内存划分为一个一个的页(框),进程在申请内存时,内核就以页为单位,把进程的一个个(虚拟)页装入到多个可能不连续的(物理)页框中。

我们以一张图来形象描述页和页框的关系:

上图除了描述页与页框的关系,还隐藏了两个重要的信息:

 

  • 一个进程的页并不是全部映射到物理内存页框中
  • Linux内核只有在万不得已得情况下才会真正为进程分配物理内存。例如进程1中的白色部分,由于进程还没使用,因此白色部分并不会指向物理内存的页框。
  • 多个进程的页可以映射到共同的物理内存页框中

上图中进程1与进程2共用蓝色页框。这些共享的页框一般是动态库,例如所有进程使用相同的C库指令。物理内存空间有限,内核没必要为每个进程单独维护一份C库指令。

3.2.    页表是什么?

3.2.1.       页表

从页与页框的图,我们知道虚拟的进程页与物理内存页框有映射关系,而且是离散映射的。那么是什么记录了这种映射关系?是页表。页表记录了“页”与“页框”的对应关系。

我们以一张图来形象描述页表:

 

上图有以下几个要点:

  • 内核为每一个进程维护一份页表,一对一!
  • 页表记录了进程页与内存页框之间的对应关系。
  • 进程的页离散存储在物理内存的页框中。例如进程第1页存放在物理第2号页框中,第2页却放在第4页框。
  • 只有在万不得已情况下,内核才真正为进程页分配物理内存。例如第0页,第3页因为进程还没真正使用,是没有页框号的。
  • 进程的页,不管有没使用,都完整记录在页表中。例如从第0页到最后的第1048575页,都记录在进程A的页表中。

3.2.2.       多级页表

是否思考过上图最后一页为什么是第1048575页?

以32位CPU有4G虚拟内存为例,以linux的4K页大小计算:

虚拟页数量 = 4G/4K = 1M ( 1048576)

从0开始算,因此最后一个页号是1048576 - 1 = 1028575。

一个页表项(就是页表的一行)通常只占4Byte,因此描述一份4G虚拟地址空间大小的页表,就有1048576*4B= 4M。而且由于页表是一个表的结构,且换算页表项地址是通过表内偏移实现,因此存放页表的物理页框必须连续。也就是说,一个进程还没开始跑就要先占用4M的内存存放页表?这么浪费,还要是连续物理地址的内存?不能忍!为了解决此问题,于是设计了2级页表,甚至多级页表。

多级页表,简单来说就是对那1级页表需要的4M连续内存再次分页,再使用另外一个页表保存分页的记录,内核只需要记录最上一层的页表,通过多次页表的转换,取得真正的物理页框号。因此,最上一层的页表就可以很小,且支持离散存储。

篇幅有限,不展开多级页表,只需要了解个概念即可,即使不理解也不影响本文的讨论。

3.3.    “快表”是什么?

页表存在于内存中,咱们暂时忽略高速缓存,那么CPU每次获取数据都需要2次访问内存:

a.         第一次访问内存的页表,获取物理地址

b.        第二次根据物理地址获取真实数据

内存访问速度虽然比磁盘要快,但跟CPU比依然不在一个数量级,CPU得浪费非常非常多时间等待内存。

为了提高极致提高访问页表的速度,于是有了快表。

快表,又名TLB(Translation Lookaside Buffer),是单独的一组寄存器,也有说是高速缓存,用于记录页表中正在频繁使用的页表项。

“由于程序和数据的访问往往具有局限性(局部性原理),因此,据统计,快表命中概率可达90%以上”

局部性原理是什么?后面还会有更详细的介绍。在这里,我们要有个认识即可:

快表是为了从概率命中的角度加速虚拟地址到物理地址的转换,设计的一组高速存储硬件模块。

1个CPU多核之间共享Cache,但是一个核却有独占一个快表。

由上一章《存储结构》我们知道, 寄存器的速度最接近于CPU,或者说,完全跟得上CPU的节奏,但很贵,因此:

l  快表容量有限,通常只能记录16~512个页表项

l  转换虚拟地址到物理地址时,优先从快表寻找

l  如果快表中找不到对应页表项,则从内存页表中获取,同时更新快表

l  更新快表时,如果快表已满,则淘汰一个最老的且已被认为不再使用的页表项

3.4.    “MMU”是什么?

本章到这,我们已经知道,什么是"页",什么是"页表",什么是"快表",也知道了他们都是为了从虚拟地址向物理地址转换服务。但我们似乎忽略了一个问题,谁来负责执行这项频繁的地址转换工作?CPU?不不,CPU已经够忙的了。这就到了我们勤勤恳恳的MMU表现的时候了。

MMU(MemoryManagememt Unit),内存管理单元。是CPU内的一个硬件模块,硬件!硬件!负责从虚拟地址转换物理地址,并提供硬件机制的内存访问权限检查。

MMU转换虚拟地址到物理地址是其内的硬件电路做的,硬件电路!硬件电路!

我们需要形成个认识即可,下文会有更详细的介绍:

CPU需要访问进程的虚拟地址,于是把虚拟地址发送给MMU,由MMU内的硬件电路实现访问快表、页表,最终得到的物理地址。

3.5.    本章总结图

 

Ps. 请试试对比总结图1,并结合本章知识解剖此图

Ps. 此处的CPU core是指运算单元,而非类似Cortex-A7等 的core,因为MMU、TLB、CPU Cache等也归属于Cortex core。

4.      什么是页面调入和页面置换?如何置换页?

4.1.    什么是页面调入和页面置换?

4.1.1.       页面调入

在第3章"什么是页"的探讨中提到:

Linux内核只有在万不得已得情况下才会实际为进程分配物理内存

这个结论同样适用于开始执行程序时的加载程序,例如一个可执行程序100M,这100M指令、数据必须要加载到内存CPU才可访问。但是物理内存容量有限,不可能为了这个程序,一执行立马为程序分配100M物理内存,否则你多执行几个程序不就没物理内存了么?

因此,这100M的程序只是逻辑上"加载"到了虚拟内存空间,为数据指定了虚拟页号,实际上还没为进程的页分配物理页框。在程序指令需要CPU读取这100M数据时,内核才"磨磨蹭蹭"的真正分配物理页框,而且是程序要什么内核才加载什么,且一次只加载一部分。

这个从下层获取进程页面的操作就叫"页面调入"。

4.1.2.       页面置换

在第2章《存储结构》中有提到,实际产品中,越上层价格越贵,因此出于成本考虑,实际使用容量越小。因此,上层只能从下层挑选一部分页通过页面调入加载到上层。

挑选什么数据加载到上层?这是由一些算法决定的,例如常见的LRU算法。算法下文继续讨论,本节我们继续探讨,什么是页面置换?

上层存储容量有限,如果上层本身已经放满数据了,要调入新的页,只能淘汰旧的页。

这个淘汰旧页,拥抱新页的过程就是"页面置换"。

淘汰的旧页不都是直接丢弃,例如脏文件数据就会回刷,不常用的页会置换到交换分区等。

4.1.3.       缺页中断

CPU/内核中有2个需要调入页面和页面置换的地方:

a.         从磁盘加载数据到内存

b.        从内存加载数据到CPU高速缓存

前者是内核软件实现的置换算法,软件!软件!后者是CPU硬件实现的置换算法,硬件!硬件!

那么,我们所说的缺页中断是怎么一回事呢?就是内核提供的软件实现页面调入和页面置换的中断处理代码。

在进程所要访问的页如果不在内存时,由MMU触发一次缺页中断,执行内核提供的中断处理程序,将数据通过页面调入和页面置换的方式,从磁盘加载到内存。

由于上层的数据来自下层,且只是下层的一个拷贝,因此当内存都没有对应数据时,上层的高速缓存也不可能有这些数据。

行文到此其实已经暴露了我在《案情回溯》中的提问有什么不对。

我的提问:

" 宋老师,cache是一个程序维护一份还是所有程序维护一份?你昨天说进程调度损耗还包括cache命中的问题,那如果一个进程一份cache不就好了? "

提问有哪里不对呢?

a.         CPU cache是一个物理设备,而不是软件层面的Pace Cache类似的概念。因此cache不是用"份"的量词来描述的,这不是软件概念。

b.        一个CPU中多个核共用CPU cache,且cache的页面调入是物理算法实现的。硬件算法只挑选最活跃的页进入高速缓存,这不是软件所能控制的。

 

4.2.    如何置换页

4.2.1.       局部性原理

在探讨"如何置换页"之前,我们必须了解什么是"局部性原理"

程序运行有以下3个特点:

时间局部性:如果一个数据/指令正在被访问,那么在近期它很可能还会被再次访问

空间局部性:在最近的将来将用到的信息很可能与现在正在使用的信息在空间地址上是临近的

顺序局部性:在典型程序中,除转移类指令外,大部分指令是顺序进行的

 简单来说,对同一个进程而言,CPU访问了某个逻辑地址的数据/指令,则CPU在将来很有可能再次访问这个虚拟地址或相邻的一小片连续地址,这就是局部性原理。

根据"局部性原理"我们知道什么地址的数据是CPU在将来的短时间内会访问的,因此我们就可以把这些数据缓存到更高速的存储设备中。

高速缓存容量有限,为了保证CPU尽可能从高速缓存中获取数据(内存速度<高速缓存),就是我们所说的提高Cache命令率,我们就要尽可能确保高速缓存中的数据是CPU将要用的。

同样的,内存容量有限,且远小于磁盘,但速度远大于磁盘,为了提高CPU获取数据的速率,我们就要尽可能保证内存中数据是CPU将要用的。

CPU优先从高速缓存获取数据,在高速缓存没有命中时,再去内存获取,如果内存也没命中,则必须从磁盘中调入。因此,页面置换尽可能保证:内存的数据是CPU将要用的,而高速缓存的数据来自内存,其数据是CPU很可能"立刻即刻马上"要用的。

因此,我们就需要根据"局部性原理",淘汰Cache/内存中被认为CPU不再使用的页,腾出空间存放被认为马上要用的页。

4.2.2.       LRU置换算法

目前常见的页面置换算法有3种:

l  FIFO:先入先出,从时间维度,淘汰最早使用的页

l  LRU:最近最少使用(LeastRecently Used),从时间维度,淘汰最久没用的页面

l  LFU:最近最不经常使用(LeastFrequently Used),从频率维度,淘汰使用次数最少的

本文主要介绍LRU算法,假设场景如下:

刚上电,此时Cache是空的,数据全部在内存。根据LRU算法从内存获取数据到Cache。某个进程首先开始执行,其执行过程中需要访问到的指令/数据的虚拟地址所在的页顺序依次是:1,2,4,2,3,1

根据上述场景,Cache内数据的变化演示如图:

 

a.         CPU执行进程指令,首先需要访问第1页,然而Cache是空的,无第1页缓存,因此触发第1次缺页。

b.        接下来的第2页,第4页,都由于Cache没命中且Cache还有空余,触发第2、3次缺页,而不会淘汰页。

c.         第4次需要访问第2页,此时第2页已经在Cache中,Cache命中。不缺页,但会调整页顺序。

d.        第5次需要访问第3页,Cache不命中,触发缺页。但此时由于Cache已经满了,则按照LRU算法,淘汰最久没使用的页,因此淘汰第1页。

e.      第6次需要访问第4页,此时第4页已经在Cache中,Cache命中。不缺页,但会调整页顺序。

4.3.    本章总结图

 

5.      CPU寻找页面的过程

CPU获取数据主要有两步:

a.         通过虚拟地址获取物理地址

b.        根据物理地址获取数据

本章主要分析这两步过程,其中【步骤a】-【步骤i】为获取物理地址的过程;【步骤A】-【步骤D】为根据物理地址获取数据过程。

5.1.    获取物理地址

参考图如下:

 

【a】CPU向MMU发送虚拟地址

【b】MMU查询快表TLB

快表命中:从快表获取物理地址,由MMU继续根据物理地址匹配数据页【步骤A】

快表不命中:查询页表【步骤c】

【c】从高速缓存查询页表

"高速缓存"缓存页框,而页表存储于页框中,因此读取页表实际上也是读页(页表的物理地址有单独的寄存器保存)

高速缓存命中:从高速缓存获取物理地址,并更新快表,进入【步骤A】

高速缓存不命中:查询内存中的页表【步骤d】

【d】从内存中查询页表

页表保存了所有虚拟地址的映射关系,而不管页是否有映射的物理页框。因此从内存页表中肯定能“命中”页。从页表获取虚拟地址的映射关系后,进入【步骤e】

【e】把查询的页表项记录到高速缓存,进入【步骤f】

【f】MMU从高速缓存中获取页表项

虚拟地址有分配物理页框:转换物理地址并更新快表,由MMU继续根据物理地址匹配数据页【步骤A】

虚拟地址没分配物理页框:触发缺页中断【步骤g】

【g】进入缺页中断处理程序

【h】为虚拟页分配物理页框

如果无空闲物理页框,则根据LRU算法选择需要淘汰的页,要淘汰的页如果是脏页则回刷,否则直接丢弃或者置换到磁盘的交换分区中。

然后,从磁盘中获取页信息更新到物理页框,更新内存页表。

【i】缺页中断处理的最后

修改CPU寄存器,使得重新启动导致缺页的指令,返回【步骤a】重新执行。

5.2.    根据物理地址获取实际数据

参考图如下:

 

【A】MMU获取物理地址后,根据物理地址查询高速缓存

高速缓存命中:CPU直接从高速缓存获取数据【步骤D】

高速缓存不命中:查询物理内存【步骤B】

【B】根据物理地址,获取物理内存中的数据

【C】硬件实现把数据页记录到高速缓存中

【D】CPU直接从高速缓存中获取数据

5.3.    最理想(Cache和TLB命中)情况下寻找页面过程

大多时候都会命中,在命中时,CPU只需要通过高速缓存和TLB即可快速获取数据。

参考图如下:

 

【a】CPU向MMU发送虚拟地址

【b】MMU查询快表TLB

快表命中:从快表获取物理地址,由MMU继续根据物理地址匹配数据页【步骤A】

【A】MMU获取物理地址后,根据物理地址查询高速缓存

高速缓存命中:CPU直接从高速缓存获取数据【步骤D】

【D】CPU直接从高速缓存中获取数据

5.4.    本章总结图

 

6.      用3句代码小结

我们以3句C代码的操作流程,对前面章节做一次小结。

char *p = malloc(4 * 1024 * 1024);

memset(p, 'g', 1024);

p[1] = '\0';

为了方便理解,假设此3行代码编译后执行的设备为32位LinuxPC,假设页大小为4K,且只有1级页表。页表项结构为:

 

需要澄清的是,本章为了方遍理解做了一些简化,实际执行过程肯定更复杂。

6.1.    第1句:char *p = malloc(4 * 1024 * 1024);

6.1.1.       页表情况

假设malloc返回p的值是 0x10240011,表示虚拟地址 [0x10240011, 0x10640011)的4M空间被这次malloc申请成功。那么其对应的虚拟页号和页内偏移如何计算呢?

为了方便计算,我们把16进制的逻辑地址转为10进制的逻辑地址,则:

开始虚拟地址:0x10240011 => 270794769

结束虚拟地址:0x10640011 => 274989073

如何计算1级页表的页号和页内偏移呢?(多级页表换算更复杂,此处为了简化,只使用1级页表)

一级页表页号 = 地址 / 页大小【求整】

一级页表页内偏移 = 地址 % 页大小【求余】

因此计算的虚拟页号为:

开始虚拟地址:270794769 / ( 4 * 1024) = 66112

结束虚拟地址:274989073 / ( 4 * 1024) = 67136

虚拟页内偏移为:

开始虚拟地址:270794769 % ( 4 * 1024) = 17 = 0x11

结束虚拟地址:274989073 % ( 4 * 1024) = 17 = 0x11

我们以这样一张图描述执行代码后的页表情况:

 

图 6-2第1句代码页表情况

上图要点解析:

a.         malloc申请的空间地址是虚拟地址,虚拟地址!虚拟地址!

b.        刚申请时并没有立刻分配页框,因此页表上并没有对应页框号

6.2.    第2句:memset(p, 'g', 1024);

第2句代码把p指向的地址1K大小内存设置为'g'

6.2.1.       页表情况

 

图 6-3第2句代码页表情况

上图要点解析:

a.         由于只初始化了1K的数据量,在例子中只涉及1个页,且虚拟页号为:66112

b.        查询页表发现第66112页并没有对应物理页框,于是为其分配了第4097页框

c.         CPU执行指令,把虚拟地址0x10240011开始的1K内存初始化为'g',因此虚拟页映射的内存物理页框偏移0x11到0x411被写入'g'

6.2.2.       CPU寻找页面过程

 

 

图 6-4获取物理地址

【a】CPU向MMU发送虚拟地址0x10240011,转换出虚拟页号为第66112页,偏移为0x11

【b】MMU查询快表TLB,快表不命中

【c】从高速缓存查询页表,高速缓存不命中

【d】从内存中查询页表,内存中有进程的完整页表

【e】把查询的页表项记录到高速缓存

【f】MMU从高速缓存中获取页表项,发现虚拟地址没分配物理页框,触发缺页中断

【g】进入缺页中断处理程序

【h】为第66112号虚拟页分配第4097页框,更新内存页表

【i】缺页中断处理的最后,修改CPU寄存器,使得重新启动导致缺页的指令

 

图 6-5根据物理地址获取数据

【A】MMU根据物理页框号4097查询高速缓存,发现没有对应页框缓存

【B】根据物理地址,获取物理内存中的数据

【C】硬件实现把第4097页框加载到高速缓存中

【D】CPU根据物理地址0x01001011,把1K大小的'g'写入高速缓存中,高速缓存再在合适时候同步到内存中

6.3.    第3句:p[1] = '\0';

把p[1]的字节写入'\0'

6.3.1.       页表情况

 

图 6-6 第3句代码页表情况

上图要点解析:

a.         由于上一行代码已经访问过对应物理地址,因此TLB和CPU Cache都会命中

6.3.2.       CPU寻找页面过程

 

图 6-7 命中时获取数据过程

【a】CPU向MMU发送虚拟地址 0x10240012,虚拟页号为第66112页,偏移为0x12

【b】MMU查询快表TLB快表命中,从快表获取物理地址为0x01001012,页框号为4097,偏移为0x12。

【A】MMU获取物理地址后,根据物理地址查询高速缓存。高速缓存命中,CPU直接从高速缓存获取数据

【D】CPU把高速缓存内物理地址为0x01001012的1字节内容改为'\0',高速缓存再在合适时候同步到内存中

7.      进程切换和花销

7.1.    进程切换时内核要做什么?

进程切换也叫进程上下文切换。什么叫进程上下文?

 

“切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文”

 

所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换,简单来说就是把正在CPU中执行的程序的所有信息保存在进程的堆栈中,腾出CPU以及紧缺的CPU寄存器给待执行进程。

进程切换(进场上下文切换)时,内核需要保存旧进程的什么?需要恢复新进程的什么?

a.         保存处理器PC寄存器的值到被中止进程的私有堆栈;

PC寄存器指向存放的是下一步要访问的内存地址

b.        保存处理器PSW寄存器的值到被中止进程的私有堆栈;

PSW寄存器反映处理器的状态及某些计算结果以及控制指令的执行

c.         保存处理器其他寄存器的值到被中止进程的私有堆栈;

d.        保存处理器SP寄存器的值到被中止进程的进程控制块;

SP寄存器指向进程私有堆栈栈顶

e.        刷掉部分TLB和Cache;

参考网友分析文

(http://www.wowotech.net/process_management/context-switch-tlb.html)        

其提到TLB和Cache在进程切换时会flush部分TLB和Cache

f.          自待运行进程的进程控制块取SP值并存入处理器的寄存器SP;

g.         自待运行进程的私有堆栈恢复处理器各寄存器的值;

h.        自待运行进程的私有堆栈中弹出PSW值并送入处理器的PSW;

i.          自待运行进程的私有堆栈中弹出PC值并送入处理器的PC

总结起来,就两个步骤:

保存:把各个寄存器保存在进程私有堆栈中,把私有堆栈指针保存在进程控制块(struct task)中

恢复:从进程控制块(structtask)中获取私有堆栈指针,再从私有堆栈中弹出之前保存的各种寄存器

7.2.    为什么说大量进程切换花销大?

进程切换花销主要包含直接花销和间接花销的2个方面。

直接花销:

        CPU寄存器需要保存和加载,系统调度器的代码需要执行。

间接花销:

        TLB和Cache由于进程切换导致不命中,前期触发大量的缺页中断。

因此,当存在大量的进程切换时,CPU需要做大量的进程上下文切换工作和缺页中断,这些花销是非常可观的。

8.      本文参考资料

8.1.    参考书

《计算机操作系统 第三版》

8.2.    参考链接

8.2.1.       存储结构

https://blog.youkuaiyun.com/u013471946/article/details/41456055

8.2.2.         页的调入、置换、缺页、DMA、Cache

http://blog.sina.com.cn/s/blog_a018ffa90101smj8.html             https://baike.baidu.com/item/CPU%E7%BC%93%E5%AD%98/3728308?fr=aladdin

http://blog.chinaunix.net/uid-13246637-id-5185352.html

https://blog.youkuaiyun.com/zssmcu/article/details/6836869

https://blog.youkuaiyun.com/chinesedragon2010/article/details/5922324

8.2.3.         TLB

https://baike.baidu.com/item/TLB/2339981?fr=aladdin

http://www.wowotech.net/process_management/context-switch-tlb.html

8.2.4.         进程切换

https://blog.youkuaiyun.com/gettogetto/article/details/74629805

https://blog.youkuaiyun.com/gatieme/article/details/51872594

Allegro File菜单实战指南:从导入网表到导出BOM的完整流程 PCB设计离不开对EDA软件菜单体系的深入理解。在Cadence Allegro中,File菜单是数据流转的中枢,它不仅是新建、打开、保存的基础入口,更是连接原理图与版图、设计与制造的关键桥梁。工程师常遇到的“导入网表报错”“导出BOM为空”“板框不闭合”等高频问题,大多源于对File菜单中Import与Export功能的理解不到位。从基础概念出发,先厘清Import(网表、DXF、IPC)与Export(BOM、ODB++、DXF)的工作原理,再结合版本兼容、封装库路径、单位设置等工程细节,即可构建完整的 阅读详情

相关推荐

Ubutntu下使用realsense d435i(二):获取某二维像素点的三维坐标

00 准备工作 准备工作包括安装驱动以及pyrealsense2,具体可参考上一篇: Ubutntu下使用realsense d435i(一):准备工作以及安装pyrealsense2 https://blog.youkuaiyun.com/gyxx1998/article/details/121461293 01 通过二维像素点获得物体三维坐标 这个在上一篇已经介绍了代码: realsense d435i获取某像素点三维坐标(计算深度和点云两种方法) https://blog.youkuaiyun.com/gyxx1998/

gyxx1998的博客 9943

廖威雄: 学习Linux必备的硬件基础一网打尽

作者简介:    廖威雄,目前就职于珠海全志科技股份有限公司从事linux嵌入式系统(Tina Linux)的开发,主要负责文件系统和存储的开发和维护,兼顾linux测试...

Linux阅码场 1122

Win10或Win8下ObjectARX2015 Wizard向导创建项目失败解决方法

[原创]objectARX 2015 Wizard安装向导在Win8/win10下无法创建项目的解决方法总结 by edata @2017-5-1 objectARX 2015 Wizard安装向导在Win8/win10下无法创建项目的解决方法 因为Win8/Win10对系统盘(默认c:)使用了高级权限管理,所以在此盘下操作文件的修改工作都需要管理员权限才行。 基于这个原因,导致非管理员安

edata的博客 7027

无废话硬核分享:Linux 基础知识点总结很详细,全的很,吐血奉献

Linux 的学习对于一个程序员的重要性是不言而喻的。前端开发相比后端开发,接触 Linux 机会相对较少,因此往往容易忽视它。但是学好它却是程序员必备修养之一。Linux 基础操作系统操作系统Operating System简称OS,是软件的一部分,它是硬件基础上的第一层软件,是硬件和其它软件沟通的桥梁。操作系统会控制其他程序运行,管理系统资源,提供最基本的计算功能,如管理及配置内存、决定系统资源供需的优先次序等,同时还提供一些基本的服务程序。什么是 LinuxLinux 系统内核与 Linux 发行套件

欢迎朋友,我是一名十多年开发经验的新手,希望多多指教。 1475

汽车诊断之UDS入门-0x19 0x0A服务

0x19 0x0A解析

weixin_44842720的博客 3833

联想ThinkPad声卡驱动安装避坑指南:从E470到X1 Carbon的通用解法

本文详细介绍了ThinkPad声卡驱动安装的全流程,从硬件识别到驱动获取、安装技巧及深度排错。涵盖E470到X1 Carbon等不同机型的通用解法,帮助用户解决音频驱动问题,提升使用体验。重点讲解了声音驱动的精准识别和安装策略,确保系统稳定运行。

weixin_29207439的博客 707

Linux硬件基础

目录 Linux硬件基础 一、计算机的相关介绍 二、运维的核心职责 三、运维的平时工作内容 四、服务器的相关介绍 五、企业案例:提升用户体验的网站解决方案 六、磁盘 七、Raid卡(阵列卡) 八、光驱 ...

andy09_2014的博客 732

EPLAN Electric P8 2025安装避坑指南:硬件、.NET 6与VM配置全解析

EPLAN Electric P8 是面向工业自动化电气设计的工程级CAD平台,其核心依赖于.NET运行时、DirectX硬件加速与本地许可证验证机制。随着2025版升级至.NET 6.0 Runtime、DirectX 12渲染引擎及双模授权校验,安装失败率显著上升——尤其在显卡驱动陈旧、虚拟机环境或企业离线场景下。技术价值在于保障PLC柜体布局、端子排自动生成、电缆清册输出等产线级功能稳定运行;典型应用场景包括高校电赛控制系统设计、包装线电控图纸交付及集成商多版本协同部署。本文聚焦EPLAN 2025安

weixin_30897079的博客 549

linux硬件基础

linux 硬件基础

江南又一春 404

YOLO26全网首发:CVPR2026 FAA| 解决YOLO Neck跨尺度方向冲突--- FAAFusion频域对齐融合模块 | 暴力涨点篇

本文改进:FAAFusion引入YOLO neck ,代替nn.Upsample和Concat

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 638

PostgreSQL:Patroni 高可用架构,基于etcd的自动故障转移集群

本文详细介绍了基于Patroni和etcd构建PostgreSQL高可用集群的方案。Patroni作为外部守护进程,通过etcd分布式协调服务实现自动故障转移、主节点选举等关键功能,弥补了PostgreSQL原生缺乏自动故障转移的不足。文章涵盖核心架构(3节点PG+3节点etcd)、环境规划、etcd集群部署(包括配置验证)以及Patroni安装配置的关键步骤,特别强调了Patroni自动初始化PG实例、配置同步复制等生产级特性。该方案通过外部组件协同工作,在不修改PG源码的情况下实现了生产环境所需的高可用

数据知道的博客 2486

系统层面知识连接收藏

收藏网络上的文章链接,供查询用。

wwwyuewww的专栏 305

工业视觉实战:用摄像头+AI高精度测量钢板尺寸(附深度分析)

摘要: 钢铁制造业正加速采用基于摄像头与深度学习的视觉测量技术,以取代传统人工测量,实现钢板长、宽、高(厚)的非接触、高精度在线检测。核心方案包括单目视觉(低成本但受限于变形)、双目/多目立体视觉(可精确测量变形钢板)及3D激光扫描(微米级精度)。系统精度受硬件(相机分辨率、镜头)、算法(亚像素技术)及环境稳定性共同影响,综合精度可达±0.1mm。实战中需重点解决热膨胀(需红外测温实时补偿)和钢板变形(必须采用三维方案)两大挑战,并通过工程化设计(散热、防振)确保系统可靠运行。该技术需融合光学、机械与算法,

数字人生 389

大模型入门必备:详细教程,从零基础到精通一网打尽

大模型,是指在人工智能领域,特别实在自然语言处理和机器学习中,拥有大量参数的深度学习模型。这些模型通过在大规模数据集上进行训练,能够学到丰富的数据表示和模式,从而在各种任务上表现出色,如文本生成,语言理解,图像识别等。大模型是具有大量参数和复杂结构的模型,这些模型通常具数十亿甚至数万亿个参数,能够处理大规模的数据和复杂的任务。通常使用深度学习技术,如深度神经网络,可以从数据中学习并提取特征来执行各种任务。

2401_84495872的博客 761

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

【亲测免费】 【面试必备】一网打尽运维&DevOps大厂真题宝典

【面试必备】一网打尽运维&DevOps大厂真题宝典 去发现同类优质开源项目:https://gitcode.com/ 在技术日新月异的今天,无论是运维工程师还是DevOps从业者,面对大厂的严格筛选,如何准备才能做到胸有成竹?一款专为IT求职者量身打造的开源宝藏——《运维开发-运维工程师-DevOps工程师-K8S大厂面试题》应运而生,这不仅是一份文档,更是你面试成功的一大助力! 技术深度...

gitblog_06648的博客 431

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

如何成为年薪70万的全栈开发和测试人员?必备技能一网打尽

年薪70万,是多少人的梦想工资?但是,要实现这样的收入并不容易。作为一名全栈开发和测试人员,你需要掌握许多技能,包括前端、后端、数据库以及接口测试等等。 这听起来似乎有些可怕,但其实只要你掌握了必备的技能,就能轻松成为年薪70万的全栈人才。 在这篇文章中,我将介绍必备的技能清单,帮助你快速成为一名全栈开发和测试人员,走上高薪之路。相信我,只要你肯努力,这个梦想并不遥远!

weixin_67553250的博客 1055

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

nmrpflash支持的Netgear型号大全:从R7000到WNDR4300一网打尽

想要拯救变砖的Netgear路由器吗?🚀 nmrpflash就是你的终极救砖神器!这款强大的Netgear路由器救砖工具利用NMRP协议,能够为众多Netgear型号刷写新固件,让你的路由器起死回生。无论你是新手用户还是技术爱好者,本文将为你详细介绍nmrpflash支持的Netgear型号大全,从经典的R7000到WNDR4300,一网打尽! ## 📋 什么是nmrpflash? **n

gitblog_00075的博客 644

NeuralForge 数据集指南:从 12MB 到 155GB 的 11 个视觉数据集训练与加载全解析

本文以 [ML/DATASETS.md](https://link.gitcode.com/i/d7f957a862795158ecdf61f9b0268fdf) 为骨架,系统梳理 NeuralForge(仓库 `ML/` 目录下的 PyTorch 深度学习训练框架)所支持的全部数据集:从 12MB 的 MNIST 到 155GB 的 ImageNet,涵盖 11 个可训练条目。读完本文,你将掌握

gitblog_00237的博客 709

玩转 Linux 文件:目录、命令、软硬链接一网打尽

本文为 CentOS7 Linux 文件管理全套实训笔记,以rm命令踩坑警示切入,完整梳理目录切换、文件增删改查、通配符批量操作、软硬链接核心原理与实操案例,覆盖日常运维高频命令;同时补充目录层级规范、文件权限基础配套实战,知识点搭配可直接复现的终端演示,零基础也能快速吃透 Linux 文件体系,规避误删等常见操作风险。

2301_79710376的博客 480

阿朱,是时候拥抱 Linux 了

阿朱,是时候拥抱 Linux 了

qq_44925249的博客 351

linux技术面试题,我总结了24家大厂100份面试题

看完美团、字节、腾讯这三家的面试问题,是不是感觉问的特别多,可能咱们又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。开篇有提及我可是足足背下了1000道题目,多少还是有点用的呢,我看了下,上面这些问题大部分都能从我背的题里找到的,所以今天给大家分享一下互联网工程师必备的面试1000题。注意不论是我说的互联网面试1000题,还是后面提及的算法与数据结构、设计模式以及更多的Java学习笔记等,皆可分享给各位朋友互联网工程师必备的面试1000题而且从上面三家来看,算法与数据结构是必备不可少的。

m0_61067876的博客 899

java架构设计书籍推荐,重要概念一网打尽!

前言 最近有很多朋友向我求教经验,因为我自己工作相对于稳定,在这里给大家分享一个粉丝朋友的经历,他作为一个曾经的菜鸡面试者,在不断的失败中成长,最终斩获了多份offer,因此特别想在此分享一下他的面试成长之路。 文章内容篇幅较长,希望大家都能认真的看完,相信对你的帮助绝对不止一点半点! 首先和大家分享一下他这个面试季所斩获的所有offer:腾讯,美团,字节跳动,拼多多,华为,金山云,共6家公司,其中好几家给到可sp offer。最终入职了:字节跳动的「互动娱乐」部门成为了一名研发同学。 二、六大性能调

m0_56835488的博客 325

看懂这些帮你轻松解决就业问题!大厂面试必备技能

前言 这段时间也一直在学习Netty相关知识,因为涉及知识点比较多,也走了不少弯路。目前网上关于Netty学习资料玲琅满目,不知如何下手,其实大家都是一样的,学习方法和技巧都是总结出来的,我们在没有找到很好的方法之前不如按部就班先从基础开始,一般从总分总的渐进方式,既观森林,又见草木。 Netty是一款提供异步的、事件驱动的网络应用程序框架和工具,是基于NIO客户端、服务器端的编程框架。所以这里我们先以NIO和依赖相关的基础铺垫来进行剖析讲解,从而作为Netty学习之旅的一个开端。 梳理知识点,是快速提升技

m0_57501730的博客 119

linux运维面试题及答案,BAT大厂最爱问的Java核心面试百题详细解析

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**0)]

q16974058的博客 797

阿朱,是时候拥抱 Linux 了!

阿朱,是时候拥抱 Linux 了!作者:赖勇浩(http://blog.youkuaiyun.com/lanphaday) 在这个微软的“黑屏”时代,作为 windows 的替代品,Linux 变得倍受关注。今天 优快云 的名博阿朱写了篇文章《我可以抱你吗?Linux》(http://blog.youkuaiyun.com/david_lv/archive/2008/10/22/3123141.aspx)提

赖勇浩的编程私伙局 1万+
上一篇: linux内核分析及应用 -- Linux 网络层数据流分析(下)
下一篇: Linux内存描述之概述--Linux内存管理(一)
废言Pro
博客等级 码龄16年 1092粉丝 142原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值