进程间通信(IPC)介绍
进程间通信(IPC,InterProcess Communication)是指在不同的进程之间传播或交换信息。IPC 的方式包括管道(无名管道和命名管道)、消息队列、信号量、共享内存、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程间通信。
1. 管道
1.1 无名管道
1.1.1 特点
- 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
- 它只能用于父子进程之间的通信。
- 管道是创建在内存中,进程结束空间释放,管道不复存在。对于它的读写可以使用普通的
read
、write
等函数。
1.1.2 函数原型
cpp
#include <unistd.h>
int pipe(int pipefd[2]);
1.1.3 返回值
成功返回 0,失败返回 -1。当一个管道建立时,它会创建两个文件描述符:fd[0]
为读而打开,fd[1]
为写而打开。
1.1.4 无名管道代码示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int fd[2];
int pid;
char buf[128];
if (pipe(fd) == -1) {
printf("create pipe fail\n");
}
pid = fork();
if (pid < 0) {
printf("create child fail\n");
} else if (pid > 0) {
sleep(3);
printf("this is father\n");
close(fd[0]);
write(fd[1], "hello from father", strlen("hello from father"));
wait(NULL);
} else {
printf("this is child\n");
close(fd[1]);
read(fd[0], buf, 128);
printf("read = %s \n", buf);
exit(0);
}
return 0;
}
注意:管道的特性:管道里没有数据时会阻塞。
1.2 命名管道
1.2.1 特点
- 命名管道可以在无关的进程之间交换数据,与无名管道不同。
- 命名管道有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
1.2.2 函数原型
cpp
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
1.2.3 命名管道代码示例
read.c
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char buf[128] = {0};
if (mkfifo("./file", 0600) == -1 && errno != EEXIST) {
printf("mkfifo fail\n");
perror("why");
}
fd = open("./file", O_RDONLY);
printf("read open success\n");
while (1) {
int n_read = read(fd, buf, 128);
printf("read %d byte, context = %s \n", n_read, buf);
}
close(fd);
return 0;
}
write.c
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char *str = "message from fifo";
fd = open("./file", O_WRONLY);
printf("write open success\n");
while (1) {
write(fd, str, strlen(str));
sleep(1);
}
close(fd);
return 0;
}
2. 消息队列
消息队列是信息的链接表,存放在内核中。一个消息队列由一个标识符(即队列 ID)来标识。
2.1 特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及内容并不会删除。
- 消息队列可以实现信息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
2.2 函数原型
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
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);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
2.3 消息队列代码示例
send.c
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main() {
struct msgbuf sendBuf = {888, "this is msg from queue"};
struct msgbuf readBuf;
key_t key = ftok(".", 1);
printf("key=%d\n", key);
int msgId = msgget(key, IPC_CREAT | 0777);
if (msgId == -1) {
perror("why:");
}
while (1) {
msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);
sleep(1);
printf("write success\n");
msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 988, 0);
sleep(1);
printf("read from queue: %s\n", readBuf.mtext);
}
msgctl(msgId, IPC_RMID, NULL);
return 0;
}
read.c
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main() {
struct msgbuf readBuf;
struct msgbuf sendBuf = {988, "this is msg from father"};
key_t key = ftok(".", 1);
printf("key=%d\n", key);
int msgId = msgget(key, IPC_CREAT | 0777);
if (msgId == -1) {
perror("why:");
}
while (1) {
msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
sleep(1);
printf("read from queue: %s\n", readBuf.mtext);
msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);
sleep(1);
printf("write success\n");
}
msgctl(msgId, IPC_RMID, NULL);
return 0;
}
3. 信号
3.1 信号的处理
信号的处理有三种方法:忽略、捕捉和默认动作。具体的信号默认动作可以使用 man 7 signal
来查看系统的具体定义。
3.2 信号处理函数的注册
信号处理函数的注册可以分为入门版和高级版:
- 入门版:
signal
函数 - 高级版:
sigaction
函数
3.3 信号处理发送函数
信号发送函数也分为入门版和高级版:
- 入门版:
kill
- 高级版:
sigqueue
3.4 signal
函数原型及示例
cpp
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
示例
cpp
#include <signal.h>
#include <stdio.h>
void handler(int signum) {
printf("get signum = %d\n", signum);
switch (signum) {
case SIGINT:
printf("SIGINT\n");
break;
case SIGKILL:
printf("SIGKILL\n");
break;
case SIGUSR1:
printf("SIGUSR1\n");
break;
}
}
int main() {
signal(SIGINT, handler);
signal(SIGKILL, handler);
signal(SIGUSR1, handler);
while (1);
return 0;
}
3.5 kill
函数原型及示例
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char **argv) {
int signum = atoi(argv[1]);
int pid = atoi(argv[2]);
printf("signum = %d , pid = %d \n", signum, pid);
kill(pid, signum);
printf("send signal ok\n");
return 0;
}
4. 信号量
信号量(semaphore)是一个计数器,用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
4.1 特点
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 支持信号量组。
4.2 函数原型
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semctl(int semid, int semnum, int cmd, ...);
4.3 信号量代码示例
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
//联合体,用于 semctl 初始化
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void pGetKey(int id) {
struct sembuf set;
set.sem_num = 0;
set.sem_op = -1;
set.sem_flg = SEM_UNDO;
semop(id, &set, 1);
printf("get key\n");
}
void pPutBackKey(int id) {
struct sembuf set;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg = SEM_UNDO;
semop(id, &set, 1);
printf("put back the key\n");
}
int main() {
int semid;
key_t key = ftok(".", 2);
//1. 获取或创建信号量
semid = semget(key, 1, IPC_CREAT | 0666);
union semun initsem;
initsem.val = 0;
//2. 初始化信号量
semctl(semid, 0, SETVAL, initsem);
int pid = fork();
if (pid > 0) {
//4. 拿锁
pGetKey(semid);
printf("this is father\n");
//5. 还锁
pPutBackKey(semid);
//6. 销毁锁
semctl(semid, 0, IPC_RMID);
} else if (pid == 0) {
printf("this is child\n");
//3. 放锁
pPutBackKey(semid);
} else {
printf("fork error\n");
}
return 0;
}
5. 共享内存
共享内存(shared memory)指两个或多个进程共享一个给定的存储区。
5.1 特点
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存储,而不需要任何数据的拷贝。
- 只能单独一个进程写或读,如果 A 和 B 进程同时写,会造成数据的混乱(需要搭配信号量来使用)。
5.2 函数原型
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
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);
5.3 共享内存代码示例
shm_write.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int shmid;
char *shmaddr;
key_t key = ftok(".", 1);
// 创建共享内存
shmid = shmget(key, 1024 * 4, IPC_CREAT | 0600);
if (shmid == -1) {
printf("create shm fail\n");
exit(-1);
}
// 连接映射共享内存
shmaddr = shmat(shmid, 0, 0);
printf("shmat OK\n");
// 将数据拷贝到共享内存
strcpy(shmaddr, "hello world\n");
sleep(5); // 等待 5 秒,避免一下子断开连接。等待另外一个进程读完。
// 断开共享内存连接
shmdt(shmaddr);
// 删除共享内存
shmctl(shmid, IPC_RMID, 0);
printf("quit\n");
return 0;
}
shm_get.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int shmid;
char *shmaddr;
key_t key = ftok(".", 1);
// 打开共享内存
shmid = shmget(key, 1024 * 4, 0);
if (shmid == -1) {
printf("create shm fail\n");
exit(-1);
}
// 连接并映射共享内存
shmaddr = shmat(shmid, 0, 0);
printf("get from shm_write message is: %s", shmaddr);
// 断开共享内存连接
shmdt(shmaddr);
printf("quit\n");
return 0;
}
6. 结合消息队列、共享内存、信号量的示例
6.1 代码示例
get.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <string.h>
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于 semctl 初始化
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// 初始化信号量
int init_sem(int sem_id, int value) {
union semun tmp;
tmp.val = value;
if (semctl(sem_id, 0, SETVAL, tmp) == -1) {
perror("Init Semaphore Error");
return -1;
}
return 0;
}
// P 操作
int sem_p(int sem_id) {
struct sembuf sbuf;
sbuf.sem_num = 0;
sbuf.sem_op = -1;
sbuf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sbuf, 1) == -1) {
perror("P operation Error");
return -1;
}
return 0;
}
// V 操作
int sem_v(int sem_id) {
struct sembuf sbuf;
sbuf.sem_num = 0;
sbuf.sem_op = 1;
sbuf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sbuf, 1) == -1) {
perror("V operation Error");
return -1;
}
return 0;
}
// 删除信号量集
int del_sem(int sem_id) {
union semun tmp;
if (semctl(sem_id, 0, IPC_RMID, tmp) == -1) {
perror("Delete Semaphore Error");
return -1;
}
return 0;
}
// 创建一个信号量集
int create_sem(key_t key) {
int sem_id;
if ((sem_id = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget error");
exit(-1);
}
init_sem(sem_id, 1); // 初值设为 1 资源未占用
return sem_id;
}
int main() {
key_t key;
int shmid, semid, msqid;
char *shm;
struct shmid_ds buf1; // 用于删除共享内存
struct msqid_ds buf2; // 用于删除消息队列
struct msg_form msg; // 消息队列用于通知对方更新了共享内存
// 获取 key 值
if ((key = ftok(".", 'z')) < 0) {
perror("ftok error");
exit(1);
}
// 创建共享内存
if ((shmid = shmget(key, 1024, IPC_CREAT | 0666)) == -1) {
perror("Create Shared Memory Error");
exit(1);
}
// 连接共享内存
shm = (char *)shmat(shmid, 0, 0);
if ((int)shm == -1) {
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, IPC_CREAT | 0777)) == -1) {
perror("msgget error");
exit(1);
}
// 创建信号量
semid = create_sem(key);
// 读数据
while (1) {
msgrcv(msqid, &msg, 1, 888, 0); // 读取类型为 888 的消息
if (msg.mtext == 'q') // quit - 跳出循环
break;
if (msg.mtext == 'r') // read - 读共享内存
{
sem_p(semid);
printf("%s\n", shm);
sem_v(semid);
}
}
// 断开连接
shmdt(shm);
// 删除共享内存、消息队列、信号量
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0;
}
send.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <string.h>
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于 semctl 初始化
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P 操作
int sem_p(int sem_id) {
struct sembuf sbuf;
sbuf.sem_num = 0;
sbuf.sem_op = -1;
sbuf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sbuf, 1) == -1) {
perror("P operation Error");
return -1;
}
return 0;
}
// V 操作
int sem_v(int sem_id) {
struct sembuf sbuf;
sbuf.sem_num = 0;
sbuf.sem_op = 1;
sbuf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sbuf, 1) == -1) {
perror("V operation Error");
return -1;
}
return 0;
}
int main() {
key_t key;
int shmid, semid, msqid;
char *shm;
struct msg_form msg;
int flag = 1; // while 循环条件
// 获取 key 值
if ((key = ftok(".", 'z')) < 0) {
perror("ftok error");
exit(1);
}
// 获取共享内存
if ((shmid = shmget(key, 1024, 0)) == -1) {
perror("shmget error");
exit(1);
}
// 连接共享内存
shm = (char *)shmat(shmid, 0, 0);
if ((int)shm == -1) {
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, 0)) == -1) {
perror("msgget error");
exit(1);
}
// 获取信号量
if ((semid = semget(key, 0, 0)) == -1) {
perror("semget error");
exit(1);
}
// 写数据
printf("***************************************\n");
printf("* IPC *\n");
printf("* Input r to send data to server. *\n");
printf("* Input q to quit. *\n");
printf("***************************************\n");
while (flag) {
char c;
printf("Please input command: ");
scanf("%c", &c);
switch (c) {
case 'r':
printf("Data to send: ");
sem_p(semid); // 访问资源
scanf("%s", shm);
sem_v(semid); // 释放资源
// 清空标准输入缓冲区
while ((c = getchar()) != '\n' && c != EOF);
msg.mtype = 888;
msg.mtext = 'r'; // 发送消息通知服务器读数据
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;
break;
default:
printf("Wrong input!\n");
// 清空标准输入缓冲区
while ((c = getchar()) != '\n' && c != EOF);
}
}
// 断开连接
shmdt(shm);
return 0;
}
7. 对比总结
通过上述对比可以看出,各种 IPC 方式各有优劣,选择合适的方式进行进程间通信可以提高程序的效率和可靠性。