操作系统基础

中断

程序执行过程中遇到紧急事件时,暂时停止现有程序的执行,转而处理紧急事件,执行完后,再执行当前程序。

intel手册:

  • 同步中断
    • 软中断
  • 异步中断
    • 硬中断

Linux中很难看到内外中断的说法,常说软硬中断

  • 硬件中断

    • 外部中断 | 异步中断
      • 由 CPU 执行指令以外的事件引起
        • I/O 完成中断,每个设备或设备集都有它自己的IRQ(中断请求)
        • 时钟中断
      • 外部中断是可以屏蔽的中断
    • 内部中断 | 异常 | 同步中断
      • 不可屏蔽
  • 软件中断

    • 处理硬中断未完成的工作,是一种推后执行的机制
    • 其实并不是真正的中断,它们只是可被调用执行的一般程序

https://r00tk1ts.github.io/2017/12/21/Linux%E4%B8%AD%E6%96%AD%E5%86%85%E5%B9%95/

IRQ Interrupt ReQuest

程序员素质二连,要么轮询、要么中断。这一思想在程序设计中随处可见,如自旋锁和睡眠锁,前者一直在BB着:“我现在可以起床了吗”,而后者则不声不响,等待其他人(进程)的唤醒(Hey guy,it’s time to get up!)。

从信号源来分,中断有两种,由外部的硬件设备产生(这称为外中断)或者是CPU内部产生的(这称为内中断,内部产生可以有两种,一种是指令执行出错比如除0,另一种是使用软中断指令int x)。而从另一个角度来看,由硬件引起的叫硬中断,软件引起的叫软中断。Linux中软中断是个“二义”的概念,之所打上引号,是因为实际上殊途同归,后面会谈到。

Linux中很难看到内外中断的说法,但是软硬中断则随处可见。按照我的理解,外中断就是硬中断,而内中断不完全是软中断(之所以这样说,是因为硬中断是Linux处理IRQ的上半部分,下半部分交由软中断完成,这里的软中断入口方式和直接使用int x指令不同,但最终结果都是找服务例程),采用软硬是为了更好的区分中断信号是硬件给的还是由软件产生的。

而按照Intel手册的区分,中断应该分为同步中断和异步中断。显然,硬中断是异步的,因为你不知道它什么时候来搞事情(如键盘中断)。相反的,软中断则是同步的,因为只有在一条指令终止执行后CPU才会发出中断(如系统调用)。

Intel手册将异步中断称为中断(interrupt),同步中断称为异常(exception)

中断处理流程

  1. 中断响应的事前准备:
  1. 系统将所有的中断信号统一进行了编号(一共256个:0~255),称为中断向量。中断向量和中断服务程序的对应关系主要是由IDT(中断向量表)负责。
  2. 中断服务程序由操作系统实现的,属于操作系统内核代码。也就是说从CPU检测中断信号到加载中断服务程序以及从中断服务程序中恢复执行被暂停的程序,这个流程基本上是硬件确定下来的,而具体的中断向量和服务程序的对应关系设置和中断服务程序的内容是由操作系统确定的。
  1. CPU检查是否有中断/异常信号

CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量。对于异常和系统调用那样的软中断,因为中断向量是直接给出的,所以和通过IRQ(中断请求)线发送的硬件中断请求不同,不会再专门去取其对应的中断向量。

  1. 根据中断向量获取中断处理程序

  2. 保护当前程序的现场

5)开始执行中断处理程序

CPU利用中断服务程序的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务程序。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。

  1. 中断服务程序处理完毕,恢复执行先前中断的程序

系统调用

https://blog.csdn.net/gatieme/article/details/50779184

计算机系统的各种硬件资源是有限的,同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)
系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?
答案是中断。操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。

在linux中系统调用是用户空间访问内核的唯一手段,

操作系统一般是通过中断从用户态切换到内核态。

一般地,系统调用都是通过软件中断实现的

进程&线程

  • 拥有资源
  • 调度
  • 系统开销

创建、撤销、切换

由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

  • 通信方面

进程

进程组织

  • 进程控制块 PCB
  • 程序段
  • 数据段

进程的调度

[GITHUB](https://github.com/CyC2018/CS-Notes/blob/master/notes/计算机操作系统 - 进程管理.md)

  • 先来先服务 first-come first-serverd(FCFS)
  • 短作业优先 shortest job first(SJF)
  • 最短剩余时间优先 shortest remaining time next(SRTN)

2.1 时间片轮转

将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系:

  • 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
  • 而如果时间片过长,那么实时性就不能得到保证。

2.2 优先级调度

为每个进程分配一个优先级,按优先级进行调度。

为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

2.3 多级反馈队列

设置多个就绪队列。在系统中设置多个就绪队列,并为每个队列赋予不同的优先级,从第一个开始逐个降低。不同队列进程中所赋予的执行时间也不同,优先级越高,时间片越小。

进程的通信

每个内核中的 I P C结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符( i d e n t i f i e r )加以引用。

Java如何支持进程间通信。我们把Java进程理解为JVM进程。很明显,传统的这些大部分技术是无法被我们的应用程序利用了(这些进程间通信都是靠系统调用来实现的)。但是Java也有很多方法可以进行进程间通信的。
除了上面提到的Socket之外,当然首选的IPC可以使用Rmi,或者Corba也可以。另外Java nio的MappedByteBuffer也可以通过内存映射文件来实现进程间通信(共享内存)

[GITHUB](https://github.com/CyC2018/CS-Notes/blob/master/notes/计算机操作系统 - 进程管理.md#进程通信)

1. 管道

管道包括三种:

  • 普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
  • 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
  • 命名管道:name_pipe:去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

管道的实质是一个内核环形缓冲区(可看作是环形队列),进程以先进先出的方式从缓冲区存取数据

它具有以下限制:

  • 只支持半双工通信(单向交替传输)

我们来看一条 Linux 的语句

netstat -tulnp | grep 8080

这条竖线是没有名字的,所以我们把这种通信方式称之为匿名管道

并且这种通信方式是单向的,只能把第一个命令的输出作为第二个命令的输入,如果进程之间想要互相通信的话,那么需要创建两个管道。

有匿名管道,那也意味着有命名管道,下面我们来创建一个命名管道。

mkfifo  test

这条命令创建了一个名字为 test 的命名管道。

接下来我们用一个进程向这个管道里面写数据,然后有另外一个进程把里面的数据读出来。

echo "this is a pipe" > test   // 写数据

这个时候管道的内容没有被读出的话,那么这个命令就会一直停在这里,只有当另外一个进程把 test 里面的内容读出来的时候这条命令才会结束。接下来我们用另外一个进程来读取

cat < test  // 读数据

我们可以看到,test 里面的数据被读取出来了。上一条命令也执行结束了。

从上面的例子可以看出,管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。

这种通信方式有什么缺点呢?显然,这种通信方式效率低下,你看,a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。

管道通信的实现细节
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

2. 消息队列

由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

消息队列是消息的链接表 ,存放在内核中并由消息队列标识符标识

3. 共享内存

允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。

需要使用信号量用来同步对共享存储的访问

多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。

我们都知道,系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。

4. 信号量

它是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制

5. 套接字

与其它通信机制不同的是,它可用于不同主器间的进程通信。

线程

https://www.cnblogs.com/wade-luffy/p/6051384.html

  1. 内核级线程:

    1. 内核级线程:线程管理(创建,销毁等)的所有工作由内核完成,应用程序没有进行线程管理的代码。内核负责维护线程信息以及调度

    2. 内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。

      程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口—轻量级进程(LWP),轻量级进程就是我们所讲的线程。每个轻量级进程都由一个内核线程支持

    3. 优点:

      1. 多处理器能并行执行
      2. 进程中的一个线程被阻塞,不影响其他线程
    4. 缺点:

      1. 线程在用户态运行,而线程的调度和管理在内核实现。
      2. 在同一进程中,控制权从一个线程交给另一个线程需要用户态->内核态->用户态模式的转变,开销大
  2. 用户级线程

    1. 用户级线程中,有关线程管理的工作都有应用程序完成,内核意识不到线程的存在。

      不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。

      所有的线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。因而使用用户线程实现的程序一般都比较复杂

    2. 优缺点和内核级线程相反

  3. 混合型线程

    1. 有用户级线程和核心级线程
      1. 用户层线程在用户线程库中实现,允许应用程序建立、调度、管理用户级线程
      2. 核心层线程在操作系统内核中实现,内核线程的管理等。。。
    2. 一个应用程序中的多个用户级线程会被映射到一些内核级线程上。
      1. 多对多模型

Java的多线程就是内核级:java并不能完全控制线程执行,切换和调度由内核完成

Go的协程就是混合型线程

Go协程被多路复用到较少的OS线程。在一个程序中数千个Go协程可能只运行在一个线程中。如果该线程中的任何一个Go协程阻塞(比如等待用户输入),那么Go会创建一个新的OS线程并将其余的Go协程移动到这个新的OS线程。所有这些操作都是 runtime 来完成的,而我们程序员不必关心这些复杂的细节,只需要利用 Go 提供的简洁的 API 来处理并发就可以了

Go 协程之间通过信道(channel)进行通信。信道可以防止多个协程访问共享内存时发生竟险(race condition)。信道可以想象成多个协程之间通信的管道。

inode

一个文件对应一个inode

inode是一个重要概念,是理解Unix/Linux文件系统和硬盘储存的基础。

文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

inode包含文件的元信息,具体来说有以下内容:

* 文件的字节数

* 文件拥有者的User ID

* 文件的Group ID

* 文件的读、写、执行权限

* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。

* 链接数,即有多少文件名指向这个inode

* 文件数据block的位置

三、inode的大小

inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。

每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令。

df -i

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。

这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

硬连接&软连接

-rw------- 2 root root 9626 Aug 16 04:09 hard
-rw------- 2 root root 9626 Aug 16 04:09 nohup.out
lrwxrwxrwx 1 root root    9 Aug 16 16:12 soft -> nohup.out

软连接类似于快捷方式

硬连接是相当于copy一份,但是会一起变化。两个文件名指向同一个inode

1443376 hard  1443376 nohup.out  2097161 soft

硬链接文件和源文件i节点号相同,并且一个i节点可以对应多个文件名。

image-20190816161500598

如图,删除了jys,只是删除了从920586到jys的映射关系,不影响它和jys.hard的映射关系。此图也解释了硬链接的同步更新,对源文件修改,操作系统只认i节点,于是操作系统就将修改内容写进所有i节点相同名字不同的文件。

修改源文件会影响硬连接,修改硬连接也会影响源文件

内存管理

虚拟内存

目的:为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

  • 操作系统将物理内存抽象成逻辑上的地址空间
  • 每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页
  • 这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

使得程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。

进程不能直接读写内存中地址为0x1位置的数据。进程中能访问的地址,只能是虚拟内存地址(virtual memory address)。操作系统会把虚拟内存地址翻译成真实的内存地址。这种内存管理方式,称为虚拟内存(virtual memory)

程序中表达的内存地址,也都是虚拟内存地址。

掌握着内存对应关系的操作系统,也掌握了应用程序访问内存的闸门。借助虚拟内存地址,操作系统可以保障进程空间的独立性。只要操作系统把两个进程的进程空间对应到不同的内存区域,就让两个进程空间成为“老死不相往来”的两个小王国。两个进程就不可能相互篡改对方的数据,进程出错的可能性就大为减少。

另一方面,有了虚拟内存地址,内存共享也变得简单。操作系统可以把同一物理内存区域对应到多个进程空间。这样,不需要任何的数据复制,多个进程就可以看到相同的数据。内核和共享库的映射,就是通过这种方式进行的。

分页系统地址映射

记录对应关系最简单的办法,就是把对应关系记录在一张表中。为了让翻译速度足够地快,这个表必须加载在内存中。不过,这种记录方式惊人地浪费。如果树莓派1GB物理内存的每个字节都有一个对应记录的话,那么光是对应关系就要远远超过内存的空间。因此,Linux采用了分页(paging)的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页(page)来管理内存。在Linux中,通常每页大小为4KB。

大多数情况下,每一页有4096个字节。由于4096是2的12次方,所以地址最后12位的对应关系天然成立。我们把地址的这一部分称为偏移量(offset)。偏移量实际上表达了该字节在页内的位置。地址的前一部分则是页编号。操作系统只需要记录页编号的对应关系。

虚拟地址=页号+页内偏移量

分页系统思想:把主存空间划分为大小相等且固定的块,作为主存的基本单位。程序执行时,以块为单位申请主存空间。会产生很小的页内碎片。

内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表

多级页表

页面淘汰算法

1. 最佳

OPT, Optimal replacement algorithm

淘汰以后最长时间不会用的,理论算法

2. 最近最久未使用

LRU, Least Recently Used

为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。

因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。

3. 最近未使用

NRU, Not Recently Used

每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:

  • R=0,M=0
  • R=0,M=1
  • R=1,M=0
  • R=1,M=1

当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。

由于修改过的页在替换之前必须写回,因而这样会节省时间。

4. 先进先出

分段

二维地址空间,现代程序的分段又编译器完成。

为什么要分段

分页将所有进程划分为页面形式,而不管进程是否具有需要在同一页面中加载的某些相关功能部分。操作系统不关心用户对过程的看法。 它可以将相同的功能划分为不同的页面,这些页面可以或不可以同时加载到存储器中。 它降低了系统的效率。

目的:每个段包含相同类型的功能,例如main函数可以包含在一个分段中,并且库函数可以包含在另一个分段中。方便编程

分页与分段的比较

  • 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。
  • 地址空间的维度:分页是一维地址空间,分段是二维的。
  • 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
  • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

段页式

程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。

自旋锁、互斥锁、信号量、读写锁、条件变量、记录锁

自旋锁

调用者一直循环看是否该自旋锁的保持者已经释放了锁

互斥锁 Mutex

条件变量Condition

通常条件变量和互斥锁同时使用,来协调线程的运行。

条件变量用来自动阻塞一个线程来等待某种条件满足。条件变量是利用线程间共享的全局变量进行同步 的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使 “条件成立”(给出条件成立信号)

读写锁

允许多个读者同时访问,但是不允许一个写者同其它任何一个进程(读者 或者写者)同时访问

信号量 Semphore

semaphore一般只是表示资源有多少。对于N=1的情况,称为binary semaphore。semaphore是允许多个线程进入,访问互斥资源。

范围锁

它是对文件某个范围的锁定。当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。它锁定的只是文件中的一个区域(也可能是整个文件)。

死锁

  • 互斥
  • 请求和保持:已经得到了某个资源的进程可以再请求新的资源。
  • 不可剥夺:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

处理方法

主要有以下四种方法:

  • 鸵鸟策略
    • 把头埋在沙子里,不管了。
  • 死锁预防
  • 死锁避免

死锁预防

在程序运行之前预防发生死锁。

3. 破坏占有和等待条件

一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。

4. 破坏环路等待

给资源统一编号,进程只能按编号顺序来请求资源。即只能申请编号大于当前持有编号的资源。

死锁避免

银行家算法

堆分配算法:

1)空闲链表–其方法是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分。当用户释放空间时将它合并到空闲链表中。

2)位图(Bitmap)–核心思想是将整个堆划分为大量的块(block),每个块大小相同。当用户请求内存的时候,总是分配整数个块的空间给用户,第一个块我们称为已分配区域的头(Head),其余的称为已分配区域的主体(Body).而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因而仅仅需要两位即可表示一个块,因此称为位图。

操作系统堆栈

栈: 它保存了一个 函数调用所需要维护的信息。

  1. 函数的返回地址,参数
  2. 临时变量
  3. 上下文:寄存器

堆: 动态申请内存,即使用new or malloc等分配到的内存,可以比栈大很多,需用户自己释放

[new/delete,malloc/free成对出现,否则会导致内存泄露,可以使用查找内存泄露的工具监控或者自己写代码监控

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值