Linux 多线程原理深剖_线程的布局,2024年最新白嫖党最爱

if (id == 0){
	//child
	g_val = 200;
	printf("child:PID:%d, PPID:%d, g\_val:%d\n", getpid(), getppid(), g_val);
	exit(0);
}
//father
sleep(3);
printf("father:PID:%d, PPID:%d, g\_val:%d\n", getpid(), getppid(), g_val);
return 0;

}


![在这里插入图片描述](https://img-blog.csdnimg.cn/dc21e54898774216b49d2e933afcc4af.png#pic_center)


#### phread🤔


phread 原生线程库,虽然在内核角度没有对应的接口可以调用,但是在用户角度,我们在实际操作是会更希望使用 thread\_create 这样的接口而不是 vfork 函数,因此系统提供了原生线程库这个原生线程库其实就是对轻量级线程进行了封装然后再用户层模拟实现了一套相关的接口。


所以现在我们的根本目标并非是学习操作系统的接口,而是学习这套用户层模拟实现的接口


### 二级页表🤔


众所周知 32 位平台下有 2^32 个地址,这就代表着有 2^32 个地址需要映射。我们说过页表就是一个简单的表结构,那么一张表就要有 2^32 个映射关系:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/04cef39c5a254bf284520e88172b2635.png#pic_center)  
 每一张表的内容除了映射关系之外,还包括一些权限信息,比如页表分为了内核级页表和用户级页表,这就是通过权限信息来进行区分的:


![在这里插入图片描述](https://img-blog.csdnimg.cn/33b9affc194c4287b9813b02208511f5.png#pic_center)  
 每个表项中存储了一个物理地址和一个虚拟地址,这里需要消耗 8 字节,,考虑到权限相关信息,这里粗略按照 **10 字节**计算。2^32 个字节,我们就需要 2^32 \* 10 个字节,也就是 **40 GB**,但是在 32 为平台下只有最多 4 GB空间, 
 
 
 
 
 
 也就是要存储这样一张表是不可能的 
 
 
 
 
 \color{red} {也就是要存储这样一张表是不可能的} 
 
 
 也就是要存储这样一张表是不可能的。


所以不能直接将页表就看成一个单纯的表,在 **32** 位平台下,页表映射过程是这样的:


1. 虚拟地址前 10 个比特位在页目录中进行查找,找到对应的页表
2. 再选择 10 个比特位在对应页表中进行查询,找到物理内存中对应页框的其实地址
3. 将最后剩余 12 个比特位作为偏移量从页框对应地址处向后偏移,找到物理内存中的某一个对应的字节数据


物理地址也绝对不是饼粘一团的,它被划分为了一个个 4 kb大小的页框的,磁盘上的程序又会被分成一个个 4 kb 大小的页帧,当内存和磁盘进行数据交换时也是以 4 kb为单位的。不难发现,其实 4 kb就是 2^12 个字节,也就是说一个页框中会有 2^12 个字节,访问内存的基础大小是 1 字节,所以最多可以有 2^12 个地址,最后 12 为作为偏移量查找即可,从而找到物理内存中某一个对应字节数据


![在这里插入图片描述](https://img-blog.csdnimg.cn/8cd892d90e804fc7b86062d1165bbf3d.png#pic_center)


这里就是一个**二级页表结构**,页目录就是一个一级页表,而表项是二级页表,每一个表大小是 10 字节,页目录和页表的表项都是 210 个,因此一个表的大小就是 210 \* 10 个字节,也就是10 KB。而页目录有 210 个表项也就意味着页表有 210 个,也就是说一级页表有1张,二级页表有210张,总共算下来大概就是 10 MB,内存消耗不会太高,因此 Linux 中实际就是这样映射的


所有的映射过程都是由  
 
 
 
 
 
 
 M 
 
 
 M 
 
 
 U 
 
 
 
 
 
 \color{red} {MMU} 
 
 
 MMU 这个硬件完成的,该硬件集成在CPU内。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式


在 Linux 中,32位平台下用的是**二级页表**,而64位平台下用的是**多级页表**。这就可以解释一下为什么修改字符串常量会出发段错误:我们修改一个常量虚拟必须找到对应的物理地址,但是查表时发现它是只读的,此时就会在 MMU 触发硬件错误,操作系统识别到报错后,就会发送信号对齐进行终止。


### 线程优点🤔


1. 创建一个线程的代价远比一个进程小的多
2. 相比进程之间的切换,线程之间的切换所需要的系统操作会少很多
3. 线程占的资源比进程少得多
4. 充分利用处理器的可并行数量
5. 在等待慢速IO结束的同时,程序可执行其他的计算任务
6. 计算密集型任务,在多处理器上运行,会分成多个线程运行
7. IO密集行任务,将IO操作重修,可提高效率,线程可以同时等待不同的IO操作


### 线程缺点🤔


1. 性能损失,如果计算密集型线程的任务比可用的处理器多,会产生较大的性能损失,性能包括额外的同步和调度开销,但是可用资源是不变的
2. 健壮性降低,多线程需要全面的思考,因时间分配上的细微偏差或者因共享了不该共享的变量会造成不良影响,也就是说**线程之间是缺乏保护的**
3. 缺乏访问控制,进程是访问控制的基本粒度,线程中调用 os 函数会对整个进程产生影响
4. 难度提高,调试和编写都涉及更复杂


### 线程异常🤔


比如一个线程出现除0,野指针问题,会直接全部垮掉;且线程是进程的执行分支名,线程出现异常,就像进程出现异常一样,会触发信号机制,此时所有的线程会全部退出。因此合理使用多线程,能提高 CPU 密集型程序的执行效率与用户体验


### 进程与线程🤔


进程是分配系统资源的基本单位,线程是系统调度的基本单位  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/31ae97e56e084d498cf3cbabb77cd804.png#pic_center)


线程共享进程数据,但也拥有自己的一部分数据,比如线程ID,线程栈(每个线程都有临时的数据,需要压栈出栈),全局变量 errno,信号屏蔽字与调度优先级


### 多线程共享🤔


同一个地址空间里面,代码段和数据段和数据段都是共享的,如果定义一个函数在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问到


各线程还共享一些进程资源和环境,文件描述符表(进程打开一个文件后,其他线程也能够看到)每种信号的处理方式,用户ID和组ID


### Linux线程控制🤔


#### POSIX线程库😋


pthread线程库是应用层的原生线程库:


**应用层**指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。原生指的是大部分Linux系统都会默认带上该线程库。  
 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread\_”打头的,要使用这些函数库,就要引入头文件<pthreaad.h>,链接这些线程函数库时,要使用编译器命令的“-lpthread”选项


传统的一些函数是,成功返回0,失败返回-1,并且对全局变量 errno 赋值以指示错误。pthreads 出错时不会设置全局变量 errno,因为大部分POSIX函数会这样做,而是将错误代码通过返回值返回。  
 pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于 pthreads 的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的 errno 变量的开销更小


### 线程的创建😋


创建线程用到 pthread\_create 函数:



int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);



> 
> **thread**:获取创建成功的线程ID,该参数是一个输出型参数  
>  **attr**:用于设置创建线程的属性,传入NULL表示使用默认属性  
>  **start\_routine**:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。  
>  **arg**:传给线程例程的参数
> 
> 
> 


当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程,主线程产生其他子线程,主线程一般最后要完成某些操作,比如各种关闭动作


主线程调用 pthread\_create 创建一个新线程,此后新线程会跑去执行自己的代码,而主线程则继续执行后续代码:



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

void* Routine(void* arg)
{
char* msg = (char*)arg;
while (1){
printf(“I am %s\n”, msg);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, (void*)“thread 1”);
while (1){
printf(“I am main thread!\n”);
sleep(2);
}
return 0;
}


运行代码后,可以看到新线程每隔一秒就会执行一次打印操作,但是主线程每隔两秒执行一次打印操作:


![在这里插入图片描述](https://img-blog.csdnimg.cn/ec36ce412cac4341b9f13d627cfaaf96.png#pic_center)  
 使用`ps -aL`命令可查看当前的轻量级进程(不带 -L 是一个个的进程;带 -L 是每个进程内的多个轻量级进程)


![在这里插入图片描述](https://img-blog.csdnimg.cn/79630f75d4a146568ea438a0bc1644ac.png#pic_center)  
 这里的 `LWP` 就是轻量级进程 ID,可以看到显示的两个轻量级进程的 PID 相同,因为它们属于同一个进程。


 
 
 
 
 
 
 注意 
 
 
 
 
 \color{red} {注意} 
 
 
 注意在应用层现场其实就是 LWP,操作系统实际调度的是 LWP 而并非 PID,之前因为我一直是作为单线程进程在写,所以 PID 和 LWP 是相等的,所以单线程下调度谁都可以


上面是主线程创建一个新线程,我们让主线程一次创建多个新线程,并让每一个新线程都去执行同一个函数,也就是说该函数是会被重入的:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
while (1){
printf(“I am %s…pid: %d, ppid: %d\n”, msg, getpid(), getppid());
sleep(1);
}
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
}
while (1){
printf(“I am main thread…pid: %d, ppid: %d\n”, getpid(), getppid());
sleep(2);
}
return 0;
}


这里就可以看到 5 个创建成功的线程:


![在这里插入图片描述](https://img-blog.csdnimg.cn/425c1f849f004a9ba3d4157ebfb9a30e.png#pic_center)  
 因为都属于同一个进程,所以他们的 PID 和 PPID 也都是一样的,此时使用 `ps -aL` 就能看到 6 个轻量级进程:


![在这里插入图片描述](https://img-blog.csdnimg.cn/c274d410b28e4f61900113432a16ab47.png#pic_center)


#### 获取线程id😎


我们可以通过 **pthread\_self** 函数获取线程 id,类似于 getpid 获取当前进程ID:



pthread_t pthread_self(void);


下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用 pthread\_self 函数获取线程ID进行打印:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
while (1){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
printf(“%s tid is %lu\n”, buffer, tid[i]);
}
while (1){
printf(“I am main thread…pid: %d, ppid: %d, tid: %lu\n”, getpid(), getppid(), pthread_self());
sleep(2);
}
return 0;
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/171f544985b047578d209ea9b2797b21.png#pic_center)  
 用 `pthread_self` 获得的线程ID与内核的 `LWP` 的值是不相等的,pthread\_self 获得的是用户级原生线程库的线程ID,而 LWP 是内核的轻量级进程ID


### 线程等待😋


需要明确的是,一个线程被创建出来就如同进程一样,也是需要被等待的。如果主线程不对新线程进行等待,资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的内存泄漏问题。


等待线程函数 pthread\_join:



int pthread_join(pthread_t thread, void **retval);


thread:被等待线程的ID,retval:线程退出时的退出码信息,线程等待成功返回0,失败返回错误码


`retval`:


1. 如果thread线程通过return返回,**retval** 所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用 pthread\_cancel 异常终止掉,retval 所指向的单元里存放的是常数`PTHREAD_CANCELED`
3. 如果thread线程是自己调用 pthread\_exit 终止的,retval 所指向的单元存放的是传给 pthread\_exit 的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传 NULL 给 retval 参数


用`grep`命令进行查找,可以发现**PTHREAD\_CANCELED**实际上就是头文件<pthread.h>里面的一个宏定义,它本质就是 -1:



grep -ER “PTHREAD_CANCELED” /usr/include/


![在这里插入图片描述](https://img-blog.csdnimg.cn/444f9711a4234d03bed56b78a93a54b7.png#pic_center)  
 下面代码中我们先不关心线程退出信息,直接将 pthread\_join 的第二次参数设置为NULL,等待线程后打印该线程的编号以及线程ID:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 0;
while (count < 5){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());
sleep(1);
count++;
}
return NULL;
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
printf(“%s tid is %lu\n”, buffer, tid[i]);
}
printf(“I am main thread…pid: %d, ppid: %d, tid: %lu\n”, getpid(), getppid(), pthread_self());
for (int i = 0; i < 5; i++){
pthread_join(tid[i], NULL);
printf(“thread %d[%lu]…quit\n”, i, tid[i]);
}
return 0;
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/457ba0fa97564f73b90fc2b3ea749607.png#pic_center)  
 线程退出时如何获取退出码呢?我们将线程退出时的退出码设置为某个特殊的值,并在成功等待线程后将该线程的退出码进行输出



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 0;
while (count < 5){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());
sleep(1);
count++;
}
return (void*)2022;
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
printf(“%s tid is %lu\n”, buffer, tid[i]);
}
printf(“I am main thread…pid: %d, ppid: %d, tid: %lu\n”, getpid(), getppid(), pthread_self());
for (int i = 0; i < 5; i++){
void* ret = NULL;
pthread_join(tid[i], &ret);
printf(“thread %d[%lu]…quit, exitcode: %d\n”, i, tid[i], (int)ret);
}
return 0;
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/a30f2ad76c5f45e0bd89a58a9fdbe8cd.png#pic_center)  
 pthread\_join 默认是以阻塞的方式进行线程等待的,那么问题来了:为什么线程退出时只能拿到线程的退出码?这是什么问题呢?比如我们可以通过 wait 或是waitpid 的输出型参数 status,获取到退出进程的退出码、退出信号以及core dump标志。就是说只能拿到退出码难道线程就不会出现异常吗?


线程当然也会出现异常,线程退出的情况有三种:



> 
> 1. 代码运行完毕,结果正确
> 2. 代码运行完毕,结果不正确
> 3. 代码异常终止
> 
> 
> 


因此我们也需要考虑线程异常终止的情况,但是 pthread\_join 无法获取线程异常退出的信息。因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行 pthread\_join,因为整个进程已经退出了


我们在线程的执行例程当中制造一个除零错误,当某一个线程执行到此处时就会崩溃,进而导致整个进程崩溃:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 0;
while (count < 5){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());
sleep(1);
count++;
int a = 1 / 0; //error
}
return (void*)2022;
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
printf(“%s tid is %lu\n”, buffer, tid[i]);
}
printf(“I am main thread…pid: %d, ppid: %d, tid: %lu\n”, getpid(), getppid(), pthread_self());
for (int i = 0; i < 5; i++){
void* ret = NULL;
pthread_join(tid[i], &ret);
printf(“thread %d[%lu]…quit, exitcode: %d\n”, i, tid[i], (int)ret);
}
return 0;
}


结果如下,程序直接暴毙了:


![在这里插入图片描述](https://img-blog.csdnimg.cn/59742508098c4fa2915418ece5d83415.png#pic_center)  
 所以 pthread\_join 只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。


### 线程终止😋


只终止某个线程而不是终止整个进程,可以有三种方法:



> 
> 1. 线程函数处进行return。
> 2. 线程可以自己调用pthread\_exit函数终止自己。
> 3. 一个线程可以调用pthread\_cancel函数终止同一进程中的另一个线程。
> 
> 
> 


 
 
 
 
 
 
 
 线程函数处进行 
 
 
 r 
 
 
 e 
 
 
 t 
 
 
 u 
 
 
 r 
 
 
 n 
 
 
 
 
 
 \color{red} {线程函数处进行return} 
 
 
 线程函数处进行return


注意在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
while (1){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());
sleep(1);
}
return (void*)0;
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++){
char* buffer = (char*)malloc(64);
sprintf(buffer, “thread %d”, i);
pthread_create(&tid[i], NULL, Routine, buffer);
printf(“%s tid is %lu\n”, buffer, tid[i]);
}
printf(“I am main thread…pid: %d, ppid: %d, tid: %lu\n”, getpid(), getppid(), pthread_self());

return 0;

}


运行代码,并不能看到新线程执行的打印操作,因为主线程的退出导致整个进程退出了


![在这里插入图片描述](https://img-blog.csdnimg.cn/bb27374ff37c4a70b44e054e24827769.png#pic_center)


 
 
 
 
 
 
 
 p 
 
 
 t 
 
 
 h 
 
 
 r 
 
 
 e 
 
 
 a 
 
 
 
 d 
 
 
 e 
 
 
 
 x 
 
 
 i 
 
 
 t 
 
 
 函数 
 
 
 
 
 
 \color{red} {pthread\_exit函数} 
 
 
 pthreade​xit函数



void pthread_exit(void *retval);


retval:线程退出时的退出码信息,该函数无返回值,跟进程一样结束时无法返回自身。


pthread\_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了


这里我们使用 pthread\_exit 终止线程,并将退出码设置为6666:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 0;
while (count < 5){
printf(“I am %s…pid: %d, ppid: %d, tid: %lu\n”, msg, getpid(), getppid(), pthread_self());

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
img

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

713016594828)]
[外链图片转存中…(img-Uo5GCTYn-1713016594828)]
[外链图片转存中…(img-j9A0sJAd-1713016594828)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
[外链图片转存中…(img-EBDULybn-1713016594829)]

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

[外链图片转存中…(img-cLhUO3yw-1713016594829)]

给大家整理的电子书资料:

[外链图片转存中…(img-3nlDHhGP-1713016594829)]

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-DUg02GSq-1713016594829)]

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值