打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
reentrant函数与thread safe函数浅析
记得以前讨论过一个关于reentrant函数与thread safe函数的帖子
很多人对于这两种函数不是很了解,
尤其是发现malloc等函数是non-reentrant函数时,对多线程编程都产生了"恐惧"
这里是我对这两种函数的一些理解,希望和大家探讨一些.欢迎批评指正.
1. reentrant函数
一个函数是reentrant的,如果它可以被安全地递归或并行调用。要想成为reentrant式的函数,该函数不能含有(或使用)静态(或全局)数据(来存储函数调用过程中的状态信息),也不能返回指向静态数据的指针,它只能使用由调用者提供的数据,当然也不能调用non-reentrant函数.
比较典型的non-reentrant函数有getpwnam, strtok, malloc等.
reentrant和non-reentrant函数的例子
 
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <math.h>
int* getPower(int i)
{
  static int result;
  result = pow(2, i);
  getchar();
  return &result;
}
void getPower_r(int i, int* result)
{
  *result = pow(2, i);
}
void handler (int signal_number) /*处理SIGALRM信号*/
{
  getPower(3);
}
int main ()
{
  int *result;
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &handler;
  sigaction(SIGALRM, &sa, NULL);
  result = getPower(5);
  printf("2^5 = %d\n", *result);
  return 0;
}
 

试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 用如下方式运行a.out:
运行./a.out,在按回车前,在另外一个终端中运行kill -14 pid (这里的pid是运行上面的ps时看到的值)
然后,按回车继续运行a.out就会看到2^5 = 8 的错误结论

对于函数int* getPower(int i)
由于函数getPower会返回一个指向静态数据的指针,在第一次调用getPower的过程中,再次调用getPower,则两次返回的指针都指向同一块内存,第二次的结果将第一次的覆盖了(很多non-reentrant函数的这种用法会导致不确定的后果).所以是non-reentrant的.

对于函数void getPower_r(int i, int* result)
getPower_r会将所得的信息存储到result所指的内存中,它只是使用了由调用者提供的数据,所以是reentrant.在信号处理函数中可以正常的使用它.

2. thread-safe函数
Thread safety是多线程编程中的概念,thread safe函数是指那些能够被多个线程同时并发地正确执行的函数.
thread safe和non thread safe的例子
 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
int count; /*共享数据*/
void* func (void* unused)
{
  if (count == 0)
    count++;
}
void* func_s (void* unused)
{
  pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  if (count == 0)
    count++;
  pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
}

int main ()
{
  pthread_t pid1, pid2;
  pthread_create(&pid1, NULL, &func, NULL);
  pthread_create(&pid2, NULL, &func, NULL);
  pthread_join(pid1, NULL);
  pthread_join(pid2, NULL);
  return 0;
}
 

函数func是non thread safe的,这是因为它不能避免对共享数据count的race condition,
设想这种情况:一开始count是0,当线程1进入func函数,判断过count == 0后,线程2进入func函数
线程2判断count==0,并执行count++,然后线程1开始执行,此时count != 0 了,但是线程1仍然要执行
count++,这就产生了错误.
func_s通过mutex锁将对共享数据的访问锁定,从而避免了上述情况的发生.func_s是thread safe的
只要通过适当的"锁"机制,thread safe函数还是比较好实现的.
3. reentrant函数与thread safe函数的区别
reentrant函数与是不是多线程无关,如果是reentrant函数,那么要求即使是同一个进程(或线程)同时多次进入该函数时,该函数仍能够正确的运作.
该要求还蕴含着,如果是在多线程环境中,不同的两个线程同时进入该函数时,该函数也能够正确的运作.
thread safe函数是与多线程有关的,它只是要求不同的两个线程同时对该函数的调用在逻辑上是正确的.
从上面的说明可以看出,reentrant的要求比thread safe的要求更加严格.reentrant的函数必是thread safe的,而thread safe的函数
未必是reentrant的. 举例说明:
 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
int count; /*共享数据*/
void* func_s (void* unused)
{
  pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  printf("locked by thead %d\n", pthread_self());
  if (count == 0)
    count++;
  getchar();
  pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  printf("lock released by thead %d\n", pthread_self());
}
void handler (int signal_number) /*处理SIGALRM信号*/
{
  printf("handler running in %d\n", pthread_self());
  func_s(NULL);
}

int main ()
{
  pthread_t pid1, pid2;
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &handler;
  sigaction(SIGALRM, &sa, NULL);
  printf("main thread's pid is: %d\n", pthread_self());
  func_s(NULL);
  pthread_create(&pid1, NULL, &func_s, NULL);
  pthread_create(&pid2, NULL, &func_s, NULL);
  pthread_join(pid1, NULL);
  pthread_join(pid2, NULL);
  func_s(NULL);
  return 0;
}
 

试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 进行下面4次运行a.out:
每次运行分别在第1,2,3,4次回车前,在另外一个终端中运行kill -14 pid (这里的pid是上面ps中看到的值)
试验结果:
1. 该进程中有3个线程:一个主线程,两个子线程
2. func_s是thread safe的
3. func_s不是reentrant的
4. 信号处理程序会中断主线程的执行,不会中断子线程的执行
5. 在第1,4次回车前,在另外一个终端中运行kill -14 pid会形成死锁,这是因为
主线程先锁住了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,
由于同一个线程锁定两次,所以形成死锁
6. 在第2,3次回车前,在另外一个终端中运行kill -14 pid不会形成死锁,这是因为一个子线程先锁住
了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,被挂起,这时,子线程
可以被继续执行.当该子线程释放掉锁以后,handler和另外一个子线程可以竞争进入临界区,然后继续执行.
所以不会形成死锁.
结论:
1. reentrant是对函数相当严格的要求,绝大部分函数都不是reentrant的(APUE上有一个reentrant函数
的列表).
什么时候我们需要reentrant函数呢?只有一个函数需要在同一个线程中需要进入两次以上,我们才需要
reentrant函数.这些情况主要是异步信号处理,递归函数等等.(non-reentrant的递归函数也不一定会
出错,出不出错取决于你怎么定义和使用该函数). 大部分时候,我们并不需要函数是reentrant的.
2. 在多线程环境当中,只要求多个线程可以同时调用一个函数时,该函数只要是thread safe的就可以了.
我们常见的大部分函数都是thread safe的,不确定的话请查阅相关文档.
3. reentrant和thread safe的本质的区别就在于,reentrant函数要求即使在同一个线程中任意地进入两次以上,
也能正确执行.
大家常用的malloc函数是一个典型的non-reentrant但是是thread safe函数,这就说明,我们可以方便的
在多个线程中同时调用malloc,但是,如果将malloc函数放入信号处理函数中去,这是一件很危险的事情.
4. reentrant函数肯定是thread safe函数,也就是说,non thread safe肯定是non-reentrant函数
不能简单的通过加锁,来使得non-reentrant函数变成 reentrant函数
这个链接是说明一些non-reentrant ===> reentrant和non thread safe ===>thread safe转换的
http://www.unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm
[ 本帖最后由 ypxing 于 2007-8-4 01:06 编辑 ]
 
--------------------------------------------------------------------------------
 lenovo 回复于:2007-08-02 21:38:57
不错,很好的帖子。

--------------------------------------------------------------------------------
 科技牛 回复于:2007-08-03 15:38:14
受教很深!

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-03 15:58:22
调用了malloc的函数肯定是non-reentrant的
引用:原帖由 bluster 于 2007-8-3 15:55 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155171&ptid=971102]
最后一点是错的,比如一个函数调用malloc并不影响这个函数是否是reentrant。
 
--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-03 15:59:35
这家伙,怎么把自己的帖子给删了?

--------------------------------------------------------------------------------
 bluster 回复于:2007-08-03 16:01:11
引用:原帖由 ypxing 于 2007-8-3 15:58 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155198&ptid=971102]
调用了malloc的函数肯定是non-reentrant的
 
你是对的,我一时有点绕。
其实,是对reentrant的定义有问题。
可重入的意思,差不多是函数的任意部分都可以并行,而线程安全的意思则是多线程环境下使用没有问题,对于非可重入的函数,使用lock来保护不可并行的部分从而线程安全。
引用:原帖由 ypxing 于 2007-8-3 15:59 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155214&ptid=971102]
这家伙,怎么把自己的帖子给删了?
无价值糊涂帖,所以删了。
[ 本帖最后由 bluster 于 2007-8-3 16:05 编辑 ]

--------------------------------------------------------------------------------
 jigloo 回复于:2007-08-03 16:11:55
>>3. reentrant和thread safe的本质的区别就在于,reentrant函数要求在同一个线程中需要进入两次以上,
并能正确执行.
这个说的不对,可重入区别在于允许任意中断函数的执行并恢复(比如信号)
http://www.ibm.com/developerworks/cn/linux/l-reent.html

--------------------------------------------------------------------------------
 思一克 回复于:2007-08-03 17:03:49
这个问题很复杂。
LZ的帖子很好。改进的地方是LZ应该多讲WHY不可重入,如何才可重入,而不是下结论。
1)调用了不可重入函数的函数不一定是不可重入的。比如LINUX KERNEL中,设备中断处理函数是不可重入的,而__do_IRQ()调用了他们,但__do_IRQ却是可重入的。
只要保证被调用的函数部分没有重入就可以了。
2)使用的全局变量的函数也不一定是不可重入的。还比如__do_IRQ()使用了全局变量来存储数据,但它是可重入的。
类似的例子:
[CODE]
int ia[32];
int func(int i)
{
    ia++;
    printf("%p i %d %d\n", &i, i, ia);
    if(i == 31) return;
    func(i+1);
}
main()
{
    func(0);
}
[/CODE]
关于这个问题,看LINUX中断处理部分非常有启发。那里逻辑复杂,各种重入(硬,软中断,多CPU)处理的非常巧妙。

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-03 18:50:12
思一克,你好
首先谢谢你的鼓励.
你给出的这个例子,函数func,既不是可重入的,也不是线程安全的,
原因如下:
假设有一个信号处理函数handler,里面调用了func
考虑这种情况:
主函数中调用了func(0) (这个时候,你的本意是先要ia[0]++,然后打印现在ia[0]的值,
再然后继续后面的操作),
在func刚执行完ia[0]++时,信号触发了handler函数,
handler函数会调用func函数,然后执行对ia的一系列操作,完成后返回.
这时,你的主函数调用的func继续执行,也就是要printf了,
这时printf的东东就不是你想要的了,而且你无法确定现在ia[0]的值是什么(因为信号
可以中断很多次很多层).所以func不是可重入的.
而且也不是线程安全的.
可重入的一个判定方法就是将它放入信号处理函数中,仔细推敲各种中断情况下,
你是不是还能得到你想要的结果.
"使用的全局变量的函数也不一定是不可重入的。"这句是正确的,只要正确使用就可以了,
但是不使用全局变量是写可重入函数的简单方法.
"调用了不可重入函数的函数不一定是不可重入的。"这句是不对的,
因为你无法保证被调用的不可重入函数部分不被重入

int ia[32];
int func(int i)
{
    ia++;
    printf("%p i %d %d\n", &i, i, ia);
    if(i == 31) return;
    func(i+1);
}
main()
{
    func(0);
}
 

--------------------------------------------------------------------------------
 思一克 回复于:2007-08-03 19:39:57
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可重入的"是不对的.因为有十分多的反例.

调用了不可重入函数的函数不一定是不可重入的。"这句是不对的,
因为你无法保证被调用的不可重入函数部分不被重入

--------------------------------------------------------------------------------
 feasword 回复于:2007-08-03 20:09:35
一直想找这两个概念是此非彼的例子,受教了
关于死锁的问题,apue里也有讲,以前也遇到过,当时干脆都弄成递归锁了

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-03 20:49:04
那么,怎么才能保证不可重入的部分不被重入呢?
引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可 ...
 
--------------------------------------------------------------------------------
 cugb_cat 回复于:2007-08-03 22:12:05
引用:原帖由 ypxing 于 2007-8-3 20:49 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156295&ptid=971102]
那么,怎么才能保证不可重入的部分不被重入呢?
 
我有同楼主相同的疑问。
另外,从lz的例子中学到一些技巧,关于调试多线程程序,感谢lz。
[ 本帖最后由 cugb_cat 于 2007-8-3 22:45 编辑 ]

--------------------------------------------------------------------------------
 飞灰橙 回复于:2007-08-03 22:18:09
引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可重入的"是不对的(语句A).因为有十分多的反例.

调用了不可重入函数的函数不一定是不可重入的。"这句是不对的(语句B),
因为你无法保证被调用的不可重入函数部分不被重入

越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的

--------------------------------------------------------------------------------
 cugb_cat 回复于:2007-08-03 22:44:57
引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]

越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的
两句意思相反~:mrgreen:

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-03 23:30:14
俺也看了好一会才看懂:em02: 
引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]

越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的
 
--------------------------------------------------------------------------------
 mingyanguo 回复于:2007-08-04 00:08:35
完了,简单的问题复杂化了 :mrgreen:

--------------------------------------------------------------------------------
 hakase 回复于:2007-08-08 20:37:06
好帖,受教了~~

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-08 23:05:51
这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷
过段时间把自己的测试代码贴出来,让大家来帮忙看看

--------------------------------------------------------------------------------
 bluster 回复于:2007-08-09 10:08:56
引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷
过段时间把自己的测试代码贴出来,让大家来帮忙看看
多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。

--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-09 10:29:51
在多线程条件下,
理论上,将malloc放入signal的handler也是会出问题的,
锁是不行的,会死锁
引用:原帖由 bluster 于 2007-8-9 10:08 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7177603&ptid=971102]
多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。
 
--------------------------------------------------------------------------------
 ypxing 回复于:2007-08-09 16:22:20
试图测试malloc不可重入性的代码如下:

main.c
/*这是主程序,用来调用malloc*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
void setUnblock()
{
  sigset_t sigset;
  sigemptyset(&sigset);
  sigprocmask(SIG_SETMASK, &sigset, NULL);
     
}

void usr1Handler (int signal_number)        /*处理SIGUSR1信号*/
{
  setUnblock(); /*使得SIGUSR1可以被嵌套*/
  free((int*)malloc(sizeof(int)*1000));
  //printf("enter handler\n");
  //getchar();
}
int main ()
{
  int *pi;
  struct sigaction sa;
 
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &usr1Handler;
  sigaction(SIGUSR1, &sa, NULL);
  pause();
 
  return 0;
}
 
 

kill.c
/*这个是用来发送SIGUSR1信号的*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[])
{
  int i;
  char killstr[30]="kill -USR1 ";
  if (argc == 2)
  {
    strcat(killstr, argv[1]);
  }
   for (i=0; i<3; i++)
  {
   fork();        /*这样会有8个进程同时发送*/
  }
 
  while(1)
  {
    system(killstr);
  }
 
  return 0;
}
 

验证方法是:
1. 编译main.c 和kill.c
gcc main.c -o main
gcc kill.c -o kill
2. 运行./main
并在另外一个终端运行ps -A|grep main查找出该进程的进程号为pid
3. 运行./kill pid (此处pid为第二步查到的pid)
运行了很长时间,也没有crash
请大家看看我的程序,讨论一个测试方案出来
引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷
过段时间把自己的测试代码贴出来,让大家来帮忙看看
 
--------------------------------------------------------------------------------
 mingyanguo 回复于:2007-08-09 17:36:23
引用:原帖由 ypxing 于 2007-8-9 16:22 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7180404&ptid=971102]
试图测试malloc不可重入性的代码如下:

main.c
/*这是主程序,用来调用malloc*/
#include 
#include 
#include 
#include 
#include 
#include 
void setUnblock()
{
  sigset_t sigset;
  s ...
我估计是因为现在的malloc是线程安全的原因所以不会crash但是死锁。
我在debian上面的一个测试代码,会死锁,top一下会发现进程状态总是sleep
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#if 0
#define PRINT(a) do { \
 printf a;  \
 fflush(stdout);  \
}while(0)
#else
#define PRINT(a)
#endif
static void
run_malloc(void)
{
 void *mem[8];
 int sz;
 int i;
 for (i = 0; i < (sizeof(mem)/sizeof(mem[0])); i++) {
  sz = random() % (1024 * 1024);
  if (sz <= 0)
   sz = 1024;
  mem = malloc(sz);
  if (mem == NULL) {
   PRINT (("[%d] malloc null...\n", i));
   exit(-1);
  }
  PRINT(("%d\n", i));
  snprintf(mem, sz, "this is a test...");
 }
 for (--i; i >= 0; i--) {
  free(mem);
 }
}
static void
sighandler(int signo)
{
 static void *mem = NULL;
 PRINT ((".\n"));
 if (mem == NULL) {
  mem = malloc(1024);
 } else {
  free(mem);
  mem = NULL;
 }
}
static void
malloc_loop(void)
{
 for (;;)
  run_malloc();
}
static void
signal_loop(pid_t child)
{
 int usec;
 for (;;) {
  kill(child, SIGUSR1);
  usec = ((unsigned int)random()) % 10;
  usleep(usec);
 }
}
int
main(int argc, char **argv)
{
 pid_t child;
 if ((child = fork()) < 0) {
  perror("fork()");
  exit(-1);
 } else if (child == 0) {
  /* child */
  if (signal(SIGUSR1, sighandler) < 0) {
   perror("signal");
   exit(-1);
  }
  malloc_loop();
 } else {
  /* parent */
  signal_loop(child);
 }
 return 0;
}
 
 

--------------------------------------------------------------------------------
 haohao06 回复于:2007-08-10 11:45:06
谢谢楼主讲解.收藏先

--------------------------------------------------------------------------------
 system888net 回复于:2008-02-23 12:12:14
顶...

--------------------------------------------------------------------------------
 dxcnjupt 回复于:2008-02-23 19:46:50
不知道这个理解对不对:
thread-safe和reentrant的区别:在发生中断时,高优先级代码抢占,此时若低优先级代码持有锁,则高优先级代码会一直等待锁打开,但是低优先级代码失去了调度机会,于是造成死锁。thread-safe不考虑这种情况,但是reentrant需要。
实现reentrant的几种方法:
1不使用临界区,把原先的全局/静态变量变成函数参数,由函数调用者维护。优点是实现简单,缺点是函数功能的封装性可能会受到影响。
2在进入临界区之前,关中断(屏蔽信号)。优点是实现简单,缺点是影响实时性能,在多核机器上可能引起瓶颈(几个核等待一个核释放信号量)。
3尝试加锁,无法加锁返回一个出错值,而不是一直等待下去。缺点是出错处理比较麻烦
4为一组临界量开启一个专门的线程进行处理。优点是可以对临界区的访问按优先级排序,以及其它可扩展操作,缺点是性能受到IPC的影响。
5使用lock-free结构取代锁。缺点是lock-free算法很多都需要memory-copy,影响效率。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
为何中断处理程序中不能使用printf?
Linux 线程学习(一)
可重入函数的概念
Linux多线程编程小结
linux下的C语言开发(多线程编程)
malloc是否是线程安全的?
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服