Linux系统中进程间通信(Inter-Process Communication, IPC)
文章目录
- 进程间通信介绍
- 进程间通信目的
- 进程间通信发展
- 管道
- 什么是管道
- 匿名管道
- 用fork来共享管道原理
- 站在文件描述符角度-深度理解管道
- 站在内核角度-管道本质
- 管道读写规则
- 管道特点
- 命名管道
- 创建一个命名管道
- 匿名管道与命名管道的区别
- 命名管道的打开规则
- 命名管道的删除
- 用命名管道实现文件拷贝
- 用命名管道实现server&client通信
- system V共享内存
- 共享内存示意图
- 共享内存数据结构
- 共享内存函数
- system V信号量 - 选学了解即可
进程间通信介绍
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信发展
进程间通信(IPC)的发展经历了多个阶段,主要包括:
-
管道(Pipes):最早期的IPC机制之一,允许具有亲缘关系的进程(如父子进程)进行通信。管道是半双工(某一时刻只允许信号在管道上的单向通信)的,数据只能单向流动。
-
命名管道(Named Pipes):与管道类似,但它们允许没有亲缘关系的进程通信,并且可以在文件系统中有一个名称。
-
System V IPC:这是一组传统的IPC机制,包括消息队列、信号量和共享内存。这些机制允许不同进程共享和传递复杂的数据结构。
-
POSIX IPC:基于System V IPC,提供了更加标准化和可移植的IPC机制,包括消息传递、同步以及共享内存的新方法。
-
套接字(Sockets):用于不同机器上的进程通信,也可以在同一台机器上的进程之间通信,非常灵活。
-
远程过程调用(RPC):允许一个进程调用另一个进程的函数或方法,就像调用本地函数一样。
-
内存映射文件(Memory-mapped files):通过映射文件到内存,不同进程可以访问同一块内存区域,实现通信。
-
信号(Signals):用于进程间的简单通信,一个进程可以向另一个进程发送信号,以通知某些事件的发生。
随着技术的发展,IPC机制也在不断进步,以满足更高效、更安全的进程通信需求。全部的进程间通信虽然体系庞大,但是其基本思想都离不开操作系统的性质–先描述,再组织。我们只需学习早期的通信方法管道和System V通信,触类旁通。
管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
匿名管道
匿名管道(Anonymous Pipe)是一种在进程间进行单向通信的机制,其中一个进程作为写入端,另一个进程作为读取端。它是一种简单而有效的进程间通信方式,常用于父子进程或兄弟进程之间的通信。
在Linux系统中,可以使用pipe
函数来创建匿名管道。pipe
函数会创建一个管道,并返回两个文件描述符,一个用于读取数据,另一个用于写入数据。其中,文件描述符0表示标准输入(stdin),文件描述符1表示标准输(stdout)。
#include <unistd.h>
int pipe(int fd[2]);
//创建失败返回-1,创建成功返回两个文件描述符构成的数组
以下是使用匿名管道进行进程间通信的基本步骤:
- 调用
pipe
函数创建管道,获取读取端和写入端的文件描述符。 - 调用
fork
函数创建一个子进程。 - 在父进程中关闭读取端的文件描述符,保留写入端的文件描述符。
- 在子进程中关闭写入端的文件描述符,保留读取端的文件描述符。
- 父进程可以通过写入端的文件描述符向管道写入数据。
- 子进程可以通过读取端的文件描述符从管道读取数据。
下面是一个简单的示例代码,演示了如何使用匿名管道进行进程间通信:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buffer[20];
pid_t pid;
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写入端
// 从管道读取数据
read(pipefd[0], buffer, sizeof(buffer));
printf("子进程接收到的数据:%s\n", buffer);
close(pipefd[0]); // 关闭读取端
} else {
// 父进程
close(pipefd[0]); // 关闭读取端
// 向管道写入数据
write(pipefd[1], "Hello, child process!", 21);
close(pipefd[1]); // 关闭写入端
}
return 0;
}
在上述示例中,父进程向管道写入了一段字符串,子进程从管道中读取该字符串并输出。通过匿名管道,父子进程之间实现了简单的通信。
需要注意的是,匿名管道只能实现单向通信,如果需要双向通信,可以考虑使用命名管道(Named Pipe)或其他进程间通信机制,如共享内存或消息队列。
用fork来共享管道原理
站在文件描述符角度-深度理解管道
站在内核角度-管道本质
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
管道读写规则
当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候 - O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程
退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 一般而言,内核会对管道操作进行同步与互斥。
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。如下图:
命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件。
创建一个命名管道
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
其中,filename 是要创建的命名管道的路径和名称。
例如,要在命令行上创建一个名为 “myfifo” 的命名管道,可以执行以下命令:
mkfifo myfifo
执行该命令后,系统将在当前目录下创建一个名为 “myfifo” 的命名管道文件。
需要注意的是,命名管道是一种特殊的文件
,可以
像普通文件一样进行读写操作
。在命令行中创建的命名管道可以被其他进程打开并进行读写操作
。
- 命名管道也可以从程序里创建,相关函数有:
mkfifo
是一个用于创建命名管道(Named Pipe)的系统调用。它可以在文件系统中创建一个特殊的文件,用于进程间的通信。
具体使用方法如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname
:要创建的命名管道的路径和名称。mode
:创建的命名管道的权限模式。
返回值:
- 成功:返回0。
- 失败:返回-1,并设置相应的错误码。
使用示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
const char *pathname = "myfifo";
mode_t mode = 0666; // 设置权限为读写所有者、组和其他用户
if (mkfifo(pathname, mode) == -1) {
perror("mkfifo");
return 1;
}
printf("命名管道创建成功:%s\n", pathname);
return 0;
}
在上述示例中,我们调用了 mkfifo
函数创建了一个命名管道,并指定了路径和名称为 “myfifo”,权限模式为 0666,表示读写所有者、组和其他用户都有权限。
需要注意的是,命名管道是一种特殊的文件,可以像普通文件一样进行读写操作。在进程间通信时,一个进程可以将数据写入命名管道,而另一个进程可以从该管道读取数据。但是,需要注意管道的读取和写入操作应该在不同的进程中进行,否则可能会造成阻塞。
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
对于命名管道(FIFO)的打开规则,取决于打开操作是为读还是为写。
-
当打开操作为读时:
- 如果打开操作没有设置
O_NONBLOCK
标志(即阻塞模式),则打开操作会阻塞,直到有其他进程以写模式打开该命名管道。 - 如果打开操作设置了
O_NONBLOCK
标志(即非阻塞模式),则打开操作会立即返回成功,不会阻塞等待。
- 如果打开操作没有设置
-
当打开操作为写时:
- 如果打开操作没有设置
O_NONBLOCK
标志(即阻塞模式),则打开操作会阻塞,直到有其他进程以读模式打开该命名管道。 - 如果打开操作设置了
O_NONBLOCK
标志(即非阻塞模式),则打开操作会立即返回失败,并返回错误码 ENXIO(表示设备或地址不存在)。
- 如果打开操作没有设置
这些规则确保了在读写操作之间建立正确的通信连接。如果读操作和写操作同时进行,并且满足打开规则,那么数据可以在进程之间通过命名管道进行传输。
需要注意的是,打开规则只适用于命名管道的打开操作,而不是创建操作。创建命名管道时,并不需要等待其他进程的打开操作。只有在打开操作时,才会根据规则进行阻塞或立即返回。
命名管道的删除
用命名管道实现文件拷贝
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<stdlib.h>
#define BUFFER_SIZE 50
int main()
{
const char *pathname = "myfifo";
mode_t mode = 0666; // 设置权限为读写所有者、组和其他用户
if (mkfifo(pathname, mode) == -1)
{
perror("mkfifo");
return 1;
}
printf("命名管道创建成功:%s\n", pathname);
// 打开源文件和目标文件
int source_fd = open("./text.txt", O_RDONLY);
if (source_fd == -1) {
perror("open source file");
return 1;
}
int destination_fd = open("./popytext.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (destination_fd == -1) {
perror("open destination file");
return 1;
}
// 打开命名管道
int fifo_fd = open("myfifo", O_RDWR);
if (fifo_fd == -1) {
perror("open fifo");
return 1;
}
// 创建子进程进行数据传输
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
if (pid == 0)
{
// 子进程从命名管道中读取数据并写入目标文件
if((bytes_read = read(fifo_fd, buffer, BUFFER_SIZE)) > 0) {
write(destination_fd, buffer, bytes_read);
}
exit(EXIT_SUCCESS);
}
// 父进程从源文件中读取数据并写入命名管道
if((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
write(fifo_fd, buffer, bytes_read);
}
close(source_fd);
close(fifo_fd);
close(destination_fd);
// 等待子进程结束
wait(NULL);
// 删除命名管道
unlink("myfifo");
return 0;
}
用命名管道实现server&client通信
comm.hpp
#pragma once
#include<iostream>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FILE_OPEN_ERR
};
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE,MODE);
if(n == -1)
{
//printf("%d:%s\n",errno,strerror(errno));
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if(m== -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
server.cc
#include"comm.hpp"
#include"log.hpp"
using namespace std;
//管理管道文件
int main()
{
/*logmessage(Info,"hello linux");*/
//创建信道
Init init;
//打开管道
int fd = open(FIFO_FILE,O_RDONLY);
if(fd<0)
{
//perror("open");
logmessage(Fatal,"error string:%s,error code:%d",strerror(errno),errno);
exit(FILE_OPEN_ERR);
}
logmessage(Info,"error string:%s,error code:%d",strerror(errno),errno);
//开始通信
while(true)
{
char buffer[1024]={0};
int x = read(fd,buffer,sizeof(buffer));
if(x>0)
{
buffer[x]=0;
cout<<"client say#"<<buffer<<endl;
}
else if(x==0)
{
logmessage(Debug,"client quit,me too!:%s,error code:%d",strerror(errno),errno);
break;
}
else
break;
}
//关闭管道
close(fd);
return 0;
}
client.cc
#include"comm.hpp"
using namespace std;
int main()
{
int fd = open(FIFO_FILE,O_WRONLY);
if(fd<0)
{
perror("open");
exit(FILE_OPEN_ERR);
}
string line;
while(true)
{
cout<<"Please Enter@";
getline(cin,line);
write(fd,line.c_str(),line.size());
}
close(fd);
return 0;
}
system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
这段代码定义了 struct shmid_ds
结构体,用于描述 System V 共享内存段的属性和状态。让我们逐个解释这些字段的含义:
-
shm_perm
:一个struct ipc_perm
类型的结构体,用于描述共享内存段的权限信息,包括所有者、所属组和访问权限等。 -
shm_segsz
:表示共享内存段的大小,以字节为单位。 -
shm_atime
:表示最后一次连接(attach)共享内存段的时间,以 UNIX 时间戳(__kernel_time_t
类型)表示。 -
shm_dtime
:表示最后一次分离(detach)共享内存段的时间,以 UNIX 时间戳表示。 -
shm_ctime
:表示最后一次修改共享内存段属性的时间,以 UNIX 时间戳表示。 -
shm_cpid
:表示创建共享内存段的进程的进程 ID(__kernel_ipc_pid_t
类型)。 -
shm_lpid
:表示最后一次操作共享内存段的进程的进程 ID。 -
shm_nattch
:表示当前连接(attach)到共享内存段的进程数量。 -
shm_unused
:一个未使用的字段,用于兼容性。 -
shm_unused2
和shm_unused3
:两个未使用的字段,可能由某些特定的实现或库使用。
通过使用 struct shmid_ds
结构体,可以获取共享内存段的属性信息,例如大小、创建时间、连接数等。这些信息对于管理和监控共享内存的使用非常有用。
请注意,struct shmid_ds
结构体中的字段类型可能会因操作系统和编译器而异,例如 __kernel_time_t
和 __kernel_ipc_pid_t
。在实际使用时,你需要查阅相关的系统文档或头文件来了解具体的字段类型和定义。
共享内存函数
shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
- key:用于标识共享内存段的键值。可以使用 `ftok` 函数生成一个唯一的键值,
- 也可以使用已知的键值来获取现有的共享内存段。不同的共享内存段应该使用不同的键值。
- size:指定共享内存段的大小,以字节为单位。共享内存段的大小应根据实际需求进行设置。
- shmflg:用于指定创建共享内存段的标志和访问权限。可以通过按位或运算符 `|` 组合多
- 个标志。常用的标志包括:
- IPC_CREAT:如果共享内存段不存在,则创建一个新的共享内存段。
- IPC_EXCL:不单独使用,与 IPC_CREAT 一起使用,如果共享内存段已存在,则返回错误。
- 0666:指定共享内存段的访问权限。这里的权限是八进制表示,表示所有者、
- 所属组和其他用户的读写权限。
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
上面的Key有讲究的:
- key是一个数字,这个数字是几,并不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
- 第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了!
- 对于一个已经创建好的共享内存,key在哪? key在共享内存的描述对象中!
- 第一次创建的时候,必须有一个key了。怎么有?可以使用 ftok 函数生成一个唯一的键值,也可以使用已知的键值来获取现有的共享内存段。不同的共享内存段应该使用不同的键值。
ftok
函数用于将一个路径名和一个整数标识符转换为一个唯一的键值,用于标识 System V 共享内存段、消息队列或信号量集。下面是ftok
函数的使用方法:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
-
pathname
:一个指向路径名的字符串,可以是任意有效的文件路径。通常选择一个与共享内存相关的文件作为路径名,确保不同的共享内存段使用不同的路径名。 -
proj_id
:一个整数标识符,用于区分不同的共享内存段。通常选择一个非零的整数作为标识符。
ftok
函数将 pathname
和 proj_id
组合起来,生成一个唯一的键值。该键值可以用于创建共享内存段、获取现有的共享内存段或操作其他 System V IPC 对象。
需要注意的是,ftok
函数的结果并不是绝对唯一的,因为它使用了路径名和整数标识符的组合。因此,在使用 ftok
生成键值时,应确保路径名和标识符的组合在整个系统中是唯一的,以避免冲突。
以下是一个示例,展示了如何使用 ftok
函数生成一个键值:
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main() {
const char *pathname = "/path/to/file"; // 路径名
int proj_id = 1; // 标识符
key_t key = ftok(pathname, proj_id);
if (key == -1) {
perror("ftok");
return 1;
}
printf("Generated key: %d\n", key);
return 0;
}
在上述示例中,我们使用 /path/to/file
作为路径名,1 作为标识符,然后调用 ftok
函数生成一个键值。如果成功,将打印生成的键值。如果发生错误,将打印相应的错误信息。
请注意,生成的键值应与其他进程共享,以便它们可以使用相同的键值访问相同的共享内存段。
shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
说明:
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
System V 共享内存的使用涉及以下几个步骤:
-
创建共享内存段:使用
shmget
函数创建一个共享内存段。该函数接受三个参数:键值(用于标识共享内存段),大小(指定共享内存的大小),和标志(用于指定权限和创建方式)。成功创建共享内存段后,shmget
函数返回一个唯一的标识符(共享内存标识符)。 -
连接共享内存:使用
shmat
函数将共享内存段连接到进程的地址空间。shmat
函数接受两个参数:共享内存标识符和地址(如果地址为 NULL,则系统会自动选择一个可用的地址)。成功连接共享内存后,shmat
函数返回共享内存的起始地址。 -
使用共享内存:一旦共享内存连接到进程的地址空间,进程可以像使用普通内存一样使用它。可以通过指针访问共享内存中的数据。
-
分离共享内存:使用
shmdt
函数将共享内存从进程的地址空间中分离。shmdt
函数接受一个参数,即共享内存的起始地址。成功分离共享内存后,进程将无法再访问共享内存中的数据。 -
删除共享内存段:使用
shmctl
函数删除共享内存段。shmctl
函数接受三个参数:共享内存标识符、命令(用于指定要执行的操作),和一个结构体指针(用于传递额外的参数)。通过指定命令为IPC_RMID
,可以删除共享内存段。
下面是一个简单的 C 语言示例,展示了如何使用 System V 共享内存:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("shared_memory_key", 1234); // 创建一个唯一的键值
int shm_id = shmget(key, sizeof(int), IPC_CREAT | 0666); // 创建共享内存段
if (shm_id == -1) {
perror("shmget");
return 1;
}
int *shared_data = (int *)shmat(shm_id, NULL, 0); // 连接共享内存
if (shared_data == (int *)-1) {
perror("shmat");
return 1;
}
*shared_data = 42; // 在共享内存中写入数据
printf("Shared data: %d\n", *shared_data);
shmdt(shared_data); // 分离共享内存
shmctl(shm_id, IPC_RMID, NULL); // 删除共享内存段
return 0;
}
在上面的示例中,我们使用 ftok
函数创建一个唯一的键值,用于标识共享内存段。然后,我们使用 shmget
函数创建一个大小为 sizeof(int)
的共享内存段,并指定了创建标志 IPC_CREAT | 0666
。接下来,我们使用 shmat
函数将共享内存连接到进程的地址空间,并将其强制转换为 int
类型的指针。然后,我们可以通过指针访问共享内存中的数据。最后,我们使用 shmdt
函数分离共享内存,并使用 shmctl
函数删除共享内存段。
请注意,System V 共享内存是一种底层的机制,需要谨慎使用,以确保正确的同步和互斥。在实际应用中,你可能需要使用其他同步机制(如信号量或互斥锁)来保护共享内存的访问。
system V信号量 - 选学了解即可
信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。
在多进程或多线程的环境中,进程互斥(Process Mutual Exclusion)是指一种机制
,用于确保同时只有一个进程(或线程)可以访问共享资源,以避免数据竞争和不一致的结果。
当多个进程或线程同时访问共享资源时,可能会出现以下问题:
-
竞态条件(Race Condition):多个进程或线程以不可预测的方式相互干扰,导致结果的正确性无法保证。
-
数据不一致:多个进程或线程对共享资源进行读写操作时,可能会导致数据的不一致性,破坏程序的正确性。
为了解决这些问题,需要使用进程互斥机制来确保共享资源的互斥访问。常见的进程互斥机制包括使用互斥锁(Mutex)、信号量(Semaphore)和临界区(Critical Section)等。
互斥锁是一种最常见的进程互斥机制,它提供了两个基本操作:上锁(Lock)和解锁(Unlock)。当一个进程或线程需要访问共享资源时,它会尝试上锁,如果成功获取到锁,则可以访问共享资源;如果锁已经被其他进程或线程持有,则该进程或线程会被阻塞,直到锁被释放。
通过使用互斥锁,可以确保同一时间只有一个进程或线程可以访问共享资源,从而避免了竞态条件和数据不一致的问题。
需要注意的是,进程互斥只是一种机制,它并不能解决所有的并发问题。在设计并发程序时,还需要考虑其他因素,如死锁(Deadlock)和活锁(Livelock)等。
进程同步(Process Synchronization)是指在多个进程或线程之间协调和控制它们的执行顺序
,以确保它们按照一定的顺序和时间间隔执行。
在并发环境中,多个进程或线程可能会同时执行,并且彼此之间的执行速度和顺序是不确定的。这可能导致一些问题,如竞态条件、数据不一致和死锁等。
进程同步的目的是通过使用同步机制来协调和控制进程或线程的执行,以避免这些问题。常见的进程同步机制包括互斥锁、信号量、条件变量和屏障等。
以下是一些常见的进程同步场景:
-
互斥访问共享资源:多个进程或线程需要访问共享资源时,通过使用互斥锁来确保同一时间只有一个进程或线程可以访问共享资源,避免数据竞争和不一致的结果。
-
同步执行顺序:在某些情况下,需要确保多个进程或线程按照特定的顺序执行。例如,一个进程可能需要等待另一个进程完成某个任务后才能继续执行。这可以通过使用信号量或条件变量来实现。
-
生产者-消费者问题:在生产者-消费者问题中,多个生产者进程和消费者进程共享一个有限的缓冲区。生产者进程将数据放入缓冲区,而消费者进程从缓冲区中取出数据。需要确保生产者和消费者之间的操作同步,以避免缓冲区溢出或下溢。这可以通过使用信号量来实现。
进程同步是并发编程中的重要概念,它可以帮助解决并发执行时可能出现的问题,确保程序的正确性和一致性。
总之以上是进程间通信的基本概念,在以后的学习中会慢慢深入进程更加详细的知识。