【Linux C | 进程】Linux 进程间通信的10种方式(1)

这篇具有很好参考价值的文章主要介绍了【Linux C | 进程】Linux 进程间通信的10种方式(1)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭

本文未经允许,不得转发!!!


【Linux C | 进程】Linux 进程间通信的10种方式(1),# ✨Linux C语言,linux,Linux进程间通信,管道,消息队列,信号量,共享内存
下表是进程间通信的十种方式

🎄一、管道(无名管道)

✨1.1 管道介绍

管道是半双工的通信方式,数据只能单向流动,管道的作用是在有亲缘关系的进程之间传递消息。所谓亲缘关系是指,只要调用进程使用pipe函数, 打开的管道文件就会在fork之后, 被各个后代进程所共享。

这个无名管道可以理解为:没有实体文件与之关联, 靠的是世代相传的文件描述符来进行数据的读写。

无名管道可以使用函数pipe来创建,函数原型如下:

#include <unistd.h>
int pipe(int pipefd[2]);

✨1.2 例子

看使用例子,父进程调用了pipe函数创建了管道文件,fork之后的子进程可以直接使用管道文件:

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

#define PIPE_INPUT		0
#define PIPE_OUTPUT		1

int main()
{
	int pipe_fds[2];
	pipe(pipe_fds); // 创建无名管道
	
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 关闭输入管道,写数据到输出管道\n", getpid()); 
		close(pipe_fds[PIPE_INPUT]);// 关闭输入管道
		write(pipe_fds[PIPE_OUTPUT], "test data", strlen("test data"));// 写入管道
        exit(0);
    }
	else if(pid > 0)
	{
		sleep(2); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 关闭输出管道,读取管道数据\n", getpid()); 
		close(pipe_fds[PIPE_OUTPUT]);// 关闭输出管道
		char buf[256] = {0,};
		int readSize = read(pipe_fds[PIPE_INPUT], buf, sizeof(buf));
		printf("父进程[%d]从管道读取到%d个字节的数据[%s]\n", getpid(), readSize, buf); 
        exit(0);
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

在这里插入图片描述

🎄二、命名管道FIFO

✨2.1 命名管道FIFO介绍

上面的无名管道没有与实体文件关联,靠的是世代相传的文件描述符来进行数据交换。命名管道就是为了解决无名管道的这个问题而引入的。 FIFO与管道类似, 最大的差别就是有实体文件与之关联。 由于存在实体文件, 不相关的、没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。

创建命名管道的3种方式:

  • 1、调用C语言接口函数mkfifo创建:mkfifo("my_fifo", 0666);
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    
  • 2、使用mkfifo命令创建:mkfifo -m 0666 my_fifo
  • 3、使用mknod命令创建:mknod -m 0666 my_fifo p

一旦FIFO文件创建好了, 就可以把它用于进程间的通信了。 一般的文件操作函数如open、 read、 write、 close、 unlink等都可以用在FIFO文件上。

✨2.2 例子

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

int main()
{
	if(0 == access("./my_fifo",F_OK))
	{
		system("rm ./my_fifo");
	}
	/*创建管道文件, 下次运行需要先删除my_fifo文件,否则mkfifo报错*/
    if(mkfifo("my_fifo", 0666) < 0)
    {
        perror("mkfifo");
        return -1;
    }
	
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 打开my_fifo文件,循环往里写数据\n", getpid());
		int fd = open("my_fifo", O_WRONLY);
		if(fd < 0)
		{
			return -1;
		}
		int i = 9;
		while(i>=0)
		{
			printf("子进程[%d]写入数据:%d\n", getpid(), i);
			char buf[256] = {0,};
			sprintf(buf,"%d",i);
			write(fd, buf, strlen(buf));
			i--;
			sleep(1);
		}
		close(fd);
		printf("子进程[%d]退出\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(5); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 打开my_fifo文件,读取数据\n", getpid()); 
		int fd = open("my_fifo", O_RDONLY);
		if(fd < 0)
		{
			return -1;
		}
		char buf[256] = {0,};
		int readSize = 0;
		while((readSize = read(fd, buf, sizeof(buf)) ) > 0)
		{
			printf("父进程[%d]读取到%d个字节数据:[%s]\n", getpid(),readSize, buf);
			memset(buf, 0, sizeof(buf));
		}
		close(fd);
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

运行结果:
在这里插入图片描述

在这里插入图片描述

🎄三、消息队列(System V IPC)

✨3.1 消息队列(System V IPC)介绍

有三种被称为XSI IPC的进程间通信,消息队列,信号量,共享内存。XSI IPC函数是基于System V的IPC函数。这里介绍的消息队列就属于其中一种,后面还有介绍其余两种,消息队列比较少用了,是一种逐渐被淘汰的通信方式,为了完整性,这里还是介绍一下,感兴趣的可以继续了解。

前面的管道通信,如果从管道中读取到100个字节,你无法确认这100个字节是单次写入的100字节, 还是分10次每次10字节写入的, 你也无法知晓这100个字节是几个消息。System V消息队列就不存在这种问题,因为它是基于消息通信的。无需从字节流解析完整的消息,而且每个消息有type字段作为消息类型。

消息队列编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种:
    ①随机选择一个整数值作为key值,这个值必须不和其他key重复,例如:#define MSG_KEY 10086
    ②使用IPC_PRIVATE,例如:id = msgget(IPC_PRIVATE,S_IRUSR | S_IWUSR);
    ③使用ftok函数, 根据文件名生成一个key,例如:key_t key = ftok(".", 100);
  • 2、使用msgget()创建/获取消息队列,返回值为队列标识符。
    服务端创建:int msgid = msgget(key, 0666|IPC_CREAT);
    客户端获取:int msgid = msgget(key, 0);
  • 3、写入/取出消息;
    服务端写入:msgsnd(msgid, &msg, sizeof(msg.buf), 0);
    客户端获取:msgrcv(msgid, &msg, sizeof(msg)-sizeof(long), 0, 0);
  • 4、msgctl删除消息队列
    msgctl(msgid, IPC_RMID, NULL);

✨3.2 例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

typedef struct _MSG_TYPE
{
    long mtype;//消息类型
    char buf[256];//有效数据
}MSG_TYPE;

int main()
{
	// 1 生成key
    key_t key = ftok(".", 100);

	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建消息队列,循环往里写数据\n", getpid());
		// 创建消息队列
		int msgid = msgget(key, 0666|IPC_CREAT);
		if(msgid == -1)
		{
			perror("msgget failed");
			exit(1);
		}
		// 发送数据
		int i = 9;
		MSG_TYPE msg;
		while(i>=0)
		{
			memset(&msg, 0, sizeof(msg));
			msg.mtype = i;
			sprintf(msg.buf, "hello-%d", i);
			msgsnd(msgid, &msg, sizeof(msg.buf), 0);//阻塞
			printf("子进程[%d]写入数据:hello-%d\n", getpid(), i);
			i--;
			sleep(1);
		}
		
		// 删除队列
		if(msgctl(msgid, IPC_RMID, NULL) == -1)
		{
			perror("msgctl failed");
			exit(3);
		}
		printf("子进程[%d]退出\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取消息队列,读取数据\n", getpid()); 
		int msgid = msgget(key, 0); 
		if(msgid == -1)
		{
			perror("msgget failed");
			exit(1);
		}
		MSG_TYPE msg;
		while(1)
		{
			memset(&msg, 0, sizeof(msg));
			int res = msgrcv(msgid, &msg, sizeof(msg)-sizeof(long), 0, 0);//阻塞
			printf("res=%d, 消息:%s, 类型:%ld\n", res, msg.buf, msg.mtype);
			if(res == -1)
			{
				perror("msgrcv failed");
				break;
			}
		}
		
		// 删除队列
		if(msgctl(msgid, IPC_RMID, NULL) == -1)
		{
			perror("msgctl failed");
			exit(3);
		}
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

运行结果:
在这里插入图片描述

在这里插入图片描述

🎄四、信号量(System V IPC)

✨4.1 消息队列(System V IPC)介绍

信号量的作用是为了同步多个进程的操作。一般来说, 信号量是和某种预先定义的资源相关联的。

信号量是一个计数器,控制访问共享资源的最大并行进程总数。可以通过下面这个故事来了解信号量。

一套豪宅里有8个一模一样的卫生间和8把通用的钥匙。最初有8把钥匙放在钥匙存放处。 当同时使用卫生间的人数小于或等于8时, 大家都可以拿到一把钥匙, 各自使用各自的卫生间。 但是到第9个人和第10个人要使用卫生间时, 发现已经没有钥匙了, 所以他们就不得不等待了。

使用最广泛的信号量是二值信号量(binary semaphore), 对于这种信号量而言, 它只有两种合法值: 0和1, 对应一个可用的资源。 若当前有资源可用, 则与之对应的二值信号量的值为1; 若资源已被占用, 则与之对应的二值信号量的值为0。 当进程申请资源时, 如果当前信号量的值为0, 那么进程会陷入阻塞, 直到有其他进程释放资源, 将信号量的值加1才能被唤醒。

资源个数超过1个的信号量称为计数信号量(counting semaphore),例如,有个8个资源,最大同时允许8个进程使用。

信号量编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种。参考上一节消息队列编程步骤;
  • 2、使用int semget(key_t key, int nsems, int semflg);创建/获取信号量集,返回值为信号量集标识符。
    第二个参数nsems表示信号量集中信号量的个数。如果并非创建信号量, 仅仅是访问已经存在的信号量集, 可以将nsems指定为0。
    semflg支持多种标志位。 目前支持IPC_CREAT和IPC_EXCL标志位
  • 3、设置信号量的初始值 int semctl(int semid, int semnum, int cmd,/* union semun arg*/);
  • 4、正常使用,实现信号量的++ --的原子性int semop(int semid, struct sembuf *sops, unsigned nsops);
  • 5、semctl删除消息信号量
    semctl(semid, 0, IPC_RMID);

✨4.2 例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 生成key
#define SEM_KEY		10086

int main()
{
	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建信号量,使用资源\n", getpid());
		// 创建信号量集
		int semid = semget(SEM_KEY, 1, IPC_CREAT|0666);
		if(semid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 设置第0个信号量的资源数量为1
		if(semctl(semid, 0, SETVAL, 1) == -1)
		{
			perror("semctl setval failed");
			exit(1);
		}
		
		// 使用资源,数量 -1
		struct sembuf op;
		op.sem_num = 0;//对下标为0的信号量操作
		op.sem_op = -1;//对信号量-1
		op.sem_flg = 0;//无法完成时阻塞等待
		semop(semid, &op, 1);
		printf("子进程[%d]访问共享资源\n", getpid());
		sleep(20);
		printf("子进程[%d]完成共享资源的访问\n",getpid());
		
		// 释放资源,数量 +1
		op.sem_op = 1;
		semop(semid, &op, 1);
		
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取信号量,准备使用资源\n", getpid()); 
		int semid = semget(SEM_KEY, 0, 0);
		if(semid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 使用资源,数量 -1
		struct sembuf op;
		op.sem_num = 0;//对下标为0的信号量操作
		op.sem_op = -1;//对信号量-1
		op.sem_flg = 0;//无法完成时阻塞等待
		semop(semid, &op, 1);
		printf("父进程[%d]访问共享资源\n", getpid());
		sleep(3);
		printf("父进程[%d]完成共享资源的访问\n",getpid());
		
		// 释放资源,数量 +1
		op.sem_op = 1;
		semop(semid, &op, 1);
		
		// 删除信号量
		if(semctl(semid, 0, IPC_RMID) == -1)
		{
			perror("semctl failed");
			exit(3);
		}
		printf("父进程[%d]退出\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

在这里插入图片描述

🎄五、共享内存(System V IPC)

✨5.1 共享内存(System V IPC)介绍

共享内存是所有IPC手段中最快的一种。 它之所以快是因为共享内存一旦映射到进程的地址空间,进程之间数据的传递就不须要涉及内核了。

建立共享内存之后, 进程从此就像操作普通进程的地址空间一样操作这块共享内存, 一个进程可以将信息写入这片内存区域, 而另一个进程也可以看到共享内存里面的信息, 从而达到通信的目的。

允许多个进程同时操作共享内存, 就不得不防范竞争条件的出现。因此, 共享内存这种进程间通信的手段通常不会单独出现, 总是和信号量、 文件锁等同步的手段配合使用。

信号量编程步骤:

  • 1、生成 key,System V IPC的标识ID都是通过key来获取的,key的生成方式有三种。参考上一节消息队列编程步骤;
  • 2、使用int shmget(key_t key, size_t size, int shmflg);创建/获取共享内存段,返回值为共享内存的标识符。
    其中第二个参数size必须是正整数, 表示要创建的共享内存的大小。
    第三个参数支持IPC_CREAT和IPC_EXCL标志位。 如果没有设置IPC_CREAT标志位, 那么第二个参数size对共享内存段并无实际意义, 但是必须小于或等于共享内存的大小, 否则会有EINVAL错误。
  • 3、映射共享内存,得到虚拟地址。void *shmat(int shmid, const void *shmaddr, int shmflg);
    其中, 第二个参数是用来指定将共享内存放到虚拟地址空间的什么位置的。 大部分的普通青年都会将第二个参数设置为NULL, 表示用户并不在意, 一切交由内核做主。
    shmat如果调用成功, 则返回进程虚拟地址空间内的一个地址。就可以像使用malloc分配的空间一样使用共享内存。
  • 4、读写共享内存数据。
  • 5、解除映射。int shmdt(const void *shmaddr);
  • 6、销毁共享内存。shmctl(shmid, IPC_RMID, NULL) ;

✨5.2 例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

// 生成key
#define SHM_KEY		10010

int main()
{
	// 2 创建子进程
	pid_t pid = fork();
    if(pid == 0) {// 子进程
		printf("子进程[%d]开始执行, 创建共享内存段,使用创建共享内存\n", getpid());
		// 2.1 创建共享内存段
		int shmid = shmget(SHM_KEY, 8, IPC_CREAT|0666);
		if(shmid == -1)
		{
			perror("semget failed");
			exit(1);
		}
		
		// 2.2 映射共享内存,得到虚拟地址
		void *p = shmat(shmid, 0, 0);
		 if((void *)-1 == p)
		{
			perror("shmat failed");
			exit(2);
		}
		
		// 2.3 读写共享内存
		int *pi = p;
		*pi = 0xaaaaaaaa;
		*(pi+1) = 0x55555555;
		printf("子进程[%d]写入%x, %x\n", getpid(), *pi, *(pi+1));
		
		// 2.4 解除映射
		if(shmdt(p) == -1)
		{
			perror("shmdt failed");
			exit(3);
		}
		printf("子进程[%d]解除映射, 结束进程\n\n", getpid());
        return 0;
    }
	else if(pid > 0)// 父进程
	{
		sleep(3); //延时一会,让子进程先运行
		printf("父进程[%d]开始执行, 获取共享内存段,准备使用资源\n", getpid()); 
		// 3.1 获取共享内存段
		int shmid = shmget(SHM_KEY, 0, 0);
		if(shmid == -1)
		{
			perror("shmget failed");
			exit(1);
		}
		
		// 3.2 映射共享内存,得到虚拟地址
		void *p = shmat(shmid, 0, 0);
		if((void *)-1 == p)
		{
			perror("shmat failed");
			exit(2);
		}
		
		// 3.3 读写共享内存
		int x = *((int *)p);
		int y = *((int *)p + 1);
		printf("父进程[%d]读取数据:x=%#x y=%#x\n",getpid(), x, y);
		
		// 3.4 解除映射
		if(shmdt(p) == -1)
		{
			perror("shmdt failed");
			exit(3);
		}
		printf("父进程[%d]解除映射\n", getpid());
		
		// 3.5 销毁共享内存
		if(shmctl(shmid, IPC_RMID, NULL) == -1)
		{
			perror("shmctl");
			exit(4);
		}
		printf("父进程[%d]销毁共享内存, 结束进程\n", getpid());
        return 0;
	}
	else
	{
		printf("Error in fork\n"); 
        exit(1); 
	}
	
	return 0;
}

允许结果:
在这里插入图片描述

在这里插入图片描述

🎄六、总结

Linux 进程间通信有10种方式,本文先介绍了5种:无名管道、命名管道、XSI消息队列、XSI信号量、XSI共享内存,下篇文章将会介绍剩下的5个方式:POSIX消息队列、POSIX信号量、POSIX共享内存、信号、网络通信。

Linux 进程间通信的10种方式(2)

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁文章来源地址https://www.toymoban.com/news/detail-824934.html

到了这里,关于【Linux C | 进程】Linux 进程间通信的10种方式(1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux C | 进程】Linux 进程间通信的10种方式(2)

    😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭 🤣本文内容🤣:🍭介绍 🍭 😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭 本文未经允许,不得转发!!! POSIX消息队列与

    2024年02月20日
    浏览(31)
  • 【Linux】进程通信之匿名管道通信

    我们往往需要多个进程协同,共同完成一些事情。 数据传输:一个进程需要将它的数据发送给另一个进程 资源共享:多个进程之间共享同样的资源。 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。

    2024年04月14日
    浏览(44)
  • 【Linux】进程通信之管道通信详解

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 其实管道通信是Unix中最古老的进程间通信的形式了: 管道通信是一种进程间通信的方式,它可以让一个进程的输出作为另一个进程的输入,实现数据的传输、资源的共享、事件的通知和进程的控制。 管道通信分为两种类型:匿名

    2023年04月19日
    浏览(38)
  • Linux进程通信:无名管道

    (1)数据传输:进程间数据传输; (2)通知事件:一个进程向另一个或一组进程发送消息,通知某个事件的发生(如子进程终止时需通知父进程); (3)资源共享:多个进程共享资源,需要内核提供同步互斥机制; (4)进程控制:某进程需要控制另一个进程的执行(如

    2023年04月24日
    浏览(52)
  • Linux——进程间通信、管道

    进程间的通信就是 在不同进程之间传播或交换信息。 举个例子: 古时,两军交战不斩来使; 因为两军互相是独立的,所以使节就是两军之间传话的进行传话的; 而在OS中,进程之间也是相互独立的,但某项工作并不是一个进程就可以完成,而是多个进程之间相互协助完成;

    2024年02月22日
    浏览(37)
  • Linux——进程间通信(管道)

    目录 进程通信的目的 管道 见见猪跑(举个例子) 文件描述符fd与管道的关系(深度理解管道) 什么是管道?  匿名管道 pipe函数概述 父子进程通信时与文件描述符的关系图(理解pipe函数的关键) pipe函数的使用  管道读写规则 管道的大小 自测  使用man 7 pipe查看 使用ulimit -a查看 管

    2024年02月03日
    浏览(83)
  • [Linux]进程间通信--管道

    数据传输:一个进程需要将它的数据发送给另一个进程 。 资源共享:多个进程之间共享同样的资源。 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 进程控制:有些进程希望完全控制另一个进程的执

    2024年02月09日
    浏览(39)
  • linux——进程间通信——管道

     ✅1主页::我的代码爱吃辣 📃2知识讲解:Linux——进程间通信——管道通信 ☂️3开发环境:Centos7 💬4前言:进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。 目录 一.什么是进程间通信 二.进程间通信目的  三.进程间通信发展 四.什么是管道

    2024年02月08日
    浏览(42)
  • 【Linux】进程通信 — 管道

    从本章开始,我们开始学习进程通信相关的知识,本章将来详细探讨一下管道,学习匿名管道和命名管道的原理和代码实现等相关操作。目标已经确定,接下来就要搬好小板凳,准备开讲了…🙆🙆🙆🙆 在我们之前的学习中,我们知道进程是具独立性的。但是不要以为进程

    2024年02月16日
    浏览(39)
  • Linux——进程间通信&&管道

    📘北尘_ :个人主页 🌎个人专栏 :《Linux操作系统》《经典算法试题 》《C++》 《数据结构与算法》 ☀️走在路上,不忘来时的初心 数据传输:一个进程需要把他的数据传给另外一个进程。 资源共享:多个进程之间共享同样的资源。 通知事件:一个进程需要向另一个或一组

    2024年04月09日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包