IO进线程概念理论

统一声明:
博客转载 声 明 : 本博客部分内容来源于网络、书籍、及各类手册。
        内容宗旨为方便查询、总结备份、开源分享。
        部分转载内容均有注明出处,如有侵权请联系博客告知并删除,谢谢!

百度云盘提取码:统一提取码: ziyu

个人网站入口:http://www.baiziqing.cn/

一、标准IO

1.1、定义:
	以 ANSIC为标准,在系统调用之上封装的接口库,
		为IO操作底层的系统调用提供一个通用接口。
1.2、文件指针:
	FILE指针,每一个被操作的文件都在内存中开辟一个区域,
		用来存储文件的相关信息,该空间的类型为结构体类型,名字为FILE。
		
	流定义:所有的IO操作仅是简单的从程序移进或者移出,这种字节流,被称为流。
			所谓的流也被称为FILE指针
	
1.3、缓存类型:
	1、全缓存:当IO缓存区满了或者满足一定条件,就会刷新缓存区	占4k	4096		
	2、行缓存:只有当遇见'\n'时,才刷新缓存区					占1k	1024
	3、不缓存:标准错误输出,stderr
1.4、刷新缓存:
	int fflush(FILE *strem);
1.5、更改缓存类型
	void setbuf(FILE *strem, char *buf);
		将 stream 流的缓存改成  用户自定义的 buf 缓存
		
1.6、linux中打开一个终端时,系统默认打开三个文件:
							 标识符
	标准输入		stdin		0
	标准输出		stdout 		1
	标准错误输出	stderr      2
	
1.7、文件操作流程:

	1.7.1、打开(创建打开)
		FILE *fopen(const char * path, const char *mode);
		功能:
			打开(创建打开)
		参数:
			path 	--->	待操作的文件名(可包含路径)
			mode 	---> 	文件打开方式(只读r、只写w、追加a、可读写r/w/a+)
		返回值:
			成功: 文件指针
			失败: NULL ,并设置errno
			
	1.7.2、数据读写
		(1)、将数据format 格式输出到 指定的stream流中 
			int fprintf(FILE *stream, const char *format, ....)
			扩展:
				1、将数据按照 format 格式输出到  buf 地址上
				int sprintf(char *buf, const char *format, ....)
				2、将 buf 地址上的数据,按照 format 格式,赋值给后面的变量
				int sscanf(const char *buf, const char *format, ....)
				
		(2)、	一次操作一个字符
			int fgetc(FILE *stream);
			功能:
				从指定的 stream 流中读取一个字符 出来
			参数:
				stream: 要指定的流
			返回值:
				成功: 返回写一个字符。
				失败: EOF
				文件末尾:EOF
				
			int fputc(int c, FILE *stream)
			功能:
				将 c 中的数据 写入指定的 stream 流中
			参数:
				c		--> 	要写入的字符数据
				stream	--> 	指定的流
			返回值:
				成功: 非负值
				失败: EOF
				
		(3)、一次操作一行数据
			char *fgets(char *buf, size_t size, FILE *stream)
			功能:
				从指定 stream 流的一行中,读取前 size 个字节的数据到  buf 地址上。
			参数:
				buf		-->		存储读取出来数据的缓存区首地址
				size 	--> 	要读取一行的字节数
				stream 	-->	 	指定的流
			返回值:
				成功:  返回 buf
				失败:  返回 NULL
				文件末尾: 返回 NULL
				
			int fputs(char *buf, FILE *stream)
			功能:
				将buf 地址上的数据 写入指定的 stream 流中,遇见'\n'结束
			参数:
				buf		-->		写入数据的来源首地址
				stream 	-->	 	指定的流
			返回值:
				成功:  返回 非负值
				失败:  返回 EOF
				
		(4)、一次操作 某种结构的数据
			size_t fread(void *buf, size_t size, int nmemb, FILE *stream)
			功能:
				从指定的 stream 流中 读取 nmemb 个结构的数据到  buf 地址上,每个结构 size  个字节。
				(也就是从 stream 流中 读取 nmemb * size 个字节的 数据到 buf 地址上)
			参数:
				buf			--> 	存放数据的缓存区首地址
				size 		--> 	读取的结构的大小
				nmemb 		--> 	读取的结构的个数
				stream 		--> 	指定的流
			返回值:
				成功: 返回实际读取到的 结构的个数
				失败: EOF
				
			size_t fwrite(void *buf, size_t size, int nmemb, FILE *stream)
			功能:
				将 buf 地址的前 nmemb * size 个字节的 数据,写入到 stream 流中
			参数:
				buf			--> 	存放数据的缓存区首地址
				size 		--> 	读取的结构的大小
				nmemb 		--> 	读取的结构的个数
				stream 		--> 	指定的流
			返回值:
				成功: 返回实际写入的结构的个数
				失败: EOF
	1.7.3、关闭
		int fclose(FILE *stream);
				
备注:cat -> a.txt  //终端可以直接输入数据	

思考练习:
	1、如何计算 缓存区的大小。	https://blog.csdn.net/weixin_43793181/article/details/104300767
	2、如何计算 当前终端中可以打开的文件个数。
	
练习:
1、使用标准IO,实现不同路径的文件的拷贝(fgetc/fgets/fread)。复制

2、题目要求:编程读写一个文件test.txt,每隔1秒向文件中写入一行数据,
	类似这样: 
		1,  2007-7-30 15:16:42  
		2,  2007-7-30 15:16:43
		该程序应该无限循环,直到按Ctrl-C中断程序。
		再次启动程序写文件时可以追加到原文件之后,并且序号能够接续上次的序号,比如: 
		1,  2007-7-30 15:16:42
		2,  2007-7-30 15:16:43
		3,  2007-7-30 15:19:02
		4,  2007-7-30 15:19:03
		5,  2007-7-30 15:19:04
		
	提示:
		1、sleep(1);
		2、要追加写入文件,同时要读取该文件的内容以决定下一个序号是几,
			应该用什么模式打开文件? 
			首先判断一下打开的文件是否为新文件,
			如果是新文件,就从序号1开始写入;
			如果不是新文件,则统计原来有多少行,比如有n行,
			然后从序号n+1开始写入。以后每写一行就把行号加1。 
			获取当前的系统时间需要调用函数time(),
			得到的结果是一个time_t类型,其实就是一个大整数,
			其值表示从UTC时间1970年1月1日00:00:00(称为UNIX的Epoch时间)
			到当前时刻的秒钟数。
			然后调用localtime()将time_t所表示的UTC时间转换为本地时间
			(我们是+8区,比UTC多8个小时)并转成struct tm类型,
			该类型的各数据成员分别表示年月日时分秒,
			请自己写出转换格式的代码,不要使用ctime()或asctime()函数。
			具体用法请查阅man page。time和localtime函数需要头文件time.h。 
			调用sleep(n)可使程序睡眠n秒,该函数需要头文件unistd.h。 

二、文件IO

2.1、不带缓存的系统调用接口函数对文件进行IO操作。

2.2、文件描述符:
	是当前系统最小的、未用的非负整数。
	是内核用来标识操作的文件。
	文件IO所有操作都将围绕 文件描述符进行。	
		
2.3、API接口
	2.3.1、打开(创建打开)
		int open(const char *path, int oflag);
		int open(const char *path, int oflag, mode_t mode);
		功能:
			以oflag方式打开或者创建 path 代表的文件。
		头文件:
			#include <sys/types.h>
			#include <sys/stat.h>
			#include <fcntl.h>
		参数:
			path: 代操作的文件名(可包含路径)
			oflag:文件操作方式:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读写)
								 O_CREAT(文件不存在就创建)
			mode: 文件存根权限,只有 O_CREAT 出现时才使用
		返回值:
			成功:返回文件描述符
			失败:返回 -1,并设置错误信息
			
	2.3.2、读写数据
		int read(int fd, void *buf, size_t n);
		功能:
			从 fd 对应的文件中读取 n 个字节的数据到 buf 地址上。
			size_t <==> ssize_t <==> int 
		头文件:
			#include <unistd.h>
		参数:
			fd:	文件描述符,open函数成功的返回值
			buf:	保存数据的缓冲区首地址,用来存储读取到的数据
			n:		要读取的字节数
		返回值:
			成功:
				> 0:表示读取的实际字节数。
				= 0:表示读取到文件末尾
			失败:
				-1:设置错误信息
				
		int write(int fd, void *buf, size_t n);
		功能:
			将buf 地址上的前 n 个字节数据写入到 fd 对应的文件中
		头文件:
			#include <unistd.h>
		参数:
			fd:	文件描述符,open函数成功的返回值
			buf:	要写入的数据来源地址
			n:		要写入的字节数
		返回值:
			成功:
				> 0:表示写入的实际字节数。
			失败:
				-1:设置错误信息
				
	2.3.3、关闭文件
		int close(int fd);
		
	2.3.4、文件的偏移定位
		off_t lseek(int fd, off_t offset, int whence);
		功能:
			将 fd 对应的文件的指针以 whence为基准点,偏移 offset个字节,返回偏移后文件指针的位置。
		
	2.3.5、获取文件属性:
		int stat(const char *path, struct stat *buf);
		功能:
			获取path对应的文件的属性,到 buf 地址上。
		头文件:
			#include <sys/types.h>
			#include <sys/stat.h>
			#include <unistd.h>
		参数:
			path:	待操作的文件名(可包含路径)
			buf: 	用来存储文件属性的首地址
		返回值:	
			成功:0
			失败:-1,并设置错误信息
			
	2.3.6、打开目录
		DIR *opendir(const char *path);
		功能:
			获得 path 目录中的所有文件信息,返回目录流指针
		头文件:
			#include <sys/types.h>
			#include <dirent.h>
		返回值:
			成功: 目录流
			失败: NULL,并设置错误信息
			
	2.3.7、获取目录刘中文件信息
		struct dirent * readdir(DIR *dir);
		功能:	
			读取目录流中,一个文件的信息。					

2.4、静态库和动态库的制作和使用:
    静态库:
		程序在编译阶段,加载库,代码体积变大,运行时与库函数无关,方便移植。
		库文件名: lib*.a
	动态库:
		程序在运行时,加载库,代码体积不变,节约空间和资源。
		库文件名: lib*.so

	1、静态库制作步骤:
		1、将 *.c 编译成 *.o
			gcc -c *.c
		2、将 目标文件 *.o 生成 lib*.a 静态库文件
			ar crs lib*.a *.o
			注意:
				libadd.a 		:为静态库文件名
				add				:为库名
				
	2、静态库文件的使用
		gcc main.c -L. -ladd
		注意:
			-L 	:指定库的搜索路径
			. 	:此处表示当前路径
			-l  :链接库名
				
练习:
	1、使用文件IO,实现图片的拷贝。
		*.jpg   *.png  
		
	2、使用代码实现,"ls -l file.txt"。

三、进程

3.1、理论知识:
	1、进程:程序的一次执行过程,是系统资源分配和调度的最小单位,是动态的。
	2、程序:存储在磁盘上的指令的有序集合,是静态的。

3.2、进程的内容:
	数据段:全局变量,malloc分配的空间
	正文段:程序的代码行
	堆栈段:函数返回值、参数、局部变量等
	
3.3、进程的类型:
	3.3.1、交互进程:由 shell 控制和运行的进程,也就是在终端中运行产生的进程。
	3.3.2、批处理进程:不属于某个终端,是在队列中被顺序执行,操作系统开机时就有很多批处理进程。
	3.3.3、守护进程:与终端无关,开机时自动执行,系统关机时 结束。(系统时间、系统中的服务器)
	
3.4、进程的状态
	就绪态:进程准备运行
	运行态:进程正在运行
	等待态:(休眠态)进程正在等待一件事情或者系统资源
		可中断和不可中断
	停止态:进程被中止的状态,还可以重新运行
	死亡态:进程被终止(终结),但是task_struct还存在,也就是进程资源还没有回收。
	
3.5、进程相关的指令
	3.5.1、以某一个优先级运行程序,产生进程
		nice -n NUM 可执行程序
		eg:
			nice -n 5 ./a.out   以优先级为 5 运行程序
			
	3.5.2、更改正在运行进程 的优先级
		renice -n NUM PID
		eg:
			renice -n 2 14326	:将14326进程的优先级改为 2
	注意:
		用户所能设置的优先级为 0 ~ 20,其中0 为最高优先级,20 为最低
		
	3.5.3、
		ctrl+z 将前台运行的进程,在后台挂起,暂停(停止),同时后台进程会有编号
		bg 编号: 将后台停止的进程,在后台运行
		fg 编号: 将后台运行的进程,转到前台运行
		./a.out & :后台运行 a.out可执行程序
		
3.6、系统调用接口
	3.6.1、创建子进程
		pid_t fork(void);
		功能:
			创建子进程。
		头文件:
			#include <sys/types.h>
			#include <unistd.h>
		参数: 无
		返回值:
			成功:
				返回 	0 			  : 表示子进程区域
				返回	>0(子进程ID号): 表示父进程区域
			失败:
				-1,并设置错误信息
				
	3.6.2、相关考点概念:
		僵尸进程: 子进程先于父进程结束,父进程没有回收子进程资源。
		孤儿进程: 父进程先于子进程结束,子进程被系统 init.d 托管。
		
	3.6.3、在进程中执行另外一个可执行程序,产生新的进程。
		原进程中除了进程号,其他的都将被替换。
		(1)、
			int execl(const char *path, const char *arg, ....);
			以 l结尾,表示第一个参数必须是 可执行文件名包含路径,后面以列表形式填写参数
			第二个参数,必须是可执行程序的 执行命令
			第三个参数,可以是 执行命令的 参数选项
			最后一个,必须以 NULL 结束
			
		(2)、
			int execlp(const char *file, const char *arg, ....);
			以 lp结尾表示,
			第一个参数,可执行文件名,后面以列表形式填写参数,自动搜索文件的路径
			第二个参数,必须是可执行程序的 执行命令
			第三个参数,可以是 执行命令 的 参数选项
			最后一个,必须以 NULL 结束
			
	3.6.4、结束进程的函数
		1、库函数 #include <stdlib.h>
			int exit(int status);
			
			结束进程,并且刷新缓冲区
		
		2、系统调用 #include <unistd.h>
			int _exit(int status);
			
			结束进程,不会刷新缓冲区
	
	3.6.5、僵尸进程的解决方法
		(1)、wait 、 waitpid
			1、pid_t wait(int *status);
				功能:
					阻塞父进程,等待任何一个子进程结束,回收资源
				头文件:
					#include <sys/types.h>
					#include <wait.h>
				参数:
					status: 	保存子进程退出时的状态或者 exit函数的实参,
								可以使用 固定的宏函数实现(WIFEXITED, WEXITSTATUS)
				返回值:
					成功: 返回退出的子进程PID
					失败: 返回 -1
				
			(2)、pid_t waitpid(pid_t pid, int *status, int option);
				功能:
					阻塞父进程,等待子进程结束,回收资源
				头文件:
					#include <sys/types.h>
					#include <wait.h>
				参数:
					pid	  :    -1, 等价于wait,回收任何一个退出的子进程资源
								>0,  为子进程PID, 表示指定要回收的子进程
								
					status: 	保存子进程退出时的状态或者 exit函数的实参,
								可以使用 固定的宏函数实现(WIFEXITED, WEXITSTATUS)
					option:		WNOHNG : 非阻塞模式
								0      :阻塞模式,等价于 wait
				返回值:
					成功: 
						阻塞模式下: 返回退出的子进程PID
						非阻塞模式下:返回 0
					失败: 返回 -1
		
	3.6.6、孤儿进程的应用
		(1)、守护进程创建步骤
			1、创建孤儿进程     (摆脱父进程的控制)
				fork();
			2、创建新的会话期   (拜托终端控制,将孤儿进程独立出来)
				setsid();
			3、更改工作目录
				chdir(...);
			4、重设权限掩码
				umask(0)
			5、关闭文件描述符
				
			6、根据具体功能要求,实现算法
		
练习:	
	1、创建一个多进程,子进程实现文件行数的统计,父进程实现计算文件大小。
	2、创建一个守护进程,实现:
			每隔一秒,向文件中写入:行数+日期
			思路:
				1、先创建守护进程
				2、在守护进程第五步操作之后,加入文件读写。
					1、打开文件
					2、计算文件中的行数
					3、循环的获取系统时间、日期
					4、在循环中将行数+1与日期拼接在一起
					5、写入文件中
    	 https://www.jb51.net/article/118822.htm

四、线程

4.1、多进程的特点:
	4.1.1、缺点:
		1、每个进程的地址空间相对独立,比较消耗系统的空间资源
		2、进程间进行任务切换时,要不停的刷新cache缓存器和TLB页表,比较消耗系统的时间
	4.1.2、优点:
		1、能实现多任务
		2、地址空间相对独立,每个进程不会形成干扰,
			也就是子进程结束,不会影响父进程的运行;父进程结束,不会影响子进程的运行。
		
4.2、线程:
	4.2.1、概念:轻量级的进程,共享同一地址空间的多个任务。
	4.2.2、特点:
		(1)、优点:
			1、同一进程中的多个线程,共享该进程地址空间,节约系统空间资源。
			2、同一进程中的多线程,进行任务切换时,提高切换的效率,
				避免额外的刷新cache和TLB页表的系统时间。
			3、同一进程中的多个多线程之间进行数据传递比较方便,可以使用全局变量。
		
		(2)、缺点:
			1、同一进程中的一个线程意外结束或死掉,那么该进程中的其他线程都不能继续运行。
			2、同一进程中多个线程,容易竞争共享资源,也就是资源抢占问题。
			3、线程所属的进程结束,那么线程就不存在。
		
4.3、库函数
	4.3.1、创建线程
		int  pthread_create(pthread_t *thread,  const pthread_attr_t *attr, 
			void * (* routine)(void *),  void *arg)
		头文件:
			#include <pthred.h>
		参数:
			thread 		:保存线程id 号的地址
			attr		:设置线程的属性,通常使用 NULL 缺省属性
			routine		:线程的执行函数名,执行函数为 void 类型的指针函数
			arg			:要传递到线程的数据,如果不传数据使用 NULL
		返回值:
			0			:成功
			-1 			:失败,并设置错误信息
			
		程序编译时,必须 -lpthread
		
4.5、线程间的通信机制:
	4.5.1、同步通信:
		多个任务按照某种约定的顺序共同配合的完成一件事情。线程中使用 信号量来实现。
		
	4.5.2、信号量:
		系统中的资源数量。
		
	4.5.3、api接口:
		(1)、初始化信号量
			int  sem_init(sem_t *sem,  int pshared,  unsigned int value)  
			头文件:
				#include <semaphore.h>
			参数:
				sem 	: 存储信号量的地址
				pshared : 信号量的使用范围(0:线程中使用,非0:进程中使用)
				value 	: 信号量的初始值
			返回值:
				成功: 返回 0
				失败: 返回-1
				
		(2)、p操作,申请资源,相当于消费者
			int sem_wait(sem_t *sem);
			功能:
				申请资源(也就是判断信号量的值)
				如果信号量值为 0, 阻塞,当信号量值 不为 0,被唤醒继续运行
				如果信号量值为 >0, 非阻塞
				资源申请成功,信号量值 -1
			参数:
				sem 	: 信号量
			返回值:
				成功 :0
				失败 :-1
				
		(3)、v操作,释放资源,相当于生产者
			int sem_post(sem_t *sem);
			功能:
				释放资源(也就是将信号量的值 + 1)
				如果系统中有等待该资源的任务,就立马唤醒该任务继续运行
				如果系统中没有等待该资源的任务,系统中信号量的数值 + 1
			参数:
				sem		:信号量
			返回值:
				成功: 0
				失败:-1
			
		(4)、临界资源:
			多个任务共享的全局数据。
			
		(5)、临界区:
			访问操作临界资源的代码行。
			
		(6)、锁粒度:
			互斥锁保护的临界区的大小。临界区越大,锁粒度就越大。
			
		(7)、保护临界资源的方法:互斥锁
			1、互斥锁目的:
				保护临界资源,保证数据的完整性,一个线程使用临界资源中,另一个线程就不能使用。
			2、api接口:			
				1、初始化互斥锁
					int  pthread_mutex_init(pthread_mutex_t  *mutex, pthread_mutexattr_t *attr) ;
					头文件:
						#include <pthread.h>
					参数:
						mutex		: 保存互斥锁id 的地址
						attr 		: 互斥锁的属性,一般写 NULL
					返回值:
						成功:返回 0
						失败:返回-1
						
				2、加锁(申请互斥锁)
					int pthread_mutex_lock(pthread_mutex_t *mutex)
					参数:
						mutex 		:互斥锁
					返回值:
						成功:返回 0
						失败:返回-1
						
				3、解锁锁(释放互斥锁)
					int pthread_mutex_unlock(pthread_mutex_t *mutex)
					参数:
						mutex 		:互斥锁
					返回值:
						成功:返回 0
						失败:返回-1
						
			3、互斥锁使用时,需要注意:
				1、同一临界资源的所有临界区,必须使用同一把互斥锁。
			
				2、每个临界区之前加锁,临界区最后必须解锁,不然会产生死锁。
			
			
4.6、动态库的制作和使用:
	4.6.1、制作方式:
		1、编译生成与地址无关的程序,*.o
			gcc -fPIC -c *.c 
		2、将 *.o 编译生成动态库文件 lib*.so
			gcc -shared *.o -o lib*.o
			
	4.6.2、动态库的使用
		1、如果动态库文件在程序的当前目录
			gcc main.c *.c -l*
		2、如果想将动态库制作成第三方库,在系统的任何路径都可以使用
			1、将 lib*.so 拷贝到 /usr/lib 或者是 /lib 目录
			2、将动态库文件对应的头文件 *.h,拷贝到 /usr/include目录
			编译时,直接:
				gcc main.c *.c -l*
					
	4.6.3、动态库步骤的制作:

	gcc - fPIC -c add.cabs
	mv add.c ../
	gcc -shared add.o -o libadd.so
	rm add.off_t
	gcc main.c -ladd
				
	
练习:
	1、编写多线程程序,实现线程1拷贝图片前半部分;线程2拷贝图片后半部分。

五、进程间的通信

5.1、进程间通信方式:
	5.1.1、传统进程间通信方式
		无名管道、有名管道、信号
		
	5.1.2、(System V5)IPC对象通信方式
		共享内存、消息队列、信号灯集
		
	5.1.3、BSD套接字通信
		网络编程 socket 通信
	
5.2、无名管道:
	5.2.1、定义
		是一个在内核中存在的特殊文件(看不到文件名),使用文件IO进行数据交互。
	5.2.2、特点:
		1、是一种半双工通信(数据只能一个方向传递),有固定的的读端和写端。
		2、必须在具有亲缘关系的进程之间使用

	5.2.3、api接口
		1、创建无名管道文件
			int pipe(int fd[2]);
			头文件:
				#include <unistd.h>
			参数:
				fd 	:数组名,用来保存读端和写端的文件描述符
			返回值:
				成功: 返回 0
				失败: 返回-1
	5.2.4、注意:
		1、管道中没有数据,读端会阻塞等待,直到有数据
		2、读端不存在时,写端写入数据,会收到内核的SIGPIPE信号,终止进程的运行。
		3、管道中缓冲区满了,写端写入数据将会阻塞,直到读端读取数据。缓冲区64k = 1024*64
	
5.3、有名管道:
	5.3.1、定义:
		是一种特殊的管道文件,在本地磁盘可见 
	5.3.2、特点:
		1、可以在具有亲缘关系的进程间使用,也可以在非亲缘关系的进程中使用。
		2、数据遵循先进先出
		3、可以使用文件IO中除了lseek之外的函数操作
	5.3.3、api接口
		1、创建管道文件
			int mkfifo(const char *filename, mode_t mode);
			头文件:
				#include <sys/types.h>
				#include <fcntl.h>
				#include <unistd.h>
			参数:
				filename:	要操作的管道文件名(可包含路径)
				mode:		8进制的权限
			返回值:
				成功: 返回0
				失败: 返回-1
	5.3.4、注意:	
		1、有名管道文件的数据交互在 内核中,本地磁盘文件中没有数据
		2、有名管道的读进程结束,写进程写入数据会终止程序运行
		3、有名管道的写进程结束,读进程会一直运行,读到0直接数据
		
		信号处理原型
		void(*signal(int signum,void(*handler)(int)))(int);

跳转:IO进线程编程!

跳转:IO进线程编程!

跳转:下一篇,网络编程!

跳转:下一篇,网络编程!

跳转:开头

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

子羽丿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值