文章目录
前言
一、管道
1. 概念
2. 匿名管道
3. 有名管道
二、内存映射区
1. 概念
2. mmap函数
3. 进程间通信(有血缘关系)
4. 进程间通信(没有血缘关系)
5. 拷贝文件
前言
在文章【嵌入式Linux】<总览> 多进程中已经介绍了进程的相关概念和创建多个进程的方法。本篇聚焦于进程间通信的方式,若涉及版权问题请联系本人删除。
一、管道
1. 概念
- 管道是进程间通信的方式之一,其数据的流动是单向的。
- 管道本质上是内核中的一块内存,即内存缓冲区。这块内存中的数据存储在环形队列中,其默认空间为4K。这个环形队列的队头就是读指针,队尾就是写指针。
- 由于管道是内核中的内存,需要使用系统调用的文件IO函数read和write函数来读写。每次读完后,数据相当于出队了!读写默认是阻塞:①读管道:管道中没有数据就会阻塞,直到有数据到来;②写管道:管道空间满了就会阻塞,直到有数据出队。通过fcntl函数能修改为非阻塞。
- 管道是独立于任何进程的,并且充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的入口和出口(读端和写端的文件描述符),那么他们之间就可以通过管道进行数据的交互。
- 当读端关闭了,管道破裂,写端进程直接退出;当写端关闭了,读端就会将管道中剩余内容读取完,最后返回0。
2. 匿名管道
【1】介绍:匿名管道是管道的一种,其没有具体的名字,只能实现拥有血缘关系的进程进行通信。
【2】创建匿名管道:pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);
//参数说明:pipefd是传出的参数
//pipefd[0]对应读端的文件描述符
//pipefd[1]对应写端的文件描述符
//返回值:0表示成功,-1表示失败
【3】进程通信的注意事项:在创建子进程之后,父进程中的读端和写端文件描述符都会被复制到子进程中。若子进程只有写的需求,那么可以将读端的文件描述符给close。若父进程只有读的需求,那么可以将写端的文件描述符给close。
程序示例:子进程执行"ps"命令,并将结果写入管道中;父进程读管道,将结果显示在终端上。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/fcntl.h> #include <string.h> int main(int argc, char **argv) { /* 1.创建匿名管道 */ int fd[2]; int pipeRet = pipe(fd); if (pipeRet < 0) { perror("创建管道失败"); return -1; } /* 2.创建子进程 */ pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } /* 3.子进程操作 */ if (pid == 0) { close(fd[0]);//关闭管道的读端 dup2(fd[1], STDOUT_FILENO);//重定向标准输出 execlp("ps", "ps", NULL);//执行ps程序 perror("execlp错误"); } /* 4.父进程操作 */ if (pid > 0) { close(fd[1]);//关闭管道的写端 //循环读取管道中的数据 char buf[4096]; while (1) { memset(buf, 0, sizeof(buf));//清空缓存 int len = read(fd[0], buf, sizeof(buf));//读取管道 if (len <= 0) {//异常或读完 break; } printf("%s", buf);//输出内容至终端 } close(fd[0]); //等待子进程 wait(NULL); } return 0; }
3. 有名管道
【1】介绍:有名管道在磁盘上有实体文件,文件类型为p(不存储真实数据)。
- 有名管道可以称为fifo.
- 有名管道的磁盘大小永远是0,因为有名管道依旧是将数据存储到内存缓冲区。打开这个磁盘文件,就能获取对应的文件描述符。
- 任意两个进程都能利用有名管道来进行通信。
【2】创建有名管道:
- 方式一:终端命令"mkfifo 有名管道名字"
- 方式二:调用函数mkfifo,其细节如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//参数说明:
//pathname: 保存的文件名
//mode: 创建的管道文件权限
//返回值:成功返回0,失败返回-1
【3】进程间通信注意事项:有名管道的读写首先需要调用open函数来打开管道文件,若管道文件中只有读端或只有写端被打开,那么就会阻塞在open函数,直到两端都被打开。
程序示例:写两个程序,一个作为写管道程序,一个作为读管道程序。①写管道:创建有名管道文件,打开该文件,向有名管道中写入数据,关闭文件。②读管道:打开有名管道文件,读取数据,并显示到终端上,关闭文件。
写管道程序:
#include <stdio.h> #include <sys/types.h> #include <sys/fcntl.h> #include <unistd.h> #include <sys/stat.h> //写管道程序 int main(int argc, char **argv) { /* 1.创建有名管道 */ int mkRet = mkfifo("./pipefile", 0664); if (mkRet < 0) { perror("创建有名管道失败"); return -1; } /* 2.打开有名管道 */ int fd = open("./pipefile", O_WRONLY); if (fd < 0) { perror("管道文件打开失败"); return -1; } /* 3.循环写入数据 */ for (int i = 0; i < 5; ++i) { char writeBuf[200] = {0}; sprintf(writeBuf, "Hello, Can! 我在努力学习中... %d\n", i); int writeRet = write(fd, writeBuf, sizeof(writeBuf)); if (writeRet < sizeof(writeBuf)) { printf("写入错误"); break; } sleep(1);//保证能够写入 } /* 4.关闭管道文件 */ close(fd); return 0; }
读管道程序:
#include <stdio.h> #include <sys/types.h> #include <sys/fcntl.h> #include <unistd.h> //读管道程序 int main(int argc, char **argv) { /* 1.打开有名管道 */ int fd = open("./pipefile", O_RDONLY); if (fd < 0) { perror("管道文件打开失败"); return -1; } /* 2.循环读取数据并打印 */ while (1) { char readBuf[1024] = {0}; int readRet = read(fd, readBuf, sizeof(readBuf)); if (readRet <= 0) {//异常或读完 break; } printf("%s", readBuf);//打印读取的内容 } /* 3.关闭管道文件 */ close(fd); return 0; }
二、内存映射区
1. 概念
- 通过mmap函数创建内存映射区是实现进程间通信的方法之一。
- 内存映射区位于每个进程的用户区(用于加载动态库的那个区域)。
- 内存映射区的读写是非阻塞的。
- 内存映射区创建成功之后,得到映射区内存的起始地址,使用内存操作函数读写数据。
- 机制:多个进程将内存映射区和同一个磁盘文件进行映射。当其中一个进程修改了它的内存映射区,数据会自动同步到磁盘文件。同时,和该磁盘文件建立映射关系的进程中的内存映射区的数据也会实时变化,因此能够实现进程间的通信。
- 注意:内存映射区使用完后通过munmap函数释放。
#include <sys/mman.h>
int munmap(void *addr, size_t length)
//参数说明:
//addr: 内存映射区的起始地址,就是mmap函数的返回值
//length: 内存映射区的大小,与mmap函数的第二个参数相同
//返回值:成功返回0,失败返回-1
2. mmap函数
【1】头文件:#include <sys/mman.h>
【2】函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
【3】参数说明:
- addr:从动态库加载区的什么位置开始创建内存映射区,一般为NULL来委托内核分配
- length:创建的内存映射区的大小(单位:字节),实际大小是按4000的整数倍去分配
- prot:对内存映射区的操作权限
- PROT_READ: 读内存映射区
- PROT_WRITE: 写内存映射区
- PROT_READ | PROT_WRITE:读写内存映射区
- flags:用于确定是否共享内存映射区
- MAP_SHARED: 多个进程可以共享数据,进行映射区的数据同步
- MAP_PRIVATE: 映射区数据是私有的,不能同步给其他进程
- fd:对应打开的磁盘文件的文件描述符。内存映射区通过此与硬盘文件建立联系。
- offset:要求>=0并且是4000的倍数,表示硬盘文件从偏移到的位置进行映射。
【4】返回值:成功返回内存映射区的首地址,失败返回MAP_FAILED其实就是(void *) -1
【5】注意事项:
- length必须要大于0。
- prot一般都是PROT_READ | PROT_WRITE。
- 内存映射区创建成功之后, 关闭参数中的文件描述符fd不会影响进程间通信。
3. 进程间通信(有血缘关系)
父进程fork子进程,子进程就会将父进程的虚拟地址空间进行复制。因此,对于有血缘关系的进程来说,通过内存映射区的方式来进行通信是简单的。
【程序实例】如下代码,父进程创建子进程,父进程向内存映射区中写数据,子进程从内存映射区中读数据。注意:磁盘文件的大小不能为0,否则会报错误:Bus error (core dumped)。
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.创建子进程,复制父进程的内存映射区 */
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
close(fd);
return -1;
}
/* 4.父进程:写数据到内存映射区 */
if (pid > 0) {
const char *content = "Hello, Can!";
memcpy(ptr, content, strlen(content)+1);
wait(NULL);
}
/* 5.子进程:从内存映射区中读数据 */
if (pid == 0) {
sleep(1);//由于读写非阻塞,这里保证父进程先行
printf("从内存映射区读取的数据:%s\n", (char*)ptr);
}
/* 6.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4000);
return 0;
}
4. 进程间通信(没有血缘关系)
进程间没有血缘关系,需要各自创建内存映射区,并且对应的磁盘文件必须是同一个。
【程序实例】两个进程打开同一个磁盘文件,然后各自构建内存映射区。一个进程向内存映射区写数据,另一个进程读数据。
写数据的进程代码:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
//写数据的进程
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.向内存映射区中写数据 */
const char *content = "------Hello, Can!------";
memcpy(ptr, content, strlen(content)+1);
sleep(1);
/* 4.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4000);
return 0;
}
读数据的进程代码:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
//读数据的进程
int main(int argc, char **argv)
{
/* 1.打开磁盘文件 */
int fd = open("./test.txt", O_RDWR);
if (fd < 0) {
perror("磁盘文件打开错误");
return -1;
}
/* 2.创建内存映射区 */
void * ptr = mmap(NULL, 4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap失败");
close(fd);
return -1;
}
/* 3.从内存映射区中读数据 */
printf("从内存映射区中读数据:%s\n", (char*)ptr);
/* 4.关闭硬盘文件,释放内存映射区 */
close(fd);
munmap(ptr, 4000);
return 0;
}
5. 拷贝文件
鉴于内存映射区和磁盘文件的同步关系,可以用来拷贝文件,其流程如下:
①打开原文件,计算原文件大小size,并映射到内存映射区A。
②打开目标文件,拓展大小为size,并映射到内存映射区B。
③拷贝内存映射区A的内容到内存映射区B。
④关闭所有文件,释放所有内存映射区。
注意:mmap中还是需要MAP_SHARED,否则vim和cat都无法识别内容。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char **argv)
{
/* 1.打开原文件,并映射到内存映射区A */
int fd1 = open("./test.txt", O_RDWR);
if (fd1 < 0) {
perror("打开原文件失败");
return -1;
}
void *ptrA = mmap(NULL, 4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if (ptrA == MAP_FAILED) {
perror("内存映射区A构建失败");
return -1;
}
int size = lseek(fd1, 0, SEEK_END);//原文件大小
/* 2.打开目标文件,拓展大小,并映射到内存映射区B */
int fd2 = open("./output.txt", O_RDWR | O_CREAT, 0664);
if (fd2 < 0) {
perror("打开输出文件失败");
return -1;
}
ftruncate(fd2, size);//将输出文件拓展到size大小
void *ptrB = mmap(NULL, 4000, PROT_READ | PROT_WRITE, MAP_SHARED, fd2, 0);
if (ptrB == MAP_FAILED) {
perror("内存映射区B构建失败");
return -1;
}
/* 3.拷贝A的空间到B */
memcpy(ptrB, ptrA, size);
/* 4.关闭文件,释放所有内存映射区 */
munmap(ptrA, 4000);
munmap(ptrB, 4000);
close(fd1);
close(fd2);
return 0;
}