Linux 详谈进程间通信(一)

Linux进程间通信机制详谈 Linux进程间通信机制 Unix系统提供的进程间通信机制主要有: 管道和FIFO(命名管道) 套接字 信号 信号量 消息队列 共享内存区 管道pipe 管道机制思想是在内存中创建一个共享文件,从而使得通信双方利用该共享文件进行交互。需要注意的是管道数据流动是单向的,是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。管道只在具有公共祖先的两个进程之间使用,可以将管道视为一个独立的文件系统,管道在管道两侧的进程看来就是一个文件,只是这个文件只存在于内存中。 一个进程写入管道的所有数 阅读详情

目录

1. 进程间通信介绍

进程间通信⽬的

进程间通信发展

进程间通信分类

2. 管道

3. 匿名管道

实例代码

⽤ fork 来共享管道原理

站在⽂件描述符⻆度-深度理解管道

站在内核⻆度-管道本质

管道样例

测试管道读写

创建进程池处理任务

管道读写规则

管道特点


1. 进程间通信介绍

进程间通信⽬的

数据传输:⼀个进程需要将它的数据发送给另⼀个进程

资源共享:多个进程之间共享同样的资源。

通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。

进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。

进程间通信发展

管道

System V进程间通信

POSIX进程间通信

进程间通信分类

管道:匿名管道pipe 命名管道 

System V IPC:System V 消息队列 System V 共享内存 System V 信号量

POSIX IPC:消息队列 共享内存 信号量 互斥量 条件变量 读写锁

2. 管道

什么是管道

管道是Unix中最古⽼的进程间通信的形式。

我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个“管道”

3. 匿名管道

#include <unistd.h>
功能:创建⼀⽆名管道
原型
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码

实例代码

//例⼦:从键盘读取数据,写⼊管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
	int fds[2];
	char buf[100];
	int len;
	if (pipe(fds) == -1)
		perror("make pipe"), exit(1);
	// read from stdin
	while (fgets(buf, 100, stdin)) {
		len = strlen(buf);
		// write into pipe
		if (write(fds[1], buf, len) != len) {
			perror("write to pipe");
			break;
		}
		memset(buf, 0x00, sizeof(buf));
		
			// read from pipe
			if ((len = read(fds[0], buf, 100)) == -1) {
				perror("read from pipe");
				break;
		}
	
			// write to stdout
			if (write(1, buf, len) != len) {
				perror("write to stdout");
				break;
		}
	}
}

⽤ fork 来共享管道原理

站在⽂件描述符⻆度-深度理解管道

站在内核⻆度-管道本质

所以,看待管道,就如同看待⽂件⼀样!管道的使⽤和⽂件⼀致,迎合了“Linux⼀切皆⽂件思想”。

管道样例

测试管道读写

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char* argv[])
{
	int pipefd[2];
	if (pipe(pipefd) == -1)
		ERR_EXIT("pipe error");
	pid_t pid;
	pid = fork();
	if (pid == -1)
		ERR_EXIT("fork error");
	if (pid == 0) {
		close(pipefd[0]);
		write(pipefd[1], "hello", 5);
		close(pipefd[1]);
		exit(EXIT_SUCCESS);
	}
	close(pipefd[1]);
	char buf[10] = { 0 };
	read(pipefd[0], buf, 10);
	printf("buf=%s\n", buf);
	return 0;
}

创建进程池处理任务

-- Channel.hpp --

#ifndef __CHANNEL_HPP__
#define __CHANNEL_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
// 先描述
class Channel
{
public:
	Channel(int wfd, pid_t who) : _wfd(wfd), _who(who)
	{
		// Channel-3-1234
		_name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
	}
	std::string Name()
	{
		return _name;
	}
	void Send(int cmd)
	{
		::write(_wfd, &cmd, sizeof(cmd));
	}
	void Close()
	{
		::close(_wfd);
	}
	pid_t Id()
	{
		return _who;
	}
	int wFd()
	{
		return _wfd;
	}
	~Channel()
	{
	}
private:
	int _wfd;
	std::string _name;
	pid_t _who;
};
#endif

-- ProcessPool.hpp --

#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Task.hpp"
#include "Channel.hpp"
// typedef std::function<void()> work_t;
using work_t = std::function<void()>;
enum
{
	OK = 0,
	UsageError,
	PipeError,
	ForkError
};
class ProcessPool
{
public:
	ProcessPool(int n, work_t w)
		: processnum(n), work(w)
	{
	}
	// channels : 输出型参数
	// work_t work: 回调
	int InitProcessPool()
	{
		// 2. 创建指定个数个进程
		for (int i = 0; i < processnum; i++)
		{
			// 1. 先有管道
			int pipefd[2] = { 0 };
			int n = pipe(pipefd);
			if (n < 0)
				return PipeError;
			// 2. 创建进程
			pid_t id = fork();
			if (id < 0)
				return ForkError;
			// 3. 建⽴通信信道
			if (id == 0)
			{
				// 关闭历史wfd
				std::cout << getpid() << ", child close history fd: ";
				for (auto& c : channels)
				{
					std::cout << c.wFd() << " ";
					c.Close();
				}
				std::cout << " over" << std::endl;
				::close(pipefd[1]); // read
				// child
				std::cout << "debug: " << pipefd[0] << std::endl;
				dup2(pipefd[0], 0);
				work();
				::exit(0);
			}
			// ⽗进程执⾏
			::close(pipefd[0]); // write
			channels.emplace_back(pipefd[1], id);
			// Channel ch(pipefd[1], id);
			// channels.push_back(ch);
		}
		return OK;
	}
	void DispatchTask()
	{
		int who = 0;
		// 2. 派发任务
		int num = 20;
		while (num--)
		{
			// a. 选择⼀个任务, 整数
			int task = tm.SelectTask();
			// b. 选择⼀个⼦进程channel
			Channel& curr = channels[who++];
			who %= channels.size();
			std::cout << "######################" << std::endl;
			std::cout << "send " << task << " to " << curr.Name() << ", 任务还剩: " << num << std::endl;
			std::cout << "######################" << std::endl;
			// c. 派发任务
			curr.Send(task);
			sleep(1);
		}
	}
	void CleanProcessPool()
	{
		// version 3
		for (auto& c : channels)
		{
			c.Close();
			pid_t rid = ::waitpid(c.Id(), nullptr, 0);
			if (rid > 0)
			{
				std::cout << "child " << rid << " wait ... success" <<
					std::endl;
			}
		}
		// version 2
		// for (auto &c : channels)
		// for(int i = channels.size()-1; i >= 0; i--)
		// {
		// channels[i].Close();
		// pid_t rid = ::waitpid(channels[i].Id(), nullptr, 0); // 阻塞了!
		// if (rid > 0)
		// {
		// std::cout << "child " << rid << " wait ... success" << std::endl;
		// }
		// }
		// version 1
		// for (auto &c : channels)
		// {
		// c.Close();
		// }
		//?
		// for (auto &c : channels)
		// {
		// pid_t rid = ::waitpid(c.Id(), nullptr, 0);
		// if (rid > 0)
		// {
		// std::cout << "child " << rid << " wait ... success" << std::endl;
		// }
		// }
	}
	void DebugPrint()
	{
		for (auto& c : channels)
		{
			std::cout << c.Name() << std::endl;
		}
	}
private:
	std::vector<Channel> channels;
	int processnum;
	work_t work;
	};
#endif

-- Task.hpp --

#pragma once
#include <iostream>
#include <unordered_map>
#include <functional>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
using task_t = std::function<void()>;
class TaskManger
{
public:
	TaskManger()
	{
		srand(time(nullptr));
		tasks.push_back([]()
			{ std::cout << "sub process[" << getpid() << " ] 执⾏访问数据库的任务\n"<< std::endl; });
		tasks.push_back([]()
			{ std::cout << "sub process[" << getpid() << " ] 执⾏url解析\n"<< std::endl; });
		tasks.push_back([]()
			{ std::cout << "sub process[" << getpid() << " ] 执⾏加密任务\n"<< std::endl; });
		tasks.push_back([]()
			{ std::cout << "sub process[" << getpid() << " ] 执⾏数据持久化任务\n"<< std::endl; });
	}
	int SelectTask()
	{
		return rand() % tasks.size();
	}
	void Excute(unsigned long number)
	{
		if (number > tasks.size() || number < 0)
			return;
		tasks[number]();
	}
	~TaskManger()
	{
	}
private:
	std::vector<task_t> tasks;
};
TaskManger tm;
void Worker()
{
	while (true)
	{
		int cmd = 0;
		int n = ::read(0, &cmd, sizeof(cmd));
		if (n == sizeof(cmd))
		{
			tm.Excute(cmd);
		}
		else if (n == 0)
		{
			std::cout << "pid: " << getpid() << " quit..." << std::endl;
			break;
		}
		else
		{
		}
	}
}

-- Main.cc --

#include "ProcessPool.hpp"
#include "Task.hpp"
void Usage(std::string proc)
{
	std::cout << "Usage: " << proc << " process-num" << std::endl;
}
// 我们⾃⼰就是master
int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		Usage(argv[0]);
		return UsageError;
	}
	int num = std::stoi(argv[1]);
	ProcessPool* pp = new ProcessPool(num, Worker);
	// 1. 初始化进程池
	pp->InitProcessPool();
	// 2. 派发任务
	pp->DispatchTask();
	// 3. 退出进程池
	pp->CleanProcessPool();
	// std::vector<Channel> channels;
	// // 1. 初始化进程池
	// InitProcessPool(num, channels, Worker);
	// // 2. 派发任务
	// DispatchTask(channels);
	// // 3. 退出进程池
	// CleanProcessPool(channels);
	delete pp;
	return 0;
}

-- Makefile --

BIN = processpool
CC = g++
FLAGS = -c - Wall - std = c++11
LDFLAGS = -o
# SRC = $(shell ls * .cc)
SRC = $(wildcard * .cc)
OBJ = $(SRC:.cc = .o)

$(BIN) :$(OBJ)
    $(CC) $(LDFLAGS) $@ $ ^
%.o: % .cc
    $(CC) $(FLAGS) $ <
	.PHONY : clean
	clean :
rm - f $(BIN) $(OBJ)
.PHONY : test
test :
	@echo $(SRC)
	@echo $(OBJ)

管道读写规则

当没有数据可读时

O_NONBLOCK disable:read调⽤阻塞,即进程暂停执⾏,⼀直等到有数据来到为⽌。

O_NONBLOCK enable:read调⽤返回-1,errno值为EAGAIN。

当管道满的时候

O_NONBLOCK disable: write调⽤阻塞,直到有进程读⾛数据

O_NONBLOCK enable:调⽤返回-1,errno值为EAGAIN

如果所有管道写端对应的⽂件描述符被关闭,则read返回0

如果所有管道读端对应的⽂件描述符被关闭,则write操作会产⽣信号SIGPIPE,进⽽可能导致write进程退出

当要写⼊的数据量不⼤于PIPE_BUF时,linux将保证写⼊的原⼦性。

当要写⼊的数据量⼤于PIPE_BUF时,linux将不再保证写⼊的原⼦性。

管道特点

只能⽤于具有共同祖先的进程(具有亲缘关系的进程)之间进⾏通信;通常,⼀个管道由⼀个进程创建,然后该进程调⽤fork,此后⽗、⼦进程之间就可应⽤该管道。

管道提供流式服务

⼀般⽽⾔,进程退出,管道释放,所以管道的⽣命周期随进程

⼀般⽽⾔,内核会对管道操作进⾏同步与互斥

管道是半双⼯的,数据只能向⼀个⽅向流动;需要双⽅通信时,需要建⽴起两个管道

Linux——进程间通信 之前写的代码都是单进程的。是无法使用并发能力,并且无法实现多进程协同传输数据,同步执行流,消息通知等进程间通信不是目的,而是一种手段。进程间通信的技术背景进程是具有独立性的。虚拟地址空间+页表。保证进程运行的独立性(进程内核数据结构+进程的代码和数据)通信成本比较高。如何理解进程间通信?进程运行具有独立性!——进程想通信,难度比较大——进程间通信的本质:需要中间媒介,先让不同的进程看到同一份资源(内存空间)所谓的进程看到同一块“内存”,属于哪一个进程? 阅读详情

相关推荐

KUKA机器人与S7-1200 PLC实现Profinet通讯配置步骤详解

在工业自动化产线中,PLC与工业机器人之间的高效数据交换是控制逻辑落地的关键。传统硬接线方案在信号点多时布线复杂、故障排查困难,而现场总线技术通过一根网线即可实现主从设备间的周期通讯。Profinet作为主流工业以太网协议,由S7-1200作为IO Controller主站,KUKA机器人作为IO Device从站,在WorkVisual与TIA Portal中完成设备组态、I/O映射及信号分配,即可替代成捆电缆,稳定传输启停、状态及报警信号。这一方案适用于汽车、3C、物流等行业的自动化产线,尤其适合需要扩

weixin_42519985的博客 251

Linux:进程间通信

一篇带你完全掌握进程间通信

Makise的博客 2098

XGBoost与Embedding混合建模实战解析

在机器学习领域,特征工程与模型融合是提升预测性能的关键技术。Embedding技术通过将高维稀疏特征(如文本、ID类数据)转化为低维稠密向量,有效捕获语义关联;而XGBoost作为强大的集成学习算法,擅长处理结构化特征并进行自动特征组合。两者结合的混合架构(如Semantic Boosted Trees)在金融风控和推荐系统等场景中展现出显著优势,尤其在处理用户行为序列、商品ID等复杂特征时,相比传统方法能获得更好的模型效果。本文通过电商推荐和金融反欺诈等案例,详解如何实现Embedding+XGBoost

weixin_28718641的博客 324

Linux进程间通信

进程间通信(IPC)是指不同进程之间的数据交换和通信。在多进程环境下,不同的进程需要共享内存、文件等资源,但是每个进程都有自己独立的地址空间,因此需要通过进程间通信来实现进程之间的数据交换和共享。进程间通信使得进程间可以进行数据传输、资源共享、通知事件等。例如,一个进程需要将它的数据发送给另一个进程,或者多个进程之间共享同样的资源,或者一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件等。本节我们将为大家介绍进程间通信的相关知识!

ARMCSKGT的博客 2752

SQLSTATE58030

在优化一条SQL的时候遇到了执行错误 gtlions =# select t1.telnumber,t2.ua,t2.url,t1.apply_name,t2.apply_name from tb1 t1 gtlions -# left outer join tb2 t2 on t1.sid = t2.ipsid gtlions-# where t1.sum_date=20141128 a

Gtlions Tracker 3659

Crazyflie 2.1固件刷写与地址配置全攻略:从开箱到连接Crazyswarm

本文详细介绍了Crazyflie 2.1微型无人机的固件刷写、地址配置及与Crazyswarm系统的集成方法。从开箱检查到多机协同飞行,涵盖了硬件准备、固件刷写、无线电地址修改等关键步骤,帮助用户快速实现集群飞行控制。特别针对Crazyswarm系统集成提供了实用配置示例和调试技巧。

weixin_42530458的博客 404

【Linux进程间通信】Linux匿名管道详解:构建进程间通信的隐形桥梁

当提及Linux系统中的进程间通信(IPC),管道(Pipes)无疑是最基础且广泛使用的一种机制。作为匿名通信的典范,管道为进程间数据交换提供了一个简单而有效的途径。在这个信息飞速传递的时代,掌握Linux管道的使用不仅是理解操作系统底层通信原理的关键一步,也是提升软件开发效率、构建复杂应用系统的必备技能

EterNity_TiMe_的计算机学习之路 4088

EBSD数据转ABAQUS的inp文件实操指南

晶体塑性有限元模拟中,如何将材料微观组织表征数据转化为可计算的有限元模型,是连接实验与仿真的关键环节。EBSD技术能够获取材料表面的晶体取向、晶粒形貌和晶界分布信息,但这类数据需要经过格式解析、欧拉角转换、网格生成等步骤,才能被ABAQUS等有限元软件读取。通过将扫描点坐标映射为节点、按像素邻域生成单元,并将Bunge约定欧拉角变换为局部坐标系方向余弦,用户可以准确构建包含真实微观结构的inp文件模型。这一流程广泛应用于金属材料形变、织构演化及应力集中分析等研究场景,有效提升仿真结果与实验的一致性。围绕数据

weixin_42509513的博客 310

【Linux】进程间通信(一)

本篇文章概述了进程间通信的目的、理解、发展及分类,并重点介绍了管道通信机制,在匿名管道部分,详细讲解了pipe函数、实现方式及小结,包括四种常见情况和五种关键特性,并提及了匿名管道在进程池中的应用。命名管道部分则从指令级和代码级两个层面,分别介绍了其创建与使用方法。

是阿建吖! 1257

N32在Keil中J-Link识别失败的排查与修复指南

本文详细解析了在Keil开发环境中,J-Link调试器无法识别N32系列MCU的常见问题。通过分步指导,从检查修改Keil工程配置文件、更新J-Link驱动,到手动添加N32专用设备支持包,系统性地提供了排查与修复方案,帮助开发者快速恢复调试连接,确保开发流程顺畅。

300

hadoop 切片机制详解

hadoop切片机制详解

congge 1万+

【Linux】进程间通信(一):认识管道

【Linux】进程间通信(一):认识管道,带你认识管道,使用管道,理解管道原理

tan_run的博客 2299

【100%通过率】华为OD机试真题 C 实现【最多等和不相交连续子序列】【2022.11 Q4 新题】

给定一个数组,我们称其中连续的元素为连续子序列,称这些元素的和为连续子序列的和。数组中可能存在几组连续子序列,组内的连续子序列互不相交且有相同的和。求一组连续子序列,组内子序列的数目最多。输出这个数目。

MISAYAONE的博客 1万+

【Linux】进程间通信

一文带你由浅入深掌握匿名管道、命名管道、共享内存、消息队列、信号量!

m0_74808907的博客 2884

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

<Linux> 进程间通信

进程间通信,匿名管道、命名管道、共享内存、消息队列、信号量

prodigy623的博客 3481

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Linux系统-进程间通信

本章主要讲解学习Linux中本系统下的进程间通信概念:进程间通信简称IPC(Inter process communication),进程间通信就是在不同进程之间传播或交换信息进程间通信目的:数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)

~ 4027

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Linux学习——进程间通信(IPC)(一)

回顾之前提到的话题,fork()函数被调用一次,都有两个返回值。两次返回值的区别是子进程的返回值是0,父进程的返回值是子进程的进程id。之所以将子进程的进程id返回给父进程是因为:一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id;对于子进程来说,之所以fork()返回0给他,是因为他可以随时调用getpid()来获取自己的进程号,也可以通过getppid()来获得父进程的进程号。

m0_70569293的博客 2087

Linux进程间通信(一)——管道通信

在学习Linux中的进程时,曾提到过进程的独立性。进程独立性的是进程与进程之间在出错时不会相互影响的重要保证。但是由此也引发的一个问题,如果进程之间想要相互传递信息怎么办,由于进程与进程之间独立,一个进程不能访问另一个进程中的资源与数据等信息。这个时候就需要在进程之间建立信道来实现进程间通信。本文介绍进程间通信中的一个方式——管道通信。

m0_73987904的博客 1802

Linux之进程间通信

Linux之进程通信

2302_81120572的博客 1942

Linux 详谈进程间通信(二)

本文介绍了两种进程间通信(IPC)方式:命名管道和共享内存。命名管道通过mkfifo创建,允许无关进程通信,其打开行为受O_NONBLOCK影响,并提供了文件拷贝和服务端-客户端通信的实现示例。共享内存是最高效的IPC形式,通过shmget创建、shmat/shmat控制连接,但缺乏同步机制,需配合管道实现访问控制。文中详细说明了相关系统调用,并给出两个完整应用案例:基础通信版本和带同步控制的改进版本,展示了如何结合管道实现共享内存的互斥访问。

yuzhaokangcode的博客 1043

Linux之进程间通信(一)

操作系统的特点之一以实现多任务并发处理.既然进程具有独立性, 那一定会让二者产生联系, 那又何谈独立性呢?进程的独立性是指的, 但. 进程与进程间是需要信息交互与状态传递等的通信. 比如说, 父进程等待子进程查看子进程状态, 在子进程完成任务时回收子进程信息, 而且有些程序的运行也需要才能完成。比如说这个指令:cat file | grep 'hello'

ZZY5707的博客 1000

【linux】进程间通信(一)匿名管道,pipe

一、进程间通信 是什么 为什么 怎么办 二、什么是管道 三、管道的原理 浅谈原理 深谈原理 四、接口pipe 五、编码实现 框架 读写方法实现 测试一 测试二 测试三 测试四 测试五 六、管道的特征 七、管道的四种情况 八、管道的应用场景 命令行中的匿名管道 | 简易版本的进程池

重铸自身根基的博客 1342

【Linux】10. 进程间通信

本文讲述了4种进程间通信的手段:管道(匿名管道和命名管道)、System V标准下的3种通信方式(共享内存、消息队列和信号量)。无论是哪种进程间通信方式,本质上都是由操作系统提供一个公共空间作为桥梁,多个进程之间在这片公共空间上进行读写操作,以此来达成进程间通信的目的。

weixin_52811588的博客 2352

Linux: 进程间通信

在我们学习进程的时候,我们知道正是因为程序地址空间的存在,所以进程之间具有独立性,他们互不影响,但是在我们的实际应用中,进程之间总会有需要通信(交流获取对方数据)。

2301_79927432的博客 2822
上一篇: Linux 详谈库制作与原理
下一篇: Linux 详谈进程间通信(二)
万我.
博客等级 码龄1年 460粉丝 93原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值