Linux内核分析:详解epoll

文章目录

今天学习的内容是,Linux高并发的核心人物:epoll

写在前面

本篇是我阅读了epoll源码后总结出的内容,总共画了这个思维导图,根据下载链接即可进行下载,epoll的处理机制会根据不同的Linux内核有不同的操作,比如说对于红黑树的加锁,在不同的版本加锁的策略可能会有所不同,我这里阅读的主要是Linux的2.6版本的源码

下载链接

链接: https://pan.baidu.com/s/1hRTh7rSesikisgRUO2GBpA?pwd=utgp 提取码: utgp

思维导图

在这里插入图片描述

下面是我在阅读中有一些比较困惑的问题,在这里总结出来:

一些问题

有一种说法是,epoll在进行调用创建的本质,是创建了一个建议的文件系统,这句话该如何理解?

关于“epoll在进行调用创建的本质,是创建了一个简易的文件系统”这一说法,我们可以从以下几个方面进行理解:

一、epoll的背景与目的
epoll是Linux内核为处理大批量文件描述符(File Descriptor,简称FD)而作的改进的poll,是Linux下多路复用IO接口select/poll的增强版本。它的主要目的是在大量并发连接中只有少量活跃的情况下,显著提高系统CPU利用率。

二、epoll的实现机制
内核空间与用户空间:
在Linux系统中,内核空间和用户空间是隔离的。内核空间运行着操作系统核心,管理着硬件资源,而用户空间则运行着用户程序。
传统的select/poll方法在处理大量文件描述符时,需要在内核空间和用户空间之间频繁进行数据的复制和传递,这会导致较大的性能开销。
epoll的文件系统:
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体。这个结构体中包含了红黑树(rb_root rbr)和双向链表(list_head rdlist)等成员。
这些数据结构并不是传统意义上的文件系统结构,但它们被用于在内核中高效地管理和存储epoll相关的事件和文件描述符。
因此,说epoll创建了一个“简易的文件系统”可能是一种形象的说法,旨在强调epoll在内核中建立了一个用于管理和存储事件的高效数据结构体系。
红黑树与就绪链表:
红黑树用于存储所有添加到epoll中的事件(即需要监控的文件描述符)。这样可以高效地实现事件的添加、删除和查找操作。
就绪链表则用于存储那些已经准备好(即发生了感兴趣的事件)的文件描述符。当epoll_wait被调用时,内核会检查就绪链表,并将准备好的事件复制到用户空间。
三、结论
综上所述,“epoll在进行调用创建的本质,是创建了一个简易的文件系统”这一说法,实际上是指epoll在内核中建立了一个用于高效管理和存储事件的数据结构体系。这个体系虽然不是传统意义上的文件系统,但它通过红黑树和就绪链表等数据结构实现了对大量文件描述符的高效管理和事件通知。因此,这种说法在一定程度上是形象且贴切的。

file_operations结构体中的poll方法,其次是第二点,第二点我能理解,意思就是比如有两个进程都在关心我这个fd,那么此时我就得有通知这两个进程的能力,因此我就得给自己弄一个等待队列头,然后这俩进程就是等待队列的成员,等待我把事件就绪后,告诉他俩说,我已经准备好了,然后这俩进程再去通知上层,可能是用户或者其他,告诉他们上层的人说这个进程已经准备好了

被poll的fd的要求
在Linux内核中,几乎所有的设备或文件类型(如字符设备、块设备、网络socket等)都通过一个叫做file_operations的结构体来定义它们的操作集合。这个结构体包含了各种文件操作函数,如open、read、write、poll等。当一个进程尝试对一个文件描述符执行某种操作时,内核会查找该文件描述符对应的file_operations结构体,并调用相应的函数。

file_operations结构体中的poll方法
poll方法是file_operations结构体中的一个重要成员,它用于检查一个文件描述符是否处于某种特定的状态(如可读、可写、异常等)。当上层应用程序(如用户空间的程序)调用poll或select系统调用来检查多个文件描述符的状态时,内核会遍历这些文件描述符,并对每个文件描述符调用其对应的file_operations结构体中的poll方法。

poll方法的实现需要能够准确地反映文件描述符的当前状态,并可能需要将等待该文件描述符状态变化的进程或线程添加到某个等待队列中。这样,当文件描述符的状态发生变化时,内核就可以通过该等待队列来通知所有关心的进程或线程。

等待队列
等待队列是Linux内核中用于实现进程同步和事件通知的一种机制。每个可以等待的资源(如文件描述符、信号量等)都可以有一个或多个等待队列与之关联。当一个进程或线程需要等待某个资源的状态发生变化时,它会将自己的等待队列成员(通常是一个wait_queue_t结构体)添加到该资源的等待队列中,并进入睡眠状态。当资源的状态发生变化时,内核会唤醒等待队列中的一个或多个进程或线程,使它们能够继续执行。

在你提到的场景中,如果一个文件描述符(fd)被多个进程或线程通过poll或select系统调用来监视,那么该文件描述符就需要有一个等待队列来存储这些等待者。当文件描述符的状态发生变化时(例如,有新的数据可读或可写),内核会遍历该等待队列,并唤醒所有等待该事件的进程或线程。

总结
因此,被poll的fd的要求主要包括两个方面:一是该文件描述符对应的file_operations结构体中必须实现了poll方法,以便内核能够检查其状态;二是该文件描述符需要有一个等待队列来存储等待其状态变化的进程或线程,以便在状态变化时能够通知它们。这两个要求共同构成了Linux内核中poll机制的基础,使得上层应用程序能够高效地监视多个文件描述符的状态。

这是两个条件,还是一个条件?这两个条件有没有逻辑关系,比如说是,poll操作是可以通知消息队列这种,还是说具备poll操作和通知监控进程自己的就绪情况这两个之间没有关系

实际上,这两个条件可以看作是文件描述符(fd)在支持poll机制时需要满足的两个方面,但它们之间是有逻辑关系的。我们可以这样理解:

第一个条件:文件描述符对应的file_operations结构体中必须实现了poll方法。
这是一个技术上的要求,即文件描述符的底层实现必须提供poll接口,以便内核能够调用它来检查文件描述符的状态。
第二个条件:文件描述符需要有一个等待队列来存储等待其状态变化的进程或线程。
这是一个功能上的要求,它涉及到文件描述符如何与等待其状态变化的进程或线程进行交互。等待队列是实现这一交互的关键机制,它允许文件描述符在状态发生变化时通知所有等待的进程或线程。
逻辑关系:

这两个条件是相互关联的,因为poll方法的实现通常会涉及到等待队列的使用。具体来说,当poll方法被调用时,它可能会检查文件描述符的当前状态,并决定是否需要让调用者(即进程或线程)进入睡眠状态。如果需要,它会将调用者添加到文件描述符的等待队列中。当文件描述符的状态发生变化时,内核会通过等待队列来唤醒所有等待的进程或线程。
因此,可以说poll方法的实现依赖于等待队列的存在,而等待队列则是poll机制能够正常工作的基础之一。
关于poll操作与通知监控进程自己的就绪情况:

poll操作本身并不直接“通知”消息队列或监控进程。相反,它是被监控进程用来检查文件描述符状态的一种方式。当监控进程调用poll时,它实际上是在询问内核:“我关心的这些文件描述符中有没有已经准备好进行读/写/异常处理等操作的?”
如果文件描述符的状态已经就绪(例如,有数据可读或可写),那么poll会返回相应的信息给监控进程。监控进程随后可以根据这些信息来决定下一步的操作。
如果文件描述符的状态尚未就绪,那么监控进程可能会被添加到文件描述符的等待队列中并进入睡眠状态。当文件描述符的状态发生变化时(即变得就绪),内核会通过等待队列来唤醒监控进程,并允许它继续执行。
因此,poll操作与通知监控进程自己的就绪情况之间是有密切关系的,但这种关系是通过内核的等待队列机制来间接实现的。

那我们继续讨论下一个问题,这就意味着,一个文件描述符想要被epoll监控,意味着这个文件描述符对应的方法集中包含了poll这样的系统调用,并且它本身也得有作为队列头通知其他进程的能力,这两个条件是相互契合共同协作,才能保证这个被监控的文件描述符能够顺利的告诉epoll,有事件就绪,你可以来进行关心了

你的理解非常准确。在Linux内核中,一个文件描述符(fd)如果想要被epoll机制监控,确实需要满足一些条件,这些条件主要涉及到文件描述符如何与epoll进行交互,并通知其状态变化。具体来说,这两个相互契合、共同协作的条件是:

文件描述符对应的方法集中包含poll或类似的系统调用:
这里的“方法集”实际上是指文件描述符的file_operations结构体,它包含了该文件描述符支持的各种操作,如open、read、write、poll等。
poll方法(或类似的机制,如select使用的select_file方法)对于epoll来说是至关重要的,因为epoll需要能够检查文件描述符的状态,以确定是否有事件发生。如果文件描述符没有实现poll方法,那么epoll就无法有效地监控它。
文件描述符本身具有作为队列头通知其他进程的能力:
这意味着文件描述符需要维护一个或多个等待队列,用于存储等待其状态变化的进程或线程。当文件描述符的状态发生变化时(如数据可读、可写或发生异常),它能够遍历这些等待队列,并唤醒所有等待的进程或线程。
epoll机制利用了这个特性来优化事件通知过程。与传统的poll或select不同,epoll使用了一种更高效的等待队列机制(基于事件驱动),它可以显著减少内核和用户空间之间的上下文切换次数,并提高在高并发场景下的性能。
这两个条件是相互契合、共同协作的。文件描述符通过实现poll方法(或类似的机制)来允许epoll检查其状态,并通过维护等待队列来通知等待的进程或线程。而epoll则利用这些特性来提供一种更加高效、可扩展的事件通知机制。

需要注意的是,虽然epoll在内部使用了与poll或select类似的机制来检查文件描述符的状态,但它在设计上进行了大量的优化,特别是在处理大量并发连接时。因此,在需要高性能事件通知的场景下,epoll通常是更好的选择。

我们再聊一下这个主动poll fd的进程的要求,这个其实就可以看成是epoll create出来的这个epfd,对于这个fd来说,它所需要具备的基本条件是,可以分配一个成员作为某个指定被监控fd的等待队列的成员,来等待着被监控的这个fd身上有事件就绪。其次是,它也要有对应的回调函数,这个回调函数能够在满足poll状态的时候被调用

在讨论主动poll fd(实际上在这里我们更可能是指epoll创建的epfd,即事件轮询文件描述符)的进程的要求时,我们需要澄清一些概念并详细探讨这些要求。

epfd(事件轮询文件描述符)
epoll是Linux内核提供的一种高效的I/O事件通知机制,它允许应用程序同时监听多个文件描述符(fd)上的事件,如可读、可写或异常。epoll_create系统调用用于创建一个新的epoll实例,并返回一个文件描述符(epfd),该文件描述符用于后续与epoll相关的操作。

主动epoll进程的要求
对于使用epoll的进程(我们称之为“主动epoll进程”),它所需要具备的基本条件包括:

能够分配和管理等待队列成员:
虽然epoll本身在内核中维护了等待队列来跟踪对特定文件描述符(被监控的fd)感兴趣的事件,但主动epoll进程并不直接分配成员到这些等待队列中。相反,它通过epoll_ctl系统调用向epoll实例(epfd)注册或注销对特定文件描述符的监控,并指定感兴趣的事件类型。
内核会处理这些注册请求,并在内部维护相应的等待队列和关联的逻辑。
具备处理事件的回调函数:
主动epoll进程通常不会直接为epoll实例(epfd)本身设置回调函数。相反,它会调用epoll_wait(或epoll_pwait)系统调用来等待一个或多个文件描述符上的事件发生。
当epoll_wait返回时,它会提供一个包含已就绪事件的文件描述符列表,以及每个事件的具体类型(如可读、可写)。主动epoll进程需要根据这些信息来决定下一步的操作,这通常是通过在接收到事件后执行特定的回调函数或处理逻辑来实现的。
这些回调函数或处理逻辑是由主动epoll进程自己定义的,它们根据事件类型和被监控的文件描述符来执行相应的操作。
总结
因此,对于主动epoll进程来说,它并不需要直接分配成员到某个被监控fd的等待队列中,也不需要为epoll实例(epfd)设置回调函数。相反,它通过epoll_ctl注册对特定文件描述符的监控,并通过epoll_wait等待事件发生,然后根据返回的事件信息执行相应的处理逻辑。

需要注意的是,虽然这里没有直接提到“回调函数”作为epoll实例(epfd)的属性,但主动epoll进程确实需要定义并执行回调函数或处理逻辑来响应epoll_wait返回的事件。这些回调函数或处理逻辑是事件驱动编程模式的核心部分,它们允许主动epoll进程以非阻塞的方式高效地处理多个I/O事件。

slab分配器是Linux内核中一种高效的内存管理机制,主要用于小块内存的分配和释放。以下是对slab分配器的通俗解释:

  1. 基本概念
    想象你有很多大小不一的箱子(这些箱子可以理解为内存块),而你需要频繁地从这些箱子中取出或放回一些小物件(这些物件可以看作是内核中的数据结构或对象)。slab分配器就像是一个智能的仓库管理员,它将这些箱子按照大小分类,并为每种大小的箱子创建一个专门的“缓存区”(slab缓存)。当你需要某个大小的小物件时,它会直接从对应的缓存区中快速取出,而不需要去翻找整个仓库。

  2. 工作原理
    slab分配器的工作原理可以概括为以下几个步骤:

分类缓存:slab分配器将内存分割成多个“缓存”(cache),每个缓存包含固定大小的对象。这些对象可以是内核中的数据结构、缓冲区或其他小块内存。
快速分配与释放:当内核需要分配一个小块内存时,它会查看对应大小的缓存区中是否有空闲对象。如果有,就直接从缓存区中取出;如果没有,slab分配器会向系统申请新的内存页(通过伙伴系统),并将其划分为多个固定大小的对象,然后添加到缓存区中。当对象不再需要时,它会被释放回对应的缓存区,以便后续重用。
减少内存碎片:通过预分配固定大小的对象,slab分配器能够减少内存碎片的产生。内存碎片是指那些无法被有效利用的小块内存空间,它们通常是由于频繁地分配和释放不同大小的内存块而产生的。
提高缓存效率:slab分配器还通过保持对象在缓存行中对齐来提高CPU缓存的使用效率。缓存行是CPU缓存中的最小数据单元,通过将对象对齐到缓存行的边界,可以减少CPU在访问对象时的缓存未命中次数,从而提高程序的执行效率。
3. 应用场景
slab分配器在Linux内核中被广泛用于需要频繁分配和释放小块内存的场景。例如,当内核需要处理大量的网络数据包、文件I/O操作或进程调度时,它会频繁地分配和释放各种数据结构(如套接字缓冲区、文件描述符表项、进程描述符等)。通过使用slab分配器,内核可以更加高效地管理这些小块内存,从而提高系统的整体性能。

  1. 总结
    总的来说,slab分配器是Linux内核中一种非常重要的内存管理机制,它通过分类缓存、快速分配与释放、减少内存碎片和提高缓存效率等方式,为内核提供了一种高效、可靠的方式来管理小块内存。这使得Linux系统能够在处理大量并发请求和复杂任务时保持高性能和稳定性。

slab 分配器是干什么的

slab分配器是Linux内核中一种高效的内存管理机制,主要用于小块内存的分配和释放。以下是对slab分配器的通俗解释:

  1. 基本概念
    想象你有很多大小不一的箱子(这些箱子可以理解为内存块),而你需要频繁地从这些箱子中取出或放回一些小物件(这些物件可以看作是内核中的数据结构或对象)。slab分配器就像是一个智能的仓库管理员,它将这些箱子按照大小分类,并为每种大小的箱子创建一个专门的“缓存区”(slab缓存)。当你需要某个大小的小物件时,它会直接从对应的缓存区中快速取出,而不需要去翻找整个仓库。

  2. 工作原理
    slab分配器的工作原理可以概括为以下几个步骤:

分类缓存:slab分配器将内存分割成多个“缓存”(cache),每个缓存包含固定大小的对象。这些对象可以是内核中的数据结构、缓冲区或其他小块内存。
快速分配与释放:当内核需要分配一个小块内存时,它会查看对应大小的缓存区中是否有空闲对象。如果有,就直接从缓存区中取出;如果没有,slab分配器会向系统申请新的内存页(通过伙伴系统),并将其划分为多个固定大小的对象,然后添加到缓存区中。当对象不再需要时,它会被释放回对应的缓存区,以便后续重用。
减少内存碎片:通过预分配固定大小的对象,slab分配器能够减少内存碎片的产生。内存碎片是指那些无法被有效利用的小块内存空间,它们通常是由于频繁地分配和释放不同大小的内存块而产生的。
提高缓存效率:slab分配器还通过保持对象在缓存行中对齐来提高CPU缓存的使用效率。缓存行是CPU缓存中的最小数据单元,通过将对象对齐到缓存行的边界,可以减少CPU在访问对象时的缓存未命中次数,从而提高程序的执行效率。
3. 应用场景
slab分配器在Linux内核中被广泛用于需要频繁分配和释放小块内存的场景。例如,当内核需要处理大量的网络数据包、文件I/O操作或进程调度时,它会频繁地分配和释放各种数据结构(如套接字缓冲区、文件描述符表项、进程描述符等)。通过使用slab分配器,内核可以更加高效地管理这些小块内存,从而提高系统的整体性能。

  1. 总结
    总的来说,slab分配器是Linux内核中一种非常重要的内存管理机制,它通过分类缓存、快速分配与释放、减少内存碎片和提高缓存效率等方式,为内核提供了一种高效、可靠的方式来管理小块内存。这使得Linux系统能够在处理大量并发请求和复杂任务时保持高性能和稳定性。

那对于epoll来说,假如它监视的这个fd上没有配备对应的poll方法,但是实际上,我们知道epoll也不会调用这个poll方法,那这个被监视的fd还可以被epoll监视吗?

对于epoll来说,即使被监视的文件描述符(fd)上没有配备对应的poll方法,这个fd仍然可以被epoll监视。这主要是因为epoll的工作原理与select或poll等传统IO多路复用机制有所不同。

epoll的工作原理
epoll是Linux下的一种IO事件通知机制,它通过使用红黑树(一种自平衡二叉查找树)来管理所有需要监视的文件描述符,这使其在处理大量文件描述符时更加高效。epoll提供了三个主要的函数:

epoll_create:创建一个epoll实例,返回一个文件描述符(epfd),用于后续操作。
epoll_ctl:向epoll实例中添加、修改或删除需要监视的文件描述符及其事件类型。
epoll_wait:等待一个或多个文件描述符上发生指定的事件,并将发生事件的文件描述符及其事件类型存储在用户提供的数组中。
epoll与poll方法的关系
epoll并不直接调用文件描述符对应的poll方法(如果有的话)。相反,epoll通过内核中的机制来高效地管理文件描述符和事件通知。当使用epoll监视文件描述符时,文件描述符的状态变化(如可读、可写等)是由内核来跟踪的,而不需要用户空间程序(如通过poll方法)来不断查询。

监视无poll方法的fd
即使一个文件描述符没有配备对应的poll方法,只要它支持IO事件通知(例如,是一个socket或管道等),它就可以被epoll监视。epoll通过内核级别的支持来感知这些文件描述符的状态变化,并将这些变化通知给用户空间程序。

结论
因此,即使被监视的文件描述符上没有配备对应的poll方法,这个fd仍然可以被epoll监视。epoll的高效性主要来自于其在内核中实现的优化,而不是依赖于用户空间中的poll方法。这使得epoll成为处理大量并发连接和高性能网络编程的优选方案之一。

总体演示

工作环境

epoll工作在应用程序和内核协议栈之间

epoll是在内核协议栈和vfs都有的情况下才有的

  • epoll机制通过内核协议栈来获取网络事件,并进行通知

  • VFS的存在为epoll机制提供了文件描述符管理和操作支持

数据结构

一个红黑树

  • 实际上是红黑树管理所有的IO,事件就绪后调用epoll回调,把IO加入到链表中

一个双向链表

  • 就绪列表存储的是就绪的socket,所以它应能够快速的插入数据

三个核心API

  • int epoll_create(int size)

    • 返回的是epfd

      • 创建链表和红黑树
  • int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)

    • 对于监听事件做修改

      • 新增回调,从红黑树加到链表
  • int epoll_wait(int epfd,struct epoll_event *events,int maxevents,nt timeout);

    • 阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中

      • 仅仅观察这个双向链表里有没有数据即可

相关概念

为什么需要epoll

  • 高效处理大量并发连接

  • 内核与用户空间数据拷贝少

  • 支持边缘触发

    • 只检查一次
  • 支持水平触发

    • 一直通知

select与poll的缺陷

  • 文件描述符数量限制

  • 效率问题

  • 信息不足

    • 不说具体是哪个,需要对所有要监视的文件描述符进行遍历
  • 信号中断

    • select 和 poll 调用可以被信号中断,这可能会导致调用失败

epoll的操作

  • 原理

    • epoll 在 linux 内核中申请了一个简易的文件系统
  • 三步

    • 调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源)

    • 调用 epoll_ctl 向 epoll 对象中添加连接的套接字

    • 调用 epoll_wait 收集发生事件的连接

实现原理

内核相关知识

  • 等待队列

    • 队列头(wait_queue_head_t)往往是资源生产者

      • 队列成员(wait_queue_t)往往是资源消费者
    • 当头的资源ready后, 会逐个执行每个成员指定的回调函数,来通知它们资源已经ready了

  • 内核的poll机制条件

    • 被动poll的fd

      • 这个fd所对应的方法集中要有poll的系统调用

      • 这个fd本身可以作为队列头通知监控线程

    • 主动poll的fd

      • 添加到被监控fd的等待队列中

      • 设置相应的回调函数

  • epollfd本身也是个fd, 所以它本身也可以被epoll

epoll_create原理

  • 内核会从 slab 缓存中分配一个 eventpoll 结构体实例,包含红黑树根节点和双向链表

  • 内核会创建一个新的、匿名的文件描述符,并将其与一个特殊的 file 结构体关联起来

  • 将 eventpoll 对象保存在 struct file 的 private_data 指针中

  • 实现 file_operations 结构中的 poll 和 release 操作

    • poll

      • 查看文件描述符上的事件状态(epoll不用这个,但是为了兼容要初始化)
    • release

      • 释放创建的相关资源

epoll_ctl原理

  • 拷贝 epoll_event 结构到内核空间

    • 需要直接访问这些信息来更新 epoll 实例的状态
  • 检查 fd 是否支持 poll 操作

    • epoll_ctl 并不直接调用 file_operations 中的 poll 方法,但是为了兼容也得有
  • 获取 event_poll 对象

    • 包含了红黑树、就绪链表和其他一些用于管理文件描述符和事件的内部数据结构
  • 根据操作类型(op)执行添加、删除或修改

  • 初始化等待队列和回调函数

    • 将事件添加到 epoll 的就绪链表中,并可能唤醒等待在 epoll_wait 上的进程
  • 调用 file_operations->poll 函数(如果需要)

    • epoll 依赖于自己的机制来检测事件

      • 但是也得初始化poll方法,因为要和前面兼容
  • 将新的epitem结构更新到红黑树中

epoll_wait原理

  • 计算睡眠时间(如果有)

  • 判断 eventpoll 对象的链表是否为空

    • 如果不睡眠,就直接返回有还是没有

      • 拷贝资源给用户空间
  • 设置进程状态为可睡眠

  • 初始化等待队列(如果有睡眠)

    • 在睡眠时将当前进程添加到该队列中
  • 超时或被唤醒

  • 拷贝资源给用户空间

协议栈通信

协议栈和epoll模块之间的通信是异步的,没有耦合,不需要等待

通知时机

  • 协议栈三次握手完成,往accept全连接队列里加入这个节点时,通知epoll有事件来了epollin

  • 客户端发了1个数据到协议栈,协议栈此时要返回ack给客户端的这里的时机,会通知epoll有事件可读 epollin

线程安全

对红黑树加锁(互斥锁)

  • 锁整棵树

    • 全局性操作

      • 简化同步逻辑(图方便)
  • 锁子树

    • 局部性操作

      • 优化性能(需要更完善的逻辑)

对就绪队列加锁(自旋锁)

  • 队列操作比较简单,等待一些时间比直接睡眠线程更高效点

ET和LT

LT(Level Triggered,电平触发)

  • 默认,会重复提醒

ET(Edge Triggered,边沿触发)

  • 高效,需要手动设置,只提醒一次
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵宝宝de派小星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值