Linux之进程通信

进程间的5种通信包括:管道(命名管道和无名管道)、消息队列、信号量、共享内存、Socket、Streams等。其中socket和streams支持不同主机之间的IPC。
一、无名管道
1、特点
a:管道是半双工的,有固定的读端和写端。父进程通过管道和子进程通信时,父进程通过写端向管道中写数据,子进程通过读端从管道中读取数据;子进程通过写端向管道中写数据,父进程通过读端从管道中读取数据。这两个过程不能同时进行,数据只能在一个方向上流动。
b:管道类似于水管,水管中的水流完就没了,管道中的数据,传输完也会丢失。它可以看成一种特殊的文件,可以用read、write来操作,但不是普通的文件,不属于任何文件系统,只存在于内存中。
c:只能用于具有亲缘关系的进程之间的通信(父子进程或兄弟进程之间)。
d:关掉管道关掉读端跟写端就可以完成。

2、函数原型

 #include <unistd.h>
 int pipe(int pipefd[2])
 参数是一个整型数组,代表两个文件描述符,pipefd[0]表示为读而打开,pipefd[1]表示为写而打开,即读数据要操作pipefd[0],写数据要往pipefd[1]里面写
 返回值:成功返回0,失败返回-1

3、demo实例

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
        int fd[2];
        char *readBuf = (char*)malloc(sizeof(char)*128);
        pid_t pid;
        if(pipe(fd)==-1)
        {
                printf("pipe failed\n");
        }
	pid = fork();//创建进程
        if(pid<0)
        {
                printf("fork failed\n");
        }
        else if(pid>0)
        {
        	//父进程关掉读端,通过写端往管道里面写内容
                close(fd[0]);
                write(fd[1],"hello pipe",strlen("hello pipe"));
        }
        else
        {
         	//子进程关掉写端,通过读端从管道里读取内容
                close(fd[1]);
                read(fd[0],readBuf,128);
                printf("read:%s\n",readBuf);
        }
        return 0;
}
执行结果:
read:hello pipe

二、FIFO(有名管道)
1、特点
a:与无名管道不同它可以在无关进程间交换数据
b:有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
2、函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数一:管道名是一个文件,后面的后面的通信通过这个文件进行读写操作
参数二:与open函数的mode相同,一旦创建了FIFO就可以用一般的文件I|O来操作它
返回值:成功返回0,失败返回-1

3、demo实例

//read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>//errno头文件
#include <stdlib.h>
#include <fcntl.h>
int main()
{
        int fd ;
        char *readBuf  = (char *)malloc(sizeof(char)*128);
        if(mkfifo("./file",0600)==-1 && errno!=EEXIST)
        {
                printf("mkfifo failed\n");
        }//以可读可写0600方式创建管道,管道存在也会创建失败,所以mkfifo返回值=-1的同时,需满足管道不存在,管道才是真正的创建失败
        fd = open("./file",O_RDONLY);
        //以只读阻塞方式打开管道,会阻塞到write函数向管道中写入数据才会停止阻塞
        if(fd ==-1)
        {
                printf("open fifo failed\n");
        }
        int n_read = read(fd,readBuf,128);
        printf("read context is %s size is %d\n",readBuf,n_read);
        close(fd);
        return 0;
}
//write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
        int fd;
        char *writeBuf = "xiaobian henshuai!";
        fd = open("./file",O_WRONLY);
        if(fd==-1)
        {
                printf("open fifo failed\n");
        }
        write(fd,writeBuf,strlen(writeBuf));
        close(fd);
        return 0;
}

当open一个fifo时,如果没有设置非阻塞标志(默认),open只读方式打开fifo会阻塞到某个进程为写而打开此fifo,若设置了非阻塞标(O_NONBLOCK),则只读open立即返回,只写open将出错返回-1,一般都以阻塞方式打开。
有名管道实际上就是创建了一个管道文件,通过open、write、read函数对文件进行操作,完成进程间通信。
三、消息队列
消息队列有自己的标识符(队列ID),存放于Linux内核中,消息队列链表如何管理,由内核处理。
1、特点
a:消息队列是面向记录的,其中的消息有特定的格式特定的优先级
b:消息队列独立于发送接收进程。进程终止时,消息队列及其内容不会消失。
c:消息队列可以实现消息的随机查询,消息队列不一定要以先进先出的次序读取,也可以按消息的类型读取。
2、函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//创建或打开消息队列,成功返回队列id,失败返回-1
int msgget(key_t key, int msgflg);
参数key:键值消息队列索引号,可自行设定也可动态获取
参数msgflag:创建或打开队列的方式,通常还加上消息队列的权限
//添加消息,成功返回0,失败返回-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int flg);
参数msgid:消息队列ID
参数const void *msgp :要发送的消息,是一个结构体
参数msgsz:消息的大小
参数flg:通常为0
//读取消息,成功返回消息数据长度,失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int flg);
参数void *msgp :要接受的消息,存放在一个结构体中
参数msgtyp:消息队列的类型
参数flg:设为0,当为默认,非阻塞的方式
//控制消息队列,成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数cmd:通过man手册查询,有相应的宏,IPC_STAT IPC_RMID  IPC_SET
参数buf:指向共享内存模式和访问权限的结构,我们不关心它,通常为NULL

以下两种情况将msgget创建一个消息队列
没有与键值key对应的消息队列,并且msgflg标志位包含了IPC_CREAT
key的参数为IPC_PRIVATE

以下两种方式获取键值key
直接写死,0x520
动态获取,ftok(".",1) (具体原因自行baidu哦)

3、demo实例

//msgsnd.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
//每一个消息都有自己的类型和大小用结构体定义消息
struct msgbuf
{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};
int main()
{
        int msgid;
        key_t key;
        key = ftok(".",1);
        if(key==-1)
        {
		printf("key create failed\n");
		exit(-1);
	}
        struct msgbuf sendbuf={888,"msgsnd give you a msg"};
        struct msgbuf readbuf;
        msgid=msgget(key,IPC_CREAT|0777);//创建消息队列的同时给它可读可写可执行的权限
        if(msgid==-1)
        {
                printf("open msg que falied\n");
        }
        //添加一个消息类型为888的消息
        msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);
        //等待接受一个消息类型为999的消息
        msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),999,0);
        printf("%s\n",readbuf.mtext);
        //将消息队列msgid从内核中移除
        msgctl(msgid,IPC_RMID,NULL);
        return 0;
}
//msgrcv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf
{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};
int main()
{
        int msgid;
        key_t key;
        key = ftok(".",1);
        if(key==-1)
        {
 		printf("key create failed\n");
  		exit(-1);
 	}
        struct msgbuf readbuf;
        struct msgbuf sendbuf = {999,"msgrcv had get your msg"};
        msgid = msgget(key,IPC_CREAT|0777);
        if(msgid==-1)
        {
                printf("msg create failed\n");
        }
        //等待接收一个消息类型为888的消息
        msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),888,0);
        printf("%s\n",readbuf.mtext);
        //添加一个消息类型为999的消息
        msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);
        //将消息队列msgid从内核中移除
        msgctl(msgid,IPC_RMID,NULL);
        return 0;
}

四、共享内存
共享内存就是允许两个或多个进程共享一定的存储区,数据直接写到这块内存,通过指针访问这块内存,完成IPC
1、特点
a:两个进程地址映射到同一片物理地址完成通信
b:共享内存是传输速度最快的通信方式,客户进程和服务进程传递的数据直接从内存里存取、放入,数据不需要在两进程间复制
c:共享内存的数据的也没有什么限制
2、函数原型

#include <sys/ipc.h>
#include <sys/shm.h>
//创建或获取一个共享内存,成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int shmflg);
参数key:系统建立IPC通讯(消息队列、信号量和共享内存)时必须指定一个ID值,通常用ftok获取
参数size:共享内存的大小,通常兆的整数倍
参数shmflg:是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。一般我们会还或上一个文件权限
//连接共享内存到当前的地址空间,成功返回指向共享内存的指针,失败返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid:shmget函数返回的共享内存标识id
参数shmaddr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
参数shmflg:shm_flg是一组标志位,通常为0
//断开与共享内存之间的连接,成功返回0,失败返回-1
int shmdt(const void *shmaddr);
参数shmaddr:是以前调用shmat时的返回值
//控制共享内存的相关信息,成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid:shmget函数返回的共享内存标识id
参数cmd:,cmd是要采取的操作,它可以取下面的三个值 :  
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。    
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值  
IPC_RMID:删除共享内存段
参数 buf:buf是一个结构指针,它指向共享内存模式和访问权限的结构,我们不关心它,通常为NULL

3、demo实例

//shmsend.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
int main()
{
        key_t key;
        key = ftok(".",1);
        int shmid;
        void *shmaddr  =NULL;
        shmid = shmget(key,1024*4,IPC_CREAT|0600);
        if(shmid ==-1)
        {
                printf("shmid failed\n");
                exit(-1);
        }
        shmaddr = shmat(shmid,0,0);
        if(shmaddr==NULL)
        {
                printf("shmaddr failed\n");
                exit(-1);
        }
        strcpy(shmaddr,"xiaobian henshuai");//往共享内存中添加数据
        printf("send msg over\n");
        sleep(5);//等待进程读取共性内存中的数据
        shmdt(shmaddr);//断开连接
        shmctl(shmid,IPC_RMID,0);//删除共享内存
        return 0;
}
//执行结果:send msg over
//shmreceive.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main()
{
        key_t key;
        key = ftok(".",1);
        char *shmaddr=NULL;
        int shmid;
        shmid = shmget(key,4*1024,0);//共享内存已经在上一个demo中创建了,不用重复创建
        if(shmid ==-1)
        {
                printf("shmid failed\n");
                exit(-1);
        }
        shmaddr=shmat(shmid,0,0);
        printf("%s\n",shmaddr);
        printf("receive msg over\n");
        shmdt(shmaddr);
        shmctl(shmid,IPC_RMID,0);
        return 0;
}
//执行结果:receive msg over
    //  xiaobian henshuai

关于共享内存的指令:
ipcs -m 查看共享内存
ipcrm -m 共享内存id号(shmid) 删除共享内存

五、信号
1、信号概述
信号是Linux系统中用于进程之间通信或操作的一种机制,对Linux来说信号是一种软中断,许多重要程序都需处理信号,信号为Linux提供了一种处理异步中断的方法。
用 kill -l 可以查看系统中信号的序号和名字
2、信号的三种处理方法
捕捉、忽略和默认动作
捕捉:类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理,当信号产生时,由内核调用用户自定义的函数,以此来实现对信号的处理。
忽略:忽略某个信号,对该信号不做任何处理,但SIGKILL和SIGSTOP两种信号不能被忽略。
默认动作:每个信号系统都有对应的默认处理动作,对该信号的处理保留系统的默认值,大多都是使进程终止。
对信号的操作可以使用kill指令发送信号
kill -9 进程号 //杀死该进程的信号
3、函数原型

//信号处理函数的注册
//初级:
 #include <signal.h>
 typedef void (*sighandler_t)(int);
 sighandler_t signal(int signum,sighandler_t handler);
//高级:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
//信号处理函数的发送
//初级:
 #include <sys/types.h>
 #include <signal.h>
 int kill(pid_t pid, int sig)
 //高级:
 #include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

4、demo实例信号初级版

//signal.c
#include <stdio.h>
#include <signal.h>
void handler(int signum)
{
        switch (signum)
        {
                case SIGUSR1:
                        printf("SIGUSR1 is catched\n");
                        break;
                case SIGUSR2:
                        printf("SIGUSR2 is catched\n");
                        break;
        }
}
int main()
{
        signal( SIGUSR1,handler);//对SIGUSR1 信号进行捕捉,指定handler作为其处理函数
        signal(SIGUSR2,handler);
        signal(SIGINT,SIG_IGN);//将Ctrl c 忽略
        while(1);
        return 0;
}
ps -aux |grep a.out //查找a.out的进程号id
kill -10 进程号
kill -12 进程号
执行结果:
SIGUSR1 is catched
SIGUSR2 is catched
//kill.c
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc ,char**argv)
{
        if(argc!=3)
        {
                printf("params error\n");
                exit(-1);
        }
        int signum;
        int pid;
        signum = atoi(argv[1]);//将ascll码转换成整型数
        pid =atoi(argv[2]);
        kill(pid,signum);
        return 0;
}
./a.out //运行信号注册可执行性程序
ps -aux |grep a.out //查找a.out的进程号id
./kill 12 进程号
//在两个终端种分别进行
执行结果:
SIGUSR2 is catched
//kill.c 的补充用system实现
#include <stdio.h>
int main(int argc,char**argv)
{
        int signum;
        int pid;
        char cmd[128];
        signum = atoi(argv[1]);
        pid  = atoi(argv[2]);
        sprintf(cmd,"kill -%d %d",signum,pid);//动态的构造字符串的函数
        system(cmd);
        return 0;
}

5、demo实例信号高级版,信号携带消息

//sigaction 函数介绍
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
参数一:要捕获的信号
参数二:一个结构体
参数三:输出先前信号的处理方式,不需要可设为NULL

结构体介绍:
struct sigaction 
{
	void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
};
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置,默认为0,可不做要求
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
	SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
 	SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
 	SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
	SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
sa_sigaction 另一个信号处理函数,它有三个参数
	参数一:捕获的信号
	参数二是一个结构体
	参数三:是一个指针,为NULL表明没有数据,否则有数据
	结构体介绍:存放捕获的信号中携带的消息
	siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               long     si_band;     /* Band event (was int in
                                        glibc 2.3.2 and earlier) */
               int      si_fd;       /* File descriptor */
               short    si_addr_lsb; /* Least significant bit of address
                                        (since kernel 2.6.32) */
		}

//sigqueue函数介绍
int sigqueue(pid_t pid, int sig, const union sigval value);
参数一:进程ID
参数二:信号类型
参数三:是一个联合体
union sigval {
               int   sival_int;
               void *sival_ptr;
           };

//sigget.c
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum, siginfo_t * info, void * context)
{
        if(context!=NULL)//指针不为空才有数据
        {
                printf("send pid =%d\n",info->si_pid);//发送信号进程的pid
                printf("received value =%d\n",info->si_int);//发送信号携带的整形数据
                printf("received value =%d\n",info->si_value.sival_int);//发送信号携带的整形数据
               
        }
}
int main()
{
        printf("get pid is %d\n",getpid());
        struct sigaction act;
        act.sa_sigaction = handler;
        act.sa_flags =SA_SIGINFO;
        sigaction(SIGUSR1,&act,NULL);//捕获信号SIGUSR1 并用sa_sigaction = handler处理函数,获取信号携带的信息
        while(1);//等待捕获信号
        return 0;
}
//sigput.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char **argv)
{
        if(argc!=3)
        {
                printf("params error\n");
                exit(-1);
        }
        printf("send pid is %d\n",getpid());
        union sigval value;//信号携带的信息为一个联合体,可以存放整型数据,也可以字符串
        value.sival_int = 100;

        int signum = atoi(argv[1]);
        int pid  = atoi(argv[2]);
        sigqueue(pid,signum,value);//给进程 pid 发送SIGUSR1信号 携带value信息
        return 0;
}

六、信号量
信号量与前面介绍的IPC结构不同,是一个计数器,用于实现进程间的互斥与同步,而不是用于存储进程间的通信数据
1、特点
a:信号量用于进程间同步,若要在进程间传递数据要结合共享内存
b:信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
2、函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//创建或获取一个信号量组,成功返回信号量组id,失败返回-1
int semget(key_t key, int nsems, int semflg);
参数一:key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到
参数二:nsem指定信号量集中需要的信号量数目,它的值几乎总是1
参数三:flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。
//对信号量组进行操作,改变信号量的值,成功返回0,失败返回-1
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数三:nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
参数二结构体:
struct sembuf
{     
 	short sem_num;   //除非使用一组信号量,否则它为0     
  	short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数  
  			//一个是-1,即P(等待)操作,                     
  			 //一个是+1,即V(发送信号)操作。		                                                
   	short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
   	               //并在进程没有释放该信号量而终止时,操作系统释放信号量                    			  };  
   	
//控制信号量的相关信息
int semctl(int semid, int semnum, int cmd, ...);
参数一:由semget返回的信号量标识符
参数二:要操作当前信号量集的哪一个信号量
参数三:cmd通常是下面两个值中的其中一个 
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。 
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。
union semun
{
	int val;    /* Value for SETVAL 使用的值*/
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET 使用的缓存区*/
        unsigned short  *array;  /* Array for GETALL, SETALL 使用的数组*/
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) 使用的缓存区*/
           };

3、demo实例

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
 union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
           };
void pGetKey(int semid)//p操作相当于拿锁的功能
{
        struct sembuf sem;
        sem.sem_num = 0;
        sem.sem_op  =-1;//拿锁 锁的数量减1
        sem.sem_flg = SEM_UNDO;
        semop(semid,&sem,1);
}
void vPutKey(int semid)//V操作 相当于放锁的功能
{
        struct sembuf sem;
        sem.sem_num = 0;
        sem.sem_op  =1;//放锁 锁的数量加1
        sem.sem_flg = SEM_UNDO;
        semop(semid,&sem,1);
}
int main()
{
        key_t key;
        key = ftok(".",1);
        int semid;
        union semun semval;
        semval.val=0;//刚开始没有锁
        semid=semget(key,1,IPC_CREAT|0600);
        if(semid==-1)
        {
                printf("sem error\n");
                exit(-1);
        }
        semctl(semid,0,SETVAL,semval);
        pid_t pid = fork();
        if(pid>0)
        {
                pGetKey(semid);//拿锁
                printf("this is father\n");
                vPutKey(semid);//放锁
                semctl(semid,0,IPC_RMID);//释放锁
        }
        else if(pid==0)
        {
                printf("this is child\n");
                vPutKey(semid);//放锁
        }
        return 0;
}

代码解析:fork一个子进程,不能保证,父子进程哪一个先运行,此时设置锁的数量为0,让父进程去拿锁,拿不到,只能子进程先运行,子进程运行完,进行v操作放锁,这时父进程才能拿锁,执行相关程序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值