(20220429)进程间通信

目录

一、进程间通信概述

1、进程通信的目的

2、进程间通信的方式

管道和有名管道

信号

        信号的处理

共享内存

消息队列

信号量

套接字(socket)

二、管道通信

无名管道

命名管道(有名管道)

三、信号通信

发送信号的主要函数有 kill和raise    

时钟信号alarm

挂起等待函数pause

信号处理函数signal

四、共享内存

shmget函数(创建共享内存)      

shmat函数(映射共享内存)

shmdt函数(解除映射)

shmctl 函数(释放共享内存)

五、消息队列

消息结构(结构体)

ftok函数(设置键值)

msgget函数(创建消息队列)

msgsnd函数(发送消息)

msgrcv函数(接收消息)

msgclt函数(删除消息队列)

六、信号量

semget函数(配置信号灯)

semop函数(信号灯处理)

semctl函数(控制信号灯)


一、进程间通信概述

1、进程通信的目的

数据传输、资源共享、通知事件、进程控制

kill -l (命令用于查看 进程能识别的信号,但是大部分进程是忽略的)

bxp@ubuntu:~/2022/0429$ kill -l
 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

(9 SIGKILL)和(15 SIGTERM) 用于杀死进程; (19 SIGSTOP) 用于停止进程(无法忽略)

(18 SIGCONT)继续进程  (3 SIGINT)相当于 ctrl + c  (4 SIGQUIT)  退出

(14 SIGALRM)时钟信号

2、进程间通信的方式

管道和有名管道

管道(无名管道)主要用于亲缘关系的进程间的通信;有名管道克服了管道没有名字的限制,除了具有管道所有的属性外,还允许无亲缘关系进程间的通信(任意两个进程间通信)。

管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。

数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞

信号

信号用于通知接收进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;

信号是进程间异步通信机制中唯一的异步通信机制,可以看作异步通知,通知接受信号的进程有哪些事件发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号(signal)机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:     

 1、当用户按某些按键时,产生信号     

 2、硬件异常产生信号:除数为0、无效的存储 访问等等。这些情况通常由硬件检测到,将其通 知内核,然后内核产生适当的信号通知进程,例 如,内核对正访问一个无效存储区的进程产生一 个SIGSEGV信号      

 3、进程用kill函数将信号发送给另一个进程    

 4、用户可用kill命令将信号发送给其他进程

下面是几种常见的信号:kill -l (命令用于查看 进程能识别的信号,但是大部分进程是忽略的)

§ SIGHUP: 从终端上发出的结束信号

§ SIGINT: 来自键盘的中断信号(Ctrl-C)

§ SIGKILL:该信号结束接收信号的进程

§ SIGTERM:kill 命令发出的信号

§ SIGCHLD:标识子进程停止或结束的信号

§ SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号

信号的处理

当某信号出现时,将按照下列三种方式中 的一种进行处理:

1、忽略此信号      

大多数信号都按照这种方式进行处理,但有两种     信号却决不能被忽略,它们是:SIGKILL\SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供了一种终止或停止进程的方法

2、执行用户希望的动作

通知内核在某种信号发生时,调用一个用户 函数。在用户函数中,执行用户希望的处理

3、执行系统默认动作 

对大多数信号的系统默认动作是终止该进程

共享内存

共享内存是被多个进程共享的一部分物理内存.共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A和进程B各自的进程地址空间。进程A可以及时看到进程B对共享内存中数据的更新。

消息队列

unix早期通信机制之一的信号能够传送的信息量有限,管道则只能传送无格式的字节流,以及缓冲区大小受限等缺点,这无疑会给应用程序开发带来不便。消息队列(也叫做报文队列)则克服了这些缺点。

消息队列就是一个消息的链表.可以把消息看作一个记录,具有特定的格式.进程可以向中按照一定的规则添加新消息;另一些进程则可以从消息队列中读走消息

目前主要有两种类型的消息队列:  

 POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用

系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除

信号量

信号量(又名:信号灯)与其他进程间通信方式不大相同,主要用途是保护临界资源.进程可以根据它判定是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步

套接字(socket)

套接字是更为一般的进程间通信机制,可用于不同机器之间的进程间通信。

二、管道通信

无名管道

属于内存文件,在内存中归内核管理(亲缘关系间的进程通信,按文件操作,因此管道创建后也需要关闭)

管道的大小固定,传输的是无格式字节流(有空和满的概念)

读与写管道的进程必须同时存在,否则管道无法使用。

无名管道的创建:

#include <unistd.h>

int pipe(int filedis[2]);    

当一个管道建立时,它会创建两个文件描述符:filedis[0] 用于读管道, filedis[1] 用于写管道;返回值:成功:0;失败:-1  (错误原因在 errno 中)

管道用于不同进程间通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。必须在系统调用fork( )前调用pipe( ),否则子进程将不会继承文件描述符

两个进程进行通信,通常要创建两个管道(一个是:子进程写,父进程读;另一个是:子进程读,父进程写),实现读和写的分离。

举例一:功能:子进程与父进程实现了读写字符串同步(创建两个管道)(子进程写入 小写字母;父进程读取小写字母,并将其转化为大写,重新写入管道; 子进程读取大写字母)

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


int main()
{
    int pip_fd[2];
    int pip_fda[2];
    int read_num;
    char buf[100];
    pid_t pid;
    int i;

    memset(buf, 0, sizeof(buf));

    if (pipe(pip_fd) < 0)
    {
        perror("create pipe error!\n");
        exit(EXIT_FAILURE);
    }

    if (pipe(pip_fda) < 0)
    {
        perror("create pipe error!\n");
        exit(EXIT_FAILURE);
    }

    if ((pid = fork()) < 0)
    {
        perror("create process error!\n");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        close(pip_fd[0]);
        close(pip_fda[1]);

        if (write(pip_fd[1], "hello", 5) < 0)
        {
            perror("write error!\n");
            exit(EXIT_FAILURE);
        }
        
        if (write(pip_fd[1], "pipe", 5) < 0)
        {
            perror("write error!\n");
            exit(EXIT_FAILURE);
        }  

        if ((read_num =read(pip_fda[0], buf, 100)) < 0)
        {
            perror("parent:read error!\n");
            exit(EXIT_FAILURE);
        }
        else
        {
            printf("child read :%d number read about buf %s\n",read_num,buf);
        }

        close(pip_fd[1]);
        close(pip_fda[0]);


    }
    else if (pid > 0)
    {
        close(pip_fda[0]);
        close(pip_fd[1]);
        sleep(1);

        if ((read_num =read(pip_fd[0], buf, 100)) < 0)
        {
            perror("parent:read error!\n");
            exit(EXIT_FAILURE);
        }
        else
        {
            printf(" parent read:%d number read about buf %s\n",read_num,buf);
        }

        for (i = 0; i < read_num; i++)
        {
            buf[i] = buf[i] - 32;
        }
        buf[--i] = '\0';

        if (write(pip_fda[1], buf, read_num) < 0)
        {
            perror("write error!\n");
            exit(EXIT_FAILURE);
        }
        //sleep(1);

        close(pip_fd[0]);
        close(pip_fd[1]);

    }
    
    
    
    

    return 0;
}
bxp@ubuntu:~/2022/0429$ ./pip 
parent read:10 number read about buf hellopipe
child read :10 number read about buf HELLOPIPE

举例二:功能:父进程写4个字符串含数字,子进程将数字取出,并输出(一个管道,一写一读)

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

int handle_cmd(int cmd)
{
    if((cmd < 0) || cmd > 256)
    {
        printf("child:invalid command!\n");
        return -1;
    }
    printf("child: the command from parent is %d\n",cmd);
    return 0;
}

int main(int argc , char *argv[])
{
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4];
    char *w_buf[256];
    int childexit = 0;
    int i;
    int cmd;

    memset(r_buf,0,sizeof(r_buf));

    if(pipe(pipe_fd) < 0)
    {
        printf("pipe create error!\n");
        return -1;
    }
    if((pid = fork()) == 0)
    {
        printf("\n");
        close(pipe_fd[1]);
        sleep(2);
        while(!childexit)
        {
            read(pipe_fd[0],r_buf,4);
            cmd = atoi(r_buf);
            if(cmd == 0)
            {
                printf("child:receive command from parent over\n now,child process exit\n");
                childexit = 1;
            }
            else if(handle_cmd(cmd) != 0)
                return 0;
            sleep(1);
        }
        close(pipe_fd[0]);
        _exit(0);
    }
    else if(pid > 0)
    {
        close(pipe_fd[0]);
        w_buf[0] = "003";
        w_buf[1] = "005";
        w_buf[2] = "077";
        w_buf[3] = "000";
        for(i = 0;i < 4;i++)
        {
            write(pipe_fd[1],w_buf[i],4);
        }
        close(pipe_fd[1]);
    }

    return 0;
}
bxp@ubuntu:~/2022/0430$ 
child: the command from parent is 3
child: the command from parent is 5
child: the command from parent is 77
child:receive command from parent over
 now,child process exit

命名管道(有名管道)

命名管道和无名管道基本相同,但也有不同点:无名管道只能由父子进程使用;但是通过命名管道,不相关的进程也能交换数据。

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char * pathname, mode_t mode)

pathname:FIFO文件名(管道名)

mode:(权限格式:S_IRUSR、S_IWUSR、S_IRGRP、S_IROTH)属性(见文件操作)   返回值:成功:0;失败:-1  (错误原因在 errno 中)

一旦创建了一个FIFO,就可用open打开它,一般的文件访问函数(close、read、write等)都可用于FIFO

举例一:功能:建立两个程序,实现非亲缘进程间的通信,(建立一个有名管道,一读一写);write 进程写入管道,read 进程读出字符串内容

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define FIFO "/tmp/myfifo"

void main(int argc,char** argv)
{
	char buf_r[100];
	int  fd;
	int  nread;
	
	if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
		printf("cannot create fifoserver\n");
	
	printf("Preparing for reading bytes...\n");
	
	memset(buf_r,0,sizeof(buf_r));
	
	fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);
	if(fd==-1)
	{
		perror("open");
		exit(1);	
	}
	while(1)
	{
		memset(buf_r,0,sizeof(buf_r));
		
		if((nread=read(fd,buf_r,100))==-1)
		{
			if(errno==EAGAIN)
				printf("no data yet\n");
		}
		printf("read %s from FIFO\n",buf_r);
		sleep(1);
	}	
	pause();
	unlink(FIFO);
}

bxp@ubuntu:~/2022/0430$ sudo ./fifo_read0 
[sudo] password for bxp: 
Preparing for reading bytes...
read  from FIFO
read  from FIFO
read hello! from FIFO
read  from FIFO
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FIFO_SERVER "/tmp/myfifo"

void main(int argc,char** argv)
{
	int fd;
	char w_buf[100];
	int nwrite;
		
	fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
	
	if(argc==1)
	{
		printf("Please send something\n");
		exit(-1);
	}
	
	strcpy(w_buf,argv[1]);

	if((nwrite=write(fd,w_buf,100))==-1)
	{
		if(errno==EAGAIN)
			printf("The FIFO has not been read yet.Please try later\n");
	}
	else 
		printf("write %s to the FIFO\n",w_buf);
}

bxp@ubuntu:~/2022/0430$ ./fifo_write0 hello!
write hello! to the FIFO

三、信号通信

发送信号的主要函数有 kill和raise    

区别:      

Kill既可以向自身发送信号,也可以向其他进程发送信号。与kill函数不同的是,raise函数是向进程自身发送信号

#include <sys/types.h>    

#include <signal.h>    

int kill(pid_t pid, int signo)    

//pid :进程id ; signo: 信号名

int raise(int signo)

//signo: 信号名

返回值:成功:0;失败:-1 

kill的pid参数有四种不同的情况:

1、pid>0    将信号发送给进程ID为pid的进程。

2、pid == 0    将信号发送给同组的进程。

3、pid < 0    将信号发送给其进程组ID等于pid绝对值的进程。

4、pid ==-1     将信号发送给所有进程。

时钟信号alarm

每个进程只能有一个闹钟时间.如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,以前登记的闹钟时间则被新值代换

如果有以前登记的尚未超过的闹钟时间,而这次seconds值是0,则表示取消以前的闹钟

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

seconds:指定多少秒后,发送信号给当前进程。如果为0,则之前设置的闹钟被取消,将剩下的时间返回。

返回值:之前闹钟剩余秒数,之前未设置闹钟则返回0

挂起等待函数pause

pause函数使调用进程挂起直至捕捉到一个信号

#include <unistd.h>             

int pause(void)

只有执行了一个信号处理函数后,挂起才结束

信号处理函数signal

当系统捕捉到某个信号时,可以忽略该信号或是使用指定的处理函数来处理该信号,或者使用系统默认(结束进程)的方式

信号处理的主要方法有两种,一种是使用简单的signal函数,另一种是使用信号集函数组

#include <signal.h>

void (*signal (int signo, void (*func)(int)))                 

signo : 信号名

void (*func)(int) : 回调函数:处理该信号的方法

func可能的值是:

1、SIG_IGN:忽略此信号

2、SIG_DFL: 按系统默认方式处理

3、信号处理函数名:使用该函数处理

四、共享内存

特殊的设备文件,在内核里面(内存形成的特殊结构的文件)

共享内存实现分为两个步骤:          

一、创建共享内存,使用shmget函数          

二、映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数

三、使用:* 或 [ ]  (当作内存用)

四、解除映射,使用shmdt函数

五、释放共享内存,使用shmctl 函数

shmget函数(创建共享内存)      

#include <sys/shm.h>

int shmget ( key_t key, int size, int shmflg )

返回值:如果成功,返回共享内存标识符id;如果失败,返回-1

key:键用于标识这个共享内存;

取键方法(三种):

key = IPC_PRIVATE(内核定义)或 key = 0 && shmflg = IPC_PRIVATE(适用于亲缘进程);

key = 123(人为定义,许多人定义不免会重复冲突);

函数定义法:

#include <sys/types.h>

#include <sys/ipc.h>

key_t ftok(char *pathname, char proj)

返回值:成功:键值, 失败:-1

pathname : 文件路径

proj : 单个字符(无实际意义)

size: 共享内存大小(以页为单位 4k)

shmflg:取值:IPC_PRIVATE(取键会用)、IPC_CREAT|IPC_EXCL(共享内存创建设置)

shmat函数(映射共享内存)

#include <sys/shm.h>

char * shmat ( int shmid, char *shmaddr, int flag)

shmid:shmget函数返回的共享存储标识符

shmaddr:共享内存起始地址映射到该参数上(就指共享内存地址,通常为 NULL)

flag:决定以什么方式来确定映射的地址(通常为0)

返回值:    如果成功,则返回共享内存映射到进程中的地址;

                   如果失败,则返回(char*)-1

stdio 输入(键盘)  stdout 输出(屏幕)

shmdt函数(解除映射)

当一个进程不再需要共享内存时,需要把它从进程地址空间中脱离。

#include <sys/shm.h>

int shmdt ( char *shmaddr )

shmaddr:共享内存起始地址

返回值:失败:-1  否则成功

shmctl 函数(释放共享内存)

#include <sys/shm.h>

int shmclt (int shmid, int cmd , struct shmid_ds *buf)

shmid: 共享内存id

cmd: 命令(IPC_RMID(将共享内存删除)   IPC_STAT(设置属性)   IPC_SET(共享内存结构属性 ))

buf:共享内存属性的结构 通常为NULL

返回值: 失败:-1 否则成功

IPC_RMID(将共享内存删除)和 buf:通常为NULL 连用

IPC_STAT(设置属性)   IPC_SET(共享内存结构属性 ) 和  buf:共享内存属性的结构 连用

五、消息队列

消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,必须提供该消息队列的键值

消息队列创建基本步骤:

构建消息结构(结构体)

一、设键值,使用 ftok函数

二、创建消息队列,使用 msgget函数

三、发送消息,使用 msgsnd函数

四、接收消息,使用msgrcv函数

五、删除消息队列,使用msgclt函数

消息结构(结构体)

struct msgbuf

{      

        long mtype;/*消息类型*/      

        char mtext[1]; /*消息数据的首地址*/      

};

//通用结构

ftok函数(设置键值)

#include <sys/types.h>    

#include <sys/ipc.h>    

key_t ftok (char*pathname, char proj)

返回值:成功:键值, 失败:-1

功能:    

返回文件名对应的键值。    

pathname:文件名  (路径)

proj:项目名(是个字符,不为0,无意义)

msgget函数(创建消息队列)

#include <sys/types.h>    

#include <sys/ipc.h>    

#include <sys/msg.h>    

int msgget(key_t key, int msgflg)

返回值:与健值key相对应的消息队列描述字(id)

key:键值,由ftok获得。    

msgflg:标志位。

IPC_CREAT    创建新的消息队列

IPC_EXCL    与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。

IPC_NOWAIT     读写消息队列要求无法得到满足时,不阻塞

在以下两种情况下,

将创建一个新的消息队列:

1、如果没有与健值key相对应的消息队列,并且 msgflg中包含了IPC_CREAT标志位。

2、key参数为IPC_PRIVATE

msgsnd函数(发送消息)

include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgsnd(int msqid, struct  msgbuf*msgp, int msgsz, int msgflg)

返回值: 成功: 0  失败:-1

功能:向消息队列中发送一条消息

msqid : 已打开的消息队列id

msgp :存放消息的结构

msgsz :消息数据长度(从起始位置开始,msgsz个字节)

msgflg :发送标志,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待; 取 0 阻塞

msgrcv函数(接收消息)

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgrcv(int msqid, struct msgbuf *msgp, int   msgsz, long msgtyp, int msgflg)

返回值: >0 (读到的信息), == 0 (空), < 0 (错误 -1)

功能:

从msqid代表的消息队列中读取一个 msgtyp类型的消息,并把消息存储在msgp指向 的msgbuf结构中。在成功地读取了一条消息以后,队列中的这条消息将被删除

msgflg :发送标志,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待; 取 0 阻塞

msgclt函数(删除消息队列)

#include <sys/shm.h>

int msgclt (int shmid, int cmd , struct shmid_ds *buf)

shmid: 共享内存id

cmd: 命令(IPC_RMID(将共享内存删除)   IPC_STAT(设置属性)   IPC_SET(共享内存结构属性 ))

buf:共享内存属性的结构 通常为NULL

返回值: 失败:-1 否则成功

六、信号量

信号量(又名:信号灯)与其他进程间通信方式不大相同,主要用途是保护临界资源.进程可以根据它判定是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步

semget函数(配置信号灯)

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg)

返回值:成功:信号灯集描述符 失败:-1

key:键值,由ftok获得。    

nsems:打开或创建的信号灯的数码

msgflg:标志位。

IPC_CREAT    创建新的消息队列

IPC_EXCL    与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。

IPC_NOWAIT     读写消息队列要求无法得到满足时,不阻塞

semop函数(信号灯处理)

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semop (int semid,  struct sembuf *sops,  unsigned nsops)

返回值:成功:0; 失败:-1

semid:信号灯集id

sembuf: 信号灯的结构(包含操作)

nsops: 信号灯结构的大小

semctl函数(控制信号灯)

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semctl (int semid,  int semnum,  int cmd,  union semun srg)

返回值:成功:返回与cmd相关的值; 失败:-1

semid:信号灯集id

semnum: 对哪个信号灯进行操作

cmd: 操作类型(命令)

arg: 用于设置或返回信号灯信息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值