【Linux】进程间通信(IPC)(第十四篇)

目录

1.消息队列

2.管道机制

1.匿名管道(PIPE)

2.命名管道

3.MMAP内存共享映射

4.信号

5.信号量

6.套接字

7.共享内存通信


进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

1.进程间通信的应用场景

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

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

常见的进程间通信的方式

进程间通信(IPC,Inter-process Communication)指的是在不同进程之间交换信息的机制和方法。常见的进程间通信方式有以下几种:

  • 管道(Pipe):管道是一种半双工的通信方式,只能在具有亲缘关系的进程之间使用。管道可以实现进程之间的通信,但只能在父子进程或兄弟进程之间使用,因为管道是单向的,而且只能在具有公共祖先的进程之间使用。

  • 命名管道(Named Pipe):命名管道是一种有名的通信方式,可以实现无关进程之间的通信。它可以在不具有亲缘关系的进程之间传递数据,并且可以实现双向通信。

  • 信号量(Semaphore):信号量是一种计数器,允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

  • 信号(Signal):信号是一种软件中断,用于通知进程发生了某个事件。它可以用于进程间的通信和同步。

  • 共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,它可以在多个进程之间共享内存区域,从而实现数据的快速交换。

  • 消息队列(Message Queue):消息队列是一种进程间通信方式,可以实现不同进程之间的异步通信。

  • 套接字(Socket):套接字是一种网络通信方式,可以实现不同主机之间的进程间通信。

1.消息队列

消息队列(Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。 消息队列的本质是消息的链表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。接收进程可以独立地接收含有不同类型的数据结构。

涉及到的函数:

key_t ftok(const char *pathname,int id);
为队列随机附加key,pathename为路径,id是子序号,虽然为int,但是只有8个比特被使用(0-255)可随意(1-255)
ftok函数内部实现过程
    在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,
换算成16进制为0x26,则最后的key_t返回值为0x26010002。
int msgget(key_t key, int msgflg); 
创建消息队列 , 返回值为该队列号
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
发送消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
接受信息,msgtyp需要指明接受的数据type
​
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//可用于消除内内核中的队列

示例:

send

#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
struct stru_msgdata
{
    long msgtype;
    char msgtext[1024];
};
​
​
int main()
{
   struct stru_msgdata msgsenddata={111,"love my son"};
   struct stru_msgdata msgrecvdata;
    key_t key =ftok(".",2);
    printf("the key is %x\n",key);
    int msgid = msgget(key,IPC_CREAT|0777);
    msgsnd(msgid,(void*)&msgsenddata,strlen(msgsenddata.msgtext)+1,0);
    ssize_t recvnum =msgrcv(msgid,(void*)&msgrecvdata,sizeof(msgrecvdata),222,0);
    if(recvnum >0)
     { printf("111 recv %s\n",msgrecvdata.msgtext);}
    
    msgctl(msgid,IPC_RMID,NULL);
    return 0;
}
​

recv:

#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
struct stru_msgdata
{
    long msgtype;
    char msgtext[1024];
};
​
​
int main()
{
   struct stru_msgdata msgsenddata={222,"love my house"};
   struct stru_msgdata msgrecvdata;
    key_t key =ftok(".",2);
    printf("the key is %x\n",key);
    int msgid = msgget(key,IPC_CREAT|0777);
    ssize_t recvnum =msgrcv(msgid,(void*)&msgrecvdata,sizeof(msgsenddata),111,0);
    if(recvnum >0)
     { printf("222 recv %s\n",msgrecvdata.msgtext);}
    
    msgsnd(msgid,(void*)&msgsenddata,strlen(msgsenddata.msgtext)+1,0);
    msgctl(msgid,IPC_RMID,NULL);
    return 0;
}

消息队列的优缺点

消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息,这和大多数通信协议是不同的。例如WWW中使用的HTTP协议(HTTP/2之前)是同步的,因为客户端在发出请求后必须等待服务器回应。然而,很多情况下我们需要异步的通信协议。比如,一个进程通知另一个进程发生了一个事件,但不需要等待回应。但消息队列的异步特点,也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。

和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。但消息队列仍然有大小限制。(用ipcs -l 命令来查看当前ipc 参数的各种设置)

消息队列除了可以当不同线程或进程间的缓冲外,更可以通过消息队列当前消息数量来侦测接收线程或进程性能是否有问题。

2.管道机制

1.匿名管道(PIPE)

匿名管道通过打开的文件描述符来标识的。——用于具有亲缘关系间进程之间的通信。

在内核空间创建一个管道缓冲区(环形队列),多进程可以通过此管道,完成数据交互。

注意:主动写数据的进程完成管道创建123

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
​
​
​
int main()
{
    int pipefd[2];
    char szbuf[100] = "love my dog ,love my house";
    if(-1 == pipe(pipefd))
     {
           perror("pipe create failed\n");
           exit(0);
     }
​
    pid_t pid = fork();
    if(pid >0)
    {
          printf("parent process id [%d]\n",getpid());
          close(pipefd[0]);
          write(pipefd[1],szbuf,sizeof(szbuf));
          close(pipefd[1]);
    }else if(pid ==0){
          char szchildbuf[100];
          close(pipefd[1]);
          read(pipefd[0],szchildbuf,sizeof(szchildbuf));
          printf("child process id [%d],szbuffer %s\n",getpid(),szchildbuf);
          close(pipefd[0]);
    }
    return 0;
}

int pipe(int fds[2]) #可以创建一个管道,创建成功后传出管道的使用描述符 fds[0] #管道读描述符,用于读取管道缓冲区的数据队列出队操作 fds[1] #管道写描述符,用于向管道写数据队列入队操作

管道特性:传输性,方向性 使用管道时确定管道的通信方向

匿名管道使用时的几种特殊情况(具有普遍意义) 1.管道读端关闭(close fds[0]): 如果写端向管道写数据,内核向写端进程发送SIGPIPE信号, 终止写端进程 2.管道读端存在,但是始终未读取数据,写端可以持续向管道写数据,写满管道后,写端进程写阻塞。 3.写端关闭(close fd[1]):如果读端读取管道剩余的数据,可以;如果没有数据继续读,则返回0.(返回0 的原因1. 写端关闭 2.管道内没有数据) 4.写端存在,但是始终未写数据,管道为空,读端读阻塞。

匿名管道的优缺点: 优点: 经典的进程间通信手段,实现使用方便 缺点:只能具有亲缘关系的进程完成数据交互(独立的进程无法使用匿名管道通信) 默认情况下匿名管道用于传输无格式字节流(接收方不清楚数据的类型和大小) ,可以自定义数据格式 匿名管道为单工通信 单工:任意时刻,非读即写 半双工:任意时刻,非读即写,不同时刻方向可切换(单工的一种) 全双工:任意时刻,可以同时读写

2.命名管道

即有名管道 创建的两种方式:1.mkfifo names #命令方式 2.mkfifo(const char* names,int mod) #函数方式 #创建有名管道成功后,都会生成一个同名的管道文件,(在内核里也会有一块缓冲区,文件指针指向缓冲区)文件类型p #使用有名管道可以完成进程通信,但是没有任何限制(是否亲缘都可以) 管道文件 是设备文件,是没有存储数据的能力

打开管道文件的同时,指定如果操作管道(例如读打开就是读操作,写打开就是写操作 读端/写端)

管道文件的使用规则,必须同时满足读写两种权限才可以访问管道文件,如果只有其中一种权限,那么open 阻塞,等待另外一种

示例:

readpipe

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
​
​
int main()
{
   int fd;
   char szbuf[100];
   fd = open("names",O_RDONLY);
   int nreadnum = read(fd,szbuf,sizeof(szbuf));
   if(nreadnum >0)
      printf("read process read :%s\n",szbuf);
    close(fd);
   return 0;
}
​

writepipe

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
​
​
int main()
{
   int fd;
   char szbuf[100] = "hello my world";
   fd = open("names",O_WRONLY);
   write(fd,szbuf,sizeof(szbuf));
   //if(nreadnum >0)
     // printf("read process read :%s\n",szbuf);
    close(fd);
   return 0;
}

命令管道使用时的特殊情况:

1.全速规则,均衡规则(原子访问或非原子访问)

非原子访问(全速规则)

全速规则使用:读写效率高,速度快,如果读端也满足高读,那么传输效率可观 缺点:会将完成的数据包分流,多次发送,读端需要处理与校验数据包,提高读端的复杂度

原子访问(均衡规则) 均衡规则:写入的数据量小于等于4096,如果写入的数据量大于缓冲区可用量,那么写端阻塞 可以极大程度上保证数据包的完整(避免复杂的数据包校验)

2.命名管道在使用时,如果一个进程里有多个读端,那么只有第一个读端会阻塞读取,其他读端都会被设置成非阻塞读取

$ man 7 pipe
PIPE_BUF
    POSIX.1 says that write(2)s of less than PIPE_BUF bytes must be atomic: the output data is written to the pipe as a contiguous sequence.  Writes of more than PIPE_BUF bytes  may  be nonatomic:  the  kernel  may  interleave  the  data  with data written by other processes. POSIX.1 requires PIPE_BUF to be at least 512 bytes.  (On Linux, PIPE_BUF is  4096  bytes.) The  precise  semantics depend on whether the file descriptor is nonblocking (O_NONBLOCK), whether there are multiple writers to the pipe, and on n, the number of bytes to be  written:
    O_NONBLOCK disabled, n <= PIPE_BUF 
    All  n  bytes are written atomically; write(2) may block if there is not room for n bytes to be written immediately
    O_NONBLOCK enabled, n <= PIPE_BUF
    If there is room to write n bytes to the pipe, then write(2) succeeds  immediately, writing all n bytes; otherwise write(2) fails, with errno set to EAGAIN.
    O_NONBLOCK disabled, n > PIPE_BUF
    The  write  is  nonatomic:  the  data  given  to  write(2)  may be interleaved with write(2)s by other process; the write(2) blocks until n bytes have been written.
    O_NONBLOCK enabled, n > PIPE_BUF
    If the pipe is full, then write(2) fails, with errno  set  to  EAGAIN.   Otherwise, from  1  to  n  bytes may be written (i.e., a "partial write" may occur; the caller should check the return value from write(2) to see how  many  bytes  were  actually written), and these bytes may be interleaved with writes by other processes.

意思就是规定了对pipe的实现时需要依据PIPE_BUF的大小来决定pipe的写入行为,而写入小于PIPE_BUF大小的数据必须是原子的:

阻塞时,写入n <= PIPE_BUF字节

写入n字节是原子的,不会发生交错,如果没有n个字节的空间,write会阻塞;

非阻塞时,写入n <= PIPE_BUF字节

如果空间足够,则成功写入,否则返回-1,并设置EAGAIN;

阻塞时,写入n > PIPE_BUF字节

写入是非原子的,数据可能发生交错,直到n字节全部写完,write才会返回;

非阻塞时,写入n > PIPE_BUF字节

如果管道满,write返回-1,并设置EAGAIN,否则可能写入任意1到n字节数据,调用者需要自己检查write的返回值,以确定是否全部写完。

PIPE_BUF的大小依据Linux内核的不同而有所不同(最小512字节,一般为4K)(通过 ulimit -a )

3.MMAP内存共享映射

共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

映射关系可以分为两种 1、文件映射 磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。 2、匿名映射 初始化全为0的内存空间。

而对于映射关系是否共享又分为 1、私有映射(MAP_PRIVATE) 多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制)的映射方式。 2、共享映射(MAP_SHARED) 多进程间数据共享,修改反应到磁盘实际文件中。

进程之间可以利用mmap中sync同步数据的机制,交互共享数据

函数原型

1.void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

参数详解:

​
start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
offset:被映射对象内容的起点。如果不偏移则传0,如果映射文件较大,可以采用偏移分段映射,分批次偏移处理某一块数据.偏移量必须是 4k 的整数倍,写 0 代表不偏移

返回值

成功执行时,如果mmap 调用成功返回映射内存的地址 void *p 如果映射失败返回map_failed,进行错误处理获取失败原因。

2.munmap(void* p,int size);映射内存使用完毕后,通过该函数释放映射内存

readmap

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
​
#define onepage  4096
​
int main()
{
  int fdread = open("tmpfile",O_RDONLY);
  char *szbuf= mmap(NULL,onepage,PROT_READ,MAP_SHARED,fdread,0);
  // strcpy(szbuf,"hello my world");
   printf("read mmap %s\n",szbuf);
   munmap(szbuf,onepage);
  
   return 0;
}

writemap

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
​
#define onepage  4096
​
int main()
{
  char *szbuf;
  int fdwrite = open("tmpfile",O_CREAT|O_RDWR,0664);
    ftruncate(fdwrite,onepage);
   szbuf= mmap(0,onepage,PROT_WRITE,MAP_SHARED,fdwrite,0);
   strcpy(szbuf,"hello my world");
   munmap(szbuf,onepage);
 
   return 0;
}

注意:匿名映射区只能实现有血缘关系的进程间的通信

示例:

优点如下:

1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

缺点如下:

1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。

2.对变长文件不适合,文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了。

3.如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。

4.信号

信号是比较复杂的通信方式,用于通知接收进程有某种事情发生。除了用于

进程间通信外,进程还可以发送信号给进程本身;Linux除了支持UNIX早期信号语义函数signal

外,还支持语义符合POSIX.1标准的信号函数sigaction。(实际上,该函数是基于BSD的,BSD即

能实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数的功能)

5.信号量

信号量主要作为进程间以及同进程不同线程之间的同步手段。

6.套接字

它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信。起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其他类UNIX系统上:Linux和System V的变种都支持套接字

7.共享内存通信

共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

共享存储允许两个或多个进程共享一个给定的存储区,是进程间通信最快的一种方式。

不要同时对共享存储空间进行写操作,通常,信号量用于同步共享存储访问。

最简单的共享内存的使用流程

①ftok函数生成键值

②shmget函数创建共享内存空间

③shmat函数获取第一个可用共享内存空间的地址

④shmdt函数进行分离(对共享存储段操作结束时的步骤,并不是从系统中删除共享内存和结构)

⑤shmctl函数进行删除共享存储空间

创建共享内存,写入共享内存

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//#include<strsep.h>
int main ()
​
{
​
        char *shmaddr;
        key_t key;
//      key_t ftok(const char *path ,int id);
        key = ftok("./",24);
        int shmgetId;
        shmgetId = shmget(key,1024*4,IPC_CREAT|0666);
        if (shmgetId == -1)
        {
                printf("shmget fail! \n");
                exit(-1);
        }
        printf("shmget success !\n");
     //函数原型: void *shmat(int shmid, const void *addr, int flag);
        shmaddr = shmat(shmgetId,0,0);
        printf("shmat success!");
        strcpy(shmaddr,"  tea eggs  hansome !");
​
        printf("write sucee\n");
        sleep(5);
        //函数原型: int shmdt(const void *addr);
        shmdt(shmaddr);
​
        //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        shmctl(shmgetId,IPC_RMID,0);
​
        return 0;
}
​

读取共享内存的值

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//#include<strsep.h>
int main ()
​
{
​
​
        char *shmaddr;
//      所需头文件:#include<sys/ipc.h>
        key_t key;
​
//      key_t ftok(const char *path ,int id);
​
        key = ftok("./",24);
​
        int shmgetId;
        shmgetId = shmget(key,1024*4,0);
​
        if (shmgetId == -1)
        {
                printf("shmget fail! \n");
                exit(-1);
        }
        printf("shmget success !\n");
​
     //函数原型: void *shmat(int shmid, const void *addr, int flag);
        shmaddr = shmat(shmgetId,0,0);
​
        printf("shmat success!");
        printf("data: %s\n",shmaddr);
​
        //函数原型: int shmdt(const void *addr);
        shmdt(shmaddr);
​
        //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
​
​
        return 0;
}
​
​
  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编程的小猴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值