上次结束了基础IO:Linux:基础IO(三.软硬链接、动态库和静态库、动精态库的制作和加载)
文章目录
- 1.认识进程间通信
- 2.管道
- 2.1匿名管道
- 2.2pipe()函数 —创建匿名管道
- 2.3匿名管道的四种情况
- 2.4管道的特征
- 3.基于管道的进程池设计
- 4.命名管道
- 4.1引入与性质
- 4.2命令行创建
- 4.3程序中创建命名管道
- 写个小项目
- 项目规划
- Cnmm.hpp
- PipeClient.cpp
- PipeServe.cpp
- 5.System V共享内存
- 5.1相关函数介绍
1.认识进程间通信
我们通过之前的知识知道,进程具有独立性。两个进程之间时不能进行数据的直接传递的
但我们之前学校的fork()函数不是能传递子进程的pid给父进程吗?——这个严格来说不算通信
-
为什么我们需要进程间通信?
- 数据传输:一个进程需要将自己的数据发送给另一个进程。这种通信方式可以实现进程之间的数据交换和共享,从而实现协作和协同工作。
- 资源共享:多个进程之间共享同样的资源,如共享内存、共享文件等。通过进程间通信,可以实现多个进程对同一资源的访问和操作,提高资源的利用率和效率。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件,如进程终止、资源可用等。通过通知事件,进程可以及时响应和处理其他进程的状态变化,实现进程之间的协作和同步。
- 进程控制:有些进程希望完全控制另一个进程的执行,如调试进程需要拦截另一个进程的陷入和异常,并能够及时知道其状态改变。通过进程控制,可以实现对其他进程的监控、调试和管理,确保系统的稳定和安全运行。
我们往往需要多个进程协同来完成一些任务
-
进程间通信是什么?
一个进程能把自己的数据给另外一个进程(一直)
本质:让不同的进程看到同一份资源(一般都是要由OS提供)
-
如何进行进程间通信
- 我们要有一个来进行数据交换的空间(一般是内存)。不是直接去另外一个进程里拿,这样会破坏进程的独立性
- 这段空间不能由这双方来提供。由OS(话事人)来提供
- OS提供的”空间“有不同的样式,就决定了有不同的通信方式
那么OS提供的样式有:
- 管道(匿名、命名)
- 共享内存
- 消息队列
- 信号量
2.管道
基于文件的,让不同进程看到同一份资源的通信方式叫做管道
管道只能被设计成为单向通信
在Linux中,管道确实可以被视为一种机制,同时也是一种特殊的文件类型。这种双重性来自于Linux操作系统的设计和其对所有资源采取的抽象化处理方式。
作为一种机制,管道用于进程间通信(IPC)。它允许一个进程的输出直接成为另一个进程的输入,从而实现了数据的快速传递。这种机制大大简化了进程间的通信过程,提高了通信效率。
从文件的角度来看,管道在Linux中被实现为一种特殊的文件类型。这意味着管道具有文件的某些属性和操作方式,比如可以通过文件描述符进行打开、读取、写入和关闭等操作。然而,与普通文件不同的是,管道并不在磁盘上占用实际的物理空间,它的内容存储在内核的缓冲区中,只在内存中存在。
这种双重性使得管道既具有机制的灵活性,又具有文件的可操作性。它可以在不同的进程之间建立连接,实现数据的传递和共享,同时也可以通过标准的文件操作接口进行访问和控制。
为了支持管道通信,OS提供了一个接口:pipe()
2.1匿名管道
匿名管道(Anonymous Pipe)Linux中提供的一种进程间通信(IPC)机制。匿名管道没有名字,它们仅存在于创建它们的进程及其子进程之间,并且一旦这些进程终止,管道也将随之消失。
匿名管道的主要特点如下:
- 单向通信:匿名管道是半双工的,这意味着数据只能在一个方向上流动。通常,一个进程向管道写入数据,而另一个进程从管道读取数据。如果需要双向通信,则需要创建两个管道,一个用于每个方向。
- 亲缘关系:匿名管道只能用于具有亲缘关系的进程之间,即一个进程和它的子进程之间。这是因为管道的文件描述符是通过fork()系统调用在父子进程之间复制的。
- 自动管理:当所有使用管道的文件描述符都被关闭时,管道将自动被删除。这意味着不需要像命名管道那样显式地打开和关闭它。
- 内存中的缓冲区:管道实际上是一个在内核中维护的缓冲区,用于存储从写入端发送但尚未被读取端读取的数据。这个缓冲区的大小是有限的,如果写入的数据超过了缓冲区的大小,写操作可能会被阻塞,直到有空间可用。
管道文件的数据是存储在内存中的(是内存级的文件),而不是磁盘上。这使得对管道的访问速度非常快,类似于对内存的直接访问
匿名管道是通过创建子进程,而子进程会继承父进程的相关属性信息,来实现不同的进程看到同一份资源
通过管道,一个进程(写端)可以将数据发送给另一个进程(读端),实现数据的共享和传递。当读端从管道中读取数据时,这些数据会被从内核的缓冲区中移除(或称为消费),从而为写端提供了更多的空间来写入新的数据
在C语言中,可以使用pipe()
函数来创建一个匿名管道。这个函数接受一个包含两个文件描述符的数组作为参数,并返回两个文件描述符:一个用于读操作,另一个用于写操作。然后,可以使用fork()
创建一个子进程,并在父进程和子进程之间使用这些文件描述符进行通信。
2.2pipe()函数 —创建匿名管道
pipe
函数用于创建管道,这是一种特殊的文件,用于连接一个程序的标准输出和另一个程序的标准输入,从而实现这两个程序之间的通信。在C语言中函数原型为:int pipe(int pipefd[2]);
参数:
pipe
函数接受一个整型数组作为参数(这是个输出型参数),即int pipefd[2]
。这个数组用于存储管道的两个文件描述符:pipefd[0]
表示管道的读端,而pipefd[1]
表示管道的写端。
作用:
调用pipe
函数后,系统会创建一个匿名管道,并将这个管道的两个端点(一个用于读,一个用于写)的文件描述符分别赋值给pipefd[0]
和pipefd[1]
。这样,一个进程就可以通过pipefd[1]
向管道写入数据,而另一个进程则可以通过pipefd[0]
从管道中读取数据。这种机制使得两个进程之间可以通过管道进行通信。
返回值:
如果pipe
函数成功创建了管道,则返回0。如果创建失败,则返回-1,并将错误原因存储在全局变量errno
中。可能的错误原因包括:
EMFILE
:进程已达到其文件描述符的最大数量。ENFILE
:系统已达到其文件描述符的最大数量。EFAULT
:传递给pipe
函数的数组地址不合法。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
void writer(int wfd)//写端的操作
{
const char *str = "hi father, I am child";
char buffer[128];
int cnt = 0;
pid_t pid = getpid();
while(1)
{
//先调用snprintf向buffer数组里写,然后在把buffer数组写到fd为wfd的文件里(这里就是管道的写端)
snprintf(buffer, sizeof(buffer), "message: %s, pid: %d, count: %d\n", str, pid, cnt);
write(wfd, buffer, strlen(buffer));
cnt++;
sleep(1);
}
}
void reader(int rfd)//读端的操作
{
char buffer[1024];
while(1)
{
ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
(void)n;
//没有使用这个 n 变量。如果编译器被配置为警告未使用的变量,那么它就会为 n 发出一个警告
printf("father gets a message: %s", buffer);
}
}
int main()
{
//创建管道
int pipefd[2];
int n = pipe(pipefd);
// pipefd[0]-->read pipefd[1]-->write 0是写端,1是读端
// 0-->嘴巴 读书 1-->钢笔 写字
if(n < 0) return 1;
//创建子进程
pid_t id = fork();
if(id == 0)
{
//child: w 我们让子进程来写
close(pipefd[0]);//那么就要关闭读端
writer(pipefd[1]);
exit(0);
}
// father: r我们让父进程来读
close(pipefd[1]);//那么就要关闭写端
reader(pipefd[0]);
wait(NULL);
return 0;
}
2.3匿名管道的四种情况
-
管道内部没有数据而且子进程不关闭自己的写端文件fd, 读端(父)就要阻塞等待,直到pipe有数据
管道中没有数据时,读端继续读取的默认行为是阻塞当前正在读取的进程。在这种情况下,进程会进入等待状态,其进程控制块(PCB)会被放置在管道文件的等待队列中。只要管道中没有新的数据到来,读端进程就会一直阻塞等待
-
管道内部被写满而且读端(父进程)不关闭自己的fd,写端(子进程)写满之后,就要阻塞等待
管道具有固定的缓冲区大小,当缓冲区中的数据量达到上限时,写端进程就会被阻塞,直到有读端进程从管道中读取数据并释放缓冲区空间
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> void writer(int wfd)//写端的操作 { const char *str = "hi father, I am child"; char buffer[128]; int cnt = 0; pid_t pid = getpid(); while(1) { char ch='A'; write(wfd, &ch, 1); cnt++; printf("cnt=%d\n",cnt); } } void reader(int rfd)//读端的操作 { char buffer[1024]; while(1) { sleep(10); ssize_t n = read(rfd, buffer, sizeof(buffer)-1); (void)n; //没有使用这个 n 变量。如果编译器被配置为警告未使用的变量,那么它就会为 n 发出一个警告 printf("father gets a message: %s", buffer); } } int main() { //创建管道 int pipefd[2]; int n = pipe(pipefd); // pipefd[0]-->read pipefd[1]-->write 0是写端,1是读端 // 0-->嘴巴 读书 1-->钢笔 写字 if(n < 0) return 1; //创建子进程 pid_t id = fork(); if(id == 0) { //child: w 我们让子进程来写 close(pipefd[0]);//那么就要关闭读端 writer(pipefd[1]); exit(0); } // father: r我们让父进程来读 close(pipefd[1]);//那么就要关闭写端 reader(pipefd[0]); wait(NULL); return 0; }
-
不再向管道写入数据并且关闭了写端(子进程)文件描述符时,读端(父进程)可以继续从管道中读取剩余的数据,直到管道中的数据全部被读取完毕。最后就会读到返回值为0,表示读结束,类似读到了文件的结尾
-
读端关闭其文件描述符并且不再读取数据时,如果写端继续向管道写入数据,操作系统会发送一个
SIGPIPE
信号给写端进程。默认情况下,这个信号会终止写端进程。SIGPIPE
信号是一个用于处理管道写端在写操作时无读端接收的情况的信号。SIGPIPE
信号(信号编号为13)的发送是为了通知写端进程,其写操作因为管道的另一端没有读端而不再有意义。这是一种保护机制,防止写端进程在没有读端的情况下无限期地等待或继续写入数据到一个不再被读取的管道中。#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> void writer(int wfd)//写端的操作 { int cnt = 0; while(1) { sleep(1); char ch='A'; write(wfd, &ch, 1); cnt++; printf("cnt=%d\n",cnt); }//子进程一直写 } void reader(int rfd)//读端的操作 { int cnt=8; char buffer[1024]; while(1) { sleep(1); ssize_t n = read(rfd, buffer, sizeof(buffer)-1); if(n>0) { printf("father get a message: %s, n : %ld\n", buffer, n); } else if(n==0) { printf("reading has done: %s %ld\n", buffer,n); break; } else { break; } cnt--; if(cnt==0) { break; } } close(rfd);//8秒后,父进程不再读,直接关闭 printf("end"); } int main() { //创建管道 int pipefd[2]; int n = pipe(pipefd); // pipefd[0]-->read pipefd[1]-->write 0是写端,1是读端 // 0-->嘴巴 读书 1-->钢笔 写字 if(n < 0) return 1; //创建子进程 pid_t id = fork(); if(id == 0) { //child: w 我们让子进程来写 close(pipefd[0]);//那么就要关闭读端 writer(pipefd[1]); exit(0); } // father: r我们让父进程来读 close(pipefd[1]);//那么就要关闭写端 reader(pipefd[0]); int status=0; pid_t rid=waitpid(id,&status,0); printf("exit code: %d, exit signal: %d\n",WEXITSTATUS(status),status&0x7f); return 0; }
2.4管道的特征
-
匿名管道自带同步机制:在匿名管道中,写端在写数据且没有写完时,读端是不可能访问管道这块公共资源的。这种机制确保了数据的完整性和一致性,避免了数据冲突和错误
-
管道(Pipe)是一种常用于具有血缘关系进程间通信的机制,特别是在父子进程之间。这里的“血缘关系”指的是进程之间的创建关系,即一个进程创建了另一个进程,它们之间存在直接的父子关系
-
管道(pipe)是面向字节流的:这意味着管道在传输数据时,是以字节为单位进行处理的。无论是字符、整数还是其他类型的数据,都会被转换成字节序列进行传输。因此,管道不关心数据的具体格式或类型,只负责将数据以字节流的形式从一个进程传递到另一个进程
-
管道(pipe)是半双工的:它只能在一个方向上传输数据,属于单向通信的特殊概念。具体来说,一个管道有一个输入端和一个输出端,数据可以从输入端流入管道,并从输出端流出。但管道不允许数据在相反的方向上流动,即不能从输出端流回输入端
半双工(Half Duplex)数据传输指的是数据可以在一个信号载体的两个方向上传输,但是不能同时传输。也就是说,在一个时间点,数据只能在一个方向上流动
-
父子进程退出后,管道会自动释放。这是由操作系统的内存管理机制决定的。当进程结束时,操作系统会回收其占用的所有资源,包括打开的文件、管道、网络连接等
-
我们之前在命令行里使用的
|
其实就是匿名管道:在命令行中,当我们使用|
来连接两个命令时,实际上是在这两个命令之间创建了一个匿名管道。这使得前一个命令的输出能够直接传输给后一个命令,实现了两个命令之间的数据共享和传输
3.基于管道的进程池设计
4.命名管道
4.1引入与性质
我们设想一个这样的情况:
- 当一个进程打开一个文件(比如
log.txt
),内核会为该进程创建一个struct file
结构体,其中包含指向inode
结构体、函数指针数组和缓冲区的指针。这个struct file
结构体会指向已加载的inode
结构体和缓冲区,用于表示文件在内核中的信息和缓存文件数据。 - 当另一个进程也打开同一个文件时,内核会为该进程创建另一个
struct file
结构体,其中也包含指向相同的inode
结构体和缓冲区的指针。这意味着多个进程可以共享相同的inode
结构体和缓冲区,而不会为每个进程创建一份完全一样的inode
结构体和缓冲区。 - 由于
inode
结构体和缓冲区是在内核中维护的,因此多个进程可以共享相同的inode
结构体和缓冲区,而不需要为每个进程复制一份。这种共享机制可以节省内存空间,并确保多个进程对同一文件的操作是一致的。
此时这两个进程就看到了同一块资源(log.txt 文件)
当两个进程共享同一个文件(例如
log.txt
)时,它们实际上是在操作同一块资源。这是因为文件系统中的路径和文件名是唯一的,所以无论哪个进程打开同一个路径下的文件,都会访问到同一个文件。在多个进程共享文件时,它们可以通过共享同一个缓冲区来进行数据交换。这个缓冲区可以被看作是一个管道,用于在进程之间传递数据。通过这种方式,进程可以实现数据共享和通信。
在上面这种情况下,这个管道(缓冲区)可以被称为命名管道(named pipe)。
命名管道是一种特殊的文件类型,它允许进程之间通过文件系统进行通信。通过路径+文件名来确定(唯一的路径+文件名来找到并访问这个管道),多个进程可以通过打开同一个命名管道来实现数据交换。
- 在这种情况下,这个管道不需要与磁盘进行交互,因为数据是在内存中进行传递的。进程通过读取和写入管道来实现数据共享,而不需要直接与磁盘进行交互。
4.2命令行创建
命名管道(Named Pipe)是一种特殊的文件,用于进程间通信。它是一种半双工通信方式,允许一个或多个进程之间通过读写同一个文件来进行通信。
-
创建命名管道:
命名管道是通过调用mkfifo
系统调用来创建的。命名管道在文件系统中以文件的形式存在,但实际上它是一个FIFO(First In First Out)的通信通道。创建命名管道的语法为:mkfifo <管道名称>
-
打开和关闭命名管道:
命名管道可以像普通文件一样被打开和关闭。进程可以通过open
系统调用打开一个命名管道文件,并通过close
系统调用关闭它。在打开命名管道时,进程需要指定相应的读写权限。 -
读写数据:
进程可以通过打开的文件描述符对命名管道进行读写操作。一个进程往管道中写入数据,另一个进程从管道中读取数据。命名管道是阻塞的,如果写入进程写入数据时,没有进程读取数据,写入进程会被阻塞直到有进程读取数据。 -
进程间通信:
命名管道通常用于实现进程间通信,特别是在父子进程或者**不相关进程之间**。一个进程可以向命名管道写入数据,另一个进程可以从命名管道读取数据,实现了进程间的数据交换。
4.3程序中创建命名管道
mkfifo
函数是一个UNIX系统中用于创建命名管道(named pipe)的函数。它的作用是在文件系统中创建一个特殊类型的文件,这个文件可以被多个进程用来进行进程间通信。
在C语言中,可以使用mkfifo
函数来创建一个命名管道,其原型如下:
int mkfifo(const char *pathname, mode_t mode);
pathname
参数是指定要创建的命名管道的路径和文件名。mode
参数是指定创建的管道的权限模式,通常以八进制表示(例如0666
)。
使用mkfifo
函数创建命名管道后,其他进程可以通过打开这个路径+文件名来访问这个管道,从而实现进程间的通信。一旦创建了命名管道,它就可以在文件系统中像普通文件一样被打开、读取和写入。
写个小项目
项目规划
- Cnmm.hpp:管道的封装,头文件的包含、宏定义等任务
- PipeClient.cpp:客户端,进行管道的写入
- PipeServe.cpp:服务端(服务器),进行管道的创建、读取
Cnmm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
#define Mode 0666
#define Path "./fifo"
class Fifo
{
public:
Fifo(const string &path) : _path(path)
{
umask(0);
int n = mkfifo(_path.c_str(), Mode);
if (n == 0)
{
cout << "mkfifo success" << endl;
}
else
{
cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
}
}
~Fifo()
{
int n = unlink(_path.c_str());
if (n == 0)
{
cout << "remove fifo file " << _path << " success" << endl;
}
else
{
cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
}
}
private:
string _path; // 文件路径+文件名
};
#endif//条件编译结束
整体上使用一个条件编译:
在C++头文件中,通常会使用条件编译指令来防止头文件被多次包含,以避免重复定义的问题。条件编译指令的一般结构如下:
#ifndef __HEADER_NAME__ #define __HEADER_NAME__ // 头文件内容 #endif
#ifndef __HEADER_NAME__
:这是条件编译指令的开始标记,用于检查是否已经定义了名为__HEADER_NAME__
的宏。如果之前没有定义这个宏,那么下面的代码将被执行。#define __HEADER_NAME__
:在条件编译指令的开始处,定义名为__HEADER_NAME__
的宏,表示这个头文件已经被包含过了。// 头文件内容
:在这个部分可以放置头文件的内容,包括类的定义、函数的声明等。#endif
:这是条件编译指令的结束标记,表示条件编译的范围结束。
#ifndef __COMM_HPP__
是条件编译指令的开始标记,而#endif
是条件编译指令的结束标记。
-
cerr
:cerr
是C++标准库中的标准错误流,它用于输出错误信息到标准错误设备(通常是显示器)。- 与
cout
(标准输出流)类似,cerr
也是一个对象,可以使用插入运算符<<
来将数据插入到cerr
中进行输出。 - 与
cout
不同的是,cerr
通常用于输出错误消息,而不是普通的程序输出。它是线程安全的,可以在多线程环境中使用。
-
errno
:errno
是一个全局变量,通常定义在<cerrno>
头文件中,用于存储函数调用发生错误时的错误码。- 当某个函数发生错误时,它会设置适当的错误码到
errno
中,以便程序能够检测和处理错误。 - 错误码是整数类型,每个错误码对应于一种特定类型的错误。可以通过查看系统的错误码表来了解每个错误码的含义。
-
strerror
:strerror
是一个C标准库函数,通常定义在<cstring>
或<string.h>
头文件中,用于将错误码转换为对应的错误消息字符串。strerror
接受一个错误码作为参数,并返回一个指向描述该错误的字符串的指针。- 通过调用
strerror(errno)
,可以获取与当前errno
值对应的错误消息字符串,以便程序输出或记录错误信息。
PipeClient.cpp
#include "Comm.hpp"
int main()
{
// 打开管道,进行写入,最后关闭
int wfd = open(Path, O_WRONLY | O_CREAT); // 以只写方式打开
if (wfd < 0)
{
cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
return 1;
}
string buffer; // 开始写入
while (true)
{
cout << "please write your message:" << endl;
getline(cin, buffer);
ssize_t n = write(wfd, buffer.c_str(), buffer.size());
if (n < 0)
{
cerr << "write failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
break;
}
}
close(wfd);
return 0;
}
PipeServe.cpp
#include "Comm.hpp"
#include <unistd.h>
int main()
{
Fifo fifo(Path);
// 打开管道,进行读取,最后关闭
int rfd = open(Path, O_RDONLY); // 以只读方式打开
if (rfd < 0)
{
cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
return 1;
}
char buffer[1024];//开始读取
while (true)
{
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
cout << "client say : " << buffer << endl;
}
else if (n == 0)
{
cout << "client quit, me too!!" << endl;
break;
}
else
{
cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
break;
}
}
close(rfd);
return 0;
}
这里我自己有个疑问:本来读端是一直堵塞在read函数的,我们一输入abcde,第一次read就能读取完,然后输出。下一次循环就应该接着读,读到末尾,返回0了吧? 但为什么这里是接着阻塞呢?
- 在非阻塞模式下,如果读取到文件末尾(没有更多的数据可读取),
read
函数会立即返回 0。- 在阻塞模式下,
read
函数会阻塞等待直到有数据可读取或者发生错误,它不会因为读取到文件末尾而返回 0。相反,只有当管道被关闭或者读取操作被中断时,read
函数才会返回 0。- 默认都是阻塞模式
文件描述符的阻塞模式和非阻塞模式指的是在进行I/O操作时的行为方式。
-
阻塞模式:
- 在阻塞模式下,当进行I/O操作时,如果数据尚未准备好或者操作无法立即完成,程序会被阻塞,也就是暂停执行,直到操作完成或者出现错误为止。
- 例如,在阻塞模式下,如果调用read函数读取一个文件描述符,但是文件中没有数据可读,程序将会被阻塞,直到有数据到达为止。类似地,如果调用write函数写入数据到一个已满的管道中,程序也会被阻塞,直到有足够的空间写入数据。
-
非阻塞模式:
- 在非阻塞模式下,进行I/O操作时,如果操作无法立即完成,程序不会被阻塞,而是立即返回一个错误或者一个特定的状态码,提示当前操作无法立即完成。
- 例如,在非阻塞模式下,如果调用read函数读取一个文件描述符,但是文件中没有数据可读,read函数将立即返回一个错误码,而不会等待数据到达。类似地,如果调用write函数写入数据到一个已满的管道中,write函数也会立即返回一个错误码,而不会等待空间可用。
5.System V共享内存
实现进程间通信的前提就是如何让不同的进程看到同一份资源
- 匿名管道我们是通过子进程继承父进程打开的资源
- 命名管道是通过两个进程都打开具有唯一性标识的命名管道文件(路径+文件名)
- 共享内存其实是通过OS创建一块shm
System V共享内存(Shared Memory)是一种Linux中用于进程间通信(IPC)的机制。它允许多个进程访问同一块物理内存区域,从而实现数据的快速共享和交换。
- 原理:
- 在物理内存中申请一块内存空间作为共享内存。
- 将这块内存空间与各个进程的页表建立映射关系,使得这些进程在虚拟地址空间中可以看到并访问这块共享内存。
- 通过这种方式,多个进程可以像访问自己的内存一样访问共享内存,从而实现数据的快速共享和交换。
- 使用方式:
- 创建:使用
shmget()
系统调用来创建共享内存。这个函数会分配一块指定大小的内存区域,并返回一个标识符,用于后续对这块共享内存的操作。 - 关联:使用
shmat()
系统调用来将共享内存关联到进程的地址空间。这个函数会将共享内存的地址告诉进程,使得进程可以通过这个地址来访问共享内存。 - 取消关联:当进程不再需要访问共享内存时,可以使用
shmdt()
系统调用来取消关联。这个函数会断开进程与共享内存之间的映射关系。 - 释放:当所有进程都不再需要这块共享内存时,可以使用
shmctl()
系统调用来释放它。这个函数会回收这块内存区域,并释放相关的资源。
- 创建:使用
5.1相关函数介绍
ftok()
函数 Linux中用于生成一个唯一的键值(key)的系统调用,这个键值通常用于在进程间通信(IPC)中标识共享内存段、消息队列或信号量集。ftok()
函数基于一个已经存在的文件路径和一个非零的标识符(通常是一个小的正整数)来生成这个键值。
#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char *pathname, int proj_id);
参数:
pathname
:指向一个已经存在的文件路径的指针。这个文件通常被用作生成键值的“种子”或“基础”。proj_id
:一个非零的标识符,通常是一个小的正整数。这个值将与文件路径一起被用于生成键值。返回值:
如果成功,ftok()
函数返回一个唯一的键值(key_t
类型),该键值可以在后续的 IPC 调用(如 shmget()
, msgget()
, semget()
等)中用作参数。如果失败,则返回 (key_t) -1
并设置 errno
以指示错误。
shmget()
:创建或获取共享内存
shmget()
系统调用用于创建一个新的共享内存对象,或者如果它已存在,则返回该对象的标识符。
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
-
key
:一个键,用于唯一标识共享内存对象。通常使用ftok()
函数生成。- 共享内存在内核中同时可以存在很多个,OS必须要管理所有的共享内存
- 如何管理呢?先描述,在组织
- 系统中会存在很多共享内存,怎么保证,多个不同的进程看到的是同共享内存呢? 要给共享内存提供唯一性的标识
key
便是那个唯一性标识符。那么为什么这个key要由我们用户来传入呢?
- 如果然系统生成,将值返回让我们得到。那我们如何给另外一个进程呢?要做到就要有进程间通信,这不倒反天罡了?
-
size
:共享内存的大小(以字节为单位)。 -
shmflg
:权限标志和选项。通常设置为IPC_CREAT
(如果对象不存在则创建,存在的话直接获取)和权限(如0666
)。若设置为
IPC_CREAT|IPC_EXCL
(如果对象不存在则创建,存在的话出错返回)
返回值:成功时返回共享内存对象的标识符;失败时返回-1并设置errno
。
shmat()
:将共享内存关联到进程的地址空间
shmat()
系统调用用于将共享内存对象关联到调用进程的地址空间。
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid
:shmget()
返回的共享内存对象标识符。shmaddr
:希望将共享内存附加到的进程的地址。如果设置为NULL,则系统选择地址。shmflg
:通常设置为0或SHM_RND
(使附加地址向下舍入到最接近的SHMLBA边界)。
返回值:成功时返回共享内存附加到进程的地址;失败时返回(void *)-1并设置errno
。
shmdt()
:取消共享内存的关联
shmdt()
系统调用用于取消之前通过shmat()
附加到进程的共享内存的关联。
函数原型:
int shmdt(const void *shmaddr);
参数:
shmaddr
:shmat()
返回的共享内存附加到进程的地址。
返回值:成功时返回0;失败时返回-1并设置errno
。
shmctl()
:控制共享内存
shmctl()
系统调用用于获取或设置共享内存的属性,或者删除共享内存对象。
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:共享内存对象标识符。cmd
:要执行的操作。例如,IPC_RMID
用于删除共享内存对象,IPC_STAT
用于获取其状态。buf
:指向shmid_ds
结构的指针,用于传递或接收共享内存的状态信息。
返回值:成功时返回0;失败时返回-1并设置errno
。
今天就到这里了,也是结束了期末周,现在就开始正常更新啦