前言
??每一个进程想要访问物理内存,都是通过访问地址空间中的虚拟地址来进行访问。访问的时候,通过各自的页表结构,查找对应的物理地址并且访问。造成了进程与进程之间的数据独立,虽然有进程间独立性的存在,在进程运行时不会相互干扰。但是造成了进程与进程之间相互协作的时候,没有较好的方法进行数据的共享。
??所以在此引入了进程间通信的概念,进程间通信可以将不同进程中的数据通过一定的手段共享给其他进程,来实现进程间的通信。
1. 进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,同时它发生了某种事件
- 进程控制:有些进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
2. 进程间通信的方式
2.1 管道
2.1.1 什么是管道
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
eg: ps aux | grep xxx
命令解释: ps a 显示现行终端机下的所有程序,包括其他用户的程序。 ps u 以用户为主的格式来显示程序状况。 ps x 显示所有程序,不以终端机来区分。 grep 用于查找文件里符合条件的字符串。 | 管道符号
所以上述命令的作用是在所有可执行程序中查找指定的程序的运行状态。
所以以上命令管道的作用为:将ps aux 的结果通过管道交给 grep ,将该结果作为grep 的输入数据
2.1.2 管道的本质
管道在内核中是一块缓冲区,供不同的进程进行读写的缓冲区。 管道相当于在内核态中建立一块缓冲区,将结果通过管道返回给用户态。
2.2 匿名管道
2.2.1 匿名管道的接口函数
int pipe(int pipefd[2]);
eg: 使用匿名管道进行父子进程间的通信
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe");
return 0;
}
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 0;
}
else if (pid == 0)
{
close(fd[1]);
char buf[1024] = { 0 };
read(fd[0], buf, sizeof(buf) - 1);
printf("child: %s\n", buf);
}
else
{
sleep(2);
close(fd[0]);
const char* str = "i am father";
write(fd[1], str, strlen(str));
}
return 0;
}
2.2.2 从内核的角度理解管道
2.2.3 匿名管道的特性
- 管道是半双工通信的,并且数据流只能从写端流向读端,同一只能进行写操作或者读操作,两者不能同时进行
- 匿名管道在内核中创建出来的缓冲区没有标识符,导致了其他进程无法直接找到这块缓冲区,但是通过
fork() 函数创建的进程可以通过读写两端的文件描述符进行操作。 - 匿名管道只支持具有亲缘性关系的进程进行进程间通信,在进行父子进程通信的时候,一定要父进程先创建管道,再去创建子进程,此时子进程的文件描述符表才会有匿名管道的读写两端的描述符
- 当文件描述符保持基础属性(阻塞),调用read读空管道时,则read函数会发生阻塞。
- 管道的默认大小为64k
- 当文件描述符保持基础属性时(阻塞),一直调用write将管道写满后,则write函数会发生阻塞
- 管道的生命周期是跟随进程的,进程消失后管道也会消失
- 管道提供字节流服务,描述符的前后两个数据之间是没有明显边界的
- 从
fd[1] 中读取内容的时候,是直接将数据读走了,而不是拷贝其中的数据,在下一次进行读管道内容时,就会发生阻塞 - 在对管道进行读写时,如果读的字节没有超过管道大小,则管道保证读写的原子性,即要么完成读,要么还没有开始。
2.2.4 如何将文件描述符设置为非阻塞
??将文件描述符的读或者写设置为非阻塞属性,再读写受到阻塞时,程序则不会等待阻塞结束,而是继续执行下列程序。
函数接口
int fcntl(int fd, int cmd, ...);
代码测试
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0)
{
perror("pipe");
return 0;
}
int flag = fcntl(fd[0], F_GETFL);
printf("fd[0]的属性为:%d\n", flag);
fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);
flag = fcntl(fd[0], F_GETFL);
printf("fd[0]的属性为:%d\n", flag);
char buf[1024];
read(fd[0], buf, 1023);
printf("buf:%s\n", buf);
return 0;
}
2.2.5 匿名管道的非阻塞特性
1.读设置为非阻塞 ??写不关闭,一直读,读端调用read函数之后,返回值为-1,errno置为EAGAIN ??写关闭,一直读,读端read函数返回0,什么都没有读到
2.写设置成非阻塞 ??读不关闭,一直写,把管道写满之后,再调用write就会返回-1. ??读关闭,一直写,写端调用write进行写的时候,就会发生崩溃。本质上是写段收到了SIGPIPE信号,导致了写段的进程崩溃。
代码验证 此段代码只写了读设置为非阻塞的情况,写设置为非阻塞的情况可以稍微修改代码进行验证
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0)
{
perror("pipe");
return 0;
}
int flag = fcntl(fd[0], F_GETFL);
fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 0;
}
else if (pid == 0)
{
char buf[1024] = { 0 };
size_t read_size = read(fd[0], buf, sizeof(buf) - 1);
printf("buf:%s len:%ld\n", buf, read_size);
}
else
{
close(fd[0]);
sleep(10);
}
return 0;
}
程序结果
2.3 命名管道
2.3.1 命名管道与匿名管道的区别
命名管道是由标识符确定的管道,其他进程可以通过这个管道的标识符找到该管道,实现不同进程间的通信。 匿名管道则不可以使用标识符进行找到
2.3.2 命名管道的创建
1.命令创建的方式 mkfifo 创建一个命名管道文件,其他进程可以通过这个管道文件进行通信。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./fifo", O_RDONLY);
if(fd < 0)
{
perror("open");
return 0;
}
char buf[1024] = { 0 };
read(fd, buf, sizeof(buf) - 1);
printf("buf: %s\n", buf);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./fifo", O_WRONLY);
if(fd < 0)
{
perror("open");
return 0;
}
write(fd, "i am process A", 14);
return 0;
}
程序验证: 通过writefifo 程序向管道中写入内容,通过readfifo 从管道中读出数据。当从管道中读取数据时,如果管道中没有数据就会变成阻塞状态,直到管道中有了数据程序才会继续运行。
2.函数创建的方式
int mkfifo(const char* pathname, mode_t mode);
代码测试
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
mkfifo("fifo", 7777);
int fd = open("./fifo", O_WRONLY);
if(fd < 0)
{
perror("open");
return 0;
}
write(fd, "i am write", 10);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./fifo", O_RDONLY);
if(fd < 0)
{
perror("open");
return 0;
}
char buf[1024] = { 0 };
read(fd, buf, 1023);
printf("readfifo: %s\n", buf);
return 0;
}
2.3.3 命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO O_NONBLOCK enable:立刻返回成功 - 如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
3. 共享内存
3.1 什么是共享内存?
- 在物理内存中开辟的一段空间
- 不同的进程通过页表将该空间映射到自己的进程虚拟空间中
- 不同的进程通过操作自己进程虚拟空间中的虚拟地址,来操作共享内存
3.2 使用步骤
1.创建或获取内存 2.附加,将进程附加至共享内存 3.分离,将虚拟地址和物理地址的映射关系,从页表中删除
下文将会根据步骤,介绍详细的接口函数
3.3 接口函数
1. 创建或获取共享内存
int shmget(key_t key, size_t size, int shmflg);
2. 附加
void* shmat(int shmid, const void* shamaddr, int shmflg);
3. 分离
int shmdt(const void* shmaddr);
4. 控制
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
代码测试
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_F 0x1234
int main()
{
int shmid = shmget(IPC_F, 1024, IPC_CREAT | 0664);
if(shmid < 0)
{
perror("shmid");
return 0;
}
void *addr = shmat(shmid, NULL, SHM_RDONLY);
if(addr == NULL)
{
perror("shmat");
return 0;
}
printf("%s\n", (char*)addr);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_F 0x1234
int main()
{
int shmid = shmget(IPC_F, 1024, IPC_CREAT | 0664);
if(shmid < 0)
{
perror("shmid");
return 0;
}
void *addr = shmat(shmid, NULL, 0);
if(addr == NULL)
{
perror("shmat");
return 0;
}
const char *str = "i am process A";
strncpy((char*)addr, str, strlen(str));
return 0;
}
结果
3.4 共享内存的特性
- 共享内存是覆盖写的方式,读的时候,访问地址中的内容
- 共享内存的生命周期跟随操作系统
- 一旦共享内存被删除后,其本质共享内存的空间已经被删除了
- 如果在删除时,共享内存附加的进程数量为0,则内核中描述共享内存的结构体也被释放了
- 如果删除时,共享内存的附加进程数量不为0,则会将共享内存的key变为0x00000000,表示当前共享内存不能被其他进程所附加。共享内存的状态被置为destory,共享内存的结构体内部的引用计数一旦为0,则该共享内存结构体被释放。
总结
以上就是所有关于进程间通信的内容,欢迎各位大佬批评指正。
|