进程间通信(IPC)

进程间通信(IPC)介绍

参考博文
进程间通信是指不同进程之间的信息传递或交互。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

进程间通信的shell指令

ipcs用法

  • ipcs -a 是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息

  • ipcs -m 打印出使用共享内存进行进程间通信的信息

  • ipcs -q 打印出使用消息队列进行进程间通信的信息

  • ipcs -s 打印出使用信号进行进程间通信的信息

  • ipcs -t 输出信息的详细变化时间

  • ipcs -u 输出当前系统下ipc各种方式的状态信息(共享内存,消息队列,信号)
    在这里插入图片描述
    ipcrm -m id
    在这里插入图片描述

管道

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

无名管道
1.特点
  1. 它是半双工的(数据只能在一个方向上流动),并且具有固定的读端和写端。
  2. 它只能在拥有血缘关系(父子进程或兄弟进程)的进程之间通信。
  3. 它可以看成是一种特殊的文件,对于它的读写可以用普通的write和read函数,但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中(内核中创建内存)。
    在这里插入图片描述
原型
int pipe(int fd[2]);
	成功,返回0,出错返回-1.
  1. 先使用pipe创建管道,再调用fork函数创建子进程。
  2. 当管道创建成功时,系统会创建两个文件描述符:fd[0]和fd[1],fd[0]是读端,fd[1]是写端。
  3. 在通信前需先固定通信的方向,关闭多余的管道口,如父进程写入时关闭fd[0],同时子进程也需要关闭fd[1]。
  4. 可以通过write和read使用文件描述符对管道进行读和写数据。当管道中的数据被读走之后,管道中就没有数据。
    在这里插入图片描述
    以下代码是简单的父子管道通信:
#include<stdio.h>
#include <unistd.h>
#include<string.h>
int main()
{
        int fd[2];
        pid_t pid;
        char*writebuf="zhou jiang hong shi da mo wang";
        char readbuf[40]={0};
        if(pipe(fd)==-1)//创建管道
        {
                perror("pipe");
        }
        if((pid=fork())<0)//创建子进程
        {
                perror("fork");
        }
        else if(pid>0)
        {
                close(fd[0]);//关闭读端
                write(fd[1],writebuf,strlen(writebuf));
        }
        else{
                close(fd[1]);//关闭写端
                read(fd[0],readbuf,sizeof(readbuf));//(子进程走得快的话)当没有数据可读时read会一直阻塞,直到读到数据。
                printf("read from father:%s\n",readbuf);
        }
        close(fd[1]);
        close(fd[0]);
        return 0;
}
  
FIFO(命名管道)

FIFO也被称命名管道,它是一种文件类型。

特点
  1. 它与无名管道之间的不同就是可以在无关的进程之间通信。
  2. 它是一个特殊文件存在于磁盘中,与路径名相关联。
原型

利用mkfifo和mkfifoat可以创建命名管道。

int mkfifo(const char *pathname, mode_t mode);
int mkfifoat(int dirfd, const char *pathname, mode_t mode);

    //成功,返回0;失败,返回-1。当FIFO文件已存在时errno会设置EEXIST的错误码

mkfifoat和mkfifo函数相似,但是mkfifoat函数可以被用来在fd文件描述符表示的目录相关位置创建一个FIFO。像其他*at函数一样,这里有3种情形:

  1. 如果path参数指定的是绝对路径名,则fd会被忽略掉,并且mkfifoat函数的行为和mkfifo类似。

  2. 如果path参数指定的是相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。

  3. 如果path参数指定是相对路径名,并且fd参数有一个特殊值AF_FDCWD,则路径名以当前枯井开始,mkfifoat和mkfifo类似。
    当我们创建一个管道后,对他的读写也是用文件I/O。

  4. open函数打开管道,只能是一边只读一边只写,否则不能通信,一直阻塞。

  5. read读时当没有读到数据会一直阻塞,除非open的mode指定为(O_NONBLOCK)。

  6. 若指定了O_NONBLOCK,则open只读立即返回。但是如果没有进程为读而打开一个FIFO,那么只写open将返回-1.并设置errno为ENXIO。
    FIFO可以支持有多个写端,但是如果不希望多个写端的数据交叉,就必须考虑原子操作。和管道一样PIPE_BUF说明了可被原子地写到FIFO的最大数据量。

以下是简单的FIFO进程通信
**写端:
**

#include<string.h>
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<errno.h>
int main()
{
        char* writebuf="qiu wawng hai shige da sha dang!";
        if(mkfifo("fifo",0600)==-1)//创建管道
        {
                if(errno==EEXIST)
                {
                        printf("pathname already exists.\n");//文件已存在
                }
                else{
                        perror("mkfifo");
                }
        }
        int fd=open("fifo",O_WRONLY);//只写
        if(fd==-1)
        {
                perror("open");
        }
        else{
                printf("open success!\n");
        }
        if(write(fd,writebuf,strlen(writebuf))==-1)
        {
                perror("write");
        }
        close(fd);
        return 0;
        
}   

读端:

#include<stdio.h>
#include<errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include<string.h>
#include <fcntl.h>

int main()
{
        char readbuf[128]={0};
        int fd=open("fifo",O_RDONLY);//只读
        if(fd==-1)
        {
                perror("open");
        }
        if(read(fd,readbuf,sizeof(readbuf))==-1)
        {
                perror("read");
        }
        printf("read:%s\n",readbuf);
        close(fd);
        return 0;

}


在这里插入图片描述

在这里插入图片描述

消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

1.特点
  1. 消息队列是面向记录的,其中消息具有特定的格式和特定的优先级

  2. 消息队列在进程终止时依然存在。

  3. 消息队列可以按照先进先出读取,也可按照消息类型读取。不像管道一样只能一边收发,双全工。

  4. msgget用于创建一个新的消息队列或者打开一个现有队列。

  5. msgsnd将消息添加到队列尾端。每个消息包含一个正的长整形类型的字段、非负的长度以及实际数据字节数(对应于长度),所有这些 消息都在将消息添加到队列时传送给msgsnd。

  6. msgrcv用于从队列中取出消息。

2.需要的api
key_t ftok(const char *pathname, int proj_id);//获取键值

1 #include <sys/msg.h>
2 // 创建或打开消息队列:成功返回非负队列ID,失败返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失败返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 读取消息:成功返回消息数据的长度,失败返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息队列:成功返回0,失败返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgget函数
创建或获取一个消息队列。
在以下两种情况下,msgget将创建一个新的消息队列:

  1. 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
  2. key参数为IPC_PRIVATE。
  • key值是给内核一个相当于地址一样的ID值(索引值),由ftok函数获得,也可以自主给出。
  • flag为队列的权限,与open一样,当需要创建一个新队列时 或上IPC_CREAT。

msgsnd函数
向消息队列添加新消息

  • msgid是消息队列的id,通过msgget返回。
  • ptr是一个空类型指针,通常这里传递的是一个指向mymesg结构体的指针,结构体中包含了一个长整形的消息类型和存放消息的字符数组。如下:
struct mymesg{
	long mtype;     //设置想要的消息类型,如:8888。
	char mtext[512];	//这里大小可以自主选择,但是不能超过512。
};
  • size是需要发送消息的大小。
  • flag的值可以指定为IPC_NOWAIT,类似于文件I/O的非阻塞标志。若消息队列已满则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果没有指定IPC_NOWAIT,则进程一直阻塞到有空间可以容纳要发送的消息;或者系统中删除了此队列;或则扑捉到一个信号,并从消息处理程序返回。
  • 当msgsnd返回成功时,消息队列相关的msgid_ds结构会随之更新,表明调用的进程ID(msg_lspid)、调用的时间(msg_stime)以及消息队列中新增的消息(msg_qnum)。

msgrcv函数
从消息队列中拿出消息。

  • 这个其他和msgsnd基本一样,type则是消息的类型,就是结构体中的mtype。

  • size指定消息缓冲区的长度,若返回的消息长度大于size,而且在flag中设置了MSG_NOERROR位,则该消息会被截断,被截去的部分将会丢失。如果没有这一标志,而消息又太长则返回出错E2BIG(消息仍在队列中)。

  • 参数type可以指定想要哪一种消息。
    (1)type==0 返回队列的第一个消息。
    (2) type>0 返回队列中消息类型为type的第一个消息。
    (3)* type<0 返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。*

  • flag的值可以指定为IPC_NOWAIT,使操作不阻塞,如果没有所指定类型的消息可用,那么则msgrcv返回-1.errno设置为ENOMSG。如果没有指定IPC_NOWAIT,则进程会一直阻塞到有了指定类型的消息可用,或者系统中删除了此队列;或则扑捉到一个信号,并从消息处理程序返回。
    msgrcv成功执行时内核会更新与该消息队列相关联的msgid_ds结构,以指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并指示消息队列中的消息数减少了一个(msg_qnum)。

msgctl函数
msgctl函数对队列执行多种操作。
struct msqid_ds *buf 删除时可以填NULL。
参数cmd说明对由msqid指定的队列要执行的命令:

  • IPC_STAT :取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
  • IPC_SET :按由buf指向结构中的值,设置与此队列相关结构中的字段。
  • IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。
    (这三条命令也可用于信号量和共享存储)

send.c

  1 #include<string.h>
  2 #include<stdio.h>
  3 #include <sys/types.h>
  4 #include <sys/ipc.h>
  5 #include <sys/msg.h>
  6 struct msgbuf
  7 {
  8         long mtype;       /* message type, must be > 0 */
  9         char mtext[100];    /* message data */
 10 };
 11 int main()
 12 {
 13         struct msgbuf sendbuf={888,"zhou jiang hong shi ge da mo wang!"};
 14         struct msgbuf readbuf;
 15         int readlen;
 16         memset(&readbuf,0,sizeof(readbuf));
 17         int key;
 18         key=ftok(".",'z');  //获取键值
 19 
 20         int msgid;
 21         msgid=msgget(key,0666|IPC_CREAT);  //创建一个消息队列
 22 
 23         if(msgid==-1)
 24         {
 25                 perror("msgget");
 26         }
 27 
 28         if(msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0)==-1)
 29         {
 30                 perror("msgsnd");
 31         }
 32 
 33         readlen=msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),999,0);//flag为0视为默认,在默认情况下不读到该类型的消息会阻塞。
 34         if(readlen==-1)
 35         {
 36                 perror("msgrcv");
 37         }
 38         else
 39                 printf("%s,readlen=%d\n",readbuf.mtext,readlen);
 40         if(msgctl(msgid,IPC_RMID,NULL)==-1)//删除时可以写NULL
 41         {
 42                 perror("msgctl");
 43         }
 44 
 45         return 0;
 46 }

get.c

  1 #include<string.h>
  2 #include<stdio.h>
  3 #include <sys/types.h>
  4 #include <sys/ipc.h>
  5 #include <sys/msg.h>
  6 struct msgbuf
  7 {
  8         long mtype;       /* message type, must be > 0 */
  9         char mtext[100];    /* message data */
 10 };
 11 int main()
 12 {
 13         struct msgbuf sendbuf={999,"ni shuo de dui,qiu wang hai shi xiao di di!"};
 14         struct msgbuf readbuf;
 15         int readlen;
 16         int sendlen;
 17         memset(&readbuf,0,sizeof(readbuf));
 18         int key;
 19         key=ftok(".",'z');
 20 
 21         int msgid;
 22         msgid=msgget(key,0666|IPC_CREAT);//IPC_CREAT如果该键值对应的消息队列已存在时不会出错,直接获取
 23 
 24         if(msgid==-1)
 25         {
 26                 perror("msgget");
 27         }
 28 
 29 
 30         readlen=msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),888,0);//flag为0视为默认,在默认情况下不读到该类型的消息会阻塞。
 31         if(readlen==-1)
 32         {
 33                 perror("msgsnd");
 34         }
 35         else
 36                 printf("get:%s,readlen=%d\n",readbuf.mtext,readlen);
 37         sendlen=msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);
 38         if(sendlen==-1)
 39         {
 40                 perror("msgsnd");
 41         }
 42 
 43 
 44         return 0;
 45 }

共享内存参考博文1 参考博文2 详细共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
ipcs -m 能查看共享内存,ipcrm -m 删除共享内存

1、特点
  • 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  • 因为多个进程可以同时操作,所以需要进行同步。
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
最简单的共享内存的使用流程

①ftok函数生成键值。

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

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

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

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

1.ftok函数生成键值

每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要)。

所需头文件:#include<sys/ipc.h>
函数原型 : key_t ftok(const char *path ,int id);

path为一个已存在的路径名

id 为0~255之间的一个数值,代表项目ID,自己取

返回值: 成功返回键值(相当于32位的int)。出错返回-1

例如:key_t key = ftok( “/tmp”, 66);

2.原型
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
3.shmget函数创建共享存储空间并返回一个共享存储标识符
  • key 第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。

  • size 为共享内存的长度,以字节为单位,它的值一般为一页大小的整数倍(未到一页,操作系统向上对齐到一页,但是用户实际能使用只有自己所申请的大小)。

  • flag 的值为IPC_CREAT:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则直接返回共享存储标识符。
    flag 的值为 IPC_CREAT | IPC_EXCL: 如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则产生错误。

  • 例如:int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666); 创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个整形共享存储标识符,如果key值已经存在有共享存储空间了,则出错返回-1。
    int id = shmget(key,4096,IPC_CREAT|0666); 创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个共享存储标识符,如果key值已经存在有共享存储空间了,则直接返回一个共享存储标识符。

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

头文件: #include<sys/shm.h>
函数原型: void *shmat(int shmid, const void *addr, int flag);

  • shmid 为shmget生成的共享存储标识符。
  • addr 指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置。
  • flag 为对数据的操作,如果指定为SHM_RDONLY则以只读方式连接此段,其他值为读写方式连接此段。

返回值: 成功返回指向共享存储段的指针;错误返回-1(打印出指针的值为全F)

例如:char addr = shmat(id, NULL, 0); 就会返回第一个可用的共享内存地址的指针的值给*addr **。

4.shmdt函数进行分离

当不需要对此共享内存进行操作时候,调用shmdt函数进行分离,不是删除此共享存储空间哟。

头文件: #include<sys/shm.h>

函数原型: int shmdt(const void *addr);

addr 为shmat函数返回的地址指针

返回值: 成功返回0;错误返回-1

例如:int ret = shmdt(addr);

5.shmctl函数对共享内存进行控制

简单的操作就是删除共享存储空间了,也可以获取和改变共享内存的状态

头文件: #include<sys/shm.h>

函数原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf);

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

常用的 cmd 有三个:
IPC_RMID : 为删除共享内存;
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中;
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内。(内核为每个共享存储段维护着一个结构,结构名为shmid_ds,这里就不讲啦,里面存放着共享内存的大小,pid,存放时间等一些参数)

buf 就是结构体shmid_ds

返回值: 成功返回0;错误返回-1

例如: int ret = shmctl(id, IPC_RMID,NULL);删除id号的共享存储空间

send.c

  1 #include<stdio.h>
  2 #include <sys/ipc.h>
  3 #include <sys/shm.h>
  4 #include <sys/types.h>
  5 #include<string.h>
  6 #include<unistd.h>
  7 int main()
  8 {
  9         key_t key;
 10         int shmid;
 11 
 12         char* shmaddr=NULL;
 13         key=ftok(".",'z');
 14         if(key==-1)
 15         {
 16                 printf("key generate failure\n");
 17         }
 18 
 19         shmid=shmget(key,4096,0666|IPC_CREAT);
 20         if(shmid==-1)
 21         {
 22                 perror("shmget");
 23         }
 24 
 25         shmaddr=shmat(shmid,NULL,0);
 26         if(shmaddr==(void*)-1)
 27         {
 28                 perror("shmat");
 29         }
 30         strcpy(shmaddr,"zhou jiang hong shi da da mo wang!");//这里也可以替换成输入的呀!
 31         sleep(5);//为了不让两个进程同时访问,这里加了延时,学会信号量后可用信号量替换
 32 
 33         printf("%s\n",shmaddr);
 34 
 35         if((shmdt(shmaddr))==-1)
 36         {
 37                 perror("shmdt");
 38         }
 39 
 40         return 0;
 41 }

get.c

 1 #include<stdio.h>
  2 #include <sys/ipc.h>
  3 #include <sys/shm.h>
  4 #include <sys/types.h>
  5 #include<string.h>
  6 int main()
  7 {
  8         key_t key;
  9         int shmid;
 10 
 11         char* shmaddr=NULL;
 12         key=ftok(".",'z');
 13         if(key==-1)
 14         {
 15                 printf("key generate failure\n");
 16         }
 17 
 18         shmid=shmget(key,4096,0666|IPC_CREAT);
 19         if(shmid==-1)
 20         {
 21                 perror("shmget");
 22         }
 23 
 24         shmaddr=shmat(shmid,NULL,0);
 25         if(shmaddr==(void*)-1)
 26         {
 27                 perror("shmat");
 28         }
 29         printf("%s\n",shmaddr);
 30         memset(shmaddr,0,sizeof(shmaddr));
 31         strcpy(shmaddr,"ni shuo de dui,qiu wang hai shi zhi zhu!");//切记是要用strcpy给字符串给内容的哦
 32         if((shmdt(shmaddr))==-1)
 33         {
 34                 perror("shmdt");
 35         }
 36 
 37         return 0;
 38 }

信号量

参考文章

1.什么是信号量

信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域

临界区域是指执行数据更新的代码需要独占式地执行。 而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

2.特点
  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  • 支持信号量组。

信号量的工作原理

信号量与共享内存的生产者消费者模型

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

原型
1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);  
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);

semget:

当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

semop:
  • sembuf: 对应numops ,当numops为1时,就一个结构体,numops数量为多个,则是一个结构体数组,结构的定义如下:
1 struct sembuf 
2 {
3     short sem_num; // //除非使用一组信号量,否则它为0
4     short sem_op;  // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
														//一个是+1,即V(发送信号)操作。
5     short sem_flg; // IPC_NOWAIT, SEM_UNDO//通常为SEM_UNDO,使操作系统跟踪信号,其中没有(信号量)钥匙时进程去拿会挂起,
																				//直到,信号量释放(钥匙归还)						   
                    			//并在进程没有释放该信号量而终止时,操作系统释放信号量

6 }
  • **numops:**对应信号量的个数。
semctl:

该函数用来直接控制信号量信息。

前两个参数与前面一个函数中的一样,cmd通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
**IPC_RMID:**用于删除一个已经无需继续使用的信号量标识符。此时没有第4为参数。

如果有第四个参数,它通常是一个union semum结构,定义如下:

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) */
           };

test.c

 1 #include<stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/ipc.h>
  5 #include <sys/sem.h>
  6 union semun {
  7                int              val;    /* Value for SETVAL */
  8                struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
  9                unsigned short  *array;  /* Array for GETALL, SETALL */
 10                struct seminfo  *__buf;  /* Buffer for IPC_INFO
 11                                            (Linux-specific) */
 12            };
 13 void pGetkey(int id)//拿锁函数(减信号量)
 14 {
 15         struct sembuf sops;//numops多个时,需要一个结构体数组
 16         sops.sem_num = 0;        /* Operate on semaphore 0 */	//对信号量0进行操作
 17         sops.sem_op = -1;         /* Wait for value to equal 0 */  //减信号量,所以是-1
 18         sops.sem_flg = SEM_UNDO;//设置SEM_UNDO没有信号量时拿锁会阻塞
 19         if(semop(id,&sops,1)==-1)
 20         {
 21                 perror("semop");
 22         }
 23 }
 24 void vPutBakekey(int id)//放锁
 25 {
 26         struct sembuf sops;
 27         sops.sem_num = 0;        /* Operate on semaphore 0 */
 28            sops.sem_op = 1;         /* Wait for value to equal 0 */  //放锁,所以信号量+1
 29            sops.sem_flg = SEM_UNDO;
 30         if(semop(id,&sops,1)==-1)
 31         {
 32                 perror("semop");
 33         }
 34 }
35 int main()
 36 {
 37         key_t key;
 38         int semid;
 39         union semun set;
 40         pid_t pid;
 41 
 42         if((key=ftok(".",'z'))==-1)
 43         {
 44                 perror("key");
 45         }
 46 		//int semget(key_t key, int nsems, int semflg);
 47         semid=semget(key,1,0666|IPC_CREAT);//nsems 为“1”设置信号量集中信号量个数为 1。
 48         if(semid==-1)
 49         {
 50                 perror("semget");
 51         }
 52 		//int semctl(int semid, int semnum, int cmd, ...);  
 53         set.val=0;//设置信号量的初值为0。
 54         if(semctl(semid,0,SETVAL,set)==-1)//semnum 设置信号量集中的第几个信号量,第一个是0。
 55         {					//SETVAL为设置信号量的初值,与union semun联合体中的val有关。
 56                 perror("semctl");
 57         }
 58 
59         if((pid=fork())==-1)
 60         {
 61                 perror("fork");
 62         }
 63         else if(pid==0)
 64         {
 65 
 66                 printf("this is chlind\n");
 67                 vPutBakekey(semid);//此时子进程以执行完毕,加锁,解救父进程。
 68         }
 69         else
 70         {
 71                 pGetkey(semid);//此时信号量的初值为0,一旦父进程拿锁即挂起,实现无论如何都是子进程“先行”。
 72                 printf("this is father\n");
 73                 vPutBakekey(semid);//拿完锁记得还锁。
 					if(semctl(semid,1,IPC_RMID)==-1)//最后不要忘记删除信号集,不然就算进程结束依然存在
   		            {						//一定要在阻塞后执行的进程删除,不然人家还堵着你就把它删了,拿锁或还锁就出错。	
                        perror("semctl");
                	}

 74         }
 75 
 76         return 0;
 77 }

信号(signal)

1.信号的基本概念参考博文

参考博文2
信号是Linux进程通讯中唯一的异步通讯方式。

信号从软件层次上看是对中断机制的一种模拟。一个进程收到信号时的处理方式与CPU收到中断请求时的处理方式一样。收到信号的进程会跳入信号处理函数,执行完后再跳回原来的位置继续执行。

信号来源:有一类信号是已经被定义好的,如数据异常、指令异常、定时器、abort等。他们都有自己特殊的用法,如:发生异常时会触发异常信号。还有一类是自定义信号。

2.信号分类
2.1. 可靠与不可靠

Linux中的信号有64个,分为可靠信号与不可靠信号两种。

不可靠信号:

Linux信号机制来继承自Unix系统,信号值小于SIGRTMIN(SIGRTMIN=32,SIGRTMAX=63)的信号都沿用了Unix的实现方式,这种方式的信号可能会丢失,所以称为不可靠信。不可靠信号的处理机制类似于中断,同一个信号同时发生多次时,会合并为一个信号,其它都会丢失。

不可靠信号都是有预定值的,每个信号都有确定的用途及含义,并且每个信号都有各自缺省的动作。如按ctrl+c时,会产生SIGINT信号,对应的默认反应是进程终止。

可靠信号:

Linux在支持Unix不可靠信号的同时,还支持改进后的可靠信号。信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。可靠信号类似于linux的软中断机制,实际上就是支持信号的排队,这样同一个信号同时发生多次时可以排队等待执行,不会丢失。

可靠信号没有被预定义,可以用于应用进程。可靠信号也都有缺省动作,默认反应都是结束进程。

2.2. 预定义与自定义

预定义信号:

不可靠信号同时也是预定义信号,它们都是有预定值的。每个信号都有确定的用途及含义,一般不会被用做其它用途,默认反应都是结束进程。每个信号根据其用途都有各自缺省的动作,如按ctrl+c时,会产生SIGINT信号。

自定义信号:

可靠信号同时也是自定义信号,它们没有被预定义。自定义可以用于应用进程,它们也都有缺省动作,默认反应都是结束进程。

3.信号的名字和编号:

每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。

在这里插入图片描述

4.信号的处理:

信号的处理有三种方法,分别是:忽略、捕捉和默认动作

  • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。

  • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴。
    具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。

5.信号处理函数的注册

信号处理函数的注册不只一种方法,分为入门版和高级版

  1. 入门版:函数signal
  2. 高级版:函数sigaction
6.信号处理发送函数

信号发送函数也不止一个,同样分为入门版和高级版

  1. 入门版:kill
  2. 高级版:sigqueue
函数原型
#include <signal.h>

       typedef void (*sighandler_t)(int);//函数指针
       sighandler_t signal(int signum, sighandler_t handler);

#include <sys/types.h>
#include <signal.h>
       int kill(pid_t pid, int sig);


例子
signal

#include<stdio.h>
#include <signal.h>
void handler(int signum)
{
        printf("signum=%d\n",signum);
}
int main()
{
        //typedef void (*sighandler_t)(int);

        //sighandler_t signal(int signum, sighandler_t handler);//signum为信号值,handler是捕捉后的处理函数,此处也可填一个忽略的宏SIG_IGN
        while(1)
        {
                if(signal(SIGINT,handler)==SIG_ERR)//SIG_ERR函数错误时返回的宏
                {
                        perror("signal");
                }
        }
        return 0;
}

kill

#include<stdio.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,char** argv)
{
        //int kill(pid_t pid, int sig);pid进程id,sig 信号值
        //int atoi(const char *nptr);将字符转成整形
        char cmd[30]={0};

        int signum;
        int pid;
        signum=atoi(argv[1]);
        pid=atoi(argv[2]);

        if(kill(pid,signum)==-1)
        {
          perror("kill");
        }

        //sprintf(cmd,"kill -%d %d",signum,pid);将字符和整形组合成字符串。
        //system(cmd);调用system实现
        return 0;
}

总结一下:
根据以上的结果可看到,基本可以实现了信号的发送,虽然不能直接发送信号名称,但是通过信号的编号,可以正常的给程序发送信号了,也是初步实现了信号的发送流程。

关于 kill 函数,还有一点需要额外说明,上面的程序限定了 pid 必须为大于0的正整数,其实 kill 函数传入的 pid 可以是小于等于0的整数。
pid > 0:将发送个该 pid 的进程
pid = 0:将会把信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限想这些进程发送信号。
pid < 0:将信号发送给进程组ID 为 pid 的绝对值得,并且发送进程具有权限向其发送信号的所有进程
pid = -1:将该信号发送给发送进程的有权限向他发送信号的所有进程。(不包括系统进程集中的进程)

信号注册函数——高级版

我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。咱么先来看看发送的函数吧。

sigaction 的函数原型

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。默认阻塞
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一
  • signum应该就是注册的信号的编号;
  • act如果不为空说明需要对该信号有新的配置;
  • oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

在这里额外说一下struct sigaction结构体中的 sa_mask 成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

sigaction 的时效性:当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用 sigaction并改变动作之前都会一直有效。

关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用 sa_sigaction 这个函数指针所指向的信号处理函数。否则,系统会默认使用 sa_handler 所指向的信号处理函数。在此,还要特别说明一下,sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

关于void (*sa_sigaction)(int, siginfo_t *, void );处理函数来说还需要有一些说明。void 是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。

 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 */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

其中的成员很多,si_signo 和 si_code 是必须实现的两个成员。可以通过这个结构体获取到信号的相关信息。
关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据。

那么,kill 函数发送的信号是无法携带数据的,我们现在还无法验证发送收的部分,那么,我们先来看看发送信号的高级用法后,我们再来看看如何通过信号来携带数据吧。

信号发送函数——高级版
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

使用这个函数之前,必须要有几个操作需要完成

  • 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。

sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。

sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。

但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。

接收

#include<stdio.h>
#include <signal.h>
void handler(int signum,siginfo_t* siginfo,void* inspection)
{							//siginfo_t结构体,数据在其中主要是以下几个。
        /*pid_t    si_pid;发送信号进程id
          sigval_t si_value;消息的联合体,man手册中在sigqueue里面找
          union sigval {
          int   sival_int;消息的整形
          void *sival_ptr;
          };

          int      si_int;消息的整形
          void    *si_ptr;*/
        if(inspection!=NULL)//非空代表有消息,反之无数据
        {
                printf("pid=%d\n",siginfo->si_pid);
                printf("signum=%d\n",signum);
                printf("masegge=%d\n",siginfo->si_value.sival_int);
        }
        else{
                printf("no data\n");
                printf("pid=%d\n",siginfo->si_pid);
                printf("signum=%d\n",signum);
        }

}
int main()
{					//信号值			//结构体						//备份的结构体,不备份填NULL
        //int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
        /*struct sigaction * {
          void     (*sa_handler)(int);这个和低级等效,不携带数据
          void     (*sa_sigaction)(int, siginfo_t *, void *);选这个可以携带消息
          sigset_t   sa_mask;//默认阻塞
          int        sa_flags;//接收消息时一定要填SA_SIGINFO
          void     (*sa_restorer)(void);
          };*/
        int signum;
        struct sigaction act;
        act.sa_sigaction=handler;
        act.sa_flags=SA_SIGINFO;
        while(1)
        {
                if(sigaction(SIGINT,&act,NULL)==-1)
                {
                        perror("sigaction");
                }
        }
}

发送

#include<stdio.h>
#include <signal.h>
int main(int argc,char** argv)
{
        //int sigqueue(pid_t pid, int sig, const union sigval value);
        /*union sigval {
               int   sival_int;
               void *sival_ptr;
           };*/
        int pid=atoi(argv[2]);
        int signum=atoi(argv[1]);
        union sigval value;
        value.sival_int=9;
        if(sigqueue(pid,signum,value)==-1)
        {
                perror("sigqueue");
        }
        return 0;

}

共享内存与信号量结合,实现进程间通信

shared_memory.h

#ifndef __SHARED_MEMORY_H
#define __SHARED_MEMORY_H

int create_shm(int shmsize);    // 创建共享内存,返回id
char*  shm_map(int shmid);   //映射
void unload(char *addr); //卸载
void del_shm(int shm_id);    //删除

#endif

shared_memory.c

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
    /*
    key_t ftok(const char *pathname, int proj_id);
    int shmget(key_t key, size_t size, int shmflg);
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    int shmdt(const void *shmaddr);
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    */

int create_shm(int shmsize)    // 创建共享内存,返回id
{
    key_t key;
    int shmid=0;

    key = ftok(".",'z');
    shmid = shmget(key,shmsize,IPC_CREAT|0666); //创建共享内存,可读可写
    if(shmid==-1)
    {
        perror("shmget");
        exit(-1);
    }
    return shmid;
}
char*  shm_map(int shmid)   //映射
{
    char* addr=NULL;
    addr = shmat(shmid,NULL,0);
    if(addr == (void*)-1)
    {
        perror("map");
        exit(-1);
    }
    return (char*)addr;
}

void unload(char *addr) //卸载
{
    if(shmdt((void*)addr)==-1)
    {
        perror("shmdt");
        exit(-1);
    }
}

void del_shm(int shm_id)
{
    if(shmctl(shm_id,IPC_RMID,NULL)==-1)
    {
        perror("shmctl");
    }
}

semaphole.h

#ifndef __SEMAPHOLE_H
#define __SEMAPHOLE_H

int create_sem();    //创建/获取信号量
void init_sem(int semid,int set_val);    //初始化信号量
//拿锁
void p(int semid) ;
//放锁
void v(int semid);
void del_sem(int semid) ;//删除信号量

#endif

semaphole.c

#include"semaphole.h"
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/*
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...);
int semop(int semid, struct sembuf *sops, size_t nsops);

*/
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) */
           };

int create_sem()    //创建/获取信号量
{
    key_t key=0;
    int semid=0;

    key=ftok(".",'y');
    semid=semget(key,1,IPC_CREAT|0666);
    if(semid==-1)
    {
        perror("semget");
        exit(-1);
    }
    return semid;
}

void init_sem(int semid,int set_val)    //初始化信号量
{
    union semun setval;
    setval.val=set_val;
    if(semctl(semid,0,SETVAL,setval)==-1)
    {
        perror("init");
    }
    
}

//拿锁
void p(int semid)   
{
    struct sembuf sops;
    sops.sem_num=0;
    sops.sem_op = -1;
    sops.sem_flg = SEM_UNDO;
    if(semop(semid,&sops,1) == -1)
    {
        perror("p");
        exit(-1);
    }
}

//放锁
void v(int semid)
{
    struct sembuf sops;
    sops.sem_num=0;
    sops.sem_op = +1;
    sops.sem_flg = SEM_UNDO;
    if(semop(semid,&sops,1) == -1)
    {
        perror("v");
        exit(-1);
    }
}

void del_sem(int semid) //删除信号量
{
    if(semctl(semid,0,IPC_RMID) == -1)
    {
        perror("del_sem");
        exit(-1);
    }
}

实现场景:
send程序先执行。send进程首先向get进程发送消息,send发送完之后,拿锁(p)访问信号量被挂起;直到get进程读取到消息,向send进程回复完以后,释放信号量(v),send进程重新活动,读取get进程发过来的消息。
接着,get进程想要读取send进程的消息,但是现在send进程还没发,所以让他拿锁(p)挂起,直到send写完,释放信号量。
如此往复
send(写)—————挂起————send(读)——(写)——释放get
get(读)——get(写)——释放send——挂起——————get(读)

send1.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<string.h>
#include"semaphole.h"
#include"shared_memory.h"



int main()
{
	int shmsize=4096;	//共享内存大小
    int i=0;
	int shm_id=0;
	int sem_id=0;
	char* addr=NULL;
	char sendbuf[30]="zhou jiang hong ";
	char getbuf[30]={0};

	sem_id =  create_sem(); //创建信号量集,集中只有一个信号量
	init_sem(sem_id,0); 	//初始化信号量


	shm_id=create_shm(4096);//创建共享内存,大小为4096
	addr = shm_map(shm_id);	//将共享内存映射到进程存储中,addr指向共享内存首地址
	while(1)
	{
        i++;
        memset(sendbuf,0,sizeof(sendbuf));
        printf("input:");
        gets(sendbuf);
		strcpy(addr,sendbuf);

        if(i != 1)			
            v(sem_id);
		p(sem_id);

		strcpy(getbuf,addr);
		printf("%s\n",getbuf);
	}
	unload(addr);

	del_shm(shm_id);

	return 0;
}

get1.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<string.h>
#include"shared_memory.h"
#include"semaphole.h"

int main()
{
	int shmsize=4096;
    int i=0;
	int shm_id=0;
	int sem_id=0;
	char* addr=NULL;
	char sendbuf[30]="wang hua bin shi zhi zhu ";
	char getbuf[30]={0};

	sem_id =  create_sem(); 
	init_sem(sem_id,0); 

	shm_id=create_shm(4096);
	addr = shm_map(shm_id);

	while(1)
	{
        i++;
        if(i !=1)
            p(sem_id);
		strcpy(getbuf,addr);
		printf("%s\n",getbuf);

		memset(addr,0,shmsize);
        memset(sendbuf,0,sizeof(sendbuf));   
        printf("input:");
        gets(sendbuf);
		strcpy(addr,sendbuf);
		v(sem_id);
	}

	unload(addr);

	//delete(shm_id);

	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值