为什么需要进程间通信
使用进程间通信来实现数据共享,也就是说进程间通信的目的,就是为了实现进程间数据共享的。
进程间通信的原理
尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。
既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的转发。
因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数据的转发,达到数据共享的目的。
Linux提供的“进程通信”方式
1)信号
信号其实也是进程间通信的一种,只不过信号是非精确通信,而本章讲的IPC是精确通信。
所谓精确通信,就是能告诉你详细信息,而信号这种非精确通信,只能通知某件事情发生了,但是无法
告诉详细信息。
2)本章的进程间通信
(a)管道
· 无名管道
· 有名管道
OS在进程之间建立一个“管道”,通过这个管道来实现进程间数据的交换。
(b)system V IPC
· 消息队列:通过消息队列来通信
· 共享内存:通过共享内存来通信
· 信号量:借助通信来实现资源的保护(一种加锁机制)
3)域套接字
无名管道
无名管道的通信原理
具体来说就是,内核会开辟一个空间,通信的进程通过共享这个空间,从而实现通信。
无名管道API
#include <unistd.h>
int pipe(int pipefd[2]);
它的功能就是创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符关联起来。
为什么无名管道只能用于亲缘进程之间通信
由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,从而实现通信。
另外:读管道时,如果没有数据的话,读操作会休眠(阻塞)
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int main(void)
{
int ret = 0;
//[0]:读文件描述符
//[1]:写文件描述符
int pipefd[2] = {0};//用于存放管道的读写文件描述符
ret = pipe(pipefd);
if(ret == -1) print_err("pipe fail");
ret = fork();
if(ret > 0)
{
//signal(SIGPIPE, SIG_IGN);
//close(pipefd[0]);
while(1)
{
write(pipefd[1], "hello", 5);
sleep(1);
}
}
else if(ret == 0)
{
// close(pipefd[1]);
// close(pipefd[0]);
while(1)
{
char buf[30] = {0};
bzero(buf, sizeof(buf));
read(pipefd[0], buf, sizeof(buf));
printf("child, recv data:%s\n", buf);
}
}
return 0;
}
父子进程双向通信Demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int main(void)
{
int ret = 0;
//[0]:读文件描述符
//[1]:写文件描述符
int pipefd1[2] = {0};//用于存放管道的读写文件描述符
int pipefd2[2] = {0};//用于存放管道的读写文件描述符
ret = pipe(pipefd1);
if(ret == -1) print_err("pipe fail");
ret = pipe(pipefd2);
if(ret == -1) print_err("pipe fail");
ret = fork();
if(ret > 0)
{
close(pipefd1[0]);
close(pipefd2[1]);
char buf[30] = {0};
while(1)
{
write(pipefd1[1], "hello", 5);
sleep(1);
bzero(buf, sizeof(buf));
read(pipefd2[0], buf, sizeof(buf));
printf("parent, recv data:%s\n", buf);
}
}
else if(ret == 0)
{
close(pipefd1[1]);
close(pipefd2[0]);
char buf[30] = {0};
while(1)
{
sleep(1);
write(pipefd2[1], "world", 5);
bzero(buf, sizeof(buf));
read(pipefd1[0], buf, sizeof(buf));
printf("child, recv data:%s\n", buf);
}
}
return 0;
}
有名管道
不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以读写文件的形式来操作的。
因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道一样,必须在通过继承的方式才能获取到文件描述符。
所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。
demo1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_open_fifo(char *fifoname, int open_mode)
{
int ret = -1;
int fd = -1;
ret = mkfifo(fifoname, 0664);
//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");
fd = open(fifoname, open_mode);
if(fd == -1) print_err("open fail");
return fd;
}
void signal_fun(int signo)
{
//unlink();
remove(FIFONAME1);
remove(FIFONAME2);
exit(-1);
}
int main(void)
{
char buf[100] = {0};
int ret = -1;
int fd1 = -1;
int fd2 = -1;
fd1 = creat_open_fifo(FIFONAME1, O_WRONLY);
fd2 = creat_open_fifo(FIFONAME2, O_RDONLY);
ret = fork();
if(ret > 0)
{
signal(SIGINT, signal_fun);
while(1)
{
bzero(buf, sizeof(buf));
scanf("%s", buf);
write(fd1, buf, sizeof(buf));
}
}
else if(ret == 0)
{
while(1)
{
bzero(buf, sizeof(buf));
read(fd2, buf, sizeof(buf));
printf("%s\n", buf);
}
}
return 0;
}
demo2
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_open_fifo(char *fifoname, int open_mode)
{
int ret = -1;
int fd = -1;
ret = mkfifo(fifoname, 0664);
//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");
fd = open(fifoname, open_mode);
if(fd == -1) print_err("open fail");
return fd;
}
void signal_fun(int signo)
{
//unlink();
remove(FIFONAME1);
remove(FIFONAME2);
exit(-1);
}
int main(void)
{
char buf[100] = {0};
int ret = -1;
int fd1 = -1;
int fd2 = -1;
fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
fd2 = creat_open_fifo(FIFONAME2, O_WRONLY);
ret = fork();
if(ret > 0)
{
signal(SIGINT, signal_fun);
while(1)
{
bzero(buf, sizeof(buf));
read(fd1, buf, sizeof(buf));
printf("recv:%s\n", buf);
}
}
else if(ret == 0)
{
while(1)
{
bzero(buf, sizeof(buf));
scanf("%s", buf);
write(fd2, buf, sizeof(buf));
}
}
return 0;
}
消息队列
消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为了消息队列。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
收发数据的过程
1)发送消息
(a)进程先封装一个消息包
这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文
写到结构体的成员中。
struct msgbuf
{
long mtype; /* 放消息编号,必须> 0 */
char mtext[msgsz]; /* 消息内容(消息正文) */
};
(b)调用相应的API发送消息
调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列,消息包
(存放消息的结构体变量)会被作为一个链表节点插入链表。
2)接收消息
调用API接收消息时,必须传递两个重要的信息,
(a)消息队列标识符
(b)你要接收消息的编号
有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号的消息,
如此就收到了别人所发送的信息。
“消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公告牌上
去取对应编号的消息,如此,发送者和接受者之间就实现了通信。
使用demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <signal.h>
#define MSG_FILE "./msgfile"
#define MSG_SIZE 1024
struct msgbuf
{
long mtype; /* 放消息编号,必须 > 0 */
char mtext[MSG_SIZE]; /* 消息内容(消息正文) */
};
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_or_get_msgque(void)
{
int msgid = -1;
key_t key = -1;
int fd = 0;
/* 创建一个消息队列的专用文件,ftok会用到这个文件的路径名 */
fd = open(MSG_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
/* 利用存在的文件路径名和8位整形数,计算出key */
key = ftok(MSG_FILE, 'a');
if(key == -1) print_err("ftok fail");
/* 利用key创建、或者获取消息队列 */
msgid = msgget(key, 0664|IPC_CREAT);
if(msgid == -1) print_err("msgget fail");
return msgid;
}
int msgid = -1;
void signal_fun(int signo)
{
msgctl(msgid, IPC_RMID, NULL);
remove(MSG_FILE);
exit(-1);
}
int main(int argc, char **argv)
{
int ret = -1;
long recv_msgtype = 0;
if(argc != 2)
{
printf("./a.out recv_msgtype\n");
exit(-1);
}
recv_msgtype = atol(argv[1]);
msgid = creat_or_get_msgque();
ret = fork();
if(ret > 0) //发送消息
{
signal(SIGINT, signal_fun);
struct msgbuf msg_buf = {0};
while(1)
{
bzero(&msg_buf, sizeof(msg_buf));
/* 封装消息包 */
scanf("%s", msg_buf.mtext);
printf("input snd_msgtype:\n");
scanf("%ld", &msg_buf.mtype);
/* 发送消息包 */
msgsnd(msgid, &msg_buf, MSG_SIZE, 0);
}
}
else if(ret == 0)//接收消息
{
struct msgbuf msg_buf = {0};
int ret = 0;
while(1)
{
bzero(&msg_buf, sizeof(msg_buf));
ret = msgrcv(msgid, &msg_buf, MSG_SIZE, recv_msgtype, 0);
if(ret > 0)
{
printf("%s\n", msg_buf.mtext);
}
}
}
return 0;
}
使用方式
root@snappyjack-VirtualBox:/home/morty# ./a.out 1
aaaaa
input snd_msgtype:
2
xxxxxxx
另一个窗口
root@snappyjack-VirtualBox:/home/morty# ./a.out 2
aaaaa
xxxxxxx
input snd_msgtype:
1
共享内存
共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。
对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,效率会非常的低。
共享内存demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//write(fd, &shmid, sizeof(shmid));
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
int get_peer_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
ret = mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只读方式打开管道 */
fifofd = open("./fifo", O_RDONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 读管道,获取“读共享内存进程”的PID */
int peer_pid;
ret = read(fifofd, &peer_pid, sizeof(peer_pid));
if(ret == -1) print_err("read fifo fail");
return peer_pid;
}
int main(void)
{
int peer_pid = -1;
/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
signal(SIGINT, signal_fun);
/* 使用有名管道获取读共享内存进程的PID */
peer_pid = get_peer_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)
{
memcpy(shmaddr, buf, sizeof(buf));
kill(peer_pid, SIGUSR1);
sleep(1);
}
return 0;
}
另一个窗口运行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//read(fd, &shmid, sizeof(shmid));
}
void signal_fun(int signo)
{
if(SIGINT == signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
else if(SIGUSR1 == signo)
{
}
}
void snd_self_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只写方式打开文件 */
fifofd = open("./fifo", O_WRONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
int pid = getpid();
ret = write(fifofd, &pid, sizeof(pid));//发送PID
if(ret == -1) print_err("write fifo fail");
}
int main(void)
{
/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
signal(SIGUSR1, signal_fun);
signal(SIGINT, signal_fun);
/* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
snd_self_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)
{
pause();
printf("%s\n", (char *)shmaddr);
bzero(shmaddr, SHM_SIZE);
}
return 0;
}
直接运行就完事了