Linux系统中进程间通信(Inter-Process Communication, IPC)

文章目录

  • 进程间通信介绍
    • 进程间通信目的
    • 进程间通信发展
  • 管道
    • 什么是管道
  • 匿名管道
    • 用fork来共享管道原理
    • 站在文件描述符角度-深度理解管道
    • 站在内核角度-管道本质
    • 管道读写规则
    • 管道特点
  • 命名管道
    • 创建一个命名管道
    • 匿名管道与命名管道的区别
    • 命名管道的打开规则
  • 命名管道的删除
  • 用命名管道实现文件拷贝
    • 用命名管道实现server&client通信
  • system V共享内存
    • 共享内存示意图
    • 共享内存数据结构
    • 共享内存函数
  • system V信号量 - 选学了解即可

在这里插入图片描述

进程间通信介绍

进程间通信目的

  1. 数据传输:一个进程需要将它的数据发送给另一个进程
  2. 资源共享:多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如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通信,触类旁通。

管道

什么是管道

  1. 管道是Unix中最古老的进程间通信的形式。
  2. 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

在这里插入图片描述

匿名管道

匿名管道(Anonymous Pipe)是一种在进程间进行单向通信的机制,其中一个进程作为写入端,另一个进程作为读取端。它是一种简单而有效的进程间通信方式,常用于父子进程或兄弟进程之间的通信。

在Linux系统中,可以使用pipe函数来创建匿名管道。pipe函数会创建一个管道,并返回两个文件描述符,一个用于读取数据,另一个用于写入数据。其中,文件描述符0表示标准输入(stdin),文件描述符1表示标准输(stdout)。

#include <unistd.h>
int pipe(int fd[2]);
//创建失败返回-1,创建成功返回两个文件描述符构成的数组

以下是使用匿名管道进行进程间通信的基本步骤:

  1. 调用pipe函数创建管道,获取读取端和写入端的文件描述符。
  2. 调用fork函数创建一个子进程。
  3. 在父进程中关闭读取端的文件描述符,保留写入端的文件描述符。
  4. 在子进程中关闭写入端的文件描述符,保留读取端的文件描述符。
  5. 父进程可以通过写入端的文件描述符向管道写入数据。
  6. 子进程可以通过读取端的文件描述符从管道读取数据。

下面是一个简单的示例代码,演示了如何使用匿名管道进行进程间通信:

#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一切皆文件思想”。

管道读写规则

当没有数据可读时

  1. O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  2. O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
    当管道满的时候
  3. O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  4. O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程
退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  2. 管道提供流式服务。
  3. 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
  4. 一般而言,内核会对管道操作进行同步与互斥。
  5. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。如下图:
    在这里插入图片描述

命名管道

  1. 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  2. 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  3. 命名管道是一种特殊类型的文件。

创建一个命名管道

  1. 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename

其中,filename 是要创建的命名管道的路径和名称。
例如,要在命令行上创建一个名为 “myfifo” 的命名管道,可以执行以下命令:

mkfifo myfifo

执行该命令后,系统将在当前目录下创建一个名为 “myfifo” 的命名管道文件。

需要注意的是,命名管道是一种特殊的文件可以像普通文件一样进行读写操作。在命令行中创建的命名管道可以被其他进程打开并进行读写操作

  1. 命名管道也可以从程序里创建,相关函数有:
    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,表示读写所有者、组和其他用户都有权限。

需要注意的是,命名管道是一种特殊的文件,可以像普通文件一样进行读写操作。在进程间通信时,一个进程可以将数据写入命名管道,而另一个进程可以从该管道读取数据。但是,需要注意管道的读取和写入操作应该在不同的进程中进行,否则可能会造成阻塞。

匿名管道与命名管道的区别

  1. 匿名管道由pipe函数创建并打开。
  2. 命名管道由mkfifo函数创建,打开用open
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道的打开规则

对于命名管道(FIFO)的打开规则,取决于打开操作是为读还是为写。

  1. 当打开操作为读时:

    • 如果打开操作没有设置 O_NONBLOCK 标志(即阻塞模式),则打开操作会阻塞,直到有其他进程以写模式打开该命名管道。
    • 如果打开操作设置了 O_NONBLOCK 标志(即非阻塞模式),则打开操作会立即返回成功,不会阻塞等待。
  2. 当打开操作为写时:

    • 如果打开操作没有设置 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;}elsebreak;}//关闭管道 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_unused2shm_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有讲究的:

  1. key是一个数字,这个数字是几,并不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
  2. 第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了!
  3. 对于一个已经创建好的共享内存,key在哪? key在共享内存的描述对象中!
  4. 第一次创建的时候,必须有一个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 函数将 pathnameproj_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

说明:

  1. shmaddr为NULL,核心自动选择一个地址
  2. shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  3. shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)
  4. 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 共享内存的使用涉及以下几个步骤:

  1. 创建共享内存段:使用 shmget 函数创建一个共享内存段。该函数接受三个参数:键值(用于标识共享内存段),大小(指定共享内存的大小),和标志(用于指定权限和创建方式)。成功创建共享内存段后,shmget 函数返回一个唯一的标识符(共享内存标识符)。

  2. 连接共享内存:使用 shmat 函数将共享内存段连接到进程的地址空间。shmat 函数接受两个参数:共享内存标识符和地址(如果地址为 NULL,则系统会自动选择一个可用的地址)。成功连接共享内存后,shmat 函数返回共享内存的起始地址。

  3. 使用共享内存:一旦共享内存连接到进程的地址空间,进程可以像使用普通内存一样使用它。可以通过指针访问共享内存中的数据。

  4. 分离共享内存:使用 shmdt 函数将共享内存从进程的地址空间中分离。shmdt 函数接受一个参数,即共享内存的起始地址。成功分离共享内存后,进程将无法再访问共享内存中的数据。

  5. 删除共享内存段:使用 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)是指一种机制,用于确保同时只有一个进程(或线程)可以访问共享资源,以避免数据竞争和不一致的结果。

当多个进程或线程同时访问共享资源时,可能会出现以下问题:

  1. 竞态条件(Race Condition):多个进程或线程以不可预测的方式相互干扰,导致结果的正确性无法保证。

  2. 数据不一致:多个进程或线程对共享资源进行读写操作时,可能会导致数据的不一致性,破坏程序的正确性。

为了解决这些问题,需要使用进程互斥机制来确保共享资源的互斥访问。常见的进程互斥机制包括使用互斥锁(Mutex)、信号量(Semaphore)和临界区(Critical Section)等。

互斥锁是一种最常见的进程互斥机制,它提供了两个基本操作:上锁(Lock)和解锁(Unlock)。当一个进程或线程需要访问共享资源时,它会尝试上锁,如果成功获取到锁,则可以访问共享资源;如果锁已经被其他进程或线程持有,则该进程或线程会被阻塞,直到锁被释放。

通过使用互斥锁,可以确保同一时间只有一个进程或线程可以访问共享资源,从而避免了竞态条件和数据不一致的问题。

需要注意的是,进程互斥只是一种机制,它并不能解决所有的并发问题。在设计并发程序时,还需要考虑其他因素,如死锁(Deadlock)和活锁(Livelock)等。

进程同步(Process Synchronization)是指在多个进程或线程之间协调和控制它们的执行顺序,以确保它们按照一定的顺序和时间间隔执行。

在并发环境中,多个进程或线程可能会同时执行,并且彼此之间的执行速度和顺序是不确定的。这可能导致一些问题,如竞态条件、数据不一致和死锁等。

进程同步的目的是通过使用同步机制来协调和控制进程或线程的执行,以避免这些问题。常见的进程同步机制包括互斥锁、信号量、条件变量和屏障等。

以下是一些常见的进程同步场景:

  1. 互斥访问共享资源:多个进程或线程需要访问共享资源时,通过使用互斥锁来确保同一时间只有一个进程或线程可以访问共享资源,避免数据竞争和不一致的结果。

  2. 同步执行顺序:在某些情况下,需要确保多个进程或线程按照特定的顺序执行。例如,一个进程可能需要等待另一个进程完成某个任务后才能继续执行。这可以通过使用信号量或条件变量来实现。

  3. 生产者-消费者问题:在生产者-消费者问题中,多个生产者进程和消费者进程共享一个有限的缓冲区。生产者进程将数据放入缓冲区,而消费者进程从缓冲区中取出数据。需要确保生产者和消费者之间的操作同步,以避免缓冲区溢出或下溢。这可以通过使用信号量来实现。

进程同步是并发编程中的重要概念,它可以帮助解决并发执行时可能出现的问题,确保程序的正确性和一致性。

总之以上是进程间通信的基本概念,在以后的学习中会慢慢深入进程更加详细的知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/196123.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Shopify二次开发之三:liquid语法学习(访问Objects和Schema数据模型)

目录 Objects &#xff08;对象&#xff09; 全局对象 all_products&#xff1a;商店中所有的商品 articles: 商店中的所有文章 collections&#xff1a;商店中所有的集合 模板对象 在product.json&#xff08;配置的section中) 访问product对象 在collection.json中可…

1D和2D布朗运动matlab

布朗运动是一种随机现象&#xff0c;下面的M函数brwnm2.m给出了二维Brown运动&#xff0c;其中[t0,tf]是时间区间&#xff0c;h是采样步长&#xff0c;w(t)&#xff0c;z(t)是布朗运动。function [t,w,z]brwnm2(t0,tf,h) tt0:h:tf; xrandn(size(t))*sqrt(h); yrandn(size(t))*s…

二叉树题目:二叉树的完全性检验

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树的完全性检验 出处&#xff1a;958. 二叉树的完全性检验 难度 5 级 题目描述 要求 给定一个二叉树的根结点 root \texttt{root} root&…

C#中GDI+图形图像技术(Graphics类、Pen类、Brush类)

目录 一、创建Graphics对象 1.创建Pen对象 2.创建Brush对象 &#xff08;1&#xff09;SolidBrush类 &#xff08;2&#xff09;HatchBrush类 ​​​​​​​&#xff08;3&#xff09;LinerGradientBrush类 用户界面上的窗体和控件非常有用&#xff0c;且引人注目&#…

销售人员一定要知道的6种获取电话号码的方法

对于销售来说&#xff0c;电话销售是必须要知道的销售方法&#xff0c;也是销售生涯中的必经之路。最开始我们并不清楚这么电话是从哪里来的&#xff0c;也不清楚是通过哪些方法渠道获取。那么今天就来分享给各位销售人员获取客户电话号码的方法。 1.打印自己的名片&#xff0…

一文读懂 ChatGPT 工作原理

生成式 AI 是如何工作的 生成式 AI 是目前最热门的技术之一&#xff0c;ChatGPT 等应用的出现使人们对于如何在各种领域中应用生成式 AI 有了许多新的思考。 但这些领域的从业者并不都具备 AI 相关的知识背景。所以我们制作了这个教程&#xff0c;向无 AI 知识背景的读者介绍…

Temu数据面板:Temu商家必备的数据分析工具

在Temu这个电商平台上&#xff0c;越来越多的商家意识到数据分析的重要性。数据分析可以帮助商家更好地了解店铺的运营情况&#xff0c;从而制定更有效的运营策略&#xff0c;提高销售业绩。而在这个过程中&#xff0c;Temu数据面板成为了一个不可或缺的工具。 先给大家推荐一款…

【ElementUI】一行代码解决图片预览

【ElementUI】一行代码解决图片预览 只需要在图片标签上加入:preview-src-list 只需要在图片标签上加入:preview-src-list 完整代码如下&#xff1a; <el-table-column label"封面" align"center" prop"cover" :sort-orders"[descend…

IDEA 保存自动ESLint格式化

作为后端人员&#xff0c;偶尔修改一下前端代码&#xff0c;ESLint总提示格式不正确。有没有什么办法实现自动格式化呢&#xff1f; 安装插件Save Actions Tool 设置中搜索eslint 勾选 Run eslint --fix on save 这样以后&#xff0c;只要保存文件就会自动格式化了。 参考 …

协同过滤算法:个性化推荐的艺术与科学

目录 引言&#xff1a; 一、协同过滤算法的基本原理 二、协同过滤算法的应用领域 三、协同过滤算法的优缺点 四、协同过滤算法的未来发展方向 五、结论 引言&#xff1a; 在当今数字化时代&#xff0c;信息过载成为了一个普遍的问题。为了帮助人们更好地发现符合个性化需…

HTTP 和 HTTPS的区别

一、HTTP 1.明文传输&#xff0c;不安全 2.默认端口号&#xff1a;80 3.TCP三次握手即可 二、HTTPS 1.加密传输&#xff0c;更安全(在HTTP层与TCP层之间加上了SSL/TTL安全协议) SSL和TTL是在不同时期的两种叫法&#xff0c;含义相同。 2.默认端口号&#xff1a;443 3.TCP三…

【广州华锐互动】风电场检修VR情景模拟提供接近真实的实操体验

风电场检修VR情景模拟系统由广州华锐互动开发&#xff0c;这是一种新兴的培训方式&#xff0c;它通过虚拟现实技术将风力发电场全范围进行1:1仿真建模还原&#xff0c;模拟监视风力发电场各种运行工况下的运行参数和指标&#xff0c;同时可进行升压站系统的巡视&#xff0c;倒闸…

Windows系列:Zabbix agent一键部署-windows版本(windows 安装zabbix客户端安装、bat文件修改文件内容)

Zabbix agent一键部署-windows版本&#xff08;windows 安装zabbix客户端安装、bat文件修改文件内容&#xff09; 一. Zabbix agent一键部署-windows版本二. windows 安装zabbix客户端安装1.下载安装zabbix agent2.配置zabbix agent2.1 修改配置文件2.2 将zabbix agent安装为wi…

同旺科技 USB TO SPI / I2C --- 调试W5500_TCP Client接收数据

所需设备&#xff1a; 内附链接 1、USB转SPI_I2C适配器(专业版); 首先&#xff0c;连接W5500模块与同旺科技USB TO SPI / I2C适配器&#xff0c;如下图&#xff1a; 发送数据6个字节的数据&#xff1a;0x11,0x22,0x33,0x44,0x55,0x66 在专业版调试软件中编辑指令&#xff0c…

【PyTorch】多层感知机

文章目录 1. 模型和代码实现1.1. 模型1.1.1. 背景1.1.2. 多层感知机1.1.3. 激活函数 1.2. 代码实现1.2.1. 完整代码1.2.2. 输出结果 2. Q&A 1. 模型和代码实现 1.1. 模型 1.1.1. 背景 许多问题要使用线性模型&#xff0c;但无法简单地通过预处理来实现。此时我们可以通过…

智能联动第三方告警中心,完美实现故障响应全闭环

前言 我们曾讨论完善的告警策略是整个数据监控系统的重要组成部分&#xff08;参见《机智的告警策略&#xff0c;完善监控系统的重要一环》&#xff09;&#xff0c;介绍了如何配置告警通知以及场景示例&#xff0c;帮助用户及时更多潜在的故障和问题&#xff0c;有效地保障系…

Redis 之 ZSET 实战应用场景,持续更新!

前言 大白话介绍 Redis 五大基本数据类型之一的 ZSET 开发中常见的应用场景 ZSET 介绍 ZSET 与 SET 相同点&#xff1a;都是是 String类型元素的集合&#xff0c;且不允许重复的成员ZSET 与 SET 不同点&#xff1a;ZSET 每个元素都会关联一个 Double 类型的分数&#xff0c;Re…

Latex去掉参考文献后面的参考文献所在页(去掉参考文献的反向超链接)

如下&#xff1a; 在使用latex插入参考文献的时候&#xff0c;最后面总是会出现这种代号。这是表明的是这条参考文献所在的页码&#xff0c;并且点击之后可以跳转到该页。正式来讲&#xff0c;这个叫超链接的BACKREF。若要去掉&#xff0c;只需要在引用hyperref的时候去掉page…

技术or管理?浅谈软件测试人员的未来职业发展,值得借鉴

我们在工作了一段时间之后&#xff0c;势必会感觉到自己已经积累了一些工作经验了&#xff0c;会开始考虑下一阶段的职业生涯会如何发展。测试人员在职业生涯中的不确定因素还是不少的&#xff0c;由于其入门门槛不高&#xff0c;不用学习太多技术性知识即可入行&#xff0c;所…

Net8 EFCore Mysql 连接

一、安装插件 Pomelo.EntityFrameworkCore.MySq (这里要选8.0.0以上版本低版本不支持.net8) 二、配置数据库连接串 appsettings.json 中配置数据库连接串 "ConnectionStrings": {"Connection": "server172.18.2.183;port3306;databasestudents;uid…