APUE笔记-进程间通信

进程间通信

部分内容参考《Unix网络编程 卷2》

管道

只能在有公共祖先的进程中使用。

实验:

 1 #include <stdio.h>                                                                                                                                
  2 #include "apue.h"
  3 #define MAXLINE 1024
  4 
  5 int main()
  6 {
  7     int fd[2];
  8     char buf[MAXLINE]={0};
  9     pid_t pid;
 10     if(pipe(fd)<0)
 11     {
 12         perror("pipe error");
 13         exit(1);
 14     }
 15     if((pid = fork())<0)
 16     {
 17         perror("fork");
 18     }
 19     else if(pid>0)
 20     {
 21         close(fd[0]);
 22         write(fd[1],"hello",5);
 23     }
 24     else
 25     {
 26         close(fd[1]);
 27         read(fd[0],buf,5);
 28         printf("I read %s\n",buf);
 29     }
 30 }

管道也可以用来实现父子进程同步(之前使用信号实现的)

函数popen和pclose

popen建立连接另一个进程的管道。

实验:大写字母转小写字母

可以看到,以读的方式popen,则子进程的标准输出会和父进程的管道读端连接起来。

  1 #include <stdio.h>
  2 #include "apue.h"
  3 #include <sys/wait.h>
  4 #define MAXLINE 1024
  5 
  6 int main()
  7 {
  8     FILE * fpin;
  9     char line[MAXLINE];
 10     fpin = popen("./tolower","r");
 11     if(fpin==NULL)
 12     {
 13         perror("popen");
 14         exit(1);
 15     }
 16     for(;;)
 17     {
 18         fputs("prompt>",stdout);
 19         fflush(stdout);
 20         if(fgets(line,MAXLINE,fpin)==NULL)
 21         {
 22             break;
 23         }
 24         if(fputs(line,stdout)==EOF)
 25         {
 26             perror("fputs");
 27             exit(2);
 28         }                                                                                                                                         
 29     }
 30     if(pclose(fpin)==-1)
 31     {
 32         perror("pclose");
 33         exit(3);
 34     }
 35     putchar('\n');
 36     exit(0);
 37 }
  1 #include <stdio.h>
  2 #include <ctype.h>
  3 #include "apue.h"
  4 
  5 int main()
  6 {
  7     char c;
  8     while((c=getchar())!=EOF)
  9     {
 10         if(isupper(c))
 11         {
 12             c = tolower(c);
 13         }
 14         if(putchar(c)==EOF)                                                                                                                       
 15         {
 16             perror("putchar");
 17             exit(1);
 18         }
 19         if(c=='\n')
 20             fflush(stdout);
 21     }
 22     exit(0);
 23 }

协同进程

一个过滤程序既产生某个过滤程序的输入,又读取某个过滤程序的输出,就变成了协同进程。

信号SIGPIPE:读进程已经终止,再写管道,会产生这个信号。

子进程:

  1 #include <stdio.h>
  2 #include "apue.h"
  3 #include <string.h>
  4 #define MAXLINE 1024
  5 
  6 int main()
  7 {
  8     int n,int1,int2;
  9     char line[MAXLINE];
 10     while((n=read(STDIN_FILENO,line,MAXLINE))>0)//必须read,不能fgets(这个会缓冲)
 11     {
 12         line[n]=0;
 13         if(sscanf(line,"%d%d",&int1,&int2)==2)
 14         {
 15             sprintf(line,"%d\n",int1+int2);
 16             n = strlen(line);
 17             if(write(STDOUT_FILENO,line,n)!=n)                                                                                                    
 18             {
 19                 perror("write");
 20                 exit(1);
 21             }
 22         }
 23         else
 24         {
 25             if(write(STDOUT_FILENO,"invalid args\n",13)!=13)
 26             {
 27                 perror("write");
 28                 exit(2);
 29             }
 30         }
 31     }
 32 
 33 }

父进程:

  1 #include <stdio.h>                                                                                                                                
  2 #include <string.h>
  3 #include "apue.h"
  4 #define MAXLINE 1024
  5 
  6 int main()
  7 {
  8     //fd1:父进程读,子进程写,fd2:父进程写,子进程读
  9     int n;
 10     int fd1[2],fd2[2];
 11     char line[MAXLINE];
 12     pid_t pid;
 13     if(pipe(fd1)<0 || pipe(fd2)<0)
 14     {
 15         perror("pipe");
 16         exit(1);
 17     }
 18     if((pid = fork())<0)
 19     {
 20         perror("fork");
 21         exit(2);
 22     }
 23     else if(pid>0)
 24     {
 25         close(fd1[1]);
 26         close(fd2[0]);
 27         while(fgets(line,MAXLINE,stdin)!=NULL)//从标准输入获得两个数字
 28         {
 29             n = strlen(line);
 30             //通过管道发送数据给子进程
 31             if(write(fd2[1],line,n)!=n)
 32             {
 33                 perror("parent write");
 34                 exit(3);
 35             }
 36             //读取子进程计算的结果
 37             if((n = read(fd1[0],line,MAXLINE))<0)
 38             {
 39                 perror("read");
 40                 exit(4);
 41             }
 42             if(n==0)
 43             {
 44                 printf("child close pipe\n");
 45                 break;
 46             }
 47             line[n]=0;
 48             if(fputs(line,stdout)==EOF)
 49             {
 50                 perror("fputs");
 51                 exit(5);
 52             }
 53         }
 54         if(ferror(stdin))
 55         {
 56             perror("fgets error");
 57             exit(6);
 58         }
 59         exit(0);
 60 
 61     }
 62     else
 63     {
 64         close(fd1[0]);
 65         close(fd2[1]);
 66         //判断是否相等,因为dup2(fd,fd2)的两个参数如果相等,直接返回fd2而不关闭它,3.12节有解释
 67         if(fd1[1]!=STDOUT_FILENO)
 68         {
 69             if(dup2(fd1[1],STDOUT_FILENO)!=STDOUT_FILENO)
 70             { 
 71                 perror("dup2 fd1[1]");
 72                 exit(1);
 73             }
 74             close(fd1[1]);
 75         }
 76         if(fd2[0]!=STDIN_FILENO)
 77         {
 78             if(dup2(fd2[0],STDIN_FILENO)!=STDIN_FILENO)
 79             {
 80                 perror("dup2 fd2[1]");
 81                 exit(2);
 82             }
 83             close(fd2[1]);
 84         }
 85         if(execl("./child","child",(char*)0)<0)
 86         {
 87             perror("execl");
 88             exit(3);
 89         }
 90     }
 91 
 92 }
 

运行结果:

1
invalid args
1
invalid args
1 2
3
1 3 
4
1 6
7
122 122
244

FIFO

命名管道

不相关的进程之间交换数据。

mkfifo

mkfifoat

管道是线性连接FIFO可以非线性连接。

用途:

1、复制输出流

例如:tee命令会将信息输出到标准输出和某个文件,可以tee fifo(fifo的名字),就不用产生临时文件了

2、客户进程----服务器进程间的通信

XSI IPC

IPC对象:

(1)内部名:非负整数的标识符。
(2)外部名:每个IPC对象与一个键相关联。

key_t ftok(const char *path,int id):根据路径和项目id产生键。

path必须引用现有文件,id只使用低8位。

三个get函数:msgget   semget   shmget都有两个相似参数:key_t类型和一个整型flag。

实验:IPC键的构造

  1 #include <stdio.h>
  2 #include <sys/ipc.h>
  3 #include <sys/msg.h>
  4 #include "apue.h"
  5 #include <sys/stat.h>
  6 
  7 
  8 int main(int argc,char** argv)
  9 {
 10     struct stat mystat;
 11     if(argc!=2)
 12     {
 13         printf("error\n");
 14         exit(1);
 15     }
 16     if(stat(argv[1],&mystat)<0)
 17     {
 18         perror("stat");
 19         exit(2);
 20     }
 21     printf("st_dev:%lx,st_ino:%lx,key:%x\n",(unsigned long)mystat.st_dev,(unsigned long)mystat.st_ino,ftok(argv[1],0x57));                        
 22     exit(0);
 23 }

结果:

./a.out ./child.c 
st_dev:801,st_ino:2b2009d,key:5701009d


./a.out /
st_dev:801,st_ino:2,key:57010002

和《Unix网络编程》卷2的结果不同。测试环境不同(本文用的Linux)。

由结果可知,id的低8位作为key的高8位,接下来是st_dev的低8位和st_ino的低16位。(和FreeBSD是一样的)

权限结构

XSI IPC为每一个IPC关联了一个struct ipc_perm结构。

#include<sys/ipc.h>

struct ipc_perm
{
key_t        key;                       
uid_t           uid;                 
gid_t          gid;                       
uid_t          cuid;                    
gid_t         cgid;                  
unsigned short   mode;    
unsigned short    seq;          
};

msgctl     semctl     和   shmctl可以修改uid   gid和mode字段。(创建者和超级用户有权修改)

ipc不具有可执行权限。

IPC使用“读”和“更改”。

下图来自《Unix网络编程》卷2

结构限制

实验:命令ipcs -l大多数限制可以通过配置内核来改变。

------ Messages Limits --------
系统最大队列数量 = 32000
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

---------- 同享内存限制 ------------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1

--------- 信号量限制 -----------
最大数组数量 = 32000
每个数组的最大信号量数目 = 32000
系统最大信号量数 = 1024000000
每次信号量调用最大操作数 = 500
semaphore max value = 32767

生存期:

管道:最后一个引用管道的进程终止,管道被删除。

FIFO:最后一个引用FIFO的进程终止,FIFO名字虽然还在系统中(需要显示删除),但是数据已经被删除。

XSI IPC:发生下列动作的时候删除

(1)调用msgrcv   或 msgctl读取消息或删除消息队列

(2)进程执行ipcrm命令

(3)正在自举的系统删除消息队列

IPC在系统中没有名字、不使用文件描述符

ipc_perm的seq变量

实验:

  1 #include <stdio.h>
  2 #include "apue.h"
  3 
  4 int main()
  5 {
  6     int msqid;
  7     for(int i=0;i<10;i++)
  8     {
  9         msqid = msgget(IPC_PRIVATE,IPC_CREAT | 0666);//这个键值是0,也可以用ftok函数返回的键值
 10         if(msqid<0)
 11         {
 12             perror("maqid");
 13             exit(1);
 14         }
 15         printf("msqid = %d\n",msqid);                                                                                                             
 16         if(msgctl(msqid,IPC_RMID,NULL)<0)
 17         {
 18             perror("msgctl");
 19             exit(2);
 20         }
 21 
 22     }
 23     return 0;
 24 }

运行结果:每删除一个IPC,seq就会增加,确保过早终止的服务器重启后不重用标识符。

防止行为不端的进程从某个应用的消息队列读取消息。

第一次运行:
msqid = 0
msqid = 32768
msqid = 65536
msqid = 98304
msqid = 131072
msqid = 163840
msqid = 196608
msqid = 229376
msqid = 262144
msqid = 294912

第二次
msqid = 327680
msqid = 360448
msqid = 393216
msqid = 425984
msqid = 458752
msqid = 491520
msqid = 524288
msqid = 557056
msqid = 589824
msqid = 622592



--------- 信号量限制 -----------
最大数组数量 = 32000
每个数组的最大信号量数目 = 32000
系统最大信号量数 = 1024000000
每次信号量调用最大操作数 = 500
semaphore max value = 32767

消息队列

消息队列是消息的链接表。由队列标识符(队列ID)标识。

msgget:创建和访问一个消息队列。

msgctl类似于ioctl(垃圾桶函数)

msgsnd:将数据放到消息队列(数据组织成结构体)

msgrcv:从消息队列中取数据(不一定先进先出)

实验:往消息队列写数据

下面的代码都是用g++编译的。

  1 #include <stdio.h>
  2 #include "apue.h"
  3 
  4 
  5 struct mytext
  6 {
  7     int len;
  8     char text[1];
  9 };
 10 
 11 
 12 int main()
 13 {
 14     int msqid;
 15     struct msqid_ds info;//消息队列的一些属性
 16     struct mytext buf;//消息队列里面的数据
 17     msqid = msgget(IPC_PRIVATE,0666|IPC_CREAT);//创建消息队列
 18     if(msqid<0)
 19     {
 20         perror("msgget");
 21         exit(1);
 22     }
 23     buf.len=1;
 24     buf.text[0]=1;
 25     if(msgsnd(msqid,&buf,1,0)<0)
 26     {
 27         perror("msgsnd");
 28         exit(1);
 29     }
 30     if(msgctl(msqid,IPC_STAT,&info)<0)
 31     {
 32         perror("msgctl");
 33         exit(3);
 34     }
 35     printf("qbytes:%lu,qnum:%lu\n",info.msg_qbytes,info.msg_qnum);
 36     system("ipcs -q");
 37     if(msgctl(msqid,IPC_RMID,NULL)<0)
 38     {
 39         perror("msgctl");
 40         exit(4);
 41     }
 42     return 0;
 43 }

运行结果:0是IPC_PRIVATE键的共同键值。

qbytes:16384,qnum:1

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x00000000 1376256    myhome     666        1            1    

实验:消息队列的创建、删除,发送,接收

创建

1 #include <stdio.h>
  2 #include "../apue.h"
  3 
  4 int main(int argc,char **argv)
  5 {
  6     char ch;
  7     int flag,msqid;
  8     key_t key;
  9     flag = 0666 | IPC_CREAT;
 10     while((ch = getopt(argc,argv,"e")!=-1))
 11     {
 12         switch(ch)                                                                                                                                
 13         {
 14         case 'e':
 15             flag |= IPC_EXCL;
 16             break;
 17         }
 18     }
 19     if(optind!=argc-1)
 20     {
 21         perror("optind");
 22         exit(1);
 23     }
 24     key = ftok(argv[optind],0);
 25     if(key<0)
 26     {
 27         perror("ftok");
 28         exit(2);
 29     }
 30     msqid = msgget(key,flag);
 31     if(msqid<0)
 32     {
 33         perror("msgget");
 34         exit(3);
 35     }
 36     return 0;
 37 }

往队列发送消息

  1 #include <stdio.h>
  2 #include "../apue.h"
  3 #include <features.h>
  4 
  5 
  6 int main(int argc,char ** argv)
  7 {
  8     if(argc!=4)
  9     {
 10         printf("error\n");
 11         exit(1);
 12     }
 13     int msqid;
 14     struct msgbuf *ptr;
 15     long type;
 16     size_t len;
 17     len = atoi(argv[2]);
 18     type = atoi(argv[3]);
 19     //后面的相当于写打开,和open的用法很相似
 20     msqid = msgget(ftok(argv[1],0),0222);
 21     if(msqid<0)
 22     {
 23         perror("msgget");
 24         exit(1);
 25     }
 26     ptr = (struct msgbuf *)calloc(sizeof(long)+len,sizeof(char));
 27     if(ptr==NULL)                                                                                                                                 
 28     {
 29         printf("calloc error\n");
 30         exit(2);
 31     }
 32     ptr->mtype=type;
 33     if(msgsnd(msqid,ptr,len,0)<0)
 34     {
 35         perror("msgsnd");
 36         exit(3);
 37     }
 38     return 0;
 39 }

从队列接收消息

  1 #include <stdio.h>                                                                                                                                
  2 #include "../apue.h"
  3 
  4 #define MAXMSG (8192+sizeof(long))
  5 
  6 int main(int argc,char**argv)
  7 {
  8     struct msgbuf *buf;
  9     int n;
 10     char ch;
 11     int flag;
 12     long type;
 13     int msqid;
 14     //type=0表示返回队列中第一个消息,也就是先进先出
 15     type = flag = 0;
 16     while((ch = getopt(argc,argv,"nt:"))!=-1)
 17     {
 18         switch(ch)
 19         {
 20         case 'n':
 21             {
 22                 flag |= IPC_NOWAIT;
 23                 break;
 24             }
 25         case 't':
 26             {
 27                 type = atoi(optarg);
 28                 break;
 29             }
 30         case '?':
 31             {
 32                 printf("error\n");
 33                 exit(1);
 34             }
 35         }
 36     }
 37     if(optind!=argc-1)
 38     {
 39         perror("optind");
 40         exit(2);
 41     }
 42     msqid = msgget(ftok(argv[optind],0),0444);
 43     if(msqid<0)
 44     {
 45         perror("msgget");
 46         exit(3);
 47     }
 48     buf = (struct msgbuf*)malloc(MAXMSG);
 49     n = msgrcv(msqid,buf,MAXMSG,type,flag);
 50     printf("read %d bytes,type=%ld\n",n,buf->mtype);
 51     return 0;
 52 }

删除消息队列

1 #include <stdio.h>
  2 #include "../apue.h"
  3 int main(int argc,char**argv)
  4 {
  5     int msqid;
  6     if(argc!=2)
  7     {
  8         printf("error\n");
  9         exit(1);
 10     }
 11     msqid = msgget(ftok(argv[1],0),0);
 12     if(msqid<0)
 13     {
 14         perror("msget");
 15         exit(2);
 16     }
 17     msgctl(msqid,IPC_RMID,NULL);
 18     exit(0);                                                                                                                                      
 19 }

makefile

  1 all:
  2     g++  ./msg_creat.c -o msg_creat
  3     g++  ./msg_snd.c   -o msg_snd
  4     g++  ./msg_rcv.c   -o msg_rec
  5     g++  ./msg_rmid.c  -o msg_rmid  

运行:

创建队列
往队里加入数据


--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0x00010061 0          wangli     666        6            3      


读取数据
./msg_rec -t 200 ./file 
read 2 bytes,type=200


./msg_rec -t 300 ./file 
read 3 bytes,type=300

./msg_rec -t 100 ./file 

read 1 bytes,type=100

信号量

信号量为正,资源可用,使用了资源单位,信号量的值减一。

测试信号量的值与信号量的值减一应该是原子操作。

二元信号量:初始值为1,控制单个资源。

1、每个信号量由一个无名结构体表示。(Unix环境网络编程卷2  是sem结构体)

2、每个信号量集合有一个semid_ds的结构体。(和消息队列类似,这里面放着属性,比如信号量的个数)

3、每个IPC都有ipc_perm结构(里面存放权限有关的信息)。

4、信号量有操作数组,sembuf(消息队列的msgbuf结构体存放消息队列的类型和值)。

5、setctl中需要用到联合体semun(存放值,semid_ds结构体等,根据命令来选择)。

总结:信号量的操作
创建信号量:指出集合里面有几个信号量
设置信号量的值,需要一个联合体semun,里面的array指向的数组就是,每个信号量的值
删除信号量semctl就可以
获取信号量集合的属性,只要拿到senid_ds结构体,通过semctl

实验:

创建:

 1 #include <stdio.h>
  2 #include "../apue.h"
  3 #include <sys/sem.h>
  4 
  5 int main(int argc,char **argv)
  6 {
  7     int semid,oflag;
  8     int nsems;
  9     char ch;
 10     oflag = 0666 | IPC_CREAT;
 11     if(argc<3)
 12     {
 13         printf("error\n");
 14         exit(1);
 15     }
 16     while((ch = getopt(argc,argv,"e"))!=-1)
 17     {
 18         switch(ch)
 19         {
 20             case 'e':
 21                 oflag |= IPC_EXCL;
 22                 break;
 23         }
 24     }
 25     if(optind!=argc-2)
 26     {
 27         perror("error");
 28         exit(2);
 29     }
 30     nsems = atoi(argv[optind]);
 31     semid = semget(ftok(argv[optind+1],0),nsems,oflag);                                                                                           
 32     if(semid<0)
 33     {
 34         perror("semget");
 35         exit(3);
 36     }
 37     exit(0);
 38     return 0;
 39 }

删除:

  1 #include <stdio.h>
  2 #include "../apue.h"
  3 #include <sys/sem.h>
  4 
  5 int main(int argc,char ** argv)
  6 {
  7     int semid;
  8     if(argc!=2)
  9     {
 10         printf("error\n");
 11         exit(1);
 12     }
 13     semid = semget(ftok(argv[1],0),0,0);
 14     if(semid<0)
 15     {
 16         perror("semget");                                                                                                                         
 17         exit(2);
 18     }
 19     if(semctl(semid,0,IPC_RMID)<0)
 20     {
 21         perror("semctl");
 22         exit(3);
 23     }
 24     exit(0);
 25 }

设置信号量集合中每个信号量的值

  1 #include <stdio.h>
  2 #include "../apue.h"
  3 #include <sys/sem.h>
  4 
  5 union semun
  6 {
  7     int val;
  8     struct semid_ds *buf;
  9     unsigned short *array;
 10 };
 11 
 12 int main(int argc,char **argv)
 13 {
 14     int semid;
 15     int nsems;
 16     struct semid_ds seminfo;
 17     union semun arg;
 18     unsigned short *ptr;
 19     if(argc<2)                                                                                                                                    
 20     {
 21         printf("error\n");
 22     }
 23     semid = semget(ftok(argv[1],0),0,0);
 24     arg.buf = &seminfo;
 25     semctl(semid,0,IPC_STAT,&seminfo);
 26     //get numbers of sem
 27     nsems = arg.buf->sem_nsems;
 28     //get value of sem from cmdline
 29     if(argc != nsems+2)
 30     {
 31         printf("error\n");
 32         exit(1);
 33     }
 34     ptr = (unsigned short *)calloc(nsems,sizeof(short));
 35     for(int i=0;i<nsems;i++)
 36     {
 37         ptr[i]=atoi(argv[i+2]);
 38     }
 39     arg.array = ptr;
 40     semctl(semid,0,SETALL,arg);
 41     exit(0);
 42 }

读取每个信号量的值

  1 #include <stdio.h>                                                                                                                                
  2 #include "../apue.h"
  3 #include <sys/sem.h>
  4 
  5 union semun
  6 {
  7     int val;
  8     struct semid_ds *buf;
  9     unsigned short *array;
 10 };
 11 int main(int argc, char ** argv)
 12 {
 13     int semid;
 14     union semun arg;
 15     unsigned short *ptr;
 16     int nsems;
 17     struct semid_ds seminfo;
 18     if(argc!=2)
 19     {
 20         printf("error\n");
 21         exit(1);
 22     }
 23     semid = semget(ftok(argv[1],0),0,0);
 24     semctl(semid,0,IPC_STAT,&seminfo);
 25     arg.buf = &seminfo;
 26     nsems = arg.buf->sem_nsems;
 27     ptr = (unsigned short *)calloc(nsems,sizeof(unsigned short));
 28     arg.array = ptr;
 29     semctl(semid,0,GETALL,arg);
 30     for(int i=0;i<nsems;i++)
 31     {
 32         printf("%d\n",arg.array[i]);
 33     }
 34     exit(0);
 35 }

makefile

  1 all:
  2     gcc ./semprint.c -o out.o
  3     gcc ./semsetvalues.c -o set.o
  4     gcc ./sem_creat.c -o creat.o
  5     gcc ./sem_rm.c -o rm.o
  6 .PHONY:
  7 clean:
  8     rm *.o           

运行:

./creat.o 5 file
ipcs
--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     
0x00010642 32768      myhome     666        5       

./set.o file 1 2 3 4 5

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     
0x00010642 32768      myhome     666        5  

 ./out.o file
1
2
3
4
5

./rm.o file 
--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems  

共享存储

多个进程共享一个给定的存储区,是最快的IPC,用信号量同步,或者记录锁,互斥量。

shmget创建共享内存,创建时指定大小。然后用shmat连接到地址空间。

/dev/zero接收任何数据,忽略这些数据。两个进程可以通过映射这个到内存,实现通信。无需创建实际的文件。只在相关进程间起作用(因为相关进程才能共享mmap返回的指针值。)。不相关进程可以用shm共享存储通信。

POSIX信号量

分为命名信号量和未命名信号量。

sem_t * sem_open创建信号量,和open创建文件的方法差不多。返回指向信号量的指针。

sem_close关闭信号量。关闭信号量,信号量的值不受影响。程序退出,内核会自动关闭打开的信号量。

sem_ulink销毁信号量,如果有打开信号量的引用,销毁会延迟到最后一个打开的引用关闭。

sem_wait  sem_trywait实现信号量的减一操作。

sem_timewait阻塞一段确定的时间。(带有超时的那些函数,基本都用的绝对时间。)

sem_post使信号量增加1。

未命名信号量,sem_t的指针作为参数可以直接创建。

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值