进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
以Linux中的C语言编程为例。
一、管道
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
1、特点:
它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
2、原型:
使用man 2 pipe 指令查看pipe用法
1 #include <unistd.h>
2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
pipe.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(int argc,char**argv){ //进程id pid_t pid; //fd[0]:read fd[1]:write int fd[2]; //待写入的内容 char *content=(char*)malloc(sizeof(char)*24); //读取缓冲 char readBuf[96]; if(pipe(fd)==-1){ perror("创建管道失败"); exit(EXIT_FAILURE); } //创建子进程 pid=fork(); if(pid<0){ perror("创建子进程失败...\n"); exit(EXIT_FAILURE); } //父进程 if(pid>0){ printf("父进程延时3s...\n"); sleep(3); printf("这里属于父进程...\n"); close(fd[0]); printf("请录入:"); //父进程后开始往管道写入数据 gets(content); printf("键盘录入的内容是:%s\n父进程开始写入...\n",content); //ssize_t write(int fd, const void *buf, size_t count); write(fd[1],content,strlen(content)); printf("父进程写入完毕...\n"); wait(); } //子进程 if(pid==0){ printf("这里属于子进程...\n"); close(fd[1]); //ssize_t read(int fd, void *buf, size_t count); read(fd[0],readBuf,96); printf("子进程读取到的内容是%s\n",readBuf); printf("子进程退出...\n"); exit(0); } puts("父进程退出...Bye Bye"); return 0; }
运行结果如👇所示
二、FIFO
FIFO,也称为命名管道,它是一种文件类型。
1、特点
FIFO可以在无关的进程之间交换数据,与无名管道不同。
FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
2、原型
使用man 3 mkfifo 指令查看mkfifo 用法
1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);
其中的 mode 参数与open
函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK
)的区别:
若没有指定O_NONBLOCK
(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
若指定了O_NONBLOCK
,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
mkfifo_read.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<errno.h> int main(){ char readBuf[24]; if(mkfifo("./file",0600)==-1 && errno!=EEXIST){ perror("创建失败\n"); } int fd=open("./file",O_RDONLY); puts("open file succeeded..."); while(1){ int nread=read(fd,readBuf,25); if(nread==0){ break; } printf("read %d bytes from file,file content is %s\n",nread,readBuf); } return 0; }
mkfifo_write.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<string.h> int main(){ int i; int fd=open("./file",O_WRONLY); char *content="time to sleep"; for(i=0;i<5;i++){ int nwrite=write(fd,content,strlen(content)); printf("write %d bytes to file\n",nwrite); puts("write file succeeded..."); printf("延时3s...\n"); sleep(3); } return 0; }
运行结果如👇所示
三、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
1、特点
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
2.原型
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);
msgRec.c
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <string.h> struct msgBuf{ long mtype; //message type char mtext[64]; //message data }; int main(){ struct msgBuf readBuf; //int msgget(key_t key, int msgflg); key_t key; //key_t ftok(const char *pathname, int proj_id); key=ftok(".",'s'); printf("key is %#x\n",key); int msgId=msgget(key,IPC_CREAT|0777); if(msgId==-1){ perror("create queue failed\n"); } //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),321,0); printf("get from queue: %s\n",readBuf.mtext); struct msgBuf sendBuf={888,"message sent from Rec...\n"}; msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); msgctl(msgId,IPC_RMID,NULL); return 0; }
msgSend.c
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <string.h> struct msgBuf{ long mtype; //message type char mtext[64]; //message data }; int main(){ struct msgBuf sendBuf={321,"message sent from Send...\n"}; struct msgBuf readBuf; //int msgget(key_t key, int msgflg); key_t key; //key_t ftok(const char *pathname, int proj_id); key=ftok(".",'s'); printf("key is %#x\n",key); int msgId=msgget(key,IPC_CREAT|0777); if(msgId==-1){ perror("create queue failed\n"); } //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); msgsnd(msgId,&sendBuf,sizeof(sendBuf.mtext),0); puts("send message over..."); //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0); printf("get from queue: %s\n",readBuf.mtext); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); msgctl(msgId,IPC_RMID,NULL); return 0; }
执行rec(接收端),创建了一个消息队列,键值为0X7305f93,此时rec一直在监听,随后执行send(发送端),接收到了send的讯息,两者互相通讯。
Send:message sent from send... (send message over...)
Rec:get from queue:message sent from send...(message sent from Rec...)
Send:get from queue:message sent from Rec...
运行结果如👇所示
四、共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
1、特点
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
2、原型
#include <sys/shm.h>
2 // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
3 int shmget(key_t key, size_t size, int flag);
4 // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
5 void *shmat(int shm_id, const void *addr, int flag);
6 // 断开与共享内存的连接:成功返回0,失败返回-1
7 int shmdt(void *addr);
8 // 控制共享内存的相关信息:成功返回0,失败返回-1
9 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
当用shmget
函数创建一段共享内存时,必须指定其 size
;而如果引用一个已存在的共享内存,则将 size 指定为0 。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat
函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
shmdt
函数是用来断开shmat
建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl
函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID
(从系统中删除该共享内存)。
shmwrite.c
#include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <string.h> int main(){ int shmid; char *shmaddr; key_t key; key=ftok(".",1); //int shmget(key_t key, size_t size, int shmflg); shmid=shmget(key,1024*4,IPC_CREAT|0666); if(shmid==-1){ perror("shmget failed...\n"); exit(-1); } shmaddr=shmat(shmid,0,0); puts("shmat finished..."); strcpy(shmaddr,"hermaniu"); sleep(3); // int shmdt(const void *shmaddr); shmdt(shmaddr); // int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmctl(shmid,IPC_RMID,NULL); puts("quit..."); return 0; }
shmread.c
#include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <string.h> #include <stdio.h> int main(){ int shmid; char *shmaddr; key_t key; key=ftok(".",1); //int shmget(key_t key, size_t size, int shmflg); shmid=shmget(key,1024*4,0); if(shmid==-1){ perror("shmget failed...\n"); exit(-1); } shmaddr=shmat(shmid,0,0); puts("shmat finished..."); printf("content: %s\n",shmaddr); // int shmdt(const void *shmaddr); shmdt(shmaddr); puts("quit..."); return 0; }
运行结果如👇所示
五、信号(signal)
signal1.c
#include <signal.h> #include <stdio.h> //typedef void (*sighandler_t)(int); //sighandler_t signal(int signum, sighandler_t handler); //处理函数:处理接收到的信号 并作出行为 void handler(int signum){ printf("get signum =%d\n",signum); puts("won't be killed...\n"); } int main(){ //ctrl+C 就会触发SIGINT 信号 signal(SIGINT,handler); //类似于kill -9 pid 该处的重写不生效 signal(SIGKILL,handler); while(1); return 0; }
SIGKILL 该信号是无法被捕获的,也就是说进程无法执行信号处理程序(handler函数),会直接发送默认行为,也就是直接退出.这也就是为何kill -9 pid一定能杀死程序的原因. 故这也造成了进程被结束前无法清理或者关闭资源等行为,这样是有弊端的
signal2.c
#include <sys/types.h> #include <signal.h> #include <stdio.h> int main(int argc,char**argv){ int signum; int pid; signum=atoi(argv[1]); pid=atoi(argv[2]); printf("signum=%d , pid=%d\n",signum,pid); //int kill(pid_t pid, int sig); 发送信号 kill(pid,signum); printf("send signla succeeded~\n"); return 0; }
运行结果如👇所示
signal3.c
#include <sys/types.h> #include <signal.h> #include <stdio.h> int main(int argc,char**argv){ int signum; int pid; char cmd[24]={0}; signum=atoi(argv[1]); pid=atoi(argv[2]); printf("signum=%d , pid=%d\n",signum,pid); //将格式化后的字符串输出到str中 //int sprintf(char *str, const char *format, ...) sprintf(cmd,"kill -%d %d",signum,pid); system(cmd); printf("send signal via system method succeeded~\n"); return 0; }
运行结果如👇所示
六、信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1、特点
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅 限于对信号量值加 1 或减 1,而且可以加减任意正整数。
- 支持信号量组。
2、原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
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, ...);
sem.c
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <unistd.h> #include <stdlib.h> //int semop(int semid, struct sembuf *sops, unsigned nsops); void sem_p(int semid){ struct sembuf sem; /* Code to set semid omitted */ sem.sem_num = 0; /* Operate on semaphore 0 */ sem.sem_op = -1; /* Wait for value to equal 0 */ sem.sem_flg = SEM_UNDO; semop(semid,&sem,1); puts("sem_p..."); } void sem_v(int semid){ struct sembuf sem; /* Code to set semid omitted */ sem.sem_num = 0; /* Operate on semaphore 0 */ sem.sem_op = 1; /* Wait for value to equal 0 */ sem.sem_flg = SEM_UNDO; semop(semid,&sem,1); puts("sem_v..."); } 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 main(){ key_t key; pid_t pid; key=ftok(".",24); int semid; //int semget(key_t key, int nsems, int semflg); semid=semget(key,1,IPC_CREAT|0666); //创建一个信号量 union semun initSem; //初始为无锁状态 initSem.val=0; //int semctl(int semid, int semnum, int cmd, ...); semctl(semid,0,SETVAL,initSem);//0代表第一个信号量,SETVAL 设置操作,初始化 pid=fork(); if(pid>0){ //父进程处于等待 sem_p(semid); puts("this is in Father..."); sem_v(semid); } else if(pid ==0){ puts("this is in Child..."); //子进程释放锁,父进程方可执行 sem_v(semid); }else{ perror("fork failed...\n"); exit(-1); } return 0; }
运行结果如👇所示
有点类似多线程的那味~ 初始时,initSem.val=0,即使父进程先执行,也会因为信号量中value状态不是1而等待,直到子进程执行完,释放锁,val=1才执行(父进程)中的代码。
至此,进程间通信(IPC)的内容告一段落,大致了解了多种方式的使用方法,写了一点小demo加深日后的理解。接下去进行线程的学习。
2023-09-12 23:00:00 Herman