tcp sk_forward_alloc

  keep sk->sk_forward_alloc as small as possible patch。!!!

预分配缓存额度sk_forward_alloc与发送缓存队列统计sk_wmem_queued一同用于计算当前套接口所占用的内存量。sk_forward_alloc属于为套接口预分配,所以缓存并没有实际分配出去。

sk->sk_rmem_alloc表示当前已经使用的接收缓冲区内存

复制代码
  /* 
     * 预分配缓存长度,这只是一个标识,目前 只用于TCP。
     * 当分配的缓存小于该值时,分配必然成功,否则需要
     * 重新确认分配的缓存是否有效。参见__sk_mem_schedule().
     * 在sk_clone()中,sk_forward_alloc被初始化为0.
     * 
     * update:sk_forward_alloc表示预分配长度。当我们第一次要为
     * 发送缓冲队列分配一个struct sk_buff时,我们并不是直接
     * 分配需要的内存大小,而是会以内存页为单位进行
     * 预分配(此时并不是真的分配内存)。当把这个新分配
     * 成功的struct sk_buff放入缓冲队列sk_write_queue后,从sk_forward_alloc
     * 中减去该sk_buff的truesize值。第二次分配struct sk_buff时,只要再
     * 从sk_forward_alloc中减去新的sk_buff的truesize即可,如果sk_forward_alloc
     * 已经小于当前的truesize,则将其再加上一个页的整数倍值,
     * 并累加如tcp_memory_allocated。
     
     *   也就是说,通过sk_forward_alloc使全局变量tcp_memory_allocated保存
     * 当前tcp协议总的缓冲区分配内存的大小,并且该大小是
     * 页边界对齐的。
     */ //这是本sock的缓存大小,如果要看整个tcp sock的缓存大小,要参考tcp_prot中的memory_allocated成员
     ////阅读函数__sk_mem_schedule可以了解proto的内存情况判断方法 。  注意和上面的sk_wmem_alloc的区别
复制代码

sk_forward_alloc初始化

对于面向连接的套接口类型如TCP,在创建子套接口时,将其sk_forward_alloc初始化为0。

struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
    newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);
    if (newsk != NULL) {
        -----------------
        newsk->sk_forward_alloc = 0;
   }
}

 

sk_forward_alloc预分配

套接口内存的页面是按照SK_MEM_QUANTUM的大小为单位,定义为4096(4K),与大多数系统的PAGE_SIZE定义相同。曾经在老一点版本的内核中,SK_MEM_QUANTUM直接使用PAGE_SIZE宏定义,导致一些将页面大小PAGE_SIZE定义为64K字节的系统,预分配不必要的大量内存额度。
————————————————

#define SK_MEM_QUANTUM 4096
#define SK_MEM_QUANTUM_SHIFT ilog2(SK_MEM_QUANTUM)
static inline int sk_mem_pages(int amt)
{       
    return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT;
}

  但是,sk_forward_alloc的单位还是以字节表示,只不过其大小为SK_MEM_QUANTUM的整数倍。 预分配额度的基础函数为__sk_mem_schedule如下,函数__sk_mem_raise_allocated判断此次分配是否符合协议的内存限定,如果不符合,需要回退预分配的额度。

复制代码
/**
 *    __sk_mem_schedule - 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)
{
    int ret, amt = sk_mem_pages(size);

    sk->sk_forward_alloc += amt << SK_MEM_QUANTUM_SHIFT;
    ret = __sk_mem_raise_allocated(sk, size, amt, kind);
    if (!ret)
        sk->sk_forward_alloc -= amt << SK_MEM_QUANTUM_SHIFT;
    return ret;
}
复制代码

  __sk_mem_raise_allocated函数还将增加协议总内存的占用统计(memory_allocated),其单位为SK_MEM_QUANTUM大小的页面数量(第三个参数amt)。由于内核的网络协议内存限额是以PAGE_SIZE大小页面为单位,如TCP协议,可通过PROC文件/proc/sys/net/ipv4/tcp_mem查看。所以在进行比较时,内核使用函数sk_prot_mem_limits将限定的页面数值转换为以SK_MEM_QUANTUM为单位的页面值。

 

  函数__sk_mem_schedule的封装函数有两个sk_wmem_schedule和sk_rmem_schedule,对应于发送SK_MEM_SEND和接收SK_MEM_RECV两个类别的缓存使用。对于sk_wmem_schedule函数,如果请求的大小在预分配额度内,进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。

复制代码
static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size <= sk->sk_forward_alloc ||
        __sk_mem_schedule(sk, size, SK_MEM_SEND);
}

static inline bool
sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size <= sk->sk_forward_alloc ||
        __sk_mem_schedule(sk, size, SK_MEM_RECV) ||
        skb_pfmemalloc(skb);
}
复制代码

  另外一个预分配缓存额度的函数为sk_forced_mem_schedule,与以上的__sk_mem_schedule函数不同,如果内存额度不够,其强制进行缓存额度的预分配,而不管是否超出网络协议的内存限定。用在比如FIN报文发送等情况下,其不必等待可尽快结束一个连接,否则可能导致FIN报文的延迟或者放弃发送FIN而关闭连接,其结束后又可释放连接的缓存占用。

复制代码
/* We allow to exceed memory limits for FIN packets to expedite
 * connection tear down and (memory) recovery.
 * Otherwise tcp_send_fin() could be tempted to either delay FIN
 * or even be forced to close flow without any FIN.
 * In general, we want to allow one skb per socket to avoid hangs
 * with edge trigger epoll()
 */
void sk_forced_mem_schedule(struct sock *sk, int size)
{
    int amt;

    if (size <= sk->sk_forward_alloc)
        return;
    amt = sk_mem_pages(size);
    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
    sk_memory_allocated_add(sk, amt);

    if (mem_cgroup_sockets_enabled && sk->sk_memcg)
        mem_cgroup_charge_skmem(sk->sk_memcg, amt);
}
复制代码

 

sk_forward_alloc预分配额度使用

sk_forward_alloc预分配额度使用有一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用。

复制代码
static inline void sk_mem_charge(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return;
    sk->sk_forward_alloc -= size;
}

static inline void sk_mem_uncharge(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return;
    sk->sk_forward_alloc += size;

    /* Avoid a possible overflow.
     * TCP send queues can make this happen, if sk_mem_reclaim()
     * is not called and more than 2 GBytes are released at once.
     *
     * If we reach 2 MBytes, reclaim 1 MBytes right now, there is
     * no need to hold that much forward allocation anyway.
     */
    if (unlikely(sk->sk_forward_alloc >= 1 << 21))
        __sk_mem_reclaim(sk, 1 << 20);
}
复制代码

  函数sk_mem_uncharge可将一定量回填到预分配额度中。如果sk_forward_alloc预分配额度大于2M字节,立即回收1M字节,预分配额度没有必要太大,立即回收以防止sk_mem_reclain回收不及时导致溢出。

在清空发送队列函数tcp_write_queue_purge和清空重传队列函数tcp_rtx_queue_purge,以及重传队列元素移除函数tcp_rtx_queue_unlink_and_free中调用sk_wmem_free_skb释放skb,并且uncharge预分配的额度。

复制代码
static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
    sk_wmem_queued_add(sk, -skb->truesize);
    sk_mem_uncharge(sk, skb->truesize);
    if (static_branch_unlikely(&tcp_tx_skb_cache_key) &&
        !sk->sk_tx_skb_cache && !skb_cloned(skb)) {
        skb_ext_reset(skb);
        skb_zcopy_clear(skb, true);
        sk->sk_tx_skb_cache = skb;
        return;
    }
    __kfree_skb(skb);
}
复制代码

sk_forward_alloc预分配额度回收

 

  基础的回收函数为__sk_mem_reclaim,以上介绍的sk_mem_uncharge函数也会使用到。除此之外,内核使用两个函数回收预分配内存额度:分别为sk_mem_reclaim和sk_mem_reclaim_partial函数。前者回收之后有可能将额度全部回收或者仅留下小于SK_MEM_QUANTUM大小的额度;而后者不会全部回收额度,其在额度大于SK_MEM_QUANTUM时,执行回收操作,并且保证留下小于SK_MEM_QUANTUM大小的额度。

复制代码
static inline void sk_mem_reclaim(struct sock *sk)
{
    if (!sk_has_account(sk))
/* TCP层是有统计内存使用的,所以条件为假 */
return; 
if (sk->sk_forward_alloc >= SK_MEM_QUANTUM) __sk_mem_reclaim(sk, sk->sk_forward_alloc);
}

static inline void sk_mem_reclaim_partial(struct sock *sk) {
if (!sk_has_account(sk)) return;
if (sk->sk_forward_alloc > SK_MEM_QUANTUM) __sk_mem_reclaim(sk, sk->sk_forward_alloc - 1);
}
/** * __sk_mem_reclaim - reclaim sk_forward_alloc and memory_allocated
* @sk: socket
* @amount: number of bytes (rounded down to a SK_MEM_QUANTUM multiple)
*/

void __sk_mem_reclaim(struct sock *sk, int amount) { amount >>= SK_MEM_QUANTUM_SHIFT; sk->sk_forward_alloc -= amount << SK_MEM_QUANTUM_SHIFT; __sk_mem_reduce_allocated(sk, amount); }
复制代码

sk_forward_alloc预分配时机

  在TCP重要的skb缓存分配函数sk_stream_alloc_skb中,如果TCP协议总的内存处于承压状态,首先回收部分预分配缓存,因为马上要为skb分配内存,不应进行全部回收。在分配skb之后有两种情况,如果指定了强制分配force_schedule参数,即强制增加分配额度而不进行内存超限判断;否则,使用sk_wmem_schedule进行额度分配。只有在分配额度成功之后返回分配的skb,反之释放skb返回失败

 

复制代码
struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp,
                    bool force_schedule)
{
    struct sk_buff *skb;

    if (likely(!size)) {
        skb = sk->sk_tx_skb_cache;
        if (skb) {
            skb->truesize = SKB_TRUESIZE(skb_end_offset(skb));
            sk->sk_tx_skb_cache = NULL;
            pskb_trim(skb, 0);
            INIT_LIST_HEAD(&skb->tcp_tsorted_anchor);
            skb_shinfo(skb)->tx_flags = 0;
            memset(TCP_SKB_CB(skb), 0, sizeof(struct tcp_skb_cb));
            return skb;
        }
    }
    /* The TCP header must be at least 32-bit aligned.  */
    size = ALIGN(size, 4);

    if (unlikely(tcp_under_memory_pressure(sk)))
        sk_mem_reclaim_partial(sk);

    skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
    if (likely(skb)) {
        bool mem_scheduled;

        if (force_schedule) {
            mem_scheduled = true;
            sk_forced_mem_schedule(sk, skb->truesize);
        } else {
            mem_scheduled = sk_wmem_schedule(sk, skb->truesize);
        }
        if (likely(mem_scheduled)) {
            skb_reserve(skb, sk->sk_prot->max_header);
            /*
             * Make sure that we have exactly size bytes
             * available to the caller, no more, no less.
             */
            skb->reserved_tailroom = skb->end - skb->tail - size;
            INIT_LIST_HEAD(&skb->tcp_tsorted_anchor);
            return skb;
        }
        __kfree_skb(skb);
    } else {
        sk->sk_prot->enter_memory_pressure(sk);
        sk_stream_moderate_sndbuf(sk);
    }
    return NULL;
}
复制代码

 

  对于sk_stream_alloc_skb函数的使用,发生在TCP发送路径上比如tcp_sendmsg_locked和do_tcp_sendpages发送函数,分片函数tcp_fragment和tso_fragment,以及tcp_mtu_probe、tcp_send_syn_data和tcp_connect函数。sk_stream_alloc_skb函数获取到了相应的缓存额度,紧接其后就需要使用此额度。如函数tcp_mtu_probe,其调用sk_mem_charge使用了skb的truesize长度的分配额度。

 

复制代码
void tcp_close(struct sock *sk, long timeout)
{
    sk_mem_reclaim(sk);
}
void inet_sock_destruct(struct sock *sk)
{
    struct inet_sock *inet = inet_sk(sk);
    sk_mem_reclaim(sk);
}
复制代码

 

   TCP的延时ACK处理函数tcp_delack_timer_handler,首先会调用函数sk_mem_reclaim_partial回收部分预分配额度,在执行最后,如果网络协议内存处于承压状态,还会调用sk_mem_reclaim回收函数。另外,在TCP超时重传函数tcp_write_timer_handler和keepalive超时函数中也有调用sk_mem_reclaim回收函数。

 

sk_forward_alloc超限判断

  在函数__sk_mem_schedule预分配额度时,使用函数__sk_mem_raise_allocated判断TCP协议内存是否超过限定值。如下在协议内存承压状态下,如果当前套接口的发送队列缓存、接收缓存已经预分配缓存之和所占用的页面数,乘以当前套接口协议的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。

/**
 *	__sk_mem_raise_allocated - increase memory_allocated
 *	@sk: socket
 *	@size: memory size to allocate
 *	@amt: pages to allocate
 *	@kind: allocation type
 *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。
 *	Similar to __sk_mem_schedule(), but does not update sk_forward_alloc
 */
int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
	struct proto *prot = sk->sk_prot;
	long allocated = sk_memory_allocated_add(sk, amt);
	bool charged = true;

	if (mem_cgroup_sockets_enabled && sk->sk_memcg &&
	    !(charged = mem_cgroup_charge_skmem(sk->sk_memcg, amt)))
		goto suppress_allocation;

	/* Under limit. TCP层的内存使用量低于最小值sysctl_tcp_mem[0]。 */
	if (allocated <= sk_prot_mem_limits(sk, 0)) {
		sk_leave_memory_pressure(sk);
		return 1;
	}

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

	/* Over hard limit. 
	 * 如果TCP层的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限
     * sk->sk_sndbuf。
     */
	if (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) < sk_get_rmem0(sk, prot))
			return 1;

	} else { /* SK_MEM_SEND */
		int wmem0 = sk_get_wmem0(sk, prot);

		if (sk->sk_type == SOCK_STREAM) {
			if (sk->sk_wmem_queued < wmem0)
				return 1;
		} else if (refcount_read(&sk->sk_wmem_alloc) < wmem0) {
				return 1;
		}
	}

	if (sk_has_memory_pressure(sk)) {
		u64 alloc;
		  /* 如果TCP不处于内存压力状态,直接返回 */
		if (!sk_under_memory_pressure(sk))
			return 1;
		alloc = sk_sockets_allocated_read_positive(sk);
		 /* 如果当前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;
	}

	if (kind == SK_MEM_SEND || (kind == SK_MEM_RECV && charged))
		trace_sock_exceed_buf_limit(sk, prot, allocated, kind);

	sk_memory_allocated_sub(sk, amt);

	if (mem_cgroup_sockets_enabled && sk->sk_memcg)
		mem_cgroup_uncharge_skmem(sk->sk_memcg, amt);

	return 0;
}

 

   TCP的核心预分配缓存额度函数为tcp_try_rmem_schedule,如果无法分配缓存额度,将首先调用tcp_prune_queue函数尝试合并sk_receive_queue中的数据包skb以减少空间占用,如果空间仍然不足,最后调用tcp_prune_ofo_queue函数清理乱序数据包队列(out_of_order_queue)。

 

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
				 unsigned int size)
{
	if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
	    !sk_rmem_schedule(sk, skb, size)) {

		if (tcp_prune_queue(sk) < 0)
			return -1;

		while (!sk_rmem_schedule(sk, skb, size)) {
			if (!tcp_prune_ofo_queue(sk))
				return -1;
		}
	}
	return 0;
}

 函数tcp_prune_queue和tcp_prune_ofo_queue在清理空间之后,都会使用函数sk_mem_reclaim回收空间。

  函数tcp_try_rmem_schedule在TCP的接收路径中调用,比如tcp_data_queue函数和tcp_data_queue_ofo函数,以及用于TCP套接口REPAIR模式的tcp_send_rcvq函数。如下为tcp_data_queue函数,如果接收队列sk_receive_queue为空,使用sk_forced_mem_schedule函数强制分配缓存额度,否则,使用tcp_try_rmem_schedule函数进行正常分配并检查缓存是否超限。最后使用tcp_queue_rcv函数完成接收工作,同时通过调用skb_set_owner_r函数使用分配的缓存额度。函数tcp_send_rcvq的缓存相关操作与此类似。

  

//函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
   if (unlikely(tcp_try_rmem_schedule(sk, skb, skb->truesize))) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPOFODROP);
		sk->sk_data_ready(sk);
		tcp_drop(sk, skb);
		return;
	}
    --------------------------------
end:
   if (skb) {
		/* For non sack flows, do not grow window to force DUPACK
		 * and trigger fast retransmit.
		 */
		if (tcp_is_sack(tp))
			tcp_grow_window(sk, skb);
		skb_condense(skb);
		skb_set_owner_r(skb, sk);
	}
}

函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。

sk_forward_alloc额度的使用与回填
预分配额度使用函数skb_set_owner_r将skb的销毁回调函数destructor设置为sock_rfree函数,其调用sk_mem_uncharge函数将skb占用的缓存回填到预分配额度sk_forward_alloc中。

  sk_forward_alloc预分配额度使用由一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用,函数sk_mem_uncharge可释放一定的额度。

sk_mem_charge函数假定预分配额度足够使用,两个函数都不会做缓存超限判断。

另外,与发送路径上的函数skb_set_owner_w类似,内核使用skb_set_owner_r封装了sk_mem_charge函数,用于接收路径。

 

/*
 * Read buffer destructor automatically called from kfree_skb.
 */
void sock_rfree(struct sk_buff *skb)
{
	struct sock *sk = skb->sk;
	unsigned int len = skb->truesize;

	atomic_sub(len, &sk->sk_rmem_alloc);
	sk_mem_uncharge(sk, len);
}

  在TCP接收过程中,内核尝试将新接收到的数据包合并到之前的数据包中(sk_receive_queue接收队列的末尾数据包),参见函数tcp_try_coalesce,如果合并成功,

将释放被合并的skb所占用的缓存,所以其skb结构体所占用缓存空间不需要使用缓存额度,仅需要计算其数据部分的缓存额度(delta)。

/**
 * tcp_try_coalesce - try to merge skb to prior one
 * @sk: socket
 * @to: prior buffer
 * @from: buffer to add in queue
 * @fragstolen: pointer to boolean
 *
 * Before queueing skb @from after @to, try to merge them
 * to reduce overall memory use and queue lengths, if cost is small.
 * Packets in ofo or receive queues can stay a long time.
 * Better try to coalesce them right now to avoid future collapses.
 * Returns true if caller should free @from instead of queueing it
 */
static bool tcp_try_coalesce(struct sock *sk,
			     struct sk_buff *to,
			     struct sk_buff *from,
			     bool *fragstolen)
{
	int delta;

	*fragstolen = false;
-----------------

	if (!skb_try_coalesce(to, from, fragstolen, &delta))
		return false;

	atomic_add(delta, &sk->sk_rmem_alloc);
	sk_mem_charge(sk, delta);
	----------------------

	return true;
}

如果合并失败,内核将skb添加到接收队列sk_receive_queue中,使用skb_set_owner_r函数将整个skb占用的缓存空间计入缓存额度。

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
				      bool *fragstolen)
{
	int eaten;
	struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

	eaten = (tail &&
		 tcp_try_coalesce(sk, tail,
				  skb, fragstolen)) ? 1 : 0;
	tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
	if (!eaten) {
		__skb_queue_tail(&sk->sk_receive_queue, skb);
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}

 

posted @   codestacklinuxer  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
历史上的今天:
2022-06-07 安全编译记录
点击右上角即可分享
微信分享提示