【北京迅为】《iTOP-3588开发板系统编程手册》-第3部分 系统编程实战 第11章 IO操作

RK3588是一款低功耗、高性能的处理器,适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用,RK3588支持8K视频编解码,内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP,内置NPU,支持INT4/INT8/INT16/FP16混合运算能力,支持安卓12和、Debian11、Build root、Ubuntu20和22版本登系统。了解更多信息可点击迅为官网   

【粉丝群】824412014

【实验平台】:迅为RK3588开发板

【内容来源】《iTOP-3588开发板系统编程手册》

【全套资料及网盘获取方式】联系淘宝客服加入售后技术支持群内下载

【视频介绍】:【强者之芯】 新一代AIOT高端应用芯片 iTOP -3588人工智能工业AI主板


第3部分 系统编程实战

第11章 IO操作

由于在之后的实战章节会多次涉及到IO操作类型相关的知识,在本章节将对IO操作中的阻塞IO、非阻塞IO、IO多路复用进行详细讲解。

11.1 IO操作介绍

I/O(Input/Output,输入/输出)是计算机系统中的一个重要组成部分,它是指计算机与外部世界之间的信息交流过程。I/O 操作是计算机系统中的一种基本操作,用于向外部设备(如硬盘、键盘、鼠标、网络等)读取数据或向外部设备写入数据。

在计算机系统中,所有的设备都被看作是输入输出设备,它们通过 I/O 接口与计算机进行数据的输入和输出。I/O 操作有多种方式,可以根据操作系统或应用程序对数据传输的处理方式来进行分类。以下是一些常见的 I/O 操作方式:

(1)同步 I/O(Synchronous I/O):在进行 I/O 操作时,程序会一直等待操作完成后再继续执行后面的代码。如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。

(2)异步 I/O(Asynchronous I/O):在进行 I/O 操作时,程序会立即返回,而不必等待操作完成。当操作完成后,操作系统会通知程序。这种方式可以允许程序在等待 I/O 操作完成的同时执行其他代码。

(3)阻塞 I/O(Blocking I/O):在进行 I/O 操作时,程序会一直等待操作完成后再继续执行后面的代码。如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。阻塞 I/O 是同步 I/O 的一种。

(4)非阻塞 I/O(Non-blocking I/O):在进行 I/O 操作时,程序会立即返回,而不必等待操作完成。如果 I/O 操作无法立即完成,程序也会立即返回,但是会周期性地检查操作是否完成。非阻塞 I/O 是同步 I/O 的一种。

(5)I/O 多路复用(I/O Multiplexing):是一种同时监视多个 I/O 事件的机制,通常使用 select、poll、epoll 等系统调用。程序通过这些调用告知操作系统它要监视哪些 I/O 事件,当有 I/O 事件发生时,操作系统通知程序,并返回发生事件的描述符。I/O 多路复用通常是异步 I/O 模型的一部分。

类型

描述

同步 I/O

程序在进行 I/O 操作时会一直等待操作完成后再继续执行后面的代码

异步 I/O

程序在进行 I/O 操作时会立即返回,操作完成后操作系统会通知程序

阻塞 I/O

在进行 I/O 操作时,程序会一直等待操作完成后再继续执行后面的代码,如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。

非阻塞 I/O

程序在进行 I/O 操作时会立即返回,如果 I/O 操作无法立即完成,程序也会立即返回,但是会周期性地检查操作是否完成

I/O 多路复用

是一种同时监视多个 I/O 事件的机制,通常使用 select、poll、epoll 等系统调用。程序通过这些调用告知操作系统它要监视哪些 I/O 事件,当有 I/O 事件发生时,操作系统通知程序

11.2 阻塞IO与非阻塞IO

本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\57”目录下,如下图所示:

阻塞 I/O(Blocking I/O):当进行 I/O 操作时,程序会一直等待操作完成后再继续执行后面的代码。如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。这种模式下,程序在进行 I/O 操作时会被阻塞,无法进行其他操作,直到 I/O 操作完成或超时。阻塞 I/O 是同步 I/O 的一种。

非阻塞 I/O(Non-blocking I/O):当进行 I/O 操作时,程序会立即返回,而不必等待操作完成。如果 I/O 操作无法立即完成,程序也会立即返回,但是会周期性地检查操作是否完成。这种模式下,程序不会被阻塞,可以继续执行其他操作。如果 I/O 操作完成,程序可以直接进行下一步处理,否则需要等待一定时间再次检查是否完成。非阻塞 I/O 是同步 I/O 的一种。

阻塞和非阻塞的主要区别在于程序在进行 I/O 操作时是否会被阻塞。阻塞模式下程序会一直等待 I/O 操作完成,无法进行其他操作,非阻塞模式下程序可以继续执行其他操作,不必等待 I/O 操作完成。

在实际应用中,阻塞 I/O 的使用场景较为有限,因为阻塞 I/O 会导致程序性能下降,造成资源浪费。非阻塞 I/O 则可以较好地解决这个问题,但需要程序周期性地检查 I/O 操作是否完成,增加了编程难度。

下面对阻塞IO进行举例,首先创建demo57_block.c文件,并向该文件写入以下内容:

#include <stdio.h>

int main() 
{
    char str[100];  // 定义字符数组str,长度为100

    printf("Enter a string: ");  // 打印提示信息
    scanf("%s", str);  // 从标准输入流(键盘)读取一个字符串,存储到str中

    printf("You entered: %s\n", str);  // 打印用户输入的字符串

    return 0;  // 返回程序运行结果
}

此程序通过标准输入流(键盘)获取用户输入的字符串,并将其输出到标准输出流(屏幕)上。其中,使用了stdio.h头文件中的printf和scanf函数。程序运行时,会在屏幕上打印提示信息,等待用户输入字符串,直到用户按下回车键。程序将用户输入的字符串存储在定义好的字符数组中,并将其输出到屏幕上。

保存退出之后,使用以下命令对 demo57_block.c进行编译,编译完成如下图所示:

gcc -o demo57_block demo57_block.c

然后使用命令“./demo57_block”进行程序的运行,在没有按下回车之前,程序会阻塞,如下图所示:    

随意输入几个字符之后按下回车,程序才会解除阻塞状态,向下执行,如下图所示:

至此关于阻塞IO的实验就完成了。这时候就要提出一个问题了,本小节程序的scanf输入表示的是标准输入,准输入(stdin)在大多数情况下默认是阻塞I/O。在终端上运行的程序通常会将stdin设置为阻塞模式,这意味着当程序试图从stdin读取输入时,如果没有可用的输入,程序将暂停并等待用户输入,那能不能将标准输入的状态修改为非阻塞IO呢,答案是可以的,这就要轮到下一小节的fcntl函数出场了。

11.3 fcntl函数

本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\58”目录下,如下图所示:

fcntl函数是一个在Linux和Unix系统中使用的系统调用,其主要用途是对已打开的文件描述符进行操作。其名称"fcntl"是"file control"的缩写,可以用于对文件描述符进行许多控制操作,例如:更改文件状态标志、获取或更改文件锁定、以及对文件描述符进行复制等。fcntl函数的函数原型和所使用的头文件如下所示:

所需头文件

函数原型

1

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

其中,fd是要操作的文件描述符,cmd是要进行的操作类型,arg是用于传递参数的可选参数列表。函数执行成功,其返回值与cmd(操作命令)有关,譬如cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、cmd=F_GETFD(获取文 件描述符标志)将返回文件描述符标志等等,执行失败情况下,返回-1,并且会设置 errno。

fcntl()函数的cmd参数可以取以下值:

参数名称

参数含义

F_DUPFD

复制一个已有的文件描述符

2

F_GETFD

获取文件描述符标记(close-on-exec)

3

F_SETFD

设置文件描述符标记(close-on-exec)

4

F_GETFL

获取文件状态标记

5

F_SETFL

设置文件状态标记

6

F_GETLK

获取文件锁

7

F_SETLK

设置文件锁(非阻塞)

8

F_SETLKW

设置文件锁(阻塞)

9

F_GETOWN

获取异步IO所有权

10

F_SETOWN

设置异步IO所有权

至此关于fcntl()函数的讲解就结束了,接下来进行实验,通过fcntl()函数将标准输入修改为非阻塞模式。

首先创建demo58_nonblock.c文件,并向该文件写入以下内容:

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

int main() 
{
    char str[100];  // 定义字符数组str,长度为100
    int flags;

    // 获取stdin的文件描述符
    int fd = fileno(stdin);

    // 获取当前标志
    flags = fcntl(fd, F_GETFL);
	
    // 设置文件描述符为非阻塞模式
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);

    printf("Enter some input: ");

    scanf("%s", str);  // 从标准输入流(键盘)读取一个字符串,存储到str中
    printf("You entered: %s\n", str);  // 打印用户输入的字符串
                                                                                                                                                                              
    return 0;
}

上述代码使用了fcntl函数将标准输入(stdin)的文件描述符设置为非阻塞模式,然后使用scanf从标准输入流读取用户输入的字符串并输出。因为文件描述符已经设置为非阻塞模式,即使没有输入数据,scanf函数将不会阻塞程序。最终程序将输出用户输入的字符串。

保存退出之后,使用以下命令对demo58_nonblock.c进行编译,编译完成如下图所示:

gcc -o demo58_nonblock demo58_nonblock.c

然后使用命令“./demo58_nonblock”进行程序的运行,如下图所示:

这次scanf函数并不会阻塞,直接向下运行了,在输入之前程序就已经执行完毕了,并没有输入的机会,但可以在scanf()函数上下加入while()循环,加入while()循环后会循环执行You entered 信息的打印,至此关于fcntl()函数的讲解就结束了。

11.4 I/O多路复用

11.4.1 IO多路复用介绍

IO多路复用是一种同时监视多个文件描述符的技术,它可以通过单个进程来处理多个IO操作,提高系统的IO效率。在IO多路复用中,一个进程可以同时监视多个文件描述符,一旦其中任意一个文件描述符准备就绪,就会通知进程进行读写操作,从而实现了同时处理多个IO事件的功能。

在Unix/Linux操作系统中,常见的IO多路复用技术包括select、poll和epoll。这些函数的基本原理是相似的,都是通过将多个文件描述符加入到一个“监视列表”中,然后等待内核的通知,以决定哪些文件描述符可以进行IO操作。当某个文件描述符准备就绪时,内核会返回相应的信息,进程再通过文件描述符进行读写操作。

IO多路复用的主要优点包括:

应用

说明

提高系统IO效率

可以同时处理多个IO事件,避免了频繁的进程切换和上下文切换。

减少资源开销

可以减少线程和进程的创建数量,降低系统的资源开销。

非阻塞IO

可以实现非阻塞IO,提高程序的响应速度和效率。

提高可维护性

可以提高程序的可维护性,代码简洁明了,易于维护。

最后对IO多路复用的实际应用进行举例

(1)实现高效的网络服务器

在网络编程中,服务器需要同时处理多个客户端请求。传统的做法是使用多进程或多线程技术,但是这种方法会增加系统开销,尤其是当客户端连接数量较大时。使用IO多路复用技术,服务器可以在一个进程或线程中同时处理多个客户端请求,减少了系统资源的占用,提高了服务器的效率和并发性能。

(2)实现异步事件处理

IO多路复用技术可以使程序异步地处理多个事件,而不必等待某一个事件完成再处理下一个事件。这样可以极大地提高程序的响应速度和吞吐量,特别是在需要处理大量事件的场景中,比如并发请求处理、文件传输等。

(3)实现高性能的日志处理

日志处理是一个常见的任务,可以使用IO多路复用技术来实现高性能的日志处理。将日志文件描述符加入到监控队列中,当有日志数据需要写入时,程序会自动触发可写事件,进行写入操作。这种方法可以有效地减少日志写入的延迟,提高程序的性能。

(4)实现消息通信

IO多路复用技术可以用于实现消息通信机制,如管道、消息队列、信号量等。程序可以将多个文件描述符加入到监控队列中,当有数据可读或可写时,程序会自动触发相应的事件,进行消息的读取或发送。

会在接下来的两个小节对实现IO多路复用的select函数和poll函数进行讲解。

11.4.2 select函数

本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\59”目录下,如下图所示:

select函数是一种IO多路复用机制,可以同时监听多个文件描述符上的IO事件,并在其中一个或多个文件描述符上发生IO事件时进行处理。它通常用于网络编程中,可以实现同时监听多个客户端连接请求,处理网络数据收发等操作。select函数所需头文件和函数原型如下所示:

所需头文件

函数原型

1

2

3

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select函数返回值表示有事件发生的文件描述符的数量,如果返回-1,则表示出错。select函数每个参数含义如下所示:

参数名称

参数含义

nfds

需要监听的文件描述符数量,是所有文件描述符中最大值+1

2

readfds

读文件描述符集合,如果其中一个文件描述符可读,则返回该文件描述符

3

writefds

写文件描述符集合,如果其中一个文件描述符可写,则返回该文件描述符

4

exceptfds

异常文件描述符集合,如果其中一个文件描述符有异常,则返回该文件描述符

5

timeout

超时时间,如果指定了非空的timeout,则select函数会阻塞等待一段时间,超时后返回0;如果timeout为NULL,则select函数会一直阻塞,直到有文件描述符有事件发生

在使用select函数之前,需要先初始化文件描述符集合,可以使用以下宏定义进行操作:

(1)FD_ZERO(fd_set *set):将文件描述符集合set清空

(2)FD_SET(int fd, fd_set *set):将文件描述符fd加入到集合set中

(3)FD_CLR(int fd, fd_set *set):将文件描述符fd从集合set中删除

(4)FD_ISSET(int fd, fd_set *set):如果文件描述符fd在集合set中,则返回非0值,否则返回0

使用select函数的步骤如下所示:

操作步骤

描述

1

初始化文件描述符集合

在使用select函数之前,需要先创建文件描述符集合readfds、writefds、exceptfds,用于存放需要监听的文件描述符。可以使用FD_ZERO宏来清空集合,并使用FD_SET宏将需要监听的文件描述符加入到对应的集合中。

2

调用select函数

调用select函数并设置超时时间timeout,等待文件描述符上的IO事件发生。

3

判断select函数返回值

如果select函数返回-1,则表示出错;如果返回0,则表示超时;否则表示有文件描述符上有事件发生,需要使用FD_ISSET宏判断具体是哪个文件描述符上有事件发生。

4

处理IO事件

如果有文件描述符上有事件发生,可以进行相关的IO操作,例如读取数据、发送数据等。

至此关于select函数相关的内容就讲解完成了,下面编写select函数的实验程序。

首先创建demo59_select.c文件,并向该文件写入以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	char buf[100];
	int fd, ret, flag;
	fd_set rdfds; // 定义用于监视读事件的文件描述符集合

	// 打开鼠标输入设备
	fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
	if (fd == -1) 
	{
		perror("open");
		exit(EXIT_FAILURE);
	}

	// 将标准输入设置为非阻塞模式
	flag = fcntl(STDIN_FILENO, F_GETFL);
	if (flag == -1) 
	{
		perror("fcntl F_GETFL");
		exit(EXIT_FAILURE);
	}
	flag |= O_NONBLOCK;
	if (fcntl(STDIN_FILENO, F_SETFL, flag) == -1) 
	{
		perror("fcntl F_SETFL");
		exit(EXIT_FAILURE);
	}

	while (1) 
	{
		FD_ZERO(&rdfds); // 清空文件描述符集合
		FD_SET(STDIN_FILENO, &rdfds); // 添加标准输入文件描述符到集合中
		FD_SET(fd, &rdfds); // 添加鼠标输入设备文件描述符到集合中

		// 使用select()等待事件
		ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
		if (ret == -1) 
		{
			if (errno == EINTR) continue;
			perror("select");
			exit(EXIT_FAILURE);
		} else if (ret == 0) 
		{
			printf("select 超时.\n");
			continue;
		}

		// 处理事件
		if (FD_ISSET(STDIN_FILENO, &rdfds)) 
		{
			memset(buf, 0, sizeof(buf));
			ret = read(STDIN_FILENO, buf, sizeof(buf));
			if (ret > 0) 
			{
				printf("键盘:成功读取了 <%d> 字节的数据\n", ret);
			}
		}
		if (FD_ISSET(fd, &rdfds)) 
		{
			memset(buf, 0, sizeof(buf));
			ret = read(fd, buf, sizeof(buf));
			if (ret > 0) 
			{
				printf("鼠标:成功读取了 <%d> 字节的数据\n", ret);
			}
		}
	}
	close(fd);
	return 0;
}

本程序是一个简单的输入设备监视器,使用 select 函数等待读事件,监视键盘和鼠标输入设备是否有数据输入。将标准输入设置为非阻塞模式的目的是为了在等待鼠标输入事件时,程序仍然能够同时处理来自标准输入的数据。

保存退出之后,使用以下命令对demo59_select.c进行编译,编译完成如下图所示:

gcc -o demo59_select demo59_select.c

然后使用命令“./demo59_select”进行程序的运行,如下图所示: 

在运行程序之后首先会阻塞,因为没有进行键盘和鼠标的任何输入,然后移动鼠标和敲击键盘之后,打印信息如下所示:

至此关于select的实验讲解就完成了。

11.4.3 poll函数

本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\60”目录下,如下图所示:

poll()也是Linux 中用于多路复用 I/O 的一个系统调用函数,用于检测一组文件描述符中是否有可读、可写或异常等事件发生,从而实现异步 I/O。它可以监视多个文件描述符(socket、标准输入输出、文件等)的状态,一旦其中有任意一个描述符就绪(可读、可写或有异常),则 poll() 函数返回,并填充一个数组来表示就绪的文件描述符。

poll()函数使用的头文件和函数原型,如下所示:

所需头文件

函数原型

1

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数返回值有三种可能,当返回值为-1时,表示发生错误,此时需要通过 errno 来确定错误原因,当返回值为0时,表示超时,没有任何文件描述符就绪,当返回值为正整数时,表示就绪的文件描述符的数量。

execvp()函数参数含义如下所示:

参数名称

参数含义

fds

用于监视的文件描述符结构体数组,每个元素包含了一个文件描述符以及该文件描述符关注的事件类型和发生的事件类型

2

nfds

fds 数组中元素的个数

3

timeout

等待超时时间,单位为毫秒。如果该值为 -1,则表示一直等待直到有事件发生;如果该值为 0,则表示轮询一次立即返回;如果该值大于 0,则表示等待该时间后返回,如果还没有事件发生,则返回 0

下面上述fds参数的pollfd结构体进行讲解

struct pollfd {
    int   fd;         // 监视的文件描述符
    short events;     // 监视的事件类型
    short revents;    // 实际发生的事件类型
};

其中,成员变量的含义如下:

参数名称

参数含义

fd

监视的文件描述符。

2

events

需要监视的事件类型,包括以下宏定义:

POLLIN:文件描述符可读。

POLLOUT:文件描述符可写。

POLLRDHUP:TCP 连接被对方关闭,或者对端关闭了写操作,或者连接被重置。

POLLERR:文件描述符发生错误。

POLLHUP:文件描述符被挂起。

POLLNVAL:文件描述符不合法。

3

revents

实际发生的事件类型,由内核填充。

poll 函数的优点在于,它能够同时监视多个文件描述符,并且不需要像 select 函数那样每次调用都需要将文件描述符重新加入到监视集合中,这样减少了不必要的系统调用。下面使用poll函数完成上一小节同样的需求,首先创建demo60_poll.c文件,并向该文件写入以下内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <poll.h>

int main(int argc, char *argv[])
{
    char buf[100]; // 用于读取输入数据的缓冲区
    int fd, ret, flag; // fd: 文件描述符, ret: poll() 函数的返回值, flag: 文件标志
    struct pollfd fds[2]; // 定义用于监视文件描述符的结构体数组,包括标准输入和鼠标输入设备

    // 打开鼠标输入设备
    fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK); // 打开设备文件,并设置为非阻塞模式
    if (fd == -1) // 如果打开失败,则输出错误信息并退出程序
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 将标准输入设置为非阻塞模式
    flag = fcntl(STDIN_FILENO, F_GETFL); // 获取标准输入的文件标志
    if (flag == -1) // 如果获取失败,则输出错误信息并退出程序
    {
        perror("fcntl F_GETFL");
        exit(EXIT_FAILURE);
    }
    flag |= O_NONBLOCK; // 将文件标志设置为非阻塞模式
    if (fcntl(STDIN_FILENO, F_SETFL, flag) == -1) // 设置标准输入为非阻塞模式
    {
        perror("fcntl F_SETFL");
        exit(EXIT_FAILURE);
    }

    // 初始化 pollfd 结构体数组
    fds[0].fd = STDIN_FILENO; // 添加标准输入文件描述符到数组中
    fds[0].events = POLLIN; // 监视读事件                                                                                                                                            
    fds[1].fd = fd; // 添加鼠标输入设备文件描述符到数组中
    fds[1].events = POLLIN; // 监视读事件

    while (1) // 循环等待事件
    {
        // 使用 poll() 等待事件
        ret = poll(fds, 2, -1); // fds 数组中有两个文件描述符,超时时间设置为 -1 表示一直等待
        if (ret == -1) // 如果 poll() 函数失败,则输出错误信息并退出程序
        {
            if (errno == EINTR) continue; // 如果是由于信号中断,则重新等待
            perror("poll");
            exit(EXIT_FAILURE);
        }

        // 处理事件
        if (fds[0].revents & POLLIN) // 如果标准输入有数据可读
        {
            memset(buf, 0, sizeof(buf)); // 清空缓冲区
            ret = read(STDIN_FILENO, buf, sizeof(buf)); // 读取标准输入中的数据到缓冲区
            if (ret > 0) // 如果读取成功
            {
                printf("键盘:成功读取了 <%d> 字节的数据\n", ret);
            }
        }
        if (fds[1].revents & POLLIN) // 如果鼠标输入有数据可读
        {
            memset(buf, 0, sizeof(buf)); // 清空缓冲区
            ret = read(fd, buf, sizeof(buf));// 读取鼠标输入中的数据到缓冲区
            if (ret > 0)  // 如果读取成功
            {
                printf("鼠标:成功读取了 <%d> 字节的数据\n", ret);
            }
        }
    }
    close(fd);
    return 0;
}

上述代码的作用是使用 poll 函数监视标准输入和鼠标输入设备的读事件,并在事件发生时输出读取的字节数。

保存退出之后,使用以下命令对demo60_poll.c进行编译,编译完成如下图所示:

gcc -o demo60_poll demo60_poll.c

然后使用命令“./demo60_poll”进行程序的运行,如下图所示: 

在运行程序之后首先会阻塞,因为没有进行键盘和鼠标的任何输入,然后移动鼠标和敲击键盘之后,打印信息如下所示:

至此关于poll函数的实验讲解就完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值