多线程与多进程

本文详细讲解了Linux线程的创建、终止、连接分离、线程属性设置、互斥量与条件变量的使用,以及守护进程的创建。涉及关键概念如pthread_create、pthread_join、线程分离状态和互斥量保护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.查看源代码

线程

1. 线程的创建与终止

  • pthread_create (thread, attr, start_routine, arg)

pthread_create 创建一个新的线程,并让它可执行。

下面是关于参数的说明:
thread    指向线程标识符指针。
attr    一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
start_routine    线程运行函数起始地址,一旦线程被创建就会执行。
arg    运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。
创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。

  • pthread_exit (status)

pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。

如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。

 2. 线程的连接与分离

  • pthread_join (threadid, status)

  • pthread_detach (threadid)

pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连。pthread_join() 函数来等待线程的完成。

3.  线程属性

  • 线程属性初始化与销毁

  注意:应先初始化线程属性,再pthread_create创建线程

  初始化线程属性(给结构体分配空间,相当于malloc)

    int pthread_attr_init(pthread_attr_t *attr); 成功:0;失败:错误号

  销毁线程属性所占用的资源(释放结构体资源,相当于free)

    int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失败:错误号

  • 线程属性之分离属性

  线程的分离状态决定一个线程以什么样的方式来终止自己。

  非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。

  只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

  分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,

  马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

  线程分离状态的函数:

  设置线程属性,分离or非分离

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

  获取程属性,分离or非分离

          int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

       参数 :attr:已初始化的线程属性

      detachstate: PTHREAD_CREATE_DETACHED(分离线程)

              PTHREAD _CREATE_JOINABLE(非分离线程)

 4. 使用互斥量保护多线程同时输出

互斥量,又称为互斥锁,是一种用来保护临界区的特殊变量,它可以处于锁定状态,也可以处于解锁状态:

(1)如果互斥锁是锁定的,就是一个特定的线程持有这个互斥锁

(2)如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态

每个互斥锁内部都有一个线程等待队列,用来保存等待该互斥锁的线程。当互斥锁处于解锁状态时,一个线程试图获取这个互斥锁时,这个线程就可以得到这个互斥锁而不会阻塞;当互斥锁处于锁定状态时,一个线程试图获取这个互斥锁时,这个线程将素色在互斥锁的等待线程队列内

  • 创建互斥量

(1)静态初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
(2)动态初始化

int pthread_mutext_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数mutex是一个指向要初始化的互斥量的指针;参数attr传递NULL来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量

函数成功返回0,否则返回一个非0的错误码,

  • 销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数mutex指向要销毁的互斥量的指针。

  • 加锁

Pthreads中有两个试图锁定互斥量的函数,pthread_mutex_lock()和pthread_mutex_trylock(),pthread_mutex_lock()函数会一直阻塞到互斥量可用为止,而pthread_mutex_trylock()会尝试加锁,通常立即返回,函数原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数mutex是需要加锁的互斥量。函数成功返回0,否则返回一个非0的错误码,其中另一个线程已持有锁的情况下,调用pthread_mutex_trylock()函数的额错误码是EBUSY

  • 解锁

pthread_mutex_unlock()函数是用来释放指定的互斥量。函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数是需要解锁的互斥量。成功返回0;否则返回一个非0的错误码

 5. 死锁

死锁是指两个或两个以上的执行序在执行过程中,因争夺资源而造成的一种互相等待的现象

当多个线程需要相同的一些锁, 但是按照不同的顺序加锁, 死锁就很容易发生, 如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生

 6. 条件变量使用

  • 初始化

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condatrr_t *restrict attr);
参数:
cond:要初始化的条件变量;
attr:NULL。

  • 销毁

int pthread_cond_destroy(pthread_cond_t *cond);

  • 等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待;
mutex:互斥量

  • 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);广播唤醒所有等待条件的休眠线程。
int pthread_cond_signal(pthread_cond_t *cond);按顺序唤醒一个休眠的线程。

进程

1. 通过env获取环境变量(l);通过environ获取环境变量(2)

/* 通过env参数获取环境变量 */
#include <stdio.h>
int main(int argc, char * argv[], char *env[])
{ 	int i = 0;
	while (env[i])
	puts(env[i++]);
	return 0;
}
/* 通过environ获取环境变量 */
#include <stdio.h>
extern char ** environ;
int main(int argc, char * argv[])
{ 	int i = 0;
	while (environ[i]) puts(environ[i++]);
	return 0;
}

 

2. 进程创建程序

/* fork创建进程 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
pid_t pid;
pid = fork(); 			/* 创建子进程 */
if (pid == 0) {			/* 子进程返回0 */
	printf("Here is child, my pid = %d, parent's pid = %d\n", getpid(), getppid());		 /*  打印父子进程 PID */
	exit(0);
} else if(pid > 0) { /* 父进程返回子进程PID */
	printf("Here is parent, my pid = %d, child's pid = %d\n", getpid(), pid);
} else {			/* 创建出错 */
	perror("fork error\n");
}
return 0;
}

 

3. 子进程加载新程序

/* 子程序执行程序-命名为sample3 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

extern char * *environ; /* 全局环境变量*/

int main(int argc, char *argv[]) {
	int i;
	printf("argc=%d\n",argc); /* 打印参数个数 */
	printf("args:");

	for(i=0;i<argc;i++)
		printf("%s",argv[i]);/* 打印参数表 */
	printf("\n");
	i = 0;
	while (environ[i])
		puts(environ[i++]); /* 打印环境变量表 */
	printf("\n");

	return 0;
}

 

/* 子进程加载新程序 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
char *env_init[] = {"USER=ujn", "HOME=/home/ujn/", NULL}; /* 为子进程定义环境变量 */
int main(int argc, char *argv[]) {
	pid_t pid;
	if ((pid = fork()) < 0) { /* 创建进程失败判断 */
		perror("fork error");
	} else if (pid == 0) { /* fork 对子进程返回 0 */
		execle("/home/ujn/sec9/sample4", "sample4", "hello", "world", (char *) 0,env_init);/*子进程装载新程序*/
		perror("execle error"); /* execle 失败时才执行 */
		exit(-1);
	} else {
		exit(0); /* 父进程退出 */
	}
	return -1;
}

4. 使用daemon创建守护进程

/* 用daemon创建守护进程  */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
int main(void) {
	int fd;
	time_t curtime;
	if (daemon(0, 0) == -1) {
		perror("daemon error");
		exit(-1);
	}
	fd = open("/tmp/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
	if (fd < 0) {
		perror("open error");
		exit(-1);
	}
	while (1) {
		curtime = time(0);
		char *timestr = asctime(localtime(&curtime));
		write(fd, timestr, strlen(timestr));
		sleep(60);
	}
	close(fd);
	return 0;
}

5. 信号函数sigaction的使用

/* sigaction函数的用法  */
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

void ouch(int sig) { /* 信号处理函数 */
	printf("\nOuch! - I got signal %d\n", sig);
}

int main(int argc, char *argv[]) {

	struct sigaction act;
	act.sa_handler = ouch; /* 设置信号处理函数 */
	sigemptyset(&act.sa_mask); /* 清空屏蔽信号集 */
	act.sa_flags = SA_RESETHAND; /* 设置信号处理之后恢复默认的处理方式 */
	sigaction(SIGINT, &act, NULL); /* 设置 SIGINT 信号的处理方法 */

	while (1) { /* 进入循环等待信号发生 */
		printf("sleeping\n");
		sleep(1);
	}
	return 0;
}

  

 二 .编译并运行

1. 线程的创建与终止

/*  线程的创建与终止 :  */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5

void *thread_function(void *index) { /* 线程函数 */
	long tid;
	tid = (long) index;
	printf("Hello World! This is thread #%ld!\n", tid); /* 打印线程对应的参数 */
	pthread_exit(NULL);    /* 退出线程 */
}

int main(int argc, char *argv[]) {
	pthread_t tid_array[NUM_THREADS];
	int returned_code_err;
	long index;
	for (index = 0; index < NUM_THREADS; index++) { /* 循环创建 5 个线程 */
		printf("In main: creating thread %ld.\n", index);
		returned_code_err = pthread_create(
			&tid_array[index], 
			NULL, 
			thread_function, 
			(void *) index
		); /* 创建线程 */
		if (returned_code_err) {
			printf("ERR: return code from pthread_create() is not 0, but %d\n", returned_code_err);
			exit(-1);
		}
	}
	printf("Main exits.\n");
	pthread_exit(NULL); /* 主线程退出 */
	return 0;
}

 用数组加循环创建了5个线程,线程创建时会调用线程函数,所以会先打印出循环的printf再打印线程函数的

 2. 线程的连接与分离

/* pthread_join 函数示例  
gcc -pthread ... -lm
Note: math lib need to be specifically linked; and link libraries after object files. 
*/
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define NUM_THREADS 4

void *thread_func(void *index) { /* 线程函数 */
	int i;
	long tid;
	double result=0.0;
	tid = (long)index;
	printf("Thread %ld starting...\n",tid);

	for (i=0; i<1000000; i++) {
		result = result + sin(i) * tan(i); /* 进行数学运算 */
	}
	printf("Thread %ld done. Result = %e\n",tid, result);
	pthread_exit((void*) index); /* 带计算结果退出 */
}


int main (int argc, char *argv[]) {
	pthread_t tid_array[NUM_THREADS];
	int err;
	long index;
	void *status;

	for(index=0; index<NUM_THREADS; index++) {
		printf("Main: creating tid_array %ld\n", index);
		err = pthread_create(
			&tid_array[index], 
			NULL, 
			thread_func, 
			(void *)index
		); /* 创建线程 */
		if (err) {
			printf("ERROR; return code from pthread_create() is %d\n", err);
			exit(-1);
		}
	}

	for(index=0; index<NUM_THREADS; index++) {
		err = pthread_join(
			tid_array[index], 
			&status
		); /*等待线程终止,并获取返回值*/
		if (err) {
			printf("ERROR; return code from pthread_join() is %d\n", err);
			exit(-1);
		}
		printf("Main: completed join with tid_array %ld having a status of %ld\n",index,(long)status);
	}

	printf("Main: program completed. Exiting.\n");
	pthread_exit(NULL);
}

 创建了4个线程,并再线程函数中进行数学运算并打印结果,调用 pthread_join() 函数来实现获取线程执行结束时返回的数据并释放它的内存空间

3.  线程属性

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

#define handle_error_en(en, msg) do {errno = en; perror(msg); exit(EXIT_FAILURE);}while (0)
#define handle_error(msg) do {perror(msg); exit(EXIT_FAILURE);}while (0)

struct thread_info {
	pthread_t thread_id;
	int thread_num;
	char *argv_string;
};

static void *thread_func(void *arg) { /* 线程运行函数 */
	struct thread_info *thread_info_struct_array = arg;
	char *uargv, *p;
	printf("Thread %d: top of stack near %p; argv_string=%s\n", /* 通过 p 的地址来计算栈的起始地址*/
	thread_info_struct_array->thread_num, &p, thread_info_struct_array->argv_string);

	uargv = strdup(thread_info_struct_array->argv_string);
	if (uargv == NULL)
		handle_error("strdup");
	for (p = uargv; *p != '\0'; p++)
		*p = toupper(*p); /* 小写字符转换大写字符 */

	return uargv; /* 将转换结果返回 */
}

int main(int argc, char *argv[]) {
	int s, index, opt, num_threads;
	struct thread_info *thread_info_struct_array;
	pthread_attr_t attr_struct;
	int stack_size;
	void *res;
	stack_size = -1;

	/* 处理参数 -s 所指定的栈大小 */
	while ((opt = getopt(argc, argv, "s:")) != -1) { 
		switch (opt) {
		case 's':
			stack_size = strtoul(optarg, NULL, 0); // string to unsigned long
			break;
		default:
			fprintf(stderr, "Usage: %s [-s stack-size] arg...\n", argv[0]);
			exit(EXIT_FAILURE);
		}
	}

	num_threads = argc - optind;
	
	/* 初始化属性对象 */
	s = pthread_attr_init(&attr_struct);
	
	if (s != 0)
		handle_error_en(s, "pthread_attr_init");

	if (stack_size > 0) {
		/* 设置属性对象的栈大小 为 用户命令行参数 -s 指定值 */
		s = pthread_attr_setstacksize(
			&attr_struct, 
			stack_size
		);
		if (s != 0)
			handle_error_en(s, "pthread_attr_setstacksize");
	}
	thread_info_struct_array = calloc(num_threads, sizeof(struct thread_info));

	if (thread_info_struct_array == NULL)
		handle_error("calloc");

	for (index = 0; index < num_threads; index++) {
		thread_info_struct_array[index].thread_num = index + 1;
		thread_info_struct_array[index].argv_string = argv[optind + index];
		s = pthread_create(
			&thread_info_struct_array[index].thread_id, 
			&attr_struct, /* 根据属性创建线程 */
			&thread_func, 
			&thread_info_struct_array[index]
		);
		if (s != 0)
			handle_error_en(s, "pthread_create");
	}

	/* 销毁属性对象 */
	s = pthread_attr_destroy(&attr_struct);
	
	if (s != 0)
		handle_error_en(s, "pthread_attr_destroy");

	for (index = 0; index < num_threads; index++) {
		s = pthread_join(thread_info_struct_array[index].thread_id, &res); /* 等待线程终止,并获取返回值 */
		if (s != 0)
			handle_error_en(s, "pthread_join");

		printf("Joined with thread %d; returned value was %s\n",
				thread_info_struct_array[index].thread_num, (char *) res);
		free(res);
	}

	free(thread_info_struct_array);
	exit(EXIT_SUCCESS);
}

 首先根据参数输入处理参数 -s 所指定的栈大小, 然后初始化属性对象,设置属性对象的栈大小,根据属性创建线程,线程运行函数中打印线程id,栈的起始地址和栈大小,最后销毁属性对象,等待线程终止,并获取返回值

 4. 使用互斥量保护多线程同时输出

/* 使用互斥量保护多线程同时输出 */
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[2];
pthread_mutex_t lock;

void* thread_func(void *arg)
{
	int id = (long)arg;
	int i = 0;

	pthread_mutex_lock(&lock); 			/* 使用互斥量保护临界区 ================== begin ================== */
	printf("Job %d started\n", id);
	for (i = 0; i < 5; i++)
	{
		printf("Job %d printing\n", id);
		usleep(10);
	}
	printf("Job %d finished\n", id);
	pthread_mutex_unlock(&lock);		/* 使用互斥量保护临界区 ================== end ================== */
	return NULL;
}

int main(void)
{
	long i = 0;
	int err;

	if (pthread_mutex_init(&lock, NULL) != 0) { 					/* (动态) 初始化  互斥量, 因为定义时候没有初始化 */
		printf("\n Mutex init failed\n");
		return 1;
	}

	while(i < 2) {
		err = pthread_create(&(tid[i]), NULL, &thread_func, (void*)i);
		if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
		i++;
	}

	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	pthread_mutex_destroy(&lock);									/* 用完销毁 */

	return 0;
}

 

 首先定义了两个线程和一个互斥量,(动态) 初始化互斥量,然后创建了两线程1和2,先调用线程函数的线程获取了互斥量,互斥量加锁,后者线程处于阻塞,当前线程完成即循环结束,互斥量解锁,后线程才能获取

5.死锁

/*  死锁产生的范例  */
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
 
pthread_t tid[2];
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化互斥量 */
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
 
void * t1(void *arg) {
	pthread_mutex_lock(&mutexA); /* 线程 1 获取 mutexA */
	printf("t1 get mutexA\n");
	usleep(1000);
	pthread_mutex_lock(&mutexB); /* 线程 1 获取 mutexB */
	printf("t1 get mutexB\n");
	pthread_mutex_unlock(&mutexB); /* 线程 1 释放 mutexB */
	printf("t1 release mutexB\n");
	pthread_mutex_unlock(&mutexA); /* 线程 1 释放 mutexA */
	printf("t1 release mutexA\n");
	return NULL;
}
 
void * t2(void *arg) {
	pthread_mutex_lock(&mutexB);
	printf("t2 get mutexB\n");
	usleep(1000);
	pthread_mutex_lock(&mutexA);
	printf("t2 get mutexA\n");
	pthread_mutex_unlock(&mutexA);
	printf("t2 release mutexA\n");
	pthread_mutex_unlock(&mutexB);
	printf("t2 release mutexB\n");
	return NULL;
}
 
int main(void) {
	int err;
	err = pthread_create(&(tid[0]), NULL, &t1, NULL ); /* 创建线程 1 */
	
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	err = pthread_create(&(tid[1]), NULL, &t2, NULL); /* 创建线程 2 */
	
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	return 0;
}

 静态初始化了两个互斥量A,B,创建了线程1,2,线程1获取了mutexA,线程2获取了mutexB,

mutexA,B锁定,线程1想但无法获取mutexA,而线程2无法获取mutexB,从而发生了死锁。

6. 条件变量的使用

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

pthread_t tid_array[3];
int sum = 0;
pthread_mutex_t sum_lock = PTHREAD_MUTEX_INITIALIZER;   /* 互斥量 (静态初始化)*/
pthread_cond_t condition_sum_ready = PTHREAD_COND_INITIALIZER; 						/* 条件量 (静态初始化) */

void * worker_thread_func(void *arg) {
	int i;
	long id = (long) arg;
	for (i = 0; i < 60; i++) {
		pthread_mutex_lock(&sum_lock);                 /* 使用互斥量保护临界变量 */
		printf("t%ld: read sum value before = %d\n", id + 1, sum);
		sum++;
		printf("t%ld: read sum value after  = %d\n", id + 1, sum);
		pthread_mutex_unlock(&sum_lock);               /* 结束互斥量保护临界变量 */
		if (sum >= 100)
			pthread_cond_signal(&condition_sum_ready); 								 /* 通过条件量 发送条件通知 -> 唤醒等待线程 */
	}
	return NULL;
}

void * waiting_thread_func(void *arg) {
	long id = (long) arg;
	pthread_mutex_lock(&sum_lock);
	while (sum < 100) 																/* 不满足条件将一直等待 */
		pthread_cond_wait(&condition_sum_ready, &sum_lock);                         /* 通过条件量 等待条件通知 -> 唤醒等待线程 */
	sum = 0;
	printf("waiting_thread_func: clear sum value [我是等待线程,已被唤醒。 ]\n");
	printf("t%ld: read sum value = %d\n", id + 1, sum);
	pthread_mutex_unlock(&sum_lock);
	return NULL;
}

int main(void) {
	int err;
	long i;
	for (i = 0; i < 2; i++) {
		err = pthread_create(&(tid_array[i]), NULL, &worker_thread_func, (void *) i);         /* 创建线程 1 线程 2 */
		if (err != 0) {
			printf("Can't create thread :[%s]", strerror(err));
		}
	}
	
	err = pthread_create(&(tid_array[2]), NULL, &waiting_thread_func, (void *) i);            /* 创建线程 3 */
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	for (i = 0; i < 3; i++)
		pthread_join(tid_array[i], NULL);
	
	return 0;
}

 静态初始化了一个互斥量和一个条件量,首先创建了线程1,2会调用worker这个线程函数,抢这个互斥量,进行循环使得sum++,创建的线程3会调用waiting这个线程函数,pthread_cond_wait函数会等待执行,当全局变量sum通过线程1,2会调用worker这个线程函数加到100时通过条件量pthread_cond_signal函数发送条件通知给唤醒等待线程3,线程3才继续执行

三, 运行开发板

~/ubuntu-18.04_imx6ul_qemu_system/gui-qemu-imx6ull-gui.sh

四, 使用开发板

1, LCD屏幕/图像

fb-test; myfb-test /dev/fb0

 

2, 串口EEPROM

cd ~ ; i2cdetect -l && i2cdetect -y o

i2c_usr test /dev/i2c-0 0x50 w1 0xff

i2c_usr_test /dev/i2c-0 0×50 r 0×1 0xff

3,命令控制LED

cd ~/led_driver_qemu/

insmod 100ask_led .ko

./ledtest /dev/100ask_led0 on

./ledtest /dev/100ask_led1 off

4, 按键控制LED

cd ~/ button_driver_qemu/

insmod button_drv .ko

insmod board_100ask_qemu_imx6u

./button_led_test 

参考文章:

(18条消息) Linux多线程——互斥量_挣扎的码农的博客-优快云博客_linux线程互斥量

(18条消息) 线程中的条件变量_编程鸟的博客-优快云博客_线程条件变量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值