这篇笔记简要记录一下进程间通信这一大话题。我们知道每个进程都使用独立的4G的虚拟内存(每个虚拟内存都对应不同的物理内存),进程之间的虚拟内存不能互相访问,保证了进程空间的独立性。独立的进程之间如何通信,这就是本篇笔记要记录的内容。
1 进程间通信
进程间通信一共有如下几种方式,下面分别记录一下。
1 信号
2 管道通信
3 消息队列
4 共享内存
5 信号量
1.1 进程通信方式1:信号
信号是一种不精确的通信方式,只能通知某件事情发生了,而不能确定具体发生了什么事情。信号优点类似“”烽火狼烟”,只能确定敌人来了,而不能确定来了多少敌人,我方要以什么战术进攻。
在Linux下,在窗口进程下按下ctrl + c 就可以结束一个正在运行的进程,这就是窗口进程发送了一个SIGTERM信号给正在运行的进程,如果正在运行的进程没有捕获该信号做其他事情(比如忽略该信号),该进程就会被中断。
通过Kill -l 命令可以查看到Linux系统下的信号。
我们可以使用signal函数来捕获信号,在捕获函数中实现自己想要的操作。比如捕获SIGINT信号,代码如下:
#include <stdio.h>
#include <signal.h>
void signal_fun1(int signo)
{
printf("SIGNAL signo = %d\n",signo);
if (signo == SIGINT)
{
printf("signo == SIGINT,%d\n",signo);
}
else if (signo == SIGQUIT)
{
printf("signo == SIGQUIT,%d\n",signo);
}
}
int main()
{
// signal(SIGINT,SIG_IGN);
signal(SIGINT,signal_fun1);
while(1);
return 0;
}
1.2 进程通信方式2:管道通信(无名管道+有名管道)
管道通信是说,两个进程通过管道文件进行通信。管道文件分为两种,有名管道和无名管道。
1.2.1 无名管道
无名管道通信原理
管道就是一个管道文件,在内存中创建一个这样的管道文件,这样两个进程通信就可以通过OS提供的文件进行通信了。如下图所示,调用无名管道的API,在内核中开辟一个管道文件,然后A进程和B进程通过操作管道文件的描述符,向文件中读数据/写数据就可以实现进程的管道通信了。
无名管道之所以是“无名”是因为不返回明确的文件描述符,所以无法通过open这样的方式打开,所以称为无名。因此,无名管道只适用于父子进程间通信,因为无名管道没有文件描述符,只能通过父子进程继承的方式,让子进程获取到父进程创建的无名管道文件描述符,实现父子进程的通信。通信原理如下:
代码实现
- 父子进程的单项通信
1. #include <unistd.h>
2. #include <stdlib.h>
3. #include <stdio.h>
4. #include <strings.h>
5.
6. #define STR "parent process write hello"
7. void printf_err(char *str)
8. {
9. perror(str);
10. exit(-1);
11. }
12.
13. int main()
14. {
15. int ret = 0;
16. int pipefd[2] = {0}; // fd[0]:read fd[1]:write
17. ret = pipe(pipefd); // 在内核中开辟一个管道文件,并返回读写文件描述符
18. if (ret == -1)
19. {
20. printf_err("pipe fail");
21. }
22. printf("fd[0] = %d,fd[1]=%d\n",pipefd[0],pipefd[1]);
23.
24. ret = fork(); // create child process
25. if (ret > 0) // parent process
26. {
27. close(pipefd[0]); // 关闭没有使用的文件描述符
28. while (1)
29. {
30. write(pipefd[1],STR,sizeof(STR));
31. sleep(1);
32. }
33. }
34. else if (ret == 0) // child process
35. {
36. close(pipefd[1]);
37.
38. char buf[30];
39. bzero(buf,sizeof(buf));
40.
41. while (1)
42. {
43. read(pipefd[0],buf,sizeof(buf));
44. printf("%s\n",buf);
45. sleep(1);
46. }
47.
48. }
49.
50.
51. return 0;
52. }
为了避免干扰,通常把没有使用的文件描述关闭。如果关闭全部的文件描述符,内核会发送一个SIGPIPE信号,这个信号的默认动作是终止,所以收到这个信号的进程或被终止。
- 父子进程的双向通信
创建两个无名管道,父进程向管道1中写数据,子进程从管道1中读数据;父进程关闭管道1的读端,子进程关闭管道1的写段;对于管道2来说,子进程关闭读端,父进程关闭写端。
1. int main()
2. {
3. int ret = 0;
4. int pipefd1[2] = {0}; // fd[0]:read fd[1]:write
5. int pipefd2[2] = {0};
6.
7. ret = pipe(pipefd1);
8. if (ret == -1)
9. {
10. printf_err("pipe1 fail");
11. }
12. ret = pipe(pipefd2);
13. if (ret == -1)
14. {
15. printf_err("pipe2 fail");
16. }
17.
18. printf("fd[0] = %d,fd[1]=%d\n",pipefd1[0],pipefd1[1]);
19. printf("fd[0] = %d,fd[1]=%d\n",pipefd2[0],pipefd2[1]);
20.
21. ret = fork(); // create child process
22. if (ret > 0) // parent process
23. {
24. close(pipefd1[0]); // 关闭fd1
25. close(pipefd2[1]);
26.
27. char buf[30] = {0};
28. while (1)
29. {
30. write(pipefd1[1],"hello",5);
31. sleep(1);
32.
33. bzero(buf,sizeof(buf));
34.
35. read(pipefd2[0],buf,sizeof(buf));
36. printf("parent ,rec data:%s\n",buf);
37. }
38. }
39. else if (ret == 0) // child process
40. {
41. close(pipefd1[1]);
42. close(pipefd2[0]);
43.
44. char buf[30] = {0};
45. while (1)
46. {
47. bzero(buf,sizeof(buf));
48.
49. read(pipefd1[0],buf,sizeof(buf));
50. printf("child ,rec data:%s\n",buf);
51.
52. write(pipefd2[1],"world",5);
53. sleep(1);
54. }
55. }
56. return 0;
57. }
无名管道缺点
无法用于非亲缘进程之间,因为非亲缘进程之间没办法继承管道的文件描述符;
无法实现多进程之间的网状通信,不能实现任意两个进程之间的通信,非亲缘关系的两个进程通信,可以用下面的有名管道。
1.2.2 有名管道
有名管道的原理
调用相应的有名管道API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的 “有名管道文件”。不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间,以文件的方式管理这段内存。
因为调用API时候,可以返回文件描述符,所以有名管道可以应用于两个非亲缘进程之间。
代码实现
- 有名管道实现的单项通信
代码1:
1. int create_open_fifo(char *filename,int open_mode)
2. {
3. int ret = -1;
4. int fd = -1;
5.
6. ret = mkfifo(filename,0664);
7. if (ret == -1 && errno != EEXIST)
8. {
9. print_err("mkfifo fail\n");
10. }
11.
12. fd = open(filename,open_mode);
13. if (fd == -1)
14. {
15. print_err("open fail\n");
16. }
17. }
18.
19.
20. int main()
21. {
22. // 1 create pipe
23. int ret = 0;
24. int fd1 = 0;
25. int fd2 = 0;
26. char buf[100] = {0};
27. fd1 = create_open_fifo(FILENAME1,O_WRONLY);
28. // fd2 = create_open_fifo(FILENAME2,O_RDWR);
29.
30. while (1)
31. {
32. bzero(buf,sizeof(buf));
33. scanf("%s",buf);
34. write(fd1,buf,sizeof(buf));
35. }
36.
37. return 0;
38. }
代码2:
1. int create_open_fifo(char *filename,int open_mode)
2. {
3. int ret = -1;
4. int fd = -1;
5.
6. ret = mkfifo(filename,0664);
7. if (ret == -1 && errno != EEXIST)
8. {
9. print_err("mkfifo fail\n");
10. }
11.
12. fd = open(filename,open_mode);
13. if (fd == -1)
14. {
15. print_err("open fail\n");
16. }
17. }
18.
19.
20. int main()
21. {
22. // 1 create pipe
23. int ret = 0;
24. int fd1 = 0;
25. int fd2 = 0;
26. char buf[100] = {0};
27. fd1 = create_open_fifo(FILENAME1,O_RDONLY);
28. // fd2 = create_open_fifo(FILENAME2,O_RDWR);
29.
30. while (1)
31. {
32. read(fd1,buf,sizeof(buf));
33. printf("%s\n",buf);
34. //sleep(1);
35. }
36.
37. return 0;
38. }
- 有名管道实现双向通信
- 原理如下:
代码1:
1. void print_err(char *str)
2. {
3. perror(str);
4. exit(-1);
5. }
6.
7. int create_open_fifo(char *filename,int open_mode)
8. {
9. int ret = -1;
10. int fd = -1;
11.
12. ret = mkfifo(filename,0664);
13. if (ret == -1 && errno != EEXIST)
14. {
15. print_err("mkfifo fail\n");
16. }
17.
18. fd = open(filename,open_mode);
19. if (fd == -1)
20. {
21. print_err("open fail\n");
22. }
23. }
24.
25. void signal_fun(int signo)
26. {
27. remove(FILENAME1);
28.
29. exit(-1);
30. }
31.
32. int main()
33. {
34. // 1 create pipe
35. int ret = 0;
36. int fd1 = 0;
37. int fd2 = 0;
38. char buf[100] = {0};
39. fd1 = create_open_fifo(FILENAME1,O_RDWR);
40. fd2 = create_open_fifo(FILENAME2,O_RDWR);
41.
42. ret = fork();
43. if (ret > 0)
44. {
45. signal(SIGINT,signal_fun);
46. while (1)
47. {
48. bzero(buf,sizeof(buf));
49. scanf("%s",buf);
50. write(fd1,buf,sizeof(buf));
51. }
52.
53. }
54. else if (ret == 0)
55. {
56. while (1)
57. {
58. bzero(buf,sizeof(buf));
59. read(fd2,buf,sizeof(buf));
60. printf("rec:%s\n",buf);
61. }
62. }
63. return 0;
64. }
代码2:
1. int main()
2. {
3. // 1 create pipe
4. int ret = 0;
5. int fd1 = 0;
6. int fd2 = 0;
7. char buf[100] = {0};
8. fd1 = create_open_fifo(FILENAME1,O_RDWR);
9. fd2 = create_open_fifo(FILENAME2,O_RDWR);
10.
11. ret = fork();
12. if (ret > 0)
13. {
14. while (1)
15. {
16. bzero(buf,sizeof(buf));
17. read(fd1,buf,sizeof(buf));
18. printf("rec:%s\n",buf);
19. }
20.
21. }
22. else if (ret == 0)
23. {
24. while (1)
25. {
26. bzero(buf,sizeof(buf));
27. scanf("%s",buf);
28. write(fd2,buf,sizeof(buf));
29. }
30. }
31.
32. return 0;
33. }
有名管道适用场景
当两个非亲缘进程通信时候,不管是亲缘还是非亲缘,都可以使用有名管道来通信。
1.3 消息队列
Linux继承了System V 中的实现的消息队列,共享内存和信号量。
消息队列通信原理
消息队列的本质就是在内核中创建的用于存放消息的队列,只不过队列通过链表实现。由于是存放消息的,所以我们就把这个链表称为消息队列,这个队列不必非要按照先进先出的方式。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
上图中, 消息编号:识别消息用;消息正文:真正的信息内容
消息队列的实现步骤
**第一步:**使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID),后续收发消息就是使用这个标识符来实现的。
**第二步:**收发消息
发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息
**第三步:**使用msgctl函数,利用消息队列标识符删除消息队列
代码实现
- 创建消息队列
1. int create_get_msgque()
2. {
3. int msgid = -1;
4. int fd = -1;
5. key_t key = 0;
6. //
7. fd = open(PATH,O_RDWR|O_CREAT,0664);
8. if (fd == -1)
9. {
10. print_err("open fail\n");
11. }
12.
13. //
14. key = ftok(PATH,'b');
15. if (key == -1)
16. {
17. print_err("ftok fail");
18. }
19.
20. //
21. msgid = msgget(key,0664|IPC_CREAT);
22. if (-1 == msgid)
23. {
24. print_err("ftok fail");
25. }
26.
27. return msgid;
28. }
29.
30. int main()
31. {
32. int ret = -1;
33. long recv_msgtype = 0;
34. int msgid = -1;
35.
36. msgid = create_get_msgque();
37.
38. ret = fork();
39. if (ret > 0)
40. {
41.
42. }
43. else if (ret == 0)
44. {
45.
46. }
47.
48.
49. return 0;
50. }
使用ipcs验证消息队列是否被创建成功
使用ipcs命令即可查看,可跟接的选项有:
- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
- m:只显示共享内存的信息
- q: 只显示消息队列的信息
- s: 只显示信号量的信息
适用消息队列方式实现进程间通信:
1. int create_get_msgque()
2. {
3. int msgid = -1;
4. int fd = -1;
5. key_t key = 0;
6. //
7. fd = open(PATH,O_RDWR|O_CREAT,0664);
8. if (fd == -1)
9. {
10. print_err("open fail\n");
11. }
12.
13. //
14. key = ftok(PATH,'b'); // 获取唯一key值,用于创建消息队列
15. if (key == -1)
16. {
17. print_err("ftok fail");
18. }
19.
20. //
21. msgid = msgget(key,0664|IPC_CREAT);
22. if (-1 == msgid)
23. {
24. print_err("ftok fail");
25. }
26.
27. return msgid;
28. }
29.
30. int main(int argc,char **argv)
31. {
32. int ret = -1;
33. long recv_msgtype = 0;
34. int msgid = -1;
35. struct msgbuf msg_buf = {0};
36.
37. if (argc != 2)
38. {
39. printf("err");
40. exit(-1);
41. }
42.
43. recv_msgtype = atol(argv[1]);
44.
45. msgid = create_get_msgque();
46.
47.
48. ret = fork();
49. if (ret > 0)
50. {
51.
52. while (1)
53. {
54. printf("input message:");
55. scanf("%s",msg_buf.mtext);
56.
57. printf("input snd_msgtype:\n");
58. scanf("%ld",&msg_buf.mtype);
59. msgsnd(msgid,&msg_buf,1024,0); // int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
60. }
61. }
62. else if (ret == 0)
63. {
64. while (1)
65. {
66. // bzero(&msg_buf,sizeof(msg_buf));
67. ret = msgrcv(msgid, &msg_buf, 1024, recv_msgtype, 0);
68. if (ret > 0)
69. {
70. printf("%s\n",msg_buf.mtext);
71. }
72. }
73. }
74.
75.
76. return 0;
77. }
1.4 共享内存
共享内存实现原理
共享内存就是OS在物理内存中开辟一大段缓存空间。管道、消息队列调用read、write、msgsnd、msgrcv等API来读写操作。而进程间使用共享内存通信时,进程是直接使用地址来共享读写的,避免了中间的调用过程。对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到超大量的数据通信时,使用“共享内存”这种直接使用地址操作的通信方式,效率会更高。
【共享内存方式就是让两个进程共享一块物理内存,这个物理内存和该进程映射到的原本的物理内存的关系:共享的物理内存只是从进程的虚拟空间中拿出来一块,映射到了新的物理内存上】
共享内存实现步骤
第一步:进程调用shmget函数创建新的或获取已有共享内存,shm是share memory的缩写。
第二步:进程调用shmat函数,将物理内存映射到自己的进程空间,实现虚拟地址和真实物理地址建立一一对应的映射关系。建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
第三步:shmdt函数,取消映射;
第四步:调用shmctl函数释放开辟的那片物理内存空间;
1.5 信号量
信号量实现原理
信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,来达到加锁的目的。这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥或同步。
当多个进程/线程进行共享操作时,使用信号量进行资源保护,以防止出现相互干扰的情况。为了避免出现相互干扰的问题,就需要加入资源保护的措施,保护的目的就是,保证每个进程在没有把数据读、写完整之前,其它进程不能进行读、写操作。
资源保护分为两种,一种是互斥,另一种是同步。互斥时候,不考虑先后顺序;而同步时候,要考虑加锁的先后顺序。
代码实现
通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。
int main(void)
{
int semid = 0;
int i = 0;
int ret = 0;
int semnum_buf[1] = {0};
semid = create_sem(NSEMS);
// init sem 0 1 2
for (i = 0; i < NSEMS;i++)
{
if (i == 0)
{ // void init_sem(int semid,int semnum,int val)
init_sem(semid,i,1); // 第二步,初始化信号量,初值设置为1表示互斥
}
else
{
init_sem(semid,i,0);
}
}
ret = fork();
if (ret > 0)
{
ret = fork();
if (ret > 0) // parent process
{
while (1)
{
semnum_buf[0] = 2;
p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
printf("333\n");
sleep(1);
semnum_buf[0] = 0;
v_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
}
}
else if (ret == 0)
{
while (1)
{
semnum_buf[0] = 1;
p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
printf("222\n");
sleep(1);
semnum_buf[0] = 2;
v_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
}
}
}
else if (ret == 0)
{
while (1)
{
semnum_buf[0] = 0;
p_sem(semid,semnum_buf,1); //p_sem(int semid,int semnum_buf[],int nsops);
printf("111\n");
sleep(1);
semnum_buf[0] = 1;
v_sem(semid,semnum_buf,1);
}
}
return 0;
}