进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC方式包括管道(匿名管道和命名管道),消息队列,共享内存,信号量等。
一、管道
管道是Unix中最古老的进程间通信方式,我们把从一个进程连接到另一个进程的一个数据流称为“管道”。
管道(匿名管道)的特点:
- 只能用于具有共同祖先的进程(具有血缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步和互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
管道分为匿名管道和命名管道,他们的区别如下:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道),之间唯一的区别在他们创建和打开的方式不同,一旦这些工作完成,他们具有相同的语义。
- 命名管道可以在无关的进程间通信,而匿名管道只能在拥有共同祖先的进程间通信
pipe(匿名管道)原型:
#include <unistd.h>
int pipe(int fd[2]);
//fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写段
//返回值:成功返回0,失败返回错误码
实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define ERR_EXIT(m)\
do \
{ \
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(){
int pipefd[2];
if(pipe(pipefd) == -1){
ERR_EXIT("pipe error");
}
pid_t pid;
pid = fork();
if(pid == -1){
ERR_EXIT("fork error");
}
if(pid == 0){
close(pipefd[0]);
write(pipefd[1], "hello", 5);
close(pipefd[1]);
exit(EXIT_SUCCESS);
}
close(pipefd[1]);
char buf[10];
read(pipefd[0], buf, 10);
printf("buf = %s\n", buf);
return 0;
}
FIFO(命名管道)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//两个参数,第一个pathname是文件名路径,第二个参数是权限
实例:
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(){
if(mkfifo("./fifo", 0644) < 0){
printf("mkfifo error!\n");
return 1;
}
int fd = open("./fifo", O_RDONLY);
if(fd < 0){
perror("open");
return 2;
}
char buf[64];
while(1){
ssize_t s = read(fd, buf, sizeof(buf) - 1);
if(s > 0){
buf[s] = 0;
printf("server# %s\n", buf);
}else if(s == 0){
printf("clident quit\n");
break;
}
}
close(fd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(){
int fd = open("./fifo", O_WRONLY);
if(fd < 0){
perror("open");
return 2;
}
char buf[64];
while(1){
printf("Pleans Enter:");
scanf("%s", buf);
if(strcmp(buf, "quit") == 0){
break;
}
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
二、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
- 消息队列提供一个从一个进程向另外一个进程发送一块数据的方法。
- 每个数据都被认为是有一个类型,接受者进程接收的数据块可以有不同的类型值。
- 消息队列也有管道一样的不足,就是每个消息的最大长度有上限,每个消息队列的总的字节数也有上限,系统上消息队列的总数也有上限。
- 消息队列是由操作系统提供,因此它不随进程的消失而消失,它是随内核的。
消息队列结构和IPC数据结构:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
消息队列函数
msgget:用于创建和访问一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
include <sys/msg.h>
int msgget(key_t key, int msgflg);
//key:某个消息队列的名字,key值有ftok函数创建
//msgflg:由九个权限标志构成,他们的用法和创建文件时使用的mode模式标志是一样的
//返回值:成功返回一个非负整数,即该消息队列的标识码;失败返回-1。
msgctl:消息队列的控制函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//maqid:消息队列标识符
//cmd:将要采取的动作(IPC_STAT,IPC_SET,IPC_RMID(删除消息队列))
//返回值:成功返回0,失败返回-1
msgsnd/msgrcv:把一条消息添加到消息队列中/从消息队列中取出一条消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//msgid:消息队列标识符
//msgp:指针,指向准备发送的消息
//msgsz:是msgp指向消息队列的长度,这个长度不含保存消息类型的那个long int长整型
//msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
//返回值:成功返回0,失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//其参数只比msgsnd多了一个msgtype,msgid和msgsz含义相同
//msgp:指针,指向准备接受的消息
//msgtype:实现接受优先级的简单形式
//msgflg:控制着队列中没有相应类型的消息可供接受将要发生的事情
//函数msgrcv在读取消息队列时,type参数有下面几种情况:
//type == 0,返回队列中的第一个消息;
//type > 0,返回队列中消息类型为 type 的第一个消息;
//type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
查看和删除消息队列命令:
ipcs -q :查看IPC消息队列资源
ipcrm -q:手动删除IPC消息队列资源
实例:
cpmm.h
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define PATH_NAME "./"
#define PROJ_ID 0x21351261
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
struct msgbuf{
long mtype;
char mtext[128];
};
int createMsgQueue();
int getMsgQueue();
int sendMsg(int msgid, char *msg, int t);
int recvMsg(int msgid, int t, char *msg);
void destroyMsgQueue(int);
#endif
comm.c
#include "comm.h"
static int commMsgQueue(int flag){
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k < 0){
printf("ftok error!\n");
return -1;
}
int msgid = msgget(k, flag);
if(msgid < 0){
printf("msgget error!\n");
return -2;
}
return msgid;
}
int createMsgQueue(){
return commMsgQueue(IPC_CREAT|IPC_EXCL|0644);
}
int getMsgQueue(){
return commMsgQueue(IPC_CREAT);
}
int sendMsg(int msgid, char *msg, int t){
struct msgbuf buf;
buf.mtype = t;
strcpy(buf.mtext, msg);
if(msgsnd(msgid, &buf, sizeof(buf.mtext), 0) < 0){
printf("msgsnd error\n");
return -1;
}
return 0;
}
int recvMsg(int msgid, int t, char *msg){
struct msgbuf buf;
if(msgrcv(msgid, &buf, sizeof(buf.mtext), t, 0) < 0){
printf("msgrcv error !\n");
return -1;
}
strcpy(msg, buf.mtext);
return 0;
}
void destroyMsgQueue(int msgid){
if(msgctl(msgid, IPC_RMID, NULL) < 0){
printf("msgctl error!\n");
}
}
srever.c
#include "comm.h"
int main(){
char buf[256];
int msgid = createMsgQueue();
while(1){
//recv
recvMsg(msgid, CLIENT_TYPE, buf);
if(strcmp(buf, "quit") == 0){
printf("client is quit,me too!\n");
break;
}
printf("client# %s\n", buf);
//send
printf("Please Entr:");
scanf("%s", buf);
sendMsg(msgid, buf, SERVER_TYPE);
}
destroyMsgQueue(msgid);
return 0;
}
clinet.c
#include "comm.h"
int main(){
char buf[256];
int msgid = getMsgQueue();
while(1){
//send
printf("Please Entr:");
scanf("%s", buf);
sendMsg(msgid, buf, CLIENT_TYPE);
if(strcmp(buf, "quit") == 0){
printf("client quit\n");
break;
}
//recv
recvMsg(msgid, SERVER_TYPE, buf);
printf("server# %s\n", buf);
}
return 0;
}
三、共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
特点:
- 生命周期随内核
- 其分配是按页(一页4K)分配
- 共享内存是最快的IPC方式,它省去传数据所要经历的两次拷贝
- 它没有提供任何保护机制
共享内存函数
#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);
当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
实例:
server.c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#define PATH_NAME "."
#define PROJ_ID 0x7777
int main(){
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k < 0){
printf("ftok error!\n");
return -1;
}
int shmid = shmget(k, 4096, IPC_CREAT|IPC_EXCL);
if(shmid < 0){
printf("shmget error!\n");
return -2;
}
char *mem = (char *)shmat(shmid, NULL, 0);
while(1){
printf("%s\r",mem);
fflush(stdout);
sleep(1);
}
shmdt(mem);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
client.c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#define PATH_NAME "."
#define PROJ_ID 0x7777
int main(){
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k < 0){
printf("ftok error!\n");
return -1;
}
int shmid = shmget(k, 4096, IPC_CREAT);
if(shmid < 0){
printf("shmget error!\n");
return -2;
}
sleep(5);
char *mem = (char *)shmat(shmid, NULL, 0);
int x = 'A';
for(; x <= 'X'; x++){
mem[x -'A'] = x;
mem[x -'A' + 1] = '\0';
sleep(1);
}
shmdt(mem);
return 0;
}
查看共享内存命令:ipcs -m
删除共享内存命令:ipcrm -m
四、信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
进程互斥:
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
- 系统中某些资源一次只允许一个进程使用,这样的资源被称为临界资源。
- 在进程中涉及到临界资源的程序段为临界区
进程同步:
指多个进程需要相互配合共同完成一项任务
特点:
- 信号量主要用于进程间的同步和互斥。
- 信号量的生命周期随内核。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
信号量函数:
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
在semop函数中,sembuf结构的定义如下:
实例:
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int creatsemset(int nums);
int initsem(int semid, int nums, int intval);
int getsem(int nums);
int P(int semid, int who);
int V(int semid, int who);
int destroysemset(int semid);
#endif
comm.c
#include "comm.h"
int commsemset(int nums, int flags){
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0){
perror("ftok");
return -1;
}
int semid = semget(k, nums, flags);
if(semid < 0){
perror("semget");
return -2;
}
return semid;
}
int creatsemset(int nums){
return commsemset(nums, IPC_CREAT|IPC_EXCL|0666);
}
int getsemset(int nums){
return commsemset(nums, IPC_CREAT);
}
int initsem(int semid, int nums, int intval){
union semun un;
un.val = intval;
if(semctl(semid, nums, SETVAL, un) < 0){
perror("semctl");
return -1;
}
return 0;
}
static int commPV(int semid, int who, int op){
struct sembuf sf;
sf.sem_num = who;
sf.sem_op = op;
sf.sem_flg = 0;
if(semop(semid, &sf, 1) < 0){
perror("semop");
return -1;
}
return 0;
}
int P(int semid, int who){
return commPV(semid, who, -1);
}
int V(int semid, int who){
return commPV(semid, who, 1);
}
int destroysemset(int semid){
if(semctl(semid , 0, IPC_RMID) < 0){
perror("semctl");
return -1;
}
return 0;
}
test.c
#include <unistd.h>
#include <sys/wait.h>
#include "comm.h"
int main(){
int semid = creatsemset(1);
initsem(semid, 0, 1);
pid_t id = fork();
if(id == 0){//child
int _semid = getsem(0);
while(1){
P(_semid, 0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(321456);
V(_semid, 0);
}
}else{
while(1){
P(semid, 0);
printf("B");
fflush(stdout);
usleep(32452345);
printf("B ");
fflush(stdout);
usleep(1234213);
V(semid, 0);
}
wait(NULL);
}
destroysemset(semid);
return 0;
}
makefile
test_sem:comm.c test_sem.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test_sem
以上就是进程间通信的几种方式,如有错误,请大佬谅解。