Linux IPC解析:匿名命名管道与共享内存

news2024/11/26 7:28:39

在这里插入图片描述

目录

  • 一.IPC机制介绍
  • 二.匿名与命名管道
      • 1.匿名管道
      • 2.命名管道
      • 3.日志
  • 三.共享内存
  • 三.System V 标准
      • 1.System V简介
      • 2.IPC在内核的数据结构设计
      • 3.信号量

一.IPC机制介绍

IPC(Inter-Process Communication,进程间通信)是计算机系统中不同进程之间交换数据和同步操作的机制。由于现代计算机系统中,程序通常会由多个进程组成,这些进程可能需要相互通信以完成任务,因此IPC非常重要。

IPC主要功能:

  1. 数据交换:允许不同进程共享数据或传递信息。
  2. 同步:协调多个进程之间的操作,以避免竞态条件和资源冲突。
  3. 互斥:控制多个进程对共享资源的访问,确保同一时间只有一个进程能够访问资源

IPC主要机制:

  1. 管道(Pipes):匿名管道:用于相关进程之间的单向通信,如父子进程或兄弟进程。
    命名管道(FIFO):用于不相关进程之间的双向或单向通信,具有一个路径名,可以在不同的进程之间共享。
  2. 消息队列(Message Queues):允许进程以消息的形式发送和接收数据,消息可以按照队列的顺序进行传递。
  3. 共享内存(Shared Memory):允许多个进程映射同一块物理内存区域,从而实现高速的数据共享和通信。
  4. 信号量(Semaphores):用于实现进程间的同步和互斥,以控制对共享资源的访问。
  5. 套接字(Sockets):尽管最常用于网络通信,套接字也可以用于同一台计算机上的进程间通信。

二.匿名与命名管道

1.匿名管道

管道是进程间通信的一种机制,通常用于将一个进程的输出数据传递给另一个进程的输入。管道可以分为两种主要类型:匿名管道和命名管道。
管道原理简易演示图:
在这里插入图片描述
上图所示,我们就将通信信道建立好了。那么我们具体应该如何编码实现呢?
在这里插入图片描述
pipe函数介绍:

pipe()函数创建一个管道,该管道提供了一对文件描述符:一个用于读操作,另一个用于写操作。数据从写端流向读端。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并将errno设置为错误代码。

下面我们写一段代码用匿名管道简单实现父子进程的通信:

#include <iostream>     // 包含输入输出流库
#include <cstdio>       // 包含标准输入输出库
#include <string>       // 包含C++字符串库
#include <cstring>      // 包含C字符串处理库
#include <cstdlib>      // 包含C标准库
#include <unistd.h>     // 包含POSIX操作系统API
#include <sys/types.h>  // 包含数据类型定义
#include <sys/wait.h>   // 包含进程等待函数

#define N 2             // 定义常量N为2
#define NUM 1024        // 定义常量NUM为1024,表示缓冲区大小

using namespace std;    // 使用标准命名空间

// Writer函数,向管道写数据
void Writer(int wfd)
{
    string s = "i am father";    // 定义字符串s
    pid_t self = getpid();       // 获取当前进程ID
    int number = 0;              // 定义一个计数器number,初始值为0

    char buffer[NUM];            // 定义缓冲区buffer
    while (true)                 // 无限循环
    {
        sleep(1);                // 休眠1秒

        buffer[0] = 0;           // 将缓冲区第一个位置置为0
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++); // 格式化字符串并存入缓冲区
        cout << buffer << endl;  // 输出缓冲区内容

        write(wfd, buffer, strlen(buffer)); // 将缓冲区内容写入管道

        if(number>=5)            // 如果计数器number大于等于5,退出循环
            break;
    }
}

// Reader函数,从管道读数据
void Reader(int rfd)
{
    char buffer[NUM];            // 定义缓冲区buffer

    while(true)                  // 无限循环
    {
        buffer[0] = 0;           // 将缓冲区第一个位置置为0
        ssize_t n = read(rfd, buffer, sizeof(buffer)); // 从管道读取数据存入缓冲区
        if(n > 0)                // 如果读取到的数据长度大于0
        {
            buffer[n] = 0;       // 将缓冲区第n个位置置为0,表示字符串结束
            cout << "child get a message[" << getpid() << "]# " << buffer << endl; // 输出读取到的内容
        }
        else if(n == 0)          // 如果读取到的数据长度为0,表示管道已关闭
        {
            printf("child read file done!\n"); // 输出读取完成信息
            break;               // 退出循环
        }
        else break;              // 如果读取错误,退出循环
    }
}

int main()
{
    int pipefd[N] = {0};         // 定义一个数组pipefd,用于存放管道的文件描述符
    int n = pipe(pipefd);        // 创建管道,返回值n小于0表示创建失败
    if (n < 0)
        return 1;                // 如果创建管道失败,返回1

    pid_t id = fork();           // 创建子进程,返回值id小于0表示创建失败
    if (id < 0)
        return 2;                // 如果创建子进程失败,返回2

    if (id == 0)
    {
        // 子进程代码
        close(pipefd[1]);        // 关闭管道的写端

        // IPC代码
        Reader(pipefd[0]);       // 调用Reader函数,从管道读取数据

        close(pipefd[0]);        // 关闭管道的读端
        exit(0);                 // 退出子进程
    }
    // 父进程代码
    close(pipefd[0]);            // 关闭管道的读端
    // IPC代码
    Writer(pipefd[1]);           // 调用Writer函数,向管道写入数据
    close(pipefd[1]);            // 关闭管道的写端

    pid_t rid = waitpid(id, nullptr, 0); // 等待子进程结束
    if(rid < 0) return 3;        // 如果等待失败,返回3

    sleep(5);                    // 休眠5秒
    return 0;                    // 返回0,表示程序成功结束
}

在这里插入图片描述
运行起来后我们可以观察到,父进程每隔一秒格式化字符串输入缓冲区,并打印缓冲区内容,接着写进管道,这时子进程就能读到管道的数据,并将内容打印出来,父进程写五次后退出,并且关闭了写端,这是读端读到结尾后退出进程后被父进程等待回收
我们再来验证些特殊情况:

void Reader(int rfd)
{
    char buffer[NUM];            // 定义缓冲区buffer

    int _count =0;

    while(true)                  // 无限循环
    {
        buffer[0] = 0;           // 将缓冲区第一个位置置为0
        ssize_t n = read(rfd, buffer, sizeof(buffer)); // 从管道读取数据存入缓冲区
        if(n > 0)                // 如果读取到的数据长度大于0
        {
            buffer[n] = 0;       // 将缓冲区第n个位置置为0,表示字符串结束
            cout << "child get a message[" << getpid() << "]# " << buffer << endl; // 输出读取到的内容
        }
        else if(n == 0)          // 如果读取到的数据长度为0,表示管道已关闭
        {
            printf("child read file done!\n"); // 输出读取完成信息
            break;               // 退出循环
        }
        else break;              // 如果读取错误,退出循环

        _count++;
        if(_count>=2)
            break;
    }
}

让子进程只读两次后就退出,那这时候写端还是正常写的,那我们运行会出现什么情况呢?
在这里插入图片描述
可以看到读端关闭后,操作系统会杀掉正在运行的写端(通过信号杀掉)。
还有其他的情况:

1.读写端正常,管道被写满,写端就会阻塞等待。
2.读写端正常,管道为口,读端就会阻塞等待。

由上述情况我们可以总结:

  • 进程间通信的原理是让不同的进程看到同一份资源
  • 匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
  • 匿名管道只能单向通信
  • 父子进程会进程协同,同步与互斥,为了保护数据安全
  • 管道基于文件,文件的生命周期随进程
  • 管道面向字节流

2.命名管道

命名管道(FIFO)是一种特殊的文件类型,用于在两个不相关的进程之间进行单向或双向通信。与匿名管道不同,命名管道存在于文件系统中,用路径和文件名标识唯一,可以被不相关的进程通过路径名来打开和使用。命名管道具有持久性,可以在创建之后长期存在,直到显式删除。
在这里插入图片描述
创建命名管道的函数mkfifo:

  • pathname:命名管道的路径名,即文件系统中创建的命名管道的路径
  • mode:设置管道的权限,与 open 或 chmod 的权限位相同,如 0666 表示读写权限
  • 成功时返回 0
  • 失败时返回 -1,并设置 errno 来指示错误类型

下面我们就利用命名管道实现两个无关系进程间的通信:
comm.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FIFO_FILE "./myfifo"


class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FIFO_FILE, 0664);
        if (n == -1)
        {
            perror("mkfifo");
            exit(1);
        }
    }
    ~Init()
    {

        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(2);
        }
    }
};

管理管道文件server.cc:

#include "comm.hpp"


using namespace std;

// 管理管道文件
int main()
{
    Init init;
  

    // 打开管道
    int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
    if (fd < 0)
    {
        exit(3);
    }

    // 开始通信
    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
        	break;     
    }

    close(fd);
    return 0;
}

通信进程client.cc:

#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file done" << endl;

    string line;
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}

当进程跑起来后,就能在自己指定的路径下看到p开头的命名管道文件:
在这里插入图片描述
两个进程都跑起来后,在client输入,就能在server上看到读取到信息了:
在这里插入图片描述

3.日志

在Linux下开发应用程序时,加入日志记录是一种非常好的编程习惯。日志记录不仅有助于调试和维护,还能提供运行时的监控和错误追踪。其中日志主要包括日志等级,内容,文件的名称等。我们可以在上述命名管道的文件下加上一个日志的小模组:

#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); // 打开日志文件
        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); // 构建不同级别的文件名
        printOneFile(filename, logtxt); // 输出到不同级别的文件
    }

    ~Log()
    {
    }

    // 重载()操作符,方便日志记录
    void operator()(int level, const char *format

接着就可以打印出日志信息了:
在这里插入图片描述

三.共享内存

在 Linux 系统中,共享内存是实现进程间通信最高效机制。通过共享内存,不同进程可以直接访问同一块内存区域,从而实现数据的快速交换。介绍共享内存的基本概念、优点、以及在 C++ 程序中的实现方法。
在这里插入图片描述

共享内存的原理就是在物理内存中申请一块空间,接着进程将其挂接到自己的进程地址空间内,然后多个进程挂接到自己的进程地址空间内,进程间就能进行通信了
我们先来了解
创建共享内存的函数:
在这里插入图片描述

  1. key是一个数字,可以在内核中唯一标识,能够让不同进程进行唯一性标识
  2. size代表创建共享内存的字节
  3. shmflg标识创建的权限:常用的有两种IPC_CREAT(单独)如果申请的共享内存不存在就创建,存在就获取并返回。 IPC_CREAT|IPC_EXCL,如果申请的内存不存在就创建,存在就出错返回(能确保我们申请一块新的共享内存

在这里插入图片描述
函数ftok用来生成一个唯一的key值,接着用来创建共享内存;

  • pathname: 一个指向现有文件的路径名。这个文件必须存在,并且调用进程必须对它有读取权限。这个参数用于生成键值的一部分.
  • proj_id: 一个项目标识符,通常是一个字符类型的整数(例如,‘A’ 或 65),用于生成键值的另一部分。proj_id 是一个 8 位的值,所以它的有效范围是 0 到 255

ipcs -m 查看共享内存,ipcrm -m shmid 删除共享内存
共享内存的生命周期是随内核的,用户不主动关闭,他就会一直存在

在这里插入图片描述
shmat可将共享内存挂接到进程的地址空间内。
在这里插入图片描述
shmat去掉挂接。
在这里插入图片描述
参数说明:

  • shmid: 共享内存段的标识符,这是由 shmget 函数返回的共享内存段 ID
  • cmd: 控制命令,指定对共享内存段执行的操作。
  • buf: 指向 shmid_ds 结构的指针,用于存储或设置共享内存段的属性。如果不需要,可以传递 nullptr

控制命令说明:

  • IPC_STAT: 获取共享内存段的当前状态信息,并将其存储在 buf 指向的 shmid_ds 结构中。
  • IPC_SET: 设置共享内存段的属性,使用 buf 指向的 shmid_ds 结构中的值。
  • IPC_RMID: 标记共享内存段以便将其删除。共享内存段在没有进程附加时会被实际删除
  • IPC_INFO: 获取系统范围内的共享内存信息(仅 Linux 提供)。
  • SHM_INFO: 获取系统范围内的共享内存信息(仅 Linux 提供)。
  • SHM_STAT: 获取共享内存段的状态信息(仅 Linux 提供)。

以下是一个共享内存代码的演示(添加日志模组):
comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "log.hpp"

using namespace std;

Log log;

const int size = 4096; 
const string pathname="./";
const int proj_id = 1;


key_t GetKey()
{
    key_t k = ftok(pathname.c_str(), proj_id);
    if(k < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag)
{
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);

    return shmid;
}

int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT); 
}

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {

        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

#endif

processa/processb.cc:

#include "comm.hpp" // 包含自定义的头文件,通常包含共享内存和日志相关的声明

extern Log log; // 声明一个外部的日志对象

int main()
{
    Init init; // 初始化对象,用于初始化环境(具体实现细节在comm.hpp中)

    int shmid = CreateShm(); // 创建共享内存,并返回共享内存段标识符
    char *shmaddr = (char*)shmat(shmid, nullptr, 0); // 将共享内存段附加到进程的地址空间,并返回指向共享内存的指针

    // IPC(进程间通信)代码在这里
    // 一旦有人把数据写入到共享内存中,我们能立刻看到数据,不需要经过系统调用,直接访问共享内存

    int fd = open(FIFO_FILE, O_RDONLY); // 以只读方式打开命名管道
    // 等待写入方打开管道后,当前进程才会继续执行,open函数会阻塞直到管道另一端被打开
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno); // 记录打开管道失败的日志
        exit(FIFO_OPEN_ERR); // 退出程序,返回管道打开错误码
    }

    struct shmid_ds shmds; // 定义共享内存数据结构,用于存储共享内存段的信息
    while(true)
    {
        char c; // 用于读取管道中的数据
        ssize_t s = read(fd, &c, 1); // 从管道中读取一个字节的数据
        if(s == 0) break; // 读取到文件末尾,退出循环
        else if(s < 0) break; // 读取出错,退出循环

        cout << "client say@ " << shmaddr << endl; // 直接从共享内存中读取数据并输出
        sleep(1); // 睡眠1秒

        shmctl(shmid, IPC_STAT, &shmds); // 获取共享内存段的状态,并存储在shmds结构中
        cout << "shm size: " << shmds.shm_segsz << endl; // 输出共享内存段的大小
        cout << "shm nattch: " << shmds.shm_nattch << endl; // 输出当前附加到共享内存段的进程数
        printf("shm key: 0x%x\n",  shmds.shm_perm.__key); // 输出共享内存段的键值
        cout << "shm mode: " << shmds.shm_perm.mode << endl; // 输出共享内存段的访问模式
    }

    shmdt(shmaddr); // 将共享内存段从当前进程分离
    shmctl(shmid, IPC_RMID, nullptr); // 标记共享内存段为删除

    close(fd); // 关闭管道文件描述符
    return 0; // 正常退出程序
}

#include "comm.hpp"

int main()
{
    int shmid = GetShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    int fd = open(FIFO_FILE, O_WRONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }
    // 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
    // 不需要调用系统调用
    // ipc code
    while(true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);

        write(fd, "c", 1); // 通知对方
    }

    shmdt(shmaddr);

    close(fd);
    return 0;
}

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 = "./";
    }
    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;
};

补充知识:

  1. 共享内存没有同步机制,导致多个进程对共享内存进行读写操作,可能导致数据的不一致性。例如,进程 A 写入数据 “Hello”,进程 B 同时写入数据 “World”,最终共享内存中的内容可能是 “Horld” 或 “Wello”。竞态条件是指多个进程同时访问共享资源,而最终结果依赖于访问的顺序和时机。共享内存没有同步机制时,进程的执行顺序和速度无法预测,可能导致竞态条件。例如,两个进程分别增加和减少共享内存中的计数器,如果没有同步机制,计数器的最终值可能不正确
  2. 共享内存的大小建议时4kb(常见块大小是4kb)的整数倍,如若不是实际底层会向上取整整数倍,但是实际你申请多少给多少
  3. 共享内存和其他的通信机制相比拷贝比较少,所以他是最快的进程间通信的方式

三.System V 标准

1.System V简介

System V 标准是 Unix 系统中的一个重要规范,提供了一系列用于进程间通信(IPC)和同步的机制。这些机制包括消息队列信号量共享内存文件锁

2.IPC在内核的数据结构设计

首先IPC在内核的数据结构设计和多态相似,在操作系统中,所有的IPC资源都是整合进操作系统的IPC模块中的!
每个通信机制结构体都有struct ipc_pem
在这里插入图片描述
类似与上图的结构体,每个通信机制的struct ipc_prem存在OS中的数组struct ipc_perm *array[];中,此数组下标就是XXXid,eg:shmid、msqid,而且struct ipc_prem和通信机制结构体的关系是基类与子类的关系。

3.信号量

信号量是操作系统中一种重要的同步工具,用于控制多个进程或线程对共享资源的访问。在 Linux 系统中,信号量不仅用于进程间的同步和互斥,还能有效地协调并发操作。我们了解下其原理即可:信号量的本质是进程间通信的一种,是一个计数器,用来描述临界资源的数量多少
如果A、B进程能同时看到一份资源,如果不加保护,会导致数据不一致的问题。我们将共享的资源并且任何时刻只允许一个执行流访问的资源叫做临界资源:我们把访问临界资源的部分代码叫做临界区。
所以我们要访问临界资源,就先要申请信号量计数器资源(计数器信号量是共享资源),PV操作即对信号量的申请释放操作时原子的。执行流申请资源,必须先申请信号资源,得到信号量之后,才能访问临界资源!!信号量值1,0两态的,二元信号量,就是互斥功能申请信号量的本质:是对临界资源的预订机制!!!

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

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

相关文章

还没用过OBS Studio?快来提升你的技术分享效率!

前言 在浩瀚的数字海洋中&#xff0c;有这么一款神器&#xff0c;它低调却光芒四射&#xff0c;默默改变着无数内容创作者的命运&#xff1b;嘿&#xff0c;你猜怎么着&#xff1f;它既不是天价的专业设备&#xff0c;也不是遥不可及的神秘黑科技&#xff0c;而是开源世界的瑰宝…

低功耗工业控制器用于风电场绿色可持续能源行业

全球对清洁能源的需求不断增长&#xff0c;风电场作为一种可再生能源的重要来源&#xff0c;正经历着快速发展。然而&#xff0c;传统的风电场管理和运营方式存在着效率低下、维护成本高等问题。为了提高风电场的运行效率和可靠性&#xff0c;实现绿色能源的可持续发展&#xf…

c语言-链表1

10 链表 一、链表是什么&#xff1f; -- 数据的一种存储方式 -- 链式存储 &#xff08;1&#xff09;线性存储 -- 地址连续 -- 自动开辟&#xff0c;自动释放 -- 默认是线性存储 &#xff08;2&#xff09;链式存储 -- 地址不连续…

【Git】git 从入门到实战系列(二)—— Git 介绍以及安装方法

文章目录 一、前言二、git 是什么三、版本控制系统是什么四、本地 vs 集中式 vs 分布式本地版本控制系统集中式版本控制系统分布式版本控制系统 五、安装 git 一、前言 本系列上一篇文章【Git】git 从入门到实战系列&#xff08;一&#xff09;—— Git 的诞生&#xff0c;Lin…

S硅谷-AI大模型实战训练

课程概述 AI大模型实战训练课程是一门专为有志于深入学习人工智能领域的学员设计的高级课程。本课程以当前人工智能领域的前沿技术——大模型为核心&#xff0c;通过理论与实践相结合的教学方式&#xff0c;培养学员在AI领域的实战能力。 课程目标 理解大模型的基本原理和架构。…

python爬虫之用scrapy下载中间件爬取网易新闻

python爬虫之用scrapy下载中间件爬取网易新闻 相关资源如下&#xff1a; 采用scrapy下载中间件爬取网易新闻国内、国际、数读、军事、航空五大板块新闻标题和内容 程序wangyi.py主代码&#xff1a; import scrapy from selenium import webdriver from selenium.webdriver.e…

PDF——分割pdf的10个工具

PDF分割器是一种可用于将PDF文档分割成更小的文档甚至单个页面的工具。分割 PDF 文档的主要原因是为了更容易共享。 但该过程的成功取决于您用于拆分 PDF 的工具。较简单的工具仅提供几个选项&#xff0c;可能并不适合所有类型的文档。我们将在本文中列出的 10 个最佳 PDF 分割…

gemini api 应用

安装 gemini Prerequisites To complete this quickstart locally, ensure that your development environment meets the following requirements: Python 3.9 An installation of jupyter to run the notebook. Install the Gemini API SDK The Python SDK for the Gemin…

手机在网时长查询接口如何对接?(一)

一、什么是手机在网时长查询接口&#xff1f; 传入手机号码&#xff0c;查询该手机号的在网时长&#xff0c;返回时间区间&#xff0c;支持携号转网号码查询。 二、手机在网时长查询接口适用于哪些场景&#xff1f; 例如&#xff1a;客户画像与精准营销 &#xff08;1&…

2个一键生成PPT目录的AI软件,轻松搞定PPT目录制作!

在各种信息都在努力争夺每个人的注意力的当下&#xff0c;一份精心制作的PPT可能成为决定成败的关键因素。而PPT目录&#xff0c;作为整个PPT演示文稿的门面和导航&#xff0c;其重要性往往被低估。 每个人的时间都是有限的&#xff0c;如果PPT目录没有让潜在观众Get到重点&am…

Vue常见问题(一)组件的使用

Failed to resolve component. 报错原因&#xff1a; 组件注册错误&#xff1a;我们在组件中使用了未注册的组件。在Vue中&#xff0c;组件必须先注册才能使用。 解决方法&#xff1a; 引用组件 &#xff1a; import ItemPage from "/components/itemPage.vue";…

Python NumPy数组维度和坐标轴的含义(ndarray demension axis)

Numpy是Python中科学计算的基础包&#xff0c;而其核心对象就是ndarray&#xff08;n维数组&#xff09;。关于如何创建n维度数组可以参考下面的文章&#xff1a; Python NumPy数组的创建方法 为了对ndarray进行数学处理&#xff0c;很多函数中都要指定一个叫做坐标轴的参数(a…

创建Apple开发者ID及ABM账号教程

申请苹果开发者账号和ABM &#xff08;Apple Business Manager&#xff09; 账号&#xff0c;以便在iPhone&#xff08;IOS平台&#xff09;上发布和管理汽应用。申请需要遵循一系列步骤。以下是详细的申请流程&#xff1a; 一、苹果开发者账号申请流程 1. 注册Apple ID - 访问…

打卡第34天------动态规划

我目前刷leetcode上的题的时候,不仅每天按照代码随想录的算法训练营的进度来刷题,也会自己开始在leetcode上刷题了,有些简单的题目,不用看题解就能做出来了,这也是一种进步呀。希望可以尽快找到下家工作单位,分秒必争,不浪费自己的一分一毫时间,与时间赛跑的过程呀。 一…

文心一言 VS 讯飞星火 VS chatgpt (319)-- 算法导论22.3 11题

十一、请解释有向图的一个结点u怎样才能成为深度优先树中的唯一结点&#xff0c;即使结点u同时有入边和出边。如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 在一个有向图中&#xff0c;如果结点u在深度优先搜索&#xff08;DFS&#xff09;的过程中成为深度优…

【面试高频,必知必会】OpenGL渲染流程

&#x1f60e; 作者介绍&#xff1a;欢迎来到我的主页&#x1f448;&#xff0c;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff08;领取大厂面经等资料&#xff09;&#xff0c;欢迎加我的…

建议收藏:如何快速搭建一套仓库管理系统?

在工作中碰到仓库出错或因统计繁琐而加班到天亮都是常有的事情&#xff0c;工作效率真的很低。我相信这种的困境&#xff0c;不少同行朋友定能深有体会&#xff01; 实话说&#xff0c;我们公司之前也曾尝试斥巨资引入传统仓库管理系统&#xff0c;但效果却不尽人意。不仅操作…

面试中的算法 [ 持续更新中 ] 基于Python语言 如何判断链表有环

本文主要介绍了如何判断链表有环的问题&#xff0c;并进行了延伸&#xff1a; 如果链表有环如何求出环的长度&#xff0c;入环节点... ...嗯&#xff0c;点个赞总可以不&#xff01;&#xff01;&#xff01; 目录 5.1如何判断链表有环 5.1.1 有一个单向链表&#xff0c;链表…

动态规划之——背包DP(进阶篇)

文章目录 概要说明多重背包(朴素算法)模板例题思路code 多重背包&#xff08;二进制优化&#xff09;模板例题思路code 多重背包(队列优化)模板例题思路 混合背包模板例题思路code1code2 二维费用背包模板例题思路code 概要说明 本文讲多重背包、混合背包以及二维费用背包&…

汇聚行业实践,树立应用典范——《Serverless应用实践案例集》重磅发布

云计算已经成为数字时代的基础设施&#xff0c;借助其规模效应实现资源的集约化利用&#xff0c;最大化发挥计算的价值。Serverless进一步优化了云服务供给模式&#xff0c;简化了云上应用的构建方式&#xff0c;代表了云计算的重要发展趋势。 2024年7月24日&#xff0c;2024可…