[linux] 详解linux进程间通信
Posted 哦哦呵呵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[linux] 详解linux进程间通信相关的知识,希望对你有一定的参考价值。
目录
前言
每一个进程想要访问物理内存,都是通过访问地址空间中的虚拟地址来进行访问。访问的时候,通过各自的页表结构,查找对应的物理地址并且访问。造成了进程与进程之间的数据独立,虽然有进程间独立性的存在,在进程运行时不会相互干扰。但是造成了进程与进程之间相互协作的时候,没有较好的方法进行数据的共享。
所以在此引入了进程间通信的概念,进程间通信可以将不同进程中的数据通过一定的手段共享给其他进程,来实现进程间的通信。
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]);
// 作用:创建一个匿名管道,用于数据的交换
// 参数:pipefd数组,输出型参数,参数由函数内部进行填充
// pipefd[0]: 管道的读端
// pipefd[1]: 管道的写端
// 返回值:
// 0: 创建匿名管道成功
// -1: 创建匿名管道失败
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, .../*arg*/);
// 函数功能:
// 1. 查看属性
// 2. 设置为非阻塞属性 O_NONBLOCK
// 参数:
// fd: 文件描述符
// cmd: 告知fcntl函数执行的功能
// F_GETFL: 获取文件描述符属性,arg参数忽略
// F_SETFL: 设置文件描述符属性,使用第三个参数
// 返回值:
// 返回文件描述符的属性。
代码测试
#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()
{
// 1.创建匿名管道
int fd[2];
int ret = pipe(fd);
if (ret < 0)
{
perror("pipe");
return 0;
}
// 2.设置读或写端为非阻塞属性
int flag = fcntl(fd[0], F_GETFL);
fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);
// 3.创建子进程
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 0;
}
// 4. 父子进程验证阻塞
else if (pid == 0)
{
// child read fd[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
{
// father write fd[1]
// 写端不关闭
close(fd[0]);
sleep(10);
}
return 0;
}
程序结果
2.3 命名管道
2.3.1 命名管道与匿名管道的区别
命名管道是由标识符确定的管道,其他进程可以通过这个管道的标识符找到该管道,实现不同进程间的通信。
匿名管道则不可以使用标识符进行找到
2.3.2 命名管道的创建
1.命令创建的方式
mkfifo
创建一个命名管道文件,其他进程可以通过这个管道文件进行通信。
// readfifo.c
#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;
}
// writefifo.c
#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);
// 作用:创建一个命名管道进行通信
// 参数:pathname 要创建命名管道文件的路径
// mode_t 命名管道的权限,8进行数字
代码测试
// writefifo
#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;
}
// readfido
#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);
// 参数:
// key_t: 共享内存的标识符
// size: 共享内存的大小
// shmflg: 共享内存的属性信息
// IPC_CREATE: 共享内存不存在,则创建共享内存
// IPC_EXCL | IPC_CREATE: 如果共享内存存在,则报错。如果共享内存不存在,则创建
// 返回值:成功则返回共享内存的操作句柄
2. 附加
void* shmat(int shmid, const void* shamaddr, int shmflg);
// 参数:
// shamid: 共享内存的操作句柄
// shmaddr: 将共享内存附加到共享区的哪一个地址,一般传递NULL,有操作系统分配
// shmflg: SHM_RDONLY 只读属性 0 可读可写
3. 分离
int shmdt(const void* shmaddr);
// 参数:shamat的返回值
4. 控制
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 参数:
// shmid: 共享内存操作句柄
// cmd: 告诉shmctl函数完成的事情
// 1.获取内存信息: IPC_SET
// 2.删除共享内存: IPC_RMID
// 3.获取共享内存属性: IPC_STAT
// buf: 共享内存数据结构buf
代码测试
// readSHM.c
#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;
}
/
// writeSHM.c
#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,则该共享内存结构体被释放。
总结
以上就是所有关于进程间通信的内容,欢迎各位大佬批评指正。
以上是关于[linux] 详解linux进程间通信的主要内容,如果未能解决你的问题,请参考以下文章
linux 进程间通信 dbus-glib实例详解二(上) 消息和消息总线(附代码)
linux 进程间通信 dbus-glib实例详解二(下) 消息和消息总线(ListActivatableNames和服务器的自动启动)(附代码)
linux进程间通信之System V共享内存详解及代码示例
linux 进程间通信 dbus-glib实例详解三(下) 数据类型和dteeth(类型签名type域)(层级结构:服务Service --> Node(对象object) 等 )(附代码)