Linux小知识---子进程与线程的一些知识点

背景介绍

最近阅读一些源码的东西,遇到这些子进程和多线程的一些调用写法,总结一下一些有价值的内容,对子进程和线程的用法增加一点了解。
在这里插入图片描述

子进程知识点

线程PCB

为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

1.子进程创建

首先来看一下子进程的创建。随处可见的一个例子

#include <stdio.h>
#include <unistd.h>

int main(void)
{
	pid_t pid;
	
	pid = fork();
	if(pid < 0)
	{
		printf("fork is error \n");
		return -1;
	}
	if(pid > 0)
	{
		printf("This is parent, parent pid is %d\n", getpid());
	}
	else if(pid == 0)
	{
		printf("This is child, child pid is %d, parent pid is %d\n", getpid(), getppid()); 
	}
	return 0;
}

这个运行结果,就挺神奇的

[root@localhost test]# gcc -o fork fork.c 
[root@localhost test]# ./fork             
This is parent, parent pid is 3815199
This is child, child pid is 3815204, parent pid is 1
[root@localhost test]# ./fork 
This is parent, parent pid is 3829770
This is child, child pid is 3829783, parent pid is 1
[root@localhost test]# ./fork 
This is parent, parent pid is 3847540
This is child, child pid is 3847555, parent pid is 3847540

没有循环,却能同时进入if条件的两个判断之中
在这里插入图片描述
原因就是一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

所以在执行了fork()之后,此时内存中运行了两个程序,会进行两次pid的判断,就类似于并行进行着两个判断,并且肯定结果还不同。

	if(pid > 0)
	{
		printf("This is parent, parent pid is %d\n", getpid());
	}
	else if(pid == 0)
	{
		printf("This is child, child pid is %d, parent pid is %d\n", getpid(), getppid()); 
	}
	//再来一次
	if(pid > 0)
	{
		printf("This is parent, parent pid is %d\n", getpid());
	}
	else if(pid == 0)
	{
		printf("This is child, child pid is %d, parent pid is %d\n", getpid(), getppid()); 
	}

2.fork的两个返回值

并且fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
在父进程中,fork返回新创建子进程的进程ID;
在子进程中,fork返回0;
如果出现错误,fork返回一个负值;

所以再来一次的时候,pid还随时会变化!!!
其实并不是pid在变化,而是这个pid,同时存在于两个地方,表面看起来是一段程序,其实是两份相同的程序在并行运行。
在这里插入图片描述

3.父子进程

子进程完全拷贝父进程的PCB;
父子进程共享代码段,但是不共享数据段:(全部变量和栈上的局部变量是独立的,在子进程中修改不会影响父进程,在父进程中修改也不会影响子进程)
简而言之,就是父子进程就是相互独立的运行空间,如果需要数据共享,那就需要进程间通讯手段了。

#include <stdio.h>
#include <unistd.h>

int main(void)
{
	pid_t pid;
	int x=5;
	
	pid = fork();
	if(pid < 0)
	{
		printf("fork is error \n");
		return -1;
	}

	if(pid == 0)
	{
		printf("This is child,x is %d\n", x);
	}
	else if(pid > 0)
	{
		x=9;
		printf("This is parent,x is %d\n", x);
	}
	return 0;
}
[root@localhost test]# ./fork 
This is parent,x is 9
This is child,x is 5

父进程只能修改到自己的x值。
在这里插入图片描述

4.孤儿进程

还有就是有的 时候父进程的ID变成了1

[root@localhost test]# ./fork             
This is parent, parent pid is 3815199
This is child, child pid is 3815204, parent pid is 1

原因就是父进程先退出了,子进程成了孤儿进程,但是系统又不允许没有父进程,子进程就被1号进程领养了。

父进程先于子进程退出后,回收子进程的父进程就不在了,会使子进程变成孤儿;
随即该孤儿进程会马上被操作系统的1号进程领养;
该进程的PCB回收也由1号进程完成;

应该怎么使得每个进程的父进程ID显示正确呢?
那就应该让该父进程别死,让它等待其子进程死后再死即可。这时候我们就应该使用一个函数了,这个函数是wait()。
作用是父进程执行到wait(),会被挂起来,等待其子进程结束后,自己才结束。

代码修改一下

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/wait.h>

int main(void)
{

	pid_t pid;
	
	pid = fork();
	if(pid < 0)
	{
		printf("fork is error \n");
		return -1;
	}

	if(pid == 0)
	{
		printf("This is child, child pid is %d, parent pid is %d\n", getpid(), getppid()); 
	}
	else if(pid > 0)
	{
		printf("This is parent, parent pid is %d\n", getpid());
		wait(NULL);
	}
	return 0;
}

就没有问题了

[root@localhost test]# gcc -o fork fork.c 
[root@localhost test]# ./fork             
This is parent, parent pid is 1672572
This is child, child pid is 1672583, parent pid is 1672572
[root@localhost test]# ./fork 
This is parent, parent pid is 1707628
This is child, child pid is 1707639, parent pid is 1707628
[root@localhost test]# ./fork 
This is parent, parent pid is 1721491
This is child, child pid is 1721498, parent pid is 1721491

在这里插入图片描述

5.僵尸进程

僵死进程概念
子进程先于父进程结束,父进程没有调用 wait 获取子进程退出码。

僵死进程的危害:

  1. 僵死进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork.
  2. 僵尸进程的内核栈无法被释放掉,为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息。

如何处理僵死进程
和前面一样,在父进程中调用 wait()完成。

//fork.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/wait.h>

int main(void)
{
	pid_t pid;
	
	pid = fork();
	if(pid < 0)
	{
		printf("fork is error \n");
		return -1;
	}

	if(pid == 0)
	{
		int i;
		
		printf("This is child, child pid is %d, parent pid is %d\n", getpid(), getppid()); 
		for(i=0;i<5;i++)
		{
			sleep(1);
			printf("child run....\n"); 
		}
		printf("child exit\n"); 
		
	}
	else if(pid > 0)
	{
		printf("This is parent, parent pid is %d\n", getpid());
		wait(NULL);
		printf("get the child exit\n"); 
	}
	return 0;
}

运行结果

[root@localhost test]# ./fork             
This is parent, parent pid is 1296361
This is child, child pid is 1296368, parent pid is 1296361
child run....
child run....
child run....
child run....
child run....
child exit
get the child exit

父进程老老实实的等到了子进程的退出,然后清理他的记录。
好一个白发人送黑发人
在这里插入图片描述

子进程参考链接

《操作系统fork()进程》
《fork()函数详解》
《Unix/Linux编程:fork()进程详解》

线程知识点

线程的创建和销毁那些就不在说了,说一点特殊的内容,以后用起来更得体。在这里插入图片描述

线程分离状态

在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。

  • 一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
  • 一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

默认创建的线程是非分离状态的,

  • 原有的线程等待创建的可结合的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
  • 分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

程序员应该根据自己的需要,选择适当的分离状态。所以如果我们在创建线程时就知道不需要了解线程的终止状态,则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。

设置方式

pthread_attr_t attr;
pthread_attr_init(&attr);
//设置可结合模式
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
//设置分离模式
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, process_main, (void *)process_para);

简单来说,就是一个是负责到底,另一个是自生自灭
在这里插入图片描述

可结合线程用法

#include <pthread.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 
#include <string.h> 

pthread_t       pid_joinable; 
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
 
int             count; 
 
void printids(const char *s) 
{ 
    pid_t       pid; 
    pthread_t   tid; 
 
    pid = getpid(); 
    tid = pthread_self(); 
 
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
        (unsigned int)tid, (unsigned int)tid); 
} 
 
 
void *cb_joinable(void *arg) 
{ 
    printids("New thread joinable begin"); 
    printids("New thread joinable:"); 
 
    int i=0; 
    for ( ; i<5; ++i)     
    { 
    	pthread_mutex_lock(&mutex); 
        printf("joinable runing %d\n", count++); 
	    pthread_mutex_unlock(&mutex); 
        sleep(1);
    }
 
    return ((void*)123); 
} 

 
int main(void) 
{ 
    int err; 
 
    count = 0; 
    pthread_mutex_init(&mutex, NULL); 
 
    err = pthread_create(&pid_joinable, NULL, cb_joinable, NULL); 
    if ( 0 != err ) 
    { 
        printf("can't create joinable thread: %s\n", strerror(err)); 
    } 
 
    int **ret; 
    err = pthread_join(pid_joinable, (void**)ret); 
    if ( err == 0 ) 
    { 
        printf("joinable return %d\n", *ret); 
    } 
    else 
    { 
        printf("can't pthread_join pid_joinable thread: %s\n", strerror(err)); 
    } 
  
    pthread_mutex_destroy(&mutex); 
 
    return 0; 
} 

主函数会阻塞在pthread_join函数运行时,直到子线程结束,才继续向下运行。主进程就像管家一样,送你到最后一程
在这里插入图片描述

分离线程用法

#include <pthread.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 
#include <string.h> 

pthread_t       pid_detached; 
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
 
int             count; 
 
void printids(const char *s) 
{ 
    pid_t       pid; 
    pthread_t   tid; 
 
    pid = getpid(); 
    tid = pthread_self(); 
 
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
        (unsigned int)tid, (unsigned int)tid); 
} 
 
void *cb_detached(void *arg) 
{ 
    printids("New thread detached begin"); 
 
    printids("New thread detached:"); 
 
    int i=0; 
    for ( ; i<10; ++i)     
    { 
 
    	pthread_mutex_lock(&mutex); 
        printf("detached runing %d\n", count++); 
    	pthread_mutex_unlock(&mutex); 
        sleep(1); 
    } 
 
    return ((void*)456); 
} 
 
int main(void) 
{ 
    int err; 
	int i =15;
    count = 0; 
    pthread_mutex_init(&mutex, NULL); 
 
    pthread_attr_t attr;     
    pthread_attr_init(&attr); 
    pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);    
    err = pthread_create(&pid_detached, &attr, cb_detached, NULL); 
    if ( 0 != err ) 
    { 
        printf("can't create detached thread:%s\n", strerror(err)); 
    } 
    pthread_attr_destroy (&attr); 
   
	while(i>0)
	{
		i--;
		sleep(1);
		
	}
    pthread_mutex_destroy(&mutex); 
 
    return 0; 
}

这里的主函数,不能够直接退出了,否则子线程根本无法运行,所以需要添加while循环,保证子线程完成它的功能之后,再退出主进程。
这并不是最好的做法,先记住了。
在这里插入图片描述

线程自主退出

如果是在子线程里面,我们可以直接调用return来退出自己这个线程。
但是前面的例子,我要退出main函数这个主线程,必须等希望运行的子线程结束后再退出。如果我们不知道子线程要运行多久,那岂不是没办法预估时间了。

其实我们只需要结束自己的线程,让子线程继续运行就可以了。这里用到的就是pthread_exit(NULL);

#include <pthread.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 
#include <string.h> 

pthread_t       pid_detached; 
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
 
int             count; 
 
void printids(const char *s) 
{ 
    pid_t       pid; 
    pthread_t   tid; 
 
    pid = getpid(); 
    tid = pthread_self(); 
 
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
        (unsigned int)tid, (unsigned int)tid); 
} 
 
void *cb_detached(void *arg) 
{ 
	int i=0; 
	pthread_mutex_init(&mutex, NULL); 
	
    printids("New thread detached begin"); 
    printids("New thread detached:"); 
 
    for ( ; i<10; ++i)     
    { 
 
    	pthread_mutex_lock(&mutex); 
        printf("detached runing %d\n", count++); 
    	pthread_mutex_unlock(&mutex); 
        sleep(1); 
    } 
    pthread_mutex_destroy(&mutex); 
 
    return ((void*)456); 
} 
 
int main(void) 
{ 
    int err; 
	int i =15;
    count = 0; 
 
    pthread_attr_t attr;     
    pthread_attr_init(&attr); 
    pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);    
    err = pthread_create(&pid_detached, &attr, cb_detached, NULL); 
    if ( 0 != err ) 
    { 
        printf("can't create detached thread:%s\n", strerror(err)); 
    } 
    pthread_attr_destroy (&attr); 
	pthread_exit(NULL);
} 

我们用pthread_exit(NULL);代替return 0,就可以让主线程退出,分离的子线程继续运行。
在这里插入图片描述

线程被动结束

如果想干脆利索的手动结束某个线程,可以使用int pthread_cancel(pthread_t thread);
参考代码如下

#include <pthread.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 
#include <string.h> 

pthread_t       pid_joinable; 
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
 
int             count; 
 
void printids(const char *s) 
{ 
    pid_t       pid; 
    pthread_t   tid; 
 
    pid = getpid(); 
    tid = pthread_self(); 
 
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
        (unsigned int)tid, (unsigned int)tid); 
} 
 
 
void *cb_joinable(void *arg) 
{ 
    printids("New thread joinable begin"); 
    printids("New thread joinable:"); 
 
    int i=0; 
    while(1)
    { 
    	pthread_mutex_lock(&mutex); 
        printf("joinable runing %d\n", count++); 
	    pthread_mutex_unlock(&mutex); 
        sleep(1);
    }
 
    return ((void*)123); 
} 

 
int main(void) 
{ 
    int err; 
 
    count = 0; 
    pthread_mutex_init(&mutex, NULL); 
 
    err = pthread_create(&pid_joinable, NULL, cb_joinable, NULL); 
    if ( 0 != err ) 
    { 
        printf("can't create joinable thread: %s\n", strerror(err)); 
    } 
	sleep(5);
	if(pthread_cancel(pid_joinable)==0)
	{
		printf("pthread_cancel success\n"); 
	}
	else
	{
		printf("pthread_cancel fail[%s]\n", strerror(err)); 
	}
  
    pthread_mutex_destroy(&mutex); 
 
    return 0; 
} 

运行结果

[root@localhost test]# ./pthreadc 
New thread joinable begin pid 1622333 tid 3521554176 (0xd1e6a700)
New thread joinable: pid 1622333 tid 3521554176 (0xd1e6a700)
joinable runing 0
joinable runing 1
joinable runing 2
joinable runing 3
joinable runing 4
pthread_cancel success

在这里插入图片描述

线程阻塞与唤醒

在生产者与消费者的模式下,生产线程与消费者线程通过线程的阻塞与唤醒进行消息的通知,用到了下面几个函数,
PS:这个cond相关的东西,是不是可以翻译成条件变量?
在这里插入图片描述

使用属性__cond_attr初始化条件变量__cond,如果后者为NULL,则使用默认值。

extern int pthread_cond_init (pthread_cond_t *__restrict __cond,const pthread_condattr_t *__restrict __cond_attr);

销毁条件变量__cond

extern int pthread_cond_destroy (pthread_cond_t *__cond);

唤醒一个等待条件变量__cond的线程

extern int pthread_cond_signal (pthread_cond_t *__cond);

唤醒所有等待条件变量__cond的线程

extern int pthread_cond_broadcast (pthread_cond_t *__cond);

等待条件变量__cond被单个唤醒或者全体唤醒,并且假设__mutex在此之前被锁住

extern int pthread_cond_wait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex);

等待条件变量__cond被单个唤醒或者全体唤醒或者已经到达__abstime超时,并且假设__mutex在此之前被锁住。__abstime是一个绝对时间结构体

extern int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex,const struct timespec *__restrict __abstime)                           

struct timespec {
    long    tv_sec;         /* seconds */
    long    tv_nsec;        /* nanoseconds */
};             

一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用便调用pthread_cond_wait,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal发送一个资源可用信号。

但是在wait成功之后,资源就一定可以被使用么,答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait返回后,资源可能已经被使用了,在这种情况下,应该使用:

while(resource == FALSE)
	pthread_cond_wait(&cond, &mutex);

锁的作用

函数传入的参数mutex用于保护条件,因为我们在调用pthread_cond_wait时,如果条件不成立我们就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进程。当pthread_cond_wait返回的时候又自动给mutex加锁。

来一个例子,理解这个用法

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//链表的结点
struct msg
{
    int num; 
    struct msg *next; 
};

struct msg *head = NULL;    //头指针
struct msg *temp = NULL;    //节点指针

//静态方式初始化互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
 
void *producer(void *arg)
{
	int count_p = 0;
	int exit_num=20;
    while (exit_num>0)   //生产20个消息就自动退出
	{
        pthread_mutex_lock(&mutex);         //加锁

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;                        //头插法
        printf("[+]产生消息---%d 总数:%d\n", temp->num,count_p++);

        pthread_mutex_unlock(&mutex);       //解锁
        pthread_cond_signal(&has_producer); //唤醒消费者线程
		
        sleep(rand() % 3);              //为了使该线程放弃cpu,让结果看起来更加明显。
		exit_num--;
    }
 
    return NULL;
}
 
void *consumer(void *arg)
{
	int count_c = 0; 
    while (1)       //线程正常不会解锁,除非收到终止信号
	{
        pthread_mutex_lock(&mutex);     //加锁
        while (head == NULL)            //如果共享区域没有数据,则解锁并等待条件变量
	    {
            pthread_cond_wait(&has_producer, &mutex);   //我们通常在一个循环内使用该函数
        }
        temp = head;
        head = temp->next;
        printf("[-]消费消息---%d 总数:%d\n", temp->num,count_c++);
        free(temp);                     //删除节点,头删法
        temp = NULL;                    //防止野指针
        pthread_mutex_unlock(&mutex);   //解锁

        sleep(rand() % 3);          //为了使该线程放弃cpu,让结果看起来更加明显。
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t ptid, ctid;
    srand(time(NULL));      //根据时间摇一个随机数种子

    //创建生产者和消费者线程
    pthread_create(&ptid, NULL, producer, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);

    //主线程回收两个子线程
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
 
    return 0;
}

生产者生产20个消息,消费者消费到20个消息时候,就一直等待了。
在这里插入图片描述
参考博客《C语言中pthread_cond_wait 详解》
在这里插入图片描述

线程池

学习了前面的条件变量,就可以来个高级用法的,说白了就是先帮你创建好线程,执行的时候就直接帮你直接执行函数体和参数,当感觉线程不太够了,就帮你创建一些,当线程太多了,再帮你取消一些。

线程池是一种多线程处理形式,大多用于高并发服务器上,它能合理有效的利用高并发服务器上的线程资源;
在Unix网络编程中,线程与进程用于处理各项分支子功能,我们通常的操作是:接收消息 ==> 消息分类 ==> 线程创建 ==> 传递消息到子线程 ==> 线程分离 ==> 在子线程中执行任务 ==> 任务结束退出;
对大多数小型局域网的通信来说,上述方法足够满足需求;
但当我们的通信范围扩大到广域网或大型局域网通信中时,我们将面临大量消息频繁请求服务器;在这种情况下,创建与销毁线程都已经成为一种奢侈的开销,特别对于嵌入式服务器来说更应保证内存资源的合理利用;
因此,线程池技术应运而生;线程池允许一个线程可以多次复用,且每次复用的线程内部的消息处理可以不相同,将创建与销毁的开销省去而不必来一个请求开一个线程;

代码可以参考下面两个博客,
《线程池原理及C语言实现线程池》
《【C语言】实现线程池》

结束语

学习是每个未成年人和成年人的重要责任,那么今天来介绍一个小知识:
就是这句话,独乐乐不如众乐乐。
其实它的发音应该是dú yuè lè bù rú zhòng yuè lè
意思是:一个人欣赏音乐快乐不如和众人一起欣赏音乐快乐
在这里插入图片描述

以前的同事去了天津上学,结果都变成了居家上网课,时不时的管控起来,以为的求学,变成了留学
在这里插入图片描述
希望来年能好一些啊

在这里插入图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

胖哥王老师

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

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

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

打赏作者

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

抵扣说明:

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

余额充值