TCP的发送系列 — 发送缓存的管理(二)

主要内容:从TCP层面判断发送缓存的申请是否合法,进程因缺少发送缓存而进行睡眠等待、

                    因为有发送缓存可写事件而被唤醒。

内核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

 

TCP的发送缓存管理发生在两个层面上:单个Socket和整个TCP层。

上一篇blog讲述了单个Socket层面上的发送缓存管理,现在来看下整个TCP层面上的发送缓存管理。

 

从TCP层面判断发送缓存的申请是否合法

 

在申请发送缓存时,会调用sk_stream_memory_free()来判断sock发送队列的大小是否超过

了sock发送缓存的上限,如果超过了,就要进入睡眠来等待sock的发送缓存可写事件。

这是从单个socket层面来判断是否允许分配发送缓存。

 

在调用sk_stream_alloc_skb()申请完发送缓存后,还要从TCP层面来判断此次的申请是否合法。

如果不合法,就使用__kfree_skb()来释放申请好的skb。可见发送缓存的申请,需要经过两重关卡。

 

从TCP层面来判断发送缓存的申请是否合法,需要考虑整个TCP层面的内存使用量,以及此socket

的发送缓存使用量。sk->sk_forward_alloc为sock预分配缓存的大小,是sock事先分配好还未使用的内存。

当申请新的发送缓存后,如果发现sk->sk_forward_alloc < skb->truesize,即预分配缓存用光了,

才需要调用sk_wme_schedule()来从TCP层面判断合法性,否则不用再做检查。

static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
    /* TCP层是有统计内存使用的,所以条件为假 */
    if (! sk_has_account(sk))
        return true;

    /* 如果本次使用的内存skb->truesize,少于sk预分配且未使用的缓存的大小,那么不用进行
     * 进一步检查。否则需要从TCP层面判断此次发送缓存的申请是否合法。
     */
    return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND);
}

static inline bool sk_has_account(struct sock *sk)
{
    /* return ture if protocol supports memory accounting */
    return !! sk->sk_prot->memory_allocated;
}
 

/* return minimum truesize of one skb containing X bytes of data */
#define SKB_TRUESIZE(X) ((X) + \
    SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
    SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))

 

__sk_mem_schedule()用来从TCP层面判断此次发送缓存的申请是否合法,如果是合法的,

会更新预分配缓存sk->sk_forward_alloc和TCP层总的内存使用量tcp_memory_allocated,

后者的单位为页。

 

Q:哪些情况下此次发送缓存的申请是合法的呢?

1. TCP层的内存使用量低于最小值sysctl_tcp_mem[0]。

2. sock的发送缓存使用量低于最小值sysctl_tcp_wmem[0]。

3. TCP层不处于内存压力状态,即TCP层的内存使用量低于sysctl_tcp_wmem[1]。

4. TCP层处于内存压力状态,但当前socket使用的内存还不是太高。

5. TCP层的内存使用量超过最大值sysctl_tcp_wmem[2],降低发送缓存的上限后,发送队列的总大小超过

    了发送缓存的上限了。因此之后会进入睡眠等待,所以也判为合法的。

 

可以看到,在绝大多数情况下发送缓存的申请都是合法的,除非TCP的内存使用量已经到极限了。

除了判断此次发送缓存申请的合法性,__sk_mem_schedule()还做了如下事情:

1. 如果TCP的内存使用量低于最小值sysctl_tcp_mem[0],就清零TCP的内存压力标志tcp_memory_pressure。

2. 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP的内存压力标志tcp_memory_pressure置为1。

3. 如果TCP的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限sk->sk_sndbuf。

 

返回值为1时,表示发送缓存的申请是合法的;返回值为0时,表示不合法。

/* increase sk_forward_alloc and memory_allocated
 * @sk: socket
 * @size: memory size to allocate
 * @kind: allocation type
 * If kind is SK_MEM_SEND, it means wmem allocation.
 * Otherwise it means rmem allocation. This function assumes that 
 * protocols which have memory pressure use sk_wmem_queued as
 * write buffer accounting.
 */

int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
    struct proto *prot = sk->sk_prot; /* 实例为tcp_prot */
    int amt = sk_mem_pages(size); /* 把size转换为页数,向上取整 */
    long allocated;
    int parent_status = UNDER_LIMIT;

    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM; /* 更新预分配缓存的大小 */

    /* 更新后的TCP内存使用量tcp_memory_allocated,单位为页 */
    allocated = sk_memory_allocated_add(sk, amt, &parent_status);

    /* Under limit. 如果TCP的内存使用量低于最小值sysctl_tcp_mem[0] */
    if (parent_status == UNDER_LIMIT && allocated <= sk_prot_mem_limits(sk, 0)) {
        sk_leave_memory_pressure(sk); /* 清零TCP层的内存压力标志tcp_memory_pressure */
        return 1;
    }

    /* Under pressure. (we or our parents).
     * 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP层的内存压力标志
     * tcp_memory_pressure置为1。
     */
    if ((parent_status > SOFT_LIMIT) || allocated > sk_prot_mem_limits(sk, 1))
        sk_enter_memory_pressure(sk);

    /* Over hard limit (we or our parents).
     * 如果TCP层的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限
     * sk->sk_sndbuf。
     */
    if ((parent_status == OVER_LIMIT || (allocated > sk_prot_mem_limits(sk, 2)))
        goto suppress_allocation;

    /* guarantee minimum buffer size under pressure */
    /* 不管是在发送还是接收时,都要保证sock至少有sysctl_tcp_{r,w}mem[0]的内存可用 */
    if (kind == SK_MEM_RECV) {
        if (atomic_read(&sk->sk_rmem_alloc) < prot->sysctl_rmem[0])
            return 1;

    } else { /* SK_MEM_SEND */
        if (sk->sk_type == SOCK_STREAM) {
            if (sk->sk_wmem_queued < prot->sysctl_wmem[0])
                return 1;
        } else if (atomic_read(&sk->sk_wmem_alloc) < prot->sysctl_wmem[0])
            return 1;
    }

   if (sk_has_memory_pressure(sk)) {
        int alloc;

        /* 如果TCP不处于内存压力状态,直接返回 */
        if (! sk_under_memory_pressure(sk))
            return 1;

        alloc = sk_sockets_allocated_read_positive(sk); /* 当前使用TCP的socket个数 */

        /* 如果当前socket使用的内存还不是太高时,返回真 */
        if (sk_prot_mem_limits(sk, 2) > alloc * sk_mem_pages(sk->sk_wmem_queued +
             atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc))
            return 1;
    }

suppress_allocation:
    if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {

        /* 减小sock发送缓冲区的上限,使得sndbuf不超过发送队列总大小的一半,
         * 不低于两个数据包的MIN_TRUESIZE。
         */
        sk_stream_moderate_sndbuf(sk); 

        /* Fail only if socket is under its sndbuf.
         * In this case we cannot block, so that we have to fail.
         */
        if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
            return 1;
    } 
    trace_sock_exceed_buf_limit(sk, prot, allocated);
 
    /* 走到这里,判定此次发送缓存的申请为不合法的,撤销之前的内存计数更新 */
    /* Alas. Undo changes. */
    sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;
    sk_memory_allocated_sub(sk, amt);
    return 0;
}

/* 把字节数amt转换为页数,向上取整 */
static inline int sk_mem_pages(int amt)
{
    return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT;
}
#define SK_MEM_QUANTUM ((int) PAGE_SIZE)

/* 返回更新后的TCP内使用量tcp_memory_allocated,单位为页 */
static inline long sk_memory_allocated_add(struct sock *sk, int amt, int *parent_status)
{
    struct proto *prot = sk->sk_prot;

    /* Cgroup相关,此处略过 */
    if (mem_cgroup_sockets_enabled && sk->sk_cgrp) {
        ...
    }

    return atomic_long_add_return(amt, prot->memory_allocated);
}

sysctl_tcp_mem[0]:最小值

sysctl_tcp_mem[1]:压力值

sysctl_tcp_mem[2]:最大值

static inline long sk_prot_mem_limits(const struct sock *sk, int index)
{
    long *prot = sk->sk_prot->sysctl_mem;

    /* Cgroup相关 */
    if (mem_cgroup_sockets_enabled && sk->sk_cgrp)
        prot = sk->sk_cgrp->sysctl_mem;

    return prot[index];
}

 

因缺少发送缓存而睡眠等待

 

在tcp_sendmsg()中,如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,

或者发送缓存中尚未发送的数据量超过了用户的设置值,就进入睡眠等待。

如果申请发送缓存失败了,也会进行睡眠等待。

 

(1) 判断条件

sk_stream_memory_free()用来判断sock是否有剩余的发送缓存。

static inline bool sk_stream_memory_free(const struct sock *sk)
{
    if (sk->sk_wmem_queued >= sk->sk_sndbuf)
        return false;

    return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true;
}

static inline bool tcp_stream_memory_free(const struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未发送的数据大小 */

    /* 当尚未发送的数据,少于配置的值时,才返回真。
     * 这是为了避免发送缓存占用过多的内存。
     */
    return notsent_bytes < tcp_notsent_lowat(tp);
}

如果有使用TCP_NOTSENT_LOWAT选项,则使用用户设置的值。

否则使用sysctl_tcp_notsent_lowat,默认为无穷大。

static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{
    return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat;
}

 

(2) 睡眠等待

如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,

或者发送缓存中尚未发送的数据量超过了用户的设置,就进入等待。

如果因为TCP层的内存不足,导致申请发送缓存失败了,也会进行睡眠等待。

 

Q:需要睡眠等待多长的时间呢?

需要分两种情况:

1. 等待的原因是TCP层的内存不足。

    刚进入函数时,会判断sock的发送缓存是否达到了上限。

    如果此时sock尚有发送缓存额度,说明是TCP层内存不足导致发送缓存申请失败的,

    设置等待时间为一个2~202ms的伪随机数,超时后就结束等待。

2. 等待的原因是sock的发送缓存不足。

    在睡眠的过程中,当有可用的发送缓存时,进程会被唤醒,从而结束等待。

    否则达到超时时间后,返回错误。

/* Wait for more memory for a socket
 * @sk: socket to wait for memory
 * @timeo_p: for how long
 */

int sk_stream_wait_memory(struct sock *sk, long *timeo_p)
{
    int err = 0;
    long vm_wait = 0;
    long current_timeo = *timeo_p;
    DEFINE_WAIT(wait); /* 初始化等待任务 */

    /* 如果sock还有发送缓存额度,说明是TCP层内存不足导致的。
     * 初始化等待时间为一个2~202ms的伪随机数。
     */
    if (sk_stream_memory_free(sk))
        current_timeo = vm_wait = (prandom_u32() % (HZ / 5)) + 2;

    while (1) {
        /* 设置异步发送时,发送缓存不够的标志 */
        set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);

        /* 把等待任务加入到socket等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
        prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);

        /* 如果连接有错误,或者不允许发送数据了,那么返回-EPIPE */
        if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
            goto do_error;

        /* 如果是非阻塞的,或者等待超时了,返回-EAGAIN */
        if (! *timeo_p)
            goto do_nonblock;

        /* 如果进程有待处理的信号,如果没有设置超时时间返回-ERESTARTSYS,
         * 否则返回-EINTR.
         */
        if (signal_pending(current))
            goto do_interrupte;
 
        clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);

        /* 如果sock已经有可用的发送缓存了。并满足以下任一条件:
         * 1. 此次等待是由于sock的发送缓存不足。 
         * 2. 此次等待是由于TCP层内存不足,经过了一次睡眠vm_wait设为0。 
         */
        if (sk_stream_memory_free(sk) && ! vm_wait)
            break;
 
        set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
        sk->sk_write_pending++;

        /* 进入睡眠等待 */
        sk_wait_event(sk, ¤tt_timeo, sk->sk_err ||
            (sk->sk_shutdown & SEND_SHUTDOWN) || 
            (sk_stream_memory_free(sk) && ! vm_wait)); 
        sk->sk_write_pending--;

        /* 如果vm_wait不为0,睡眠2~202ms后,就把vm_wait清零了 */
        if (vm_wait) {
            vm_wait -= current_timeo;
            current_timo = *timeo_p;

            if (current_timeo != MAX_SCHEDULE_TIMEOUT &&
                (current_timeo -= vm_wait) < 0)
                current_timeo = 0;

            vm_wait = 0;
        }

        *timeo_p = current_timeo; /* 更新发送的超时等待时间 */
    }

out:
    /* 把等待任务从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
    finish_wait(sk_sleep(sk), &wait);
    return err;

do_error:
    err = -EPIPE;
    goto out;

do_nonblock:
    err = -EAGAIN;
    goto out;

do_interrupted:
    err = sock_intr_errno(*timeo_p);
    goto out;
}

 

因有发送缓存可写事件而被唤醒

 

sk->sk_write_space的实例为sock_def_write_space()。

如果socket是SOCK_STREAM类型的,那么函数指针的值会更新为sk_stream_write_space()。

sk_stream_write_space()在TCP中的调用路径为:

tcp_rcv_established / tcp_rcv_state_process

    tcp_data_snd_check

        tcp_check_space

            tcp_new_space

static void tcp_check_space(struct sock *sk)
{
    /* 如果发送队列中有skb被释放了 */
    if (sock_flag(sk, SOCK_QUEUE_SHRUNK)) {

        sock_reset_flag(sk, SOCK_QUEUE_SHRUNK);

        /* 如果设置了同步发送时,发送缓存不足的标志 */
        if (sk->sk_socket && test_bit(SOCK_NOSPACE, &sk->sk_socket->flags))
            tcp_new_space(sk); /* 更新发送缓存 */
    }
}
/* When incoming ACK allowed to free some skb from write_queue,
 * we remember this event in flag SOCK_QUEUE_SHRUNK and wake up socket
 * on the exit from tcp input handler.
 */
static void tcp_new_space(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tcp_should_expand_sndbuf(sk)) {
        tcp_sndbuf_expand(sk);
        tp->snd_cwnd_stamp = tcp_time_stamp;
    }

    /* 检查是否需要触发有缓存可写事件 */
    sk->sk_write_space(sk);
}
void sk_stream_write_space(struct sock *sk)
{
    struct socket *sock = sk->sk_socket;
    struct socket_wq *wq; /* 等待队列和异步通知队列 */

    /* 如果剩余的发送缓存不低于发送缓存上限的1/3,且尚未发送的数据不高于一定值时 */
    if (sk_stream_is_writeable(sk) && sock) {
        clear_bit(SOCK_NOSPACE, &sock->flags); /* 清除发送缓存不够的标志 */

        rcu_read_lock();
        wq = rcu_dereference(sk->sk_wq); /* socket的等待队列和异步通知队列 */
        if (wq_has_sleeper(wq)) /* 如果等待队列不为空,则唤醒一个睡眠进程 */
            wake_up_interruptible_poll(&wq->wait, POLLOUT | POLLWRNORM | POLLWRBAND);

        /* 异步通知队列不为空,且允许发送数据时。
         * 检测sock的发送队列是否曾经到达上限,如果有的话发送SIGIO信号,告知异步通知队列上
         * 的进程有发送缓存可写。
         */
        if (wq && wq->fasync_list && !(sk->sk_shutdown & SEND_SHUTDOWN))
            sock_wake_async(sock, SOCK_WAKE_SPACE, POLL_OUT);

        rcu_read_unlock();
    }
}

#define wake_up_interruptible_poll(x, m) \
    __wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m))


如果剩余的发送缓存大于发送缓存上限的1/3,且尚未发送的数据少于一定值时,才会触发有发送

缓存可写的事件。

static inline bool sk_stream_is_writeable(const struct sock *sk)
{
    return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) &&
        sk_stream_memory_free(sk);
}

static inline int sk_stream_wspace(const struct sock *sk)
{
    return sk->sk_sndbuf - sk->sk_wmem_queued;
}

static inline int sk_stream_min_wspace(const struct sock *sk)
{
    return sk->sk_wmem_queued >> 1;
}

检查尚未发送的数据是否已经够多了,如果超过了用户设置的值,就不用触发有发送缓存可写事件,

以免使用过多的内存。

static inline bool sk_stream_memory_free(const struct sock *sk)
{
    if (sk->sk_wmem_queued >= sk->sk_sndbuf)
        return false;

    return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true;
}

static inline bool tcp_stream_memory_free(const struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未发送的数据大小 */

    /* 当尚未发送的数据,少于配置的值时,才触发有发送缓存可写的事件。
     * 这是为了避免发送缓存占用过多的内存。
     */
    return notsent_bytes < tcp_notsent_lowat(tp);
}

如果有使用TCP_NOTSENT_LOWAT选项,则使用用户设置的值。

否则使用sysctl_tcp_notsent_lowat,默认为无穷大。

static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{
    return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat;
}

 

 

转载于:https://www.cnblogs.com/aiwz/p/6333229.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: LWIP(Lightweight IP)是一个用于嵌入式系统的轻量级TCP/IP协议栈。使用LWIP库可以方便地实现通过串口发送数据到TCP服务器的功能。 首先,需要初始化LWIP协议栈,并创建一个TCP客户端连接。通过配置串口通信参数,确保串口与设备之间的数据传输正常。然后,使用串口接收数据的中断函数,获取待发送的数据,将其存储在一个缓冲区中。 在TCP客户端连接建立后,可以通过LWIP库提供的API函数,将缓冲区中的数据发送TCP服务器。通过调用lwip_send()函数,将数据写入到TCP发送缓冲区中,并通过TCP协议栈将数据发送到远程服务器。同时,可以使用lwip_recv()函数接收服务器返回的数据,并进行相关处理。 在发送数据时,应注意TCP发送缓冲区的空闲空间。如果发送速度过快,发送缓冲区可能会满,导致数据丢失或发送失败。因此,可以使用lwip_sndbuf()函数查询缓冲区剩余的可用空间,以便根据情况适时调整发送速度。 另外,为了提高数据传输的可靠性,可以使用LWIP的重传机制。如果发送的数据在网络中丢失或未收到响应,TCP协议栈会自动重传丢失的数据包,以确保数据的可靠传输。 需要注意的是,在使用LWIP发送数据时,应遵循TCP协议的相关规定,例如TCP的流控制和拥塞控制等机制,以避免网络拥塞和数据丢失的问题。 总之,通过使用LWIP库,可以方便地实现通过串口发送数据到TCP服务器的功能,并提供可靠的数据传输机制,以满足嵌入式系统中对网络通信的需求。 ### 回答2: LWIP是一个轻量级的开源的TCP/IP协议栈。它提供了一种在嵌入式系统中实现TCP/IP网络通信的解决方案。 在LWIP中,实现串口数据通过TCP发送的过程需要以下步骤: 首先,需要配置串口的相关参数,例如波特率、数据位数、停止位等。可以通过调用LWIP提供的接口函数来进行配置。 然后,需要创建一个TCP连接。可以通过调用lwip_tcp_new函数来创建一个新的TCP连接,并将其绑定到一个特定的端口号上。 接下来,需要绑定一个回调函数到TCP连接上,用于处理接收到的数据。当有数据从串口读取并发送TCP连接上时,回调函数将被自动触发。 在回调函数中,可以通过调用lwip_tcp_write函数将接收到的串口数据写入到TCP连接中。也可以通过调用lwip_tcp_output函数将数据发送出去。需要注意的是,在发送数据之前,应确保TCP连接已经建立成功。 最后,需要调用lwip_tcp_close函数来关闭TCP连接,释放相关资源。 需要特别注意的是,由于LWIP是一个单线程的协议栈,所以在实际使用中可能需要使用操作系统提供的多线程功能,例如使用操作系统提供的线程库将串口读取和TCP发送的过程分别放在不同的线程中。 总的来说,使用LWIP实现串口数据通过TCP发送,需要配置串口参数、创建TCP连接、绑定回调函数、读取串口数据并发送TCP连接中,并最后关闭TCP连接。以上是一个简单的描述,具体实现还需要根据具体的应用场景和需求进行相应的定制。 ### 回答3: 使用lwIP(轻型IP协议栈)库,可以很方便地实现通过串口发送数据的TCP连接。 首先,需要在代码中引入lwIP库,并初始化lwIP协议栈。具体的初始化过程可以参考lwIP官方文档或者示例代码。 在初始化完成后,需要创建一个TCP服务器或者客户端连接。创建TCP服务器可以使用`tcp_new()`函数,指定一个回调函数来处理接收到的数据。创建TCP客户端可以使用`tcp_connect()`函数,指定要连接的服务器地址和回调函数。回调函数可以在接收到数据时进行相应的处理操作。 若要通过串口发送数据,可以使用标准串口库,例如STM32的HAL库中的函数`HAL_UART_Transmit()`。在接收到需要发送的数据后,可以通过调用该函数将数据通过串口发送出去。 具体的步骤如下: 1. 初始化lwIP协议栈。 2. 创建TCP服务器或者客户端连接,并指定回调函数。 3. 接收到需要发送的数据后,调用串口发送函数将数据发送出去。 4. 回调函数中可以针对接收到的数据进行处理,例如将数据缓存起来或者做其他操作。 需要注意的是,lwIP是一个基于事件驱动的协议栈,需要在主循环中定期调用`tcpip_input()`函数来处理网络数据。此外,还需要配置lwIP的IP地址、子网掩码、默认网关等网络参数,以便能够正常连接到网络。 总之,使用lwIP库可以方便地实现通过串口发送数据的TCP连接。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值