进程间的通讯

进程间的通讯

【1】进程间的通信方式

1.传统的进程间通信方式

1)无名管道 				    pipe
2)有名管道(命名管道)	    fifo
3)信号 						signal

2.system V操作系统的IPC对象 inter process communication

1)共享内存 				share memory
2)消息队列				message queue
3)信号灯集 				semaphore

3.BSD

1)套接字 socket

【2】管道

1. 管道的原理

在进程的3-4G空间中,创建一个管道(特殊的文件),管道中的数据直接保存在内存中。
管道

2. 管道的特性

1. 管道可以看做是一个特殊的文件,一般文件的内容存储在磁盘上,而管道的内容存储在内核的内存中。
2. 管道遵循先进先出的原则。
3. 对于管道的读写是一次性操作,如果对管道进行读写,那么被读取的数据会从管道中删除。
4. 管道的大小:64K = 64*1024 = 65536byte;
5. 管道是一种半双工的通信方式,但是平时会把它当做单工来使用。
    单工:只能A发消息给B,B不能发消息给A;
    半双工:同一时间只能A发消息给B,B发消息给A;
    全双工:同一时间既能满足A发消息给B,也能满足B发消息给A;
6. 向管道中写数据
  -当读端没有关闭
  1)管道缓冲区中,如果有剩余空间,写进程会试图向管道中写数据。
  2)如果管道满了,则写函数会阻塞。
  -当读端关闭
  1)当读端关闭,向管道中写入数据,会造成管道破裂,退出进程。
  2)进程会收到一个信号: SIGPIPE(管道破裂信号)
7. 从管道中读取数据
  -当写端没有关闭
  1)管道中如果没有数据,则read阻塞。
  2)当请求读取的数据个数 n 大于管道中存储的数据个数 m 时,则实际读取的格式为管道中存储的数据个数。
  3)当请求读取的数据个数 n 小于管道中存储的数据个数 m 时,则实际读取个数为请求读取的数据个数。
  -当写端关闭的时候
  1)从管道中读取数据,当管道中的数据读取完毕,read立即返回,不阻塞,且返回值为0;

3. 无名管道

1)无名管道的特点
  1. 对于无名管道只能用于具有 亲缘关系 的进程间通信;
  2. 对于无名管道的读写,可以使用文件IO,如read write。但是不能使用lseek函数.
  3. 无名管道使用的时候不需要open,但是需要手动close;
2)pipe
功能:创建一个无名管道,并返回读写文件描述符;
头文件:
       #include <unistd.h>
原型:
       int pipe(int pipefd[2]);
参数:
    int pipefd[2]:存储打开的两个文件描述符,需要传入一个int类型的数组,且容量是2;
    		pipefd[0]:读端;
			pipefd[1]:写端;
返回值:
    成功,返回0;
	失败,返回-1;
小练习
使用无名管道实现父子进程对话,要求
1)父进程发送一句话,子进程接收到然后打印;父进程等待子进程的回话
2)子进程发送一句话,然后父进程接收到打印;子进程等待父进程的回话
3)重复1) 2)步骤
4)当父进程或子进程接收到quit,直接结束两个进程。

注意
当为获取终端输入的时候,不能用read,因为stdin是int型,可以用fgets

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    //创建一个无名管道
    //父进程发送,子进程接收
    int mypipe1[2] = {0};
    int ret = pipe(mypipe1);
    if(ret < 0)
    {
        perror("pipe");
        return -1;
    }
    //子进程发送,父进程接收
    int mypipe2[2] = {0};
    ret = pipe(mypipe2);
    if(ret < 0)
    {
        perror("pipe");
        exit(1);
    }
    //创建进程
    char buf[BUFSIZ] = "";
    pid_t pid = fork();
    if(pid > 0)
    {
        //父进程 
        close(mypipe1[0]);  //父进程 关闭1的读端
        close(mypipe2[1]);  //父进程 关闭2的写端   
        while(1) 
        {
            bzero(buf, sizeof(buf));
            //父进程写
            fprintf(stderr, "父进程说:");
            fgets(buf, BUFSIZ-1, stdin);
            ret = write(mypipe1[1], buf, strlen(buf)-1);
            if(ret < 0)
            {
                perror("write");
                exit(1);
            }
            if(strncasecmp(buf, "quit", 4) == 0)
            {
                break;
            }
            //父进程读取
            bzero(buf, strlen(buf));
            ret = read(mypipe2[0], buf, BUFSIZ);
            if(ret < 0)
            {
                perror("read");
                exit(1);
            }
            fprintf(stderr, "听到儿子说:");
            printf("%s\n\n", buf);
            if(strncasecmp(buf, "quit", 4) == 0)
            {
                break;
            }
        }
        close(mypipe1[1]);  //父进程 关闭1的读端
        close(mypipe2[0]);  //父进程 关闭2的写端
        wait(NULL);
    }
    else if(0 == pid)
    {
        //子进程
        close(mypipe1[1]);  //子进程 关闭1的写端
        close(mypipe2[0]);  //子进程 关闭2的读端
        while(1)
        {
            //子进程读取
            bzero(buf, strlen(buf));
            ret = read(mypipe1[0], buf, BUFSIZ);
            if(ret < 0)
            {
                perror("read");
                exit(1);
            }
            fprintf(stderr, "听到父亲说:");
            printf("%s\n\n", buf);
            if(strncasecmp(buf, "quit", 4) == 0)
            {
                break;
            }
            //子进程写
            bzero(buf, strlen(buf));
            fprintf(stderr, "子进程说:");
            fgets(buf, BUFSIZ-1, stdin);
            ret = write(mypipe2[1], buf, strlen(buf)-1);
            if(ret < 0)
            {
                perror("write");
                exit(1);
            }
            if(strncasecmp(buf, "quit", 4) == 0)
            {
                break;
            }
        }
        close(mypipe1[0]);  //子进程 关闭1的读端
        close(mypipe2[1]);  //子进程 关闭2的写端
    }
    else
    {
        perror("fork");
        return -1;
    }
    return 0;
}

4. 有名管道(fifo)

有名管道:顾名思义,就是名字可见的管道文件,但是数据依然存在于内存上,不可见。
有名管道

1)有名管道的特点
1. 可以使用在互不相关的进程间通信。
2. 有名管道的读写,可以使用文件IO,例如read,write,但是不能使用lseek;
3. 有名管道的使用需要手动open,close管道文件。
4. 遵循先进先出的原则
5. 当读写端都关闭后,释放内存。
2)创建有名管道文件
1. 用shell指令创建
$ mkfifo 有名管道名
例子:
$ mkfifo myfifo
2. 用 mkfifo函数
功能:创建一个有名管道;
头文件:
       #include <sys/types.h>
       #include <sys/stat.h>
原型:
       int mkfifo(const char *pathname, mode_t mode);
参数:
    char *pathname:指定要创建的有名管道路径+文件名;
	mode_t mode:有名管道的权限(mode & ~umask);
返回值:
    成功,返回0;
	失败,返回-1,更新errno。如果文件存在,errno == EEXIST;
3)有名管道的使用
1. 第一步:通过open函数打开有名管道
2. 第二步,通过文件IO方式读写有名管道
   ​				与通过文件IO读写正常文件的方式一致。

有名管道open打开规则

O_RDONLY 			只读
O_WRONLY 			只写
O_RDWR 				读写方式
--------以上三种方式必须选一种---------
O_NONBLOCK 		非阻塞模式打开
1.flags = O_RDONLY ;	
​		open函数将会阻塞,直到另外一个进程以写的方式打开同一个FIFO文件;
2.flags = O_WRONLY;
​		open函数将会阻塞,直到另外一个进程以读的方式打开同一个FIFO文件;
3.flags = O_RDONLY | O_NONBLOCK;
​		此时,就算没有另外一个进程以写的方式打开同一个FIFO文件,open函数也会立即返回。
​		open函数会返回成功,此时FIFO文件打开成功。
4.flags = O_WRONLY| O_NONBLOCK;
​		此时,就算没有另外一个进程以读的方式打开同一个FIFO文件,open函数也会立即返回。
​		open函数会返回失败,此时FIFO文件打开失败。
/*写*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, const char *argv[])
{
    //创建一个有名管道
    if(mkfifo("./talk", 0777) < 0)
    {
        if(errno != EEXIST)
        {
            perror("mkfifo");
            return -1;
        }
    }
    printf("fifo文件准备完毕\n");
    //打开fifo文件
    int fd_w = open("./talk", O_WRONLY); //阻塞等待另外一个进程以读的方式打开
    if(fd_w < 0)
    {
        perror("open");
        return -1;
    }
    printf("打开fifo写端成功\n");
    char buf[128] = "";
    int ret = -1;
    while(1)
    {
        bzero(buf, sizeof(buf));
        fprintf(stderr, "请输入:");
        fgets(buf, 128-1, stdin);
        ret = write(fd_w, buf, strlen(buf)-1);
        if(ret < 0)
        {
            perror("write");
            break;
        }
        printf("成功写入%d个字符\n", ret);
    }
    close(fd_w);
    return 0;
}
/*读*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, const char *argv[])
{
    //创建一个有名管道
    if(mkfifo("./talk", 0777) < 0)
    {
        if(errno != EEXIST)
        {
            perror("mkfifo");
            return -1;
        }
    }
    printf("fifo文件准备完毕\n");
    //打开fifo文件
    int fd_r = open("./talk", O_RDONLY); //阻塞等待另外一个进程以写的方式打开
    if(fd_r < 0)
    {
       perror("open");   
       return -1;
    }
    printf("打开fifo读端成功\n");
    char buf[128] = "";
    int ret = -1;
    while(1)
    {
        bzero(buf, sizeof(buf));
        ret = read(fd_r, buf, sizeof(buf));
        if(ret < 0)
        {
            perror("read");
            break;
        }
        else if(0 == ret)
        {
            fprintf(stderr, "写端关闭\n");
            break;
        }
        printf("成功读取到%d个字符:%s\n", ret, buf);
    }
    close(fd_r);
    return 0;
}
小练习
使用有名管道实现AB进程对话,要求
1)A进程发送一句话,B进程接收到然后打印;A进程等待B进程的回话
2)B进程发送一句话,然后A进程接收到打印;B进程等待A进程的回话
3)重复1) 2)步骤
4)当A进程或B程接收到quit,直接结束两个进程。

【3】信号(signal)

1.信号的概念

1)信号的原理
  1. 信号是软件层次上,对中断的一种模拟。
  2. 是一种异步通信方式。
    信号
2)进程对信号的处理方式
1. 忽略信号 	(忽略你妈叫你吃饭的信号)
   对信号不做处理,但是有两个信号不能忽略 9)SIGKILL  19)SIGSTOP  (你妈叫你吃饭,你再不下来我就打死你)
2. 捕获信号  (你决定要减肥,只要你妈叫你吃饭,你就出去跑步)
   定义信号函数,当信号发生的时候,立即执行相应处理函数。(用户自定义处理函数)
3. 执行默认(缺省)操作 (你妈叫你吃饭,你就去吃饭)
   linux对每个信号都规定了默认的操作函数。
3)常见的信号

$ kill -l 查看所有信号,共64个。(进程64种死法)

1) SIGHUP	 	2) SIGINT	 	3) SIGQUIT	 	4) SIGILL	 	5) SIGTRAP
 6) SIGABRT	 	7) SIGBUS	 	8) SIGFPE	 	9) SIGKILL		10) SIGUSR1
11) SIGSEGV		12) SIGUSR2		13) SIGPIPE		14) SIGALRM		15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD		18) SIGCONT		19) SIGSTOP		20) SIGTSTP
21) SIGTTIN		22) SIGTTOU		23) SIGURG		24) SIGXCPU		25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF		28) SIGWINCH	29) SIGIO		30) SIGPWR
31) SIGSYS		34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	
1. 硬件能按出来的信号
​	2)SIGINT 		默认处理:退出进程 	ctrl+c
​	3) SIGQUIT 		默认处理:退出进程 	ctrl+\
​	20) SIGTSTP     默认处理:挂起进程 	ctrl+z
2. 无法被忽略、缺省、捕捉的信号
9) SIGKILL 	     默认处理:退出进程 	kill 	-9 	pid用的就是这个信号
19) SIGSTOP		 默认处理:进程停止 
3. 常见的信号
11) SIGSEGV 		段错误信号,默认处理:退出进程。
​13) SIGPIPE			管道破裂信号,默认处理:退出进程。
14) SIGALRM 		时钟信号
15) SIGTERM 		功能与9号信号一致,但是可以被捕捉、缺省、忽略
17) SIGCHLD 		子进程状态改变,子进程退出,父进程会收到该信号。
18) SIGCONT 		使默认挂起的程序,继续运行。
4. 用户自定义信号
10) SIGUSR1  没有默认处理函数,需要用户自定义处理函数,用户捕捉到该函数后,运行自定义函数。
12) SIGUSR2

2.信号的相关函数

1)signal
功能:为信号注册处理函数,捕获信号;
头文件:
       #include <signal.h>
原型:
       typedef void (*sighandler_t)(int) 		//数据类型,指针类型,数据类型的名字为 sighandler_t
       sighandler_t signal(int signum, sighandler_t handler);
参数:
    int signum:信号值, kill -l;可以填对应的宏,也可以填数值;
	sighandler_t handler:函数指针变量;
			1.该指针指向信号的处理函数,函数的原型如下: void handler(int sig);
			2.SIG_IGN:忽略信号;
			3.SIG_DFL:执行默认处理函数;
返回值:
    成功,返回设置之前的信号处理函数的地址。
    失败,返回SIG_ERR,更新errno;
  
例子
typedef void (*sighandler_t)(int);
void handler(int sig)
{
	printf("触发%d号信号 %s\n", sig, strsignal(sig));
}
int main(int argc, const char *argv[])
{
	sighandler_t s = signal(SIGINT, handler);	
	if(s == SIG_ERR)
	{
		perror("signal");
		return -1;
	}
	s = signal(20, handler);
	if(s == SIG_ERR)
	{
		perror("signal");
		return -1;
	}
	printf("注册成功 %p\n", s);
	while(1)
	{
		printf("signal\n");
		sleep(1);
	}
	return 0;
}
2)strsignal
功能:打印指定信号的默认功能;
头文件:
       #include <string.h>
原型:
       char *strsignal(int sig);
参数:
    int sig:指定信号的值,或对应的宏。   
3)kill
功能:发送信号给指定进程或进程组;
头文件:
       #include <sys/types.h>
       #include <signal.h>
原型:
       int kill(pid_t pid, int sig);
参数:
    pid_t pid:指定进程id;
    	pid > 0; 	指定进程号发送信号;
		pid == 0; 	发送信号给当前进程组下的任意进程;
		pid == -1;	发送信号给系统内的所有进程,除了init进程;
		pid < -1; 	发送信号给进程组ID为pid的绝对值的所有进程;
	int sig:指定要发送的信号,可以是信号值也可以是对应的宏;
		sig == 0; 没有发送信号,可以用于判断pid进程或进程组是否存在;
返回值:
    成功,返回0;
	失败,返回-1,更新errno; 
例子
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
typedef void(*sighandler_t)(int);
void handler(int sig)
{
    printf("你爸叫你吃饭\n");
}
int main(int argc, const char *argv[])
{
    //1.注册信号
    sighandler_t s = signal(SIGUSR1, handler);
    if(s == SIG_ERR)
    {
        perror("signal");
        return -1;
    }
    //创建进程
    pid_t pid = fork();
    if(pid > 0)
    {
        //父进程
        while(1)
        {
            int ret = kill(pid, SIGUSR1);
            if(ret < 0)    
            {
                perror("kill");
                break;
            }
            sleep(2);
        }
        wait(NULL);
    }
    else if(0 == pid)
    {
        while(1)
        {
            printf("打游戏ing......\n");
            sleep(1);
        }
    }
    else
    {
        perror("fork");
        return -1;
    }
    return 0;
}
4)raise
功能:给自身发送一个信号;
头文件:
       #include <signal.h>
原型:
       int raise(int sig);
参数:
    int sig:指定要发送的信号;
返回值:
    成功,返回0;
	失败,返回非0;
相当于:kill(getpid(), sig);
5)alarm
功能:设置定时器,等到时间结束,产生一个14)SIGALRM信号;
头文件:
       #include <unistd.h>
原型:
       unsigned int alarm(unsigned int seconds);
参数:
    unsigned int seconds:设置定时时间,单位为秒;
					seconds == 0;删除定时器;且并不会产生SIGALRM信号;
返回值:
    成功,返回上一个定时器没有走完的时间;

注意
1.alarm是一个非阻塞函数,会立即返回;
2.alarm会返回上一次设定后,没有走完的时间,所以一个进程只能有一个定时器;
3.alarm(0),会清除设定的闹钟,且不会产生SIGALRM信号。返回值为旧闹钟没有走完的时间。

例子
如何让代码用alarm函数实现,3秒触发一次SIGALRM信号。不用sleep;
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
typedef void (*sighandler_t)(int);
void handler(int sig)
{
    printf("%d号信号:%s\n", sig, strsignal(sig));
    int ret = alarm(3);
    printf("%d\n",ret);
}
int main(int argc, const char *argv[])
{
    //注册信号
    sighandler_t s = signal(14, handler);
    if(SIG_ERR == s)
    {
        perror("signal");
        return -1;
    }
    printf("注册成功\n");
    int ret = alarm(3);
    while(1)
    {
        printf("signal\n");
        sleep(1);
    }
    return 0; 
}
6)pause
功能:使进程阻塞,直到当前进程收到任意信号,并执行了信号处理函数;
头文件:
       #include <unistd.h>
原型:
       int pause(void);
返回值:
    -1, pause一直阻塞,直到执行了信号处理函数,并从其返回后,pause才会返回,返回值是-1;
例子
typedef void (*sighandler_t)(int);
void handler(int sig)
{
	printf("%d号信号:%s\n", sig, strsignal(sig));
	int ret = alarm(3);
	printf("%d\n",ret);
}
int main(int argc, const char *argv[])
{
	//注册信号
	sighandler_t s = signal(14, handler);
	if(SIG_ERR == s)
	{
		perror("signal");
		return -1;
	}
	printf("注册成功\n");

	int ret = alarm(3);

	pause();
	return 0;
}
小练习
1.创建两个无血缘关系的进程A,B。通过A进程杀死B进程。要求使用 10) SIGUSR1
提示:通过管道将B进程的pid发送给A进程,A进程发送信号杀死B进程。
2.创建两个无血缘关系的进程A,B.
​	1.A进程拷贝图片前半部分,拷贝结束后退出进程.
​	2.等待A进程拷贝结束后,B进程拷贝后半部分。
​	3.不能使用sleep函数
​	提示:使用有名管道,信号,pause函数

第1题

A进程
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    //创建有名管道
    umask(0);
    int ret = -1;
    ret = mkfifo("./fifo", 0777);
    if(ret < 0)
    {
        if(errno != EEXIST)
        {
            perror("mkfifo");
            return -1;     
        }
    }
    printf("fifo准备完毕\n");
    int fd = open("./fifo", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    pid_t pid = 0;
    if(read(fd, &pid, sizeof(pid_t))<0)
    {
        perror("read");
        close(fd);
        return -1;
    }
    printf("pid = %d\n", pid);
    sleep(4);
    if(kill(pid, SIGUSR1) < 0)
    {
        perror("kill");
        close(fd);
        return -1;
    }
    printf("发送%d信号成功\n", SIGUSR1);
    close(fd);
    return 0;
}
---------------------------------------------
    B进程
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
typedef void(*sighandler_t)(int);
void handler(int sig)
{
    printf("收到%d号信号\n", sig);
    exit(1);
}
int main(int argc, const char *argv[])
{
    //信号注册
    sighandler_t s = signal(SIGUSR1, handler);
    if(SIG_ERR == s)
    {
        perror("signal");   
        return -1;
    }
    //创建有名管道
    umask(0);
    int ret = -1;
    ret = mkfifo("./fifo", 0777);
    if(ret < 0)
    {
        if(errno != EEXIST)
        {
            perror("mkfifo");
            return -1;
        }
    }
    printf("fifo准备完毕\n");
    int fd = open("./fifo", O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    pid_t pid = getpid();
    if(write(fd, &pid, sizeof(pid_t))<0)
    {
        perror("write");
        close(fd);
        return -1;
    }
    while(1)
    {
        sleep(1);
    }
    close(fd);
    return 0;
}

【4】共享内存(share memory)

1. 概念

1)共享内存的原理

将同一物理地址分别映射到不同进程,进程只需要操作自己的虚拟地址就能实现对物理地址的操作。
共享内存

2)共享内存的特点
1. 共享内存是 **最高效的** 进程间通信方式;
   进程可以直接读写内存中的数据,不需要任何的数据拷贝;
2. 共享内存在内核空间中创建,可以被进程映射到用户空间;
3. 多进程,可以同时访问共享内存,因此需要引入同步互斥机制。
4. 一旦申请共享内存成功,就算退出程序,共享内存依然存在。需要手动删除,或退出系统。
3)查看共享内存
查看共享内存:
$ ipcs -m
删除共享内存:
$ ipcrm -m shmid 
$ ipcrm -m 2588681
$ ipcrm -M key
$ ipcrm -M 0x61013995

2. 共享内存的函数

1)ftok
功能:通过文件路径pathname 以及 proj_id 获取 key值(键值,共享内存的唯一标识),只要文件路径和proj_id不变,key值不变;
	如果另外一个进程也输入同样的文件路径pathname 以及 proj_id,那么就能获取同样的共享文件标识.
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
原型:
       key_t ftok(const char *pathname, int proj_id);
参数:
    char *pathname:文件路径(目录路径 或普通文件);
			注意:路径一定要真实存在,而且可以被访问。
	int proj_id:非0参数,用自定义;
返回值:
    成功,返回key值(键值);
	失败,返回-1,更新errno;     
2)shmget
功能:创建共享内存;
头文件:
       #include <sys/ipc.h>
       #include <sys/shm.h>
原型:
       int shmget(key_t key, size_t size, int shmflg);
参数:
    key_t key:键值,(ftok()获取到的);
	size_t size:申请多大的内存。以字节为单位;
	int shmflg:指定操作标识
				1)IPC_CREAT 创建共享内存,如果没有该选项,则判断用户是否有访问该段的权限。
        		2)IPC_EXCL 	判断共享内存是否存在.当与IPC_CREAT一起使用,如果存在,则返回失败.
        		3)IPC_CREAT|0777,如果内核中不存在与key值相同的共享内存,则创建共享内存,并设置8进制权限.
        						如果存在,则直接返回共享内存的标识符;
				4)IPC_CREAT|IPC_EXCL|0777, 如果内核中不存在与key值相同的共享内存,则创建共享内存,并设置8进制权限.
                    						如果存在,则报错, errno == EEXIST;
返回值:
    成功,返回共享内存标识符;
	失败,返回-1, 更新errno;
3)shmat
功能:将共享内存映射到用户空间的虚拟地址中;
头文件:
       #include <sys/types.h>
       #include <sys/shm.h>
原型:
       void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
    int shmid:共享内存标识符;
	void *shmaddr:进程中用户空间的虚拟地址,指定共享内存映射的位置。
        			一般填NULL,系统自定义;
	int shmflg:指定进程对共享内存的操作;
			0:可读可写,
            SHM_RDONLY:只读;
返回值:
    成功,返回共享内存映射到用户空间的地址;
	失败,返回 (void*)-1,更新errno;
4)shmdt
功能:断开共享内存在用户空间的映射;
头文件:
       #include <sys/types.h>
       #include <sys/shm.h>
原型:
       int shmdt(const void *shmaddr);   
参数:
	void *shmaddr:需要断开映射的虚拟地址
	
返回值:
    成功,返回0;
	失败,返回 -1,更新errno;
5)shmctl
功能:控制共享内存;
头文件:
       #include <sys/ipc.h>
       #include <sys/shm.h>
原型:
       int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
    int shmid:共享内存id;
	int cmd:控制方式
			IPC_STAT 	获取共享内存的属性,存放在第三个参数中,struct shmid_ds *buf;
        				需要确保,共享内存有可读权限;
			IPC_SET 	设置共享内存的属性;
			IPC_RMID	删除共享内存,第三个参数填NULL;
返回值:
    成功,返回0;
	失败,返回-1,更新errno;

共享内存删除的时间点:第一个进程创建共享内存,最后一个进程删除内存
shmctl(shmid,IPC_RMID,NULL)添加删除标记
当所有的进程都取消对共享内存的映射,且添加了删除标记,共享内存才会被真正删除,nattach系统会检查,每个进程都有nattach,nattach记录了有几个进程映射了共享内存,一个进程调用一次nattach,nattach就加为1,直到所有进程都取消了映射,即nattach=0且添加了删除标记,才会真正删除
而消息队列和信号灯会立即删除

例子
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(int argc, const char *argv[])
{
    //1.摇号 ftok
    key_t key = ftok("./", 'a');
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    printf("%x\n", key);
    //2.买车 创建共享内存 
    int shmid = shmget(key, 20, IPC_CREAT|0777);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    printf("创建共享内存成功:%d\n", shmid);
    system("ipcs -m");
    //3.上车 将共享内存映射到用户空间
    void* shmaddr = NULL;
    shmaddr = shmat(shmid, NULL, 0);
    if(shmaddr == (void*)-1)
    {
        perror("shmat");
        return -1;
    }
    printf("映射成功,地址%p\n", shmaddr);
    char str[20] = "hello";
    char dest[20] = "";
    strcpy(dest, str);
    printf("%s\n", dest);
    strcpy((char*)shmaddr, str);
    printf("%s\n", (char*)shmaddr);
    //4.下车,断开映射
    if(shmdt(shmaddr) == -1)
    {
        perror("shmdt");   
        return -1;
    }
    //5.炸车,删除共享内存
    if(shmctl(shmid, IPC_RMID, NULL)<0)
    {
        perror("shmctl");
        return -1;
    }
    printf("删除成功\n");
    return 0;
}
小练习
使用共享内存实现,一个进程读,一个进程写
写
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main(int argc, const char *argv[])
{
    //1.key
    key_t key = ftok("./", 'a');
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }

    //2.shmget
    int shmid = shmget(key, 20, IPC_CREAT|0777);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }

    //3.shmat
    void* shmaddr = NULL;
    if((shmaddr=shmat(shmid, NULL, 0)) == (void*)-1)
    {
        perror("shmat");
        return -1;
    }

    char *ptr = (char*)shmaddr;
    char buf[20] = "";
    //写
    bzero(ptr, 20);
    fprintf(stderr, "请输入:");
    fgets(ptr, 20-1, stdin);


    //4.shmdt
    shmdt(shmaddr);                                                                                             
    ptr = NULL;

    //5.shmctl

    return 0;
}
---------------------------------------------------------------------------------
读;
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main(int argc, const char *argv[])
{
    //1.key
    key_t key = ftok("./", 'a');
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }

    //2.shmget
    int shmid = shmget(key, 20, IPC_CREAT|0777);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }                                                                                                                                                   

    //3.shmat
    void* shmaddr = NULL;
    if( (shmaddr=shmat(shmid, NULL, 0)) ==(void*)-1 )
    {
        perror("shmat");
        return -1;
    }

    //读
    printf("%s\n", (char*)shmaddr);

    //4.shmdt
    shmdt(shmaddr);

    //5.shmctl
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

【5】消息队列

1.概念

1)消息队列的原理
1. 消息队列是在内核的内存中创建一个容器(队列),进程只需要将数据节点添加到队列结尾,或从队列中读取结点,实现进程间的通信。
2. 一个消息队列,是由一个标识符来标识(队列ID)

消息队列

2)消息队列的特点
1. 消息队列是面向记录的,其中消息具有特定的格式以及优先级。
2. 消息队列是独立于进程的,所以进程结束,消息队列及其内容不会消失;
3. 消息队列可以实现消息的随机查询,消息不一定是先进先出的顺序,也可以按照消息类型读取。
3)查看消息队列
查看消息队列
$ ipcs -q 
删除消息队列
$ ipcrm -q	msqid
$ ipcrm -Q 	key

2.消息队列的函数

1)ftok
功能:通过文件路径pathname 以及 proj_id 获取 key值(键值,消息队列的唯一标识),只要文件路径和proj_id不变,key值不变;
	如果另外一个进程也输入同样的文件路径pathname 以及 proj_id,那么就能获取同样的消息队列标识.
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
原型:
       key_t ftok(const char *pathname, int proj_id);
参数:
    char *pathname:文件路径(目录路径 或普通文件);
			注意:路径一定要真实存在,而且可以被访问。
	int proj_id:非0参数,用自定义;
返回值:
    成功,返回key值(键值);
	失败,返回-1,更新errno;  
2)msgget
功能:创建消息队列;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
原型:
       int msgget(key_t key, int msgflg);
参数:
    key_t key:key值;
	int msgflg:指定操作标识
				1)IPC_CREAT 创建消息队列,如果没有该选项,则判断用户是否有访问该段的权限。
        		2)IPC_EXCL 	判断消息队列是否存在.当与IPC_CREAT一起使用,如果存在,则返回失败.
        		3)IPC_CREAT|0777,如果内核中不存在与key值相同的消息队列,则创建消息队列,并设置8进制权限.
        						如果存在,则直接返回消息队列的标识符;
								如果不加上权限,会访问不了消息队列。
				4)IPC_CREAT|IPC_EXCL|0777, 如果内核中不存在与key值相同的消息队列,则创建消息队列,并设置8进制权限.
                    						如果存在,则报错, errno == EEXIST;
返回值:
    成功,返回消息队列标识符;msqid;
	失败,返回-1,更新errno;
== 3)msgsnd==
功能:向消息队列发送数据;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
原型:
       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
    int msqid:指定的消息队列id;
	void *msgp:要发送的数据,需要按照以下格式:
           struct msgbuf {
               long mtype;       /* message type, must be > 0 */ 消息类型,必须大于0;
               char mtext[1];    /* message data */ 消息;
           };
	size_t msgsz:消息的大小,不包括消息类型:long mtype;
	int msgflg:
        	0,阻塞方式发送,如果消息队列满了,则该函数阻塞;
		IPC_NOWAIT:非阻塞方式发送,如果消息队列满了,该函数不会阻塞,立即返回,并且更新errno == ENOMSG;
返回值:
    成功,返回0;
	失败,返回-1;
4)msgrcv
	功能:从消息队列中拿数据;拿过的数据,会从消息队列中删除;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
原型:
       ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
    int msqid:指定消息队列的id;
	void *msgp:存储获取到的消息队列数据包,这个的数据类型要与想要接受的消息数据类型一致。
        	struct msgbuf {
               long mtype;       /* message type, must be > 0 */ 消息类型,必须大于0;
               char mtext[1];    /* message data */ 消息;
           };
	size_t msgsz:读取的数据大小,以字节为单位;
	long msgtyp:想要接收的消息类型;
		msgtyp == 0; 读取消息队列中的第一个消息;这个时候是遵循先进先出的原则;
		msgtyp > 0; 读取消息队列中第一条消息类型 (mtype) == msgtyp的消息;
		msgtyp < 0; 读取消息队列中第一条消息类型 (mtype)<= |msgtyp|的消息;
    int msgflg:
        	0,阻塞方式接收,如果消息队列没有数据,则该函数阻塞;
		IPC_NOWAIT:非阻塞方式接收,如果消息队列没有,该函数不会阻塞,立即返回,并且更新errno == ENOMSG;
5)msgctl
功能:操作消息队列;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
原型:
       int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
    int msqid:指定要操作的消息队列id;
    int cmd:控制方式
			IPC_STAT 	获取消息队列的属性,存放在第三个参数中,struct shmid_ds *buf;
        				需要确保,消息队列有可读权限;
			IPC_SET 	设置消息队列的属性;
			IPC_RMID	删除消息队列,第三个参数填NULL;
	struct msqid_ds *buf:存储消息队列的属性;
返回值:
        成功,返回0;
		失败,返回-1,更新errno;
小练习
1.通过消息队列实现,对讲机版本的进程对话 1)A进程讲,B进程接收。2)B进程讲,A进程接收
2.通过消息队列实现,打电话版本的进程对话,AB可以随时发送,随时接收

第1题

/*A先发送给B,然后等待B发送给A*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define SND 1
#define RCV 2
#define SIZE 128
struct msgbuf
{
    long mtype;
    char mtext[SIZE];
};
int main(int argc, const char *argv[])
{
    //1.摇号    
    key_t key = ftok("./", 10);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    //2.买车
    int msqid = msgget(key, IPC_CREAT|0777);
    if(msqid < 0)
    {
        perror("msgget");
        return -1;
    }
    struct msgbuf snd_buf;
    snd_buf.mtype = SND;        //发送的消息类型
    struct msgbuf rcv_buf;
    rcv_buf.mtype = RCV;        //接收的消息类型
    while(1)
    {
        //3.送货上车
        bzero(snd_buf.mtext, SIZE);
        fprintf(stderr, "请输入:");
        fgets(snd_buf.mtext, SIZE, stdin);
        if(msgsnd(msqid, (void*)&snd_buf, strlen(snd_buf.mtext), 0)<0)
        {
            perror("msgsnd");
            return -1;
        }
        //4.卸货
        bzero(rcv_buf.mtext, SIZE);
        if(msgrcv(msqid, (void*)&rcv_buf, SIZE, RCV, 0)<0)
        {
            perror("msgrcv");
            return -1;
        }
        printf("对方说:%s\n", rcv_buf.mtext);
    }
    //删除
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}
------------------------------------------------------------------------
/*先等待A发送给B,然后B发送给A*/
#include <stdio.h>      
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define RCV 1
#define SND 2
#define SIZE 128
struct msgbuf
{
    long mtype;
    char mtext[SIZE];
};
int main(int argc, const char *argv[])
{
    //1.摇号
    key_t key = ftok("./", 10);
    if(key < 0)
    {   
        perror("ftok");
        return -1; 
    }   
    //2.买车
    int msqid = msgget(key, IPC_CREAT|0777);
    if(msqid < 0)
    {   
        perror("msgget");
        return -1; 
    }   
    struct msgbuf snd_buf;
    snd_buf.mtype = SND;        //发送的消息类型
    struct msgbuf rcv_buf;
    rcv_buf.mtype = RCV;        //接收的消息类型
    while(1)
    {   
        bzero(rcv_buf.mtext, SIZE);
        //4.卸货
        if(msgrcv(msqid, (void*)&rcv_buf, SIZE, RCV, 0)<0)
        {
            perror("msgrcv");
            return -1; 
        }
        printf("对方说:%s\n", rcv_buf.mtext);
        //3.送货上车
        fprintf(stderr, "请输入:");
        fgets(snd_buf.mtext, SIZE, stdin);
        if(msgsnd(msqid, (void*)&snd_buf, strlen(snd_buf.mtext), 0)<0)
        {
            perror("msgsnd");
            return -1; 
        }
    }
    //删除
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}

【6】信号灯集(semaphore)

PV操作:是一种能够实现进程同步与互斥的有效方式,可用于共享内存的同步互斥。
P操作:通过信号量,-1操作
V操作:释放信号量,+1操作

1.概念

1)什么是信号灯集
信号灯:也叫信号量,它可以完成不同进程或给定进程内不同线程间的同步互斥。
信号灯集:信号量的集合,一个信号灯集合中有一个或多个信号灯。

信号灯集

2)核心操作
P操作:通过信号量,-1操作
V操作:释放信号量,+1操作
注意:在操作信号灯集的时候,是不能被中断的,即cpu是不能被切换走的。
3)查看信号灯集合
查看信号灯集
$ ipcs -s
删除
$ ipcrm -s semid
$ ipcrm -S key

2.信号灯集的函数

1)ftok
功能:通过文件路径pathname 以及 proj_id 获取 key值(键值,信号灯集的唯一标识),只要文件路径和proj_id不变,key值不变;
	如果另外一个进程也输入同样的文件路径pathname 以及 proj_id,那么就能获取同样的信号灯集标识.
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
原型:
       key_t ftok(const char *pathname, int proj_id);
参数:
    char *pathname:文件路径(目录路径 或普通文件);
			注意:路径一定要真实存在,而且可以被访问。
	int proj_id:非0参数,用自定义;
返回值:
    成功,返回key值(键值);
	失败,返回-1,更新errno;
2)semget
功能:创建信号灯集,创建成功后信号灯集中的信号量默认初始化为0;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>
原型:
       int semget(key_t key, int nsems, int semflg);
参数:
    key_t key:
    int nsems:指定信号灯集中有几个信号量;
	int msgflg:指定操作标识
				1)IPC_CREAT 创建信号灯集,如果没有该选项,则判断用户是否有访问该段的权限。
        		2)IPC_EXCL 	判断信号灯集是否存在.当与IPC_CREAT一起使用,如果存在,则返回失败.
        		3)IPC_CREAT|0777,如果内核中不存在与key值相同的消息队列,则创建信号灯集,并设置8进制权限.
        						如果存在,则直接返回信号灯集的标识符;
								如果不加上权限,会访问不了信号灯集。
				4)IPC_CREAT|IPC_EXCL|0777, 如果内核中不存在与key值相同的信号灯集,则创建信号灯集,并设置8进制权限.
                    						如果存在,则报错, errno == EEXIST;
返回值:
    成功,返回semid,信号灯集的id;
	失败,返回-1;
3)semop
功能:通过P(-1)/V(+1)信号灯集中的信号量;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>
原型:
       int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:
    int semid:指定信号灯集的id;
	struct sembuf *sops:如何操作信号灯集的信号量。P、V;
		struct sembuf{
           unsigned short sem_num;  /* semaphore number */ 信号灯的编号,从0开始
           short          sem_op;   /* semaphore operation */  信号灯的操作
               													正数,例如+1,释放信号量 V操作
               													0,等待,直到信号量的值为0
               													负数,例如-1,通过信号量,P操作
           short          sem_flg;  /* operation flags */ 	运行方式,一般填0;
            													0:以阻塞方式运行,当信号量为0,阻塞等待
                                                                IPC_NOWAIT:非阻塞方式,当信号量为0,不阻塞;
        };
	unsigned nsops:信号操作结构体的数量;
返回值:
    成功,返回0;
	失败,返回-1,更新errno;
4)semctl
功能:控制信号灯集;
头文件:
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>
原型:
       int semctl(int semid, int semnum, int cmd, ...);
参数:
    int semid:信号灯集id;
	int semnum:指定要控制的信号灯编号;
	int cmd:控制方式
			IPC_STAT:获取信号灯集的属性,存在第四个参数中,确保信号灯集有可读权限。
			IPC_SET:设置信号灯集的属性;
        	IPC_RMID:删除信号灯集,第四个参数不填;第二个参数,可以随便填,因为没有任何意义。
        	GETALL:获取信号灯集中所有信号灯的值,存储在第四个参数中,忽略int semnum;
			SETALL:设置信号灯集中所有信号灯的值,存储在第四个参数中,忽略int semnum;
			GETVAL:获取指定信号灯的值;
			SETVAL:设置指定信号灯的值;
	...:不定参数,不同的cmd,该参数不同;
           union semun {
               int              val;    /* Value for SETVAL */ 				cmd == SETVAL,使用这个参数
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */	cmd == IPC_STAT IPC_SET
               unsigned short  *array;  /* Array for GETALL, SETALL */		cmd == GETALL SETALL
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
           };
返回值:
    成功,返回大于或等于0;
	失败,返回-1;
例子
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main(int argc, const char *argv[])
{
    key_t key = ftok("./", 'c');
    int semid = semget(key, 2, IPC_CREAT|0777);
    //SETVAL
    int val = 2;
    if(semctl(semid, 0, SETVAL, val)<0)
    {
        perror("semid");
        return -1;
     }   
    printf("设置成功\n");
    //GETALL
    unsigned short getall[2] = {0};
    if(semctl(semid, 20, GETALL, getall)<0)
    {
        perror("semid");
        return -1;
    }
    printf("0:%d, 1:%d\n", getall[0], getall[1]);
    //GETVAL
    unsigned short getval = -1;
    getval = semctl(semid, 0, GETVAL);
    printf("0:%d\n", getval);
    system("ipcs -s");
    //删除信号灯集
    semctl(semid, 20, IPC_RMID);
    system("ipcs -s");
    return 0;
}
小练习
1.创建俩没有亲缘关系的进程,一个进程倒置字符串,一个进程打印该字符串。
提示:共享内存,信号灯集
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/shm.h>
#include<string.h>
#include<errno.h>
int main(int argc,const char*argv[])
{
	char arr[20]="wuhu qifei!";
	key_t key1 = ftok("./",2);
	key_t key2 = ftok("./",3);
	if(key1<0||key2<0)
	{
		perror("key");
		return -1;
	}
	int shmid = shmget(key1,64,IPC_CREAT|0777);
	if(shmid<0)
	{
		if(errno != EEXIST)
		{
			perror("shmget");
			return -1;
		}
	}
	void* shmarr=NULL;
	shmarr = shmat(shmid,NULL,0);
	if(shmarr == (void*)-1)
	{
		perror("shmat");
		return -1;
	}
	int semid = semget(key2,2,IPC_CREAT|0777);
	if(semid<0)
	{
		if(errno != EEXIST)
		{
			perror("semget");
			return -1;
		}
	}
	struct sembuf semstart;
	semstart.sem_num=0;
	semstart.sem_op=-1;
	semstart.sem_flg=0;
	struct sembuf semend;
	semend.sem_num=1;
	semend.sem_op=+1;
	semend.sem_flg=0;
	int a=1;
	if(semctl(semid,0,SETVAL,a)<0)
	{
		perror("semctl");
		return -1;

	}
	while(1)
	{
		if(semop(semid,&semstart,1)<0)
		{
			perror("semop:start");
			return -1;
		}
		bzero(shmarr,64);
		int i=0;
		char temp;
		for(i=0;i<strlen(arr)/2;i++)
		{
			temp=arr[i];
			arr[i]=arr[strlen(arr)-i-1];
			arr[strlen(arr)-i-1]=temp;
		}
		strcpy(shmarr,arr);
		if(semop(semid,&semend,1)<0)
		{
			perror("semop:end");
			return -1;
		}
	}
	return 0;
}
----------------------------------------------------
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/shm.h>
#include<string.h>
#include<errno.h>
int main(int argc,const char*argv[])
{
	key_t key1 = ftok("./",2);
	key_t key2 = ftok("./",3);
	if(key1<0||key2<0)
	{
		perror("key");
		return -1;
	}
	int shmid = shmget(key1,64,IPC_CREAT|0777);
	if(shmid<0)
	{
		if(errno != EEXIST)
		{
			perror("shmget");
			return -1;
		}
	}
	void* shmarr=NULL;
	shmarr = shmat(shmid,NULL,SHM_RDONLY);
	if(shmarr == (void*)-1)
	{
		perror("shmat");
		return -1;
	}
	int semid = semget(key2,2,IPC_CREAT|0777);
	if(semid<0)
	{
		if(errno != EEXIST)
		{
			perror("semget");
			return -1;
		}
	}
	struct sembuf semstart;
	semstart.sem_num=1;
	semstart.sem_op=-1;
	semstart.sem_flg=0;
	struct sembuf semend;
	semend.sem_num=0;
	semend.sem_op=+1;
	semend.sem_flg=0;
	while(1)
	{
		if(semop(semid,&semstart,1)<0)
		{
			perror("semop:start");
			return -1;
		}
		printf("%s\n",(char*)shmarr);
		if(semop(semid,&semend,1)<0)
		{
			perror("semop:end");
			return -1;
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值