【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解

news2024/11/16 4:46:57

今天,带来Linux下的进程间通信讲解。文中不足错漏之处望请斧正!


是什么

进程间通信,Inter-Process Communication(IPC):

进程间通过访问同一块内存空间来进行数据的交流。

为什么

为什么要有IPC,它的作用是什么,场景是什么?

  1. 数据传输
  2. 资源共享
  3. 通知时间
  4. 控制进程

怎么做

让要通信的进程们拥有公共资源。

*谁实现IPC,公共资源都得由OS来提供。

为什么?

进程具有独立性(数据结构独立,代码和数据也独立),任何由进程提供的资源都只能被自己看见(进程地址空间)。所以进程们能看到的同一份公共资源,绝对只能由OS提供。

通信方式/风格

不同的公共资源来源,对应出不同的通信方式。

主要有三种方式:

  • 管道
  • System V IPC(重本地)
  • POSIX IPC(重网络)

管道

是什么

内核的一块缓冲区,在内存中。

虽然形式上是文件,但匿名或命名管道文件只是一种标识符,使得进程能通过这个标识符访问内存中的同一块缓冲区。

管道也是用文件作进程公共资源的IPC方式。

分类:

  • 匿名管道
  • 命名管道

匿名管道

是什么

没有也不需要名字的管道文件。

实现原理和过程

通过血缘关系,打开同一个管道文件。

创建子进程,子进程会继承父进程的文件描述符表。有了同样的表,就可指向同一被打开文件。

  1. 父进程创建管道文件(并以读和写的方式分别打开)

在这里插入图片描述

  1. 创建子进程(子进程拷贝父进程fd_array

在这里插入图片描述

  1. 根据读写需求关闭父子的读/写端

在这里插入图片描述

相关接口

int pipe(int fildes[2]);

  • 作用
    • 创建并打开管道文件。
  • 参数
    • fds[2] :是输出型参数,内含两个fd,分别是以读和写打开pipe文件的
      • fd[0]:读(0像嘴巴,读)
      • fd[1]:写(1像钢笔,写)
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

测试代码

int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    pid_t id = fork();
    assert(id >= 0);

    if(id == 0) 
    {
        close(fds[0]); 
        int cnt = 1;
        //循环写,但父进程读端关闭,所以写端进程也被OS发信号终止(异常退出)
        while(1)
        {
            cout << "count:" << cnt << endl;
            char buf[1024];
            snprintf(buf, sizeof buf, "|child(%d): %d|", getpid(), cnt++);
            write(fds[1], buf, strlen(buf));

            sleep(1);
        }

        close(fds[1]);
        cout << "子进程写端关闭!" << endl;
        exit(0);
    }

    //读3次后关闭读端
    close(fds[1]); 
    for(int i = 0; i < 3; ++i)
    {
        char buf[1024];
        ssize_t read_ret = read(fds[0], buf, sizeof(buf) - 1);
        if(read_ret > 0)
            buf[read_ret] = '\0';

        printf("parent(%d) got msg -- %s\n", getpid(), buf);
    }
    close(fds[0]);
    cout << "父进程读端关闭!" << endl;

    //父进程关闭读端后,子进程写端直接被OS终止

    int status = 0;    
    n = waitpid(id, &status, 0);
    assert(n == id);
    cout << "wait success!" << endl;

    return 0;
}
[bacon@VM-12-5-centos mypipe]$ ./mypipe 
count:1
parent(26629) got msg -- |child(26630): 1|
count:2
parent(26629) got msg -- |child(26630): 2|
count:3
parent(26629) got msg -- |child(26630): 3|
父进程读端关闭!
count:4
wait success!

应用:进程池

在这里插入图片描述
设计思路:
父进程通过管道向子进程发送任务码,子进程获取并执行任务。

#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

typedef void(*ptask)();

#define PROCESS_NUM 5
#define PLANT_SEED() srand((unsigned int)time(nullptr) ^ rand() ^ rand())

//进程池:
//准备子进程并让他们等待执行任务 ==> 发送任务码给子进程 ==> 子进程执行 ==> 获取子进程信息

class subEP
{
public:
    subEP(pid_t subId, int writeFd)
        :_subId(subId), _writeFd(writeFd)
    {
        char numBuf[512];
        snprintf(numBuf, sizeof(numBuf), "[process%d]: pid=%d | fd=%d", _nameNum++, subId, _writeFd);
        _name = numBuf;
    }
public:
    std::string _name;
    pid_t _subId;
    int _writeFd; //父进程眼中管道的写端fd
    static int _nameNum;
};
int subEP::_nameNum = 0;

void task1() {std::cout << getpid() << " 完成任务1\n" << std::endl; sleep(1);}
void task2() {std::cout << getpid() << " 完成任务2\n" << std::endl; sleep(1);}
void task3() {std::cout << getpid() << " 完成任务3\n" << std::endl; sleep(1);}
void loadTask(std::vector<ptask>& taskV)
{
    taskV.push_back(task1);
    taskV.push_back(task2);
    taskV.push_back(task3);
}

//向父进程眼中管道的写端写入任务码
void sendTask(const subEP& proc, int taskCode) 
{
    int write_ret = write(proc._writeFd, &taskCode, sizeof(taskCode));
    assert(write_ret == sizeof(taskCode));
    std::cout << "TaskCode " << taskCode << " sent to " << proc._name << std::endl;
}

//从子进程眼中管道的读端读出任务码
int reciveTask(int readFd)
{
    int taskCode = 0;
    int read_ret = read(readFd, &taskCode, sizeof(taskCode));
    //正常读到任务码
    if(read_ret == sizeof(taskCode)) return taskCode;
    //读到0,说明  写端关闭 && 该读的读完了
    else if(read_ret == 0)           return -1;
    else                             return 8848; //不可能的情况
}

std::vector<subEP>& getSubProcessWaittingTask(std::vector<subEP>& subEPs, const std::vector<ptask>& taskV)
{
    //创建子进程
    //bug?
    std::vector<int> fdToDelete;
    for(int i = 0; i < PROCESS_NUM; ++i)
    {
        int fds[2];
        int pipe_ret = pipe(fds);
        assert(pipe_ret == 0);

        pid_t fork_ret = fork();
        //子进程等待任务
        if(fork_ret == 0)
        {
            //关闭之前子进程的写端,免得影响别人自己的终止
            for(int i = 0; i < fdToDelete.size(); ++i) close(fdToDelete[i]);

            close(fds[1]);
            while(true)
            {
                int taskCode = reciveTask(fds[0]);

                if(taskCode >= 0 && taskCode < taskV.size()) taskV[taskCode]();
                else if(taskCode == -1) break; 
            }
            exit(0);
        }
        //父进程保存当前子端点,让父进程后续能均衡发送任务码
        close(fds[0]);
        subEPs.push_back(subEP(fork_ret, fds[1]));
				//保存要删除的fd(下一个子进程要关闭的写端)
        fdToDelete.push_back(fds[1]);
    }
}

void balancedTaskSending(std::vector<subEP> subEPs, const std::vector<ptask>& taskV, int taskCount)
{
    bool infinite = taskCount == 0 ? true : false;
    while(infinite || taskCount)
    {
        //1. 选择一个子进程
        int procIndex = rand() % subEPs.size();
        //2. 选择一个任务
        int taskCode = rand() % taskV.size();
        //3. 将任务对应的任务码发送给子进程
        sendTask(subEPs[procIndex], taskCode);

        sleep(1);
        --taskCount;
    }
}

//1. 绕过bug
// void waitSubProcess(std::vector<subEP> subEPs)
// {
//     for(int i = 0; i < subEPs.size(); ++i) close(subEPs[i]._writeFd);
//     for(int i = 0; i < subEPs.size(); ++i)
//     {
//         waitpid(subEPs[i]._subId, nullptr, 0);
//         std::cout << "wait sub process success: " << subEPs[i]._name << std::endl;
//     }
// }

//2. 硬钢bug
void waitSubProcess(std::vector<subEP> subEPs)
{
    for(int i = 0; i < subEPs.size(); ++i)
    {
        close(subEPs[i]._writeFd);
        waitpid(subEPs[i]._subId, nullptr, 0);
        std::cout << "wait sub process success: " << subEPs[i]._name << std::endl;
    }
}

int main()
{
    //0.准备任务,设置随机数
    std::vector<ptask> taskV;
    loadTask(taskV);
    PLANT_SEED();

    //1.获取正在等待执行任务的子进程
    std::vector<subEP> subEPs; //等待执行任务的子进程
    getSubProcessWaittingTask(subEPs, taskV);

    //2.向subEPs发送任务
    int taskCount = 5; 
    // int taskCount = 0; //无限个任务
    balancedTaskSending(subEPs, taskV, taskCount);

    //3.正在等待任务的子进程获取到任务并执行

    //4.任务执行完毕,子进程退出

    //5.获取子进程退出信息
    waitSubProcess(subEPs);

    return 0;
}

逻辑梳理:

  1. 准备任务,设置随机数
  2. 获取正在等待执行任务的子进程
  3. 向subEPs发送任务
  4. 正在等待任务的子进程获取到任务并执行
  5. 任务执行完毕,子进程退出
  6. 获取子进程退出信息

BUG:同一父进程的多个子进程写端不唯一

原因:

关闭父进程中关于第一个的写端,我们希望OS检测到写端关闭从而终止第一个子进程。
但第一个子进程的写端被后续的进程也拿了一份,就导致关了一个写端还有其他写端,该关的时候反而关不掉。总结一句话,某个子进程的写端被别的子进程拿了,该退退不掉。

为什么刚刚的代码没问题?
最后一个子进程的写端没有被别人拿,所以关了写端就会终止读端进程。
顺序把全部子进程的写端关闭,实质上先终止的是最后一个子进程,倒数第二个的写端变为0个,才关闭;倒数第三个的写端变为0,才关闭……

解决:

  1. 倒着关
  2. 每次创建子进程都把不属于自己的管道文件关了
//关闭之前子进程的写端,免得耦合
for(int i = 0; i < fdToDelete.size(); ++i) close(fdToDelete[i]);

fdToDelete.push_back(fds[1]);

命名管道

是什么

是通过mkpipe创建的有名管道文件。

特点

  • 内容不会刷新到磁盘,而是放在内存中
    • 不会有IO
    • 可以理解命名管道文件是一种内存级的缓冲区

实现原理

不同文件通过路径打开同一个命名管道文件。

具体步骤

  1. A进程创建/打开命名管道
  2. B进程打开命名管道
  3. 通信
  4. (删除管道文件)

相关接口

int mkfifo(const char *pathname, mode_t mode);

  • 作用
    • 创建一个命名管道
  • 参数
    • path:命名管道创建的路径
    • mode:命名管道的权限
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

int unlink(const char *path);

  • 作用
    • 删除目录条录(可用于删除一个命名管道)
  • 参数
    • path:要删除的目录条录(命名管道)的路径
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

测试代码

comm.hpp

#define PIPE_PATH "/tmp/myPipe"

bool createFifo(const std::string& path)
{
    umask(0);
    int mkdfifoRet = mkfifo(path.c_str(), 0666);
    if(mkdfifoRet == 0) return true;
    else
    {
        std::cout << "err: " << strerror(errno) << std::endl;
        exit(errno);
    }
}

void removeFifo(const std::string& path)
{
    int unlinkRet = unlink(path.c_str());
    assert(unlinkRet == 0);
    (void)unlinkRet; //象征性用一下,以防unused variable
}

server.cc

#include "comm.hpp"

int main()
{
    createFifo(PIPE_PATH);

    std::cout << "server will open soon" << std::endl;
    int rfd = open(PIPE_PATH, O_RDONLY);
    if(rfd < 0) exit(-1);
    std::cout << "server open done" << std::endl;

    //read
    char buf[1024];
    while(true)
    {
        int read_ret = read(rfd, buf, sizeof(buf));
        if(read_ret > 0) std::cout << "server got msg: " << buf;
        else if(read_ret == 0) //读到0说明写端不写了,就可以退出
        {
            std::cout << "communication complete!" << std::endl;
            break;
        }
        else
        {
            std::cout << "err: " << strerror(errno) << std::endl;
        }
    }

    close(rfd);
    removeFifo(PIPE_PATH);
    return 0;
}

client

#include "comm.hpp"

int main()
{
    std::cout << "client will open soon" << std::endl;
    int wfd = open(PIPE_PATH, O_WRONLY);
    if(wfd < 0) exit(-1);
    std::cout << "client open done" << std::endl;

    //write
    char buf[1024];
    while(true)
    {
        std::cout << "please input msg:> ";
        fgets(buf, sizeof(buf), stdin);
        int write_ret = write(wfd, buf, strlen(buf));
        assert(write_ret == strlen(buf));
    }
    
    close(wfd);
    return 0;
}

匿名管道和命名管道打开区别

  • 匿名管道的pipe会创建并打开管道。
  • 命名管道的mkfifo只会创建命名管道,需要通过open打开。

管道读写特性:同步与互斥

  • 读快,写慢 = 读端阻塞读
  • 写快,读慢 = 写端写满阻塞
  • 写关闭,读 = 读端读完退出
  • 读关闭 = OS发信号终止写端(不写直接退出)

管道的特性

  • 管道生命周期随进程
  • 具有血缘关系的进程间都能用管道通信(常用于父子进程)
  • 管道是面向字节流的(网络)
  • 半双工 – 单向通信(半双工的一种特殊概念)
  • 互斥与同步机制(相互照顾)

总结

IPC之管道:管道是一种利用文件进行通信的IPC方式,通常是父子进程使用这种方式,因为可以轻易看到同一份文件。


System V

是什么

OS给我们提供的一种本地IPC方案。

*System V的方式用得并不多,原因是有二:

  1. 它是本地IPC
  2. 共享内存标识符、消息队列标识符、信号量标识符对文件的标识符fd兼容得不好,而Linux下又一切皆文件

方式:

  • 共享内存
  • 消息队列
  • 信号量

共享内存

是什么

共享内存,shared memory,一块能同时被不同进程看到的内存。是System V IPC的一种。

实现原理和过程

不同进程关联同一块内存空间。

同样的,只要是IPC,免不了的前提:进程间能有一份公共资源。

  1. 开辟物理上的内存空间
  2. 将物理内存空间映射给不同内存

在这里插入图片描述

共享内存的抽象

不免有个问题:多条通信同时进行,每一组通信的进程都会有自己的共享内存,OS是如何标识不同内存,如何区分不同shm?

			struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Last change time */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };
			struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
           };

底层通过key_t key 来标识共享内存。

特点和优缺点

特点:生命周期随OS

优点:是所有IPC中最快的方式(一般没有不必要拷贝)

缺点:没有同步与互斥机制,对数据没有保护

相关接口

int shmget(key_t key, size_t size, int shmflg);

  • 作用
    • 获取一块共享内存段
  • 参数
    • key:即将开辟的内存的唯一标识(会被填入struct ipc_perm的第一个字段key)
    • size:即将开辟的内存的大小(建议4KB的整数倍,因为内存也是以4KB划分)
    • shmflg:二进制标志位
      • IPC_CREAT
        • 目标不存在:创建
        • 目标存在:获取
      • IPC_EXCL(和IPC_CREAT搭配使用,达到强制创建的效果)
        • 目标不存在:创建
        • 目标存在:出错
      • 还可以添加一个0XXX:对内存的读写权限
    • int:shm的用户级标识符,一般称shmid ,是某个内核数据结构(数组)的下标
  • 返回值
    • 成功返回非负整数
    • 失败返回-1,错误码被设置

#获取同一个key

说得挺好,key是共享内存的唯一标识,但不同进程怎么用同一个key创建/获取一块共享内存?

看一个接口:

key_t ftok(const char *pathname, int proj_id);

  • 作用
    • pathname和proj_id通过某种算法计算后得到一个SystemV IPC的key
  • 参数
    • 其实两者是什么无所谓,只要不同进程调用ftok传的这两个参数是一样的就行
  • 返回值
    • 成功返回创建好的key
    • 失败返回-1,错误码被设置

怎么理解?key的值到底是多少根本不重要,只要不同进程能获取到同一个key就行——只需要传同一个pathname和proj_id就能得到同一个key。只要得到同一个key,也就能看到同一份内存。

#共享内存的唯一标识

key和shmid都是shm的“唯一”标识?

是的:

  • key是底层OS层面的标识
  • shmid是上层用户层面的标识

很像fd、inode和file的关系:

上层→下层:fd → inode → file

上层→下层:shmid → key → memBlock

为什么要搞俩,统一用一个不行吗?

分开:

  • 学校 学号
  • 公司 工号
  • 社会 身份证号

工号改了,不影响我在学校由学号标识,在社会由身份证号标识。

不分开:

  • 学校 身份证号
  • 公司 身份证号
  • 社会 身份证号

身份证号改了,影响我在学校由学号标识,在社会由身份证号标识。

分开 = 不互相影响,不分开 = 互相影响。前者其实就是不耦合,后者就是耦合。

如果不分开,唯一的key/shmid变了,其他地方全部受影响。

说简单点:shmid是用户管理共享内存用的,key是OS管理内存用的。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • 作用
    • 控制shm
  • 参数
    • shmid:要控制的shm
    • cmd
      • IPC_STAT:获取shm属性到buf
      • IPC_SET:把准备好的属性设置到shm
      • IPC_RMID:删除shm
    • buf:可传入一个结构体,获取属性
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

void *shmat(int shmid, const void *shmaddr, int shmflg);

  • 作用
    • 将进程和一块shm关联
  • 参数
    • shmid:要关联的shm

    • shmaddr:要关联到进程空间的哪个地址?(一般传nullptr就行)

      If shmaddr is a null pointer, the segment is attached at the first available address as selected by the system.

    • shmflg:以什么方式(读/写)关联(0代表读写)

  • 返回值
    • 成功返回shm的地址
    • 失败返回-1,错误码被设置

int shmdt(const void *shmaddr);

  • 作用
    • 将进程和一块shm去关联
  • 参数
    • shmaddr
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

相关指令

  • ipcs -m :查看shm属性
  • ipcrm -m将共享内存的链接数-1(为0时共享内存会被释放)

测试代码

comm.h

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATH_NAME "."
#define PROJ_ID  0x8848
#define SHM_SIZE 4096

key_t getKey()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if(key == -1)
    {
        std::cerr << "err: " << strerror(errno) << std::endl;
        exit(1);
    }
    return key;
}

int shmHelper(key_t key, int shmFlag)
{
    //创建共享内存时传入的key,会被填入共享内存的一个字段key_t k
    //k : shm = 1 : 1
    int shmId = shmget(key, SHM_SIZE, shmFlag);
    if(shmId < 0)
    {
        std::cerr << "err:" << strerror(errno) << std::endl;
        exit(2);
    }
    
    return shmId;
}

int getShm(key_t key) 
{
    return shmHelper(key, IPC_CREAT);
}

int createShm(key_t key) 
{
    //创建shm并填入key
    return shmHelper(key, IPC_CREAT | IPC_EXCL | 0666); //对shm读写执行的权限
}

void* attachShm(int shmId)
{
    void* mem = shmat(shmId, nullptr, 0);
    // if((int)mem == -1) //64位系统,指针64bits ==> 32bits == err
    if((long long)mem == -1L)
    {
        std::cerr << "err: " << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

void detachShm(void* start)
{
    if(shmdt(start) == -1)
    {
        std::cerr << "err: " << strerror(errno) << std::endl;
        exit(4);
    }
}

void removeShm(int shmId)
{
    if(-1 == shmctl(shmId, IPC_RMID, nullptr))
    {
        std::cerr << "err: " << strerror(errno) << std::endl;
        exit(-1);
    }
}

shm_server.cc

#include "comm.hpp"

int main()
{
    printf("sercer pid = %d", getpid());
    //获取内核级shm标识:key
    key_t key = getKey();
    printf("server key = 0x%x\n", key);

    //创建shm
    int shmId = createShm(key);
    // printf("server shmId = 0x%x\n", shmId);

    //关联shm和进程
    char* start = (char*)attachShm(shmId);
    printf("server attatch shm success, address start: %p\n", start);

    //通信
    while(true)
    {
        printf("got msg: %s\n", start);
        struct shmid_ds ds;
        shmctl(shmId, IPC_STAT, &ds);
        printf("server stat: |memSegSz=%d| |creator=%d| |key=0x%x|\n", ds.shm_segsz, ds.shm_cpid, ds.shm_perm.__key);
        sleep(1); 
    }

    //去关联shm和进程
    detachShm(start);

    //删除shm
    removeShm(shmId);

    return 0;
}
#include "comm.hpp"

int main()
{
    //获取内核级shm标识:key
    key_t key = getKey();
    // printf("client key = 0x%x\n", key);
    int shmId = getShm(key);
    // printf("client shmId = 0x%x\n", shmId);

    //关联shm和进程
    char* start = (char*)attachShm(shmId);
    printf("server attatch shm success, address start: %p\n", start);

    //通信
    int cnt = 1;
    while(true)
    {
        snprintf(start, SHM_SIZE, "hello server, I'm [%d] | cnt=%d", getpid(), cnt++); 
        sleep(1);
    }

    //去关联shm和进程
    detachShm(start);

    return 0;
}

逻辑梳理:

  1. A、B进程通过同样的pathName和proj_id获取同一个key如8848
  2. A进程通过key(8848)创建共享内存
  3. B进程通过key(8848)获取共享内存
  4. 进程和共享内存关联
  5. 通信

共享内存和管道的对比

主要是公共资源用得不一样:

  • 管道:文件
  • 共享内存:内存

总结

IPC之共享内存:不同进程通过关联同一块内存空间来看到公共资源,进而完成通信。


消息队列

是什么

Linux的内核级队列。

在这里插入图片描述

消息队列中的结点主要的字段有type和buf,type是数据块的类型,buf是数据块内的数据。可以理解为:消息队列中存放类型为type的数据块buf。

进程可以根据type来区分数据块,来读写自己对应的数据块。

消息队列的抽象

			struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };
			struct ipc_perm {
               key_t          __key;       /* Key supplied to msgget(2) */
               uid_t          uid;         /* Effective UID of owner */
               gid_t          gid;         /* Effective GID of owner */
               uid_t          cuid;        /* Effective UID of creator */
               gid_t          cgid;        /* Effective GID of creator */
               unsigned short mode;        /* Permissions */
               unsigned short __seq;       /* Sequence number */
           };

有了抽象,管理就是水到渠成的事了。

怎么玩

int msgget(key_t key, int msgflg);

  • 作用
    • 获取消息队列
  • 参数
    • key:和shmget的一样
    • msgflg:和shmget的一样
  • 返回值
    • 成功返回消息队列的标识符
    • 失败返回-1,错误码被设置

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

  • 作用
    • 控制消息队列
  • 参数
    • msqid:msq的标识符
    • cmd
      • IPC_STAT
      • IPC_SET
      • IPC_RMID
    • buf:msq属性
  • 返回值
    • 成功返回0
    • 失败返回-1,错误码被设置

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  • 作用
    • 发送消息给消息队列(消息队列会生成一个结点,填入属性和这段信息)
  • 参数
    • msqid:消息队列的id

    • msgp:要发送的消息(是一个结构体struct msgbuf)

      struct msgbuf {
      	long mtype;       /* message type, must be > 0 **/
      	char mtext[1];    /** message data */
      };
      
    • msgsz:消息的大小

    • msgflg:给0就好

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

  • 作用
    • 从消息队列接收消息
  • 参数
    • msqid:消息队列的id

    • msgp:输出型参数,用来接收消息(是一个结构体struct msgbuf)

      struct msgbuf {
        	long mtype;       /* message type, must be > 0 **/
          char mtext[1];    /** message data */
      };
      
    • msgsz:消息的大小

    • msgflg:给0就好

具体的例子就不写了,了解个大概即可。

指令

ipcs -q

总结

IPC之消息队列:以系统维护的内核级队列——消息队列作为公共资源,不同通过系统调用读写消息队列来完成通信。


信号量

是什么

一个衡量共享资源使用情况的计数器,提供了一种“预定机制”。

  • >0代表仍有资源
  • =0代表没有资源

伪代码:

信号量 sem = 20; //有20块共享资源

//进程1预定一块资源(申请信号量)
sem--; //衡量共享资源使用情况的计数器sem就需要对应地自减

//进程2预定一块资源(申请信号量)
sem--;

//访问...

//进程1用完了,回收资源
sem++; //衡量共享资源使用情况的计数器sem就需要对应地自增

//进程2用完了,回收资源
sem++;

其中,

  • sem--; 就称为 P操作
  • sem++; 就称为 V操作

怎么理解“预定机制”?

:P操作是对共享资源的预定,V操作是对共享资源的释放。

为什么

可以管理、保护好资源。

怎么说?

我们可以通过共享资源的使用来理解。

#概念铺垫

  1. 临界资源:受保护的共享资源

  2. 临界区:访问临界资源的代码片段

  3. 互斥:进程对公共资源的访问是相互排斥的(同一共享资源,同一时刻只能有一个进程访问)

  4. 原子性:独立不可分割的操作

    可理解为只有两态(转账:要么转账成功,要么转账不成功,没有其他状态)

共享资源的使用

使用方式有两种:

  1. 整体使用:信号量初始值为1
  2. 拆分使用:信号量初始值大于1

初始值为1的信号量能实现互斥(我申请了,信号量从1减到0,别人就不能申请),这种信号量叫二元信号量。

进程访问共享资源需要先申请信号量,就像看电影,需要先“买票”。

  • 申请到信号量,某一部分共享资源就属于进程,就像我买到票,电影院的一个座位就属于我。
  • 若没申请到信号量,进程就无法访问共享资源——信号量的意义。

进程想访问共享资源,得先申请信号量,这需要所有进程都得能看到同一个信号量,所以信号量本身就是共享资源。

这样的话,信号量又是怎么管理和保护自己的呢?

只需要令P操作和V操作(对共享资源的申请和释放)都是原子,要么申请成功,要么申请失败;要么释放成功,要么释放失败。

信号量的抽象

			struct semid_ds {
               struct ipc_perm sem_perm;  /* Ownership and permissions */
               time_t          sem_otime; /* Last semop time */
               time_t          sem_ctime; /* Last change time */
               unsigned long   sem_nsems; /* No. of semaphores in set */
           };

			struct ipc_perm {
               key_t          __key; /* Key supplied to semget(2) */
               uid_t          uid;   /* Effective UID of owner */
               gid_t          gid;   /* Effective GID of owner */
               uid_t          cuid;  /* Effective UID of creator */
               gid_t          cgid;  /* Effective GID of creator */
               unsigned short mode;  /* Permissions */
               unsigned short __seq; /* Sequence number */
           };

怎么玩

int semget(key_t key, int nsems, int semflg);

  • 作用
    • 申请信号量
  • 参数
    • key
    • nsems:要申请的信号量数量
    • semflg
  • 返回值
    • 成功返回信号量集的标识符
    • 失败返回-1,错误码被设置

int semctl(int semid, int semnum, int cmd, ...);

  • 作用
    • 控制信号量
  • 参数
    • semnum:是信号量集合的下标,表示要控制哪个信号量

int semop(int semid, struct sembuf *sops, unsigned nsops);

  • 作用
    • P操作和V操作(- -和++)
  • 参数
    • sops:sem 的options

    • struct sembuf

             unsigned short sem_num;  /* semaphore number */
             short          sem_op;   /* semaphore operation */
             short          sem_flg;  /* operation flags */
      
      • set_num:下标,表示你要对信号量集中的哪一个操作
      • sem_op:P or V
        • -1 = P操作
        • 1 = V操作
      • sem_flg:设0即可
    • nspos:n个sops

具体怎么用,后面多进程时演示。

指令

ipcs -s


IPC资源的管理

先看共享内存、消息队列和信号量的抽象:

			struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Last change time */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };
			struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
           };
			struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };
			struct ipc_perm {
               key_t          __key;       /* Key supplied to msgget(2) */
               uid_t          uid;         /* Effective UID of owner */
               gid_t          gid;         /* Effective GID of owner */
               uid_t          cuid;        /* Effective UID of creator */
               gid_t          cgid;        /* Effective GID of creator */
               unsigned short mode;        /* Permissions */
               unsigned short __seq;       /* Sequence number */
           };
			struct semid_ds {
               struct ipc_perm sem_perm;  /* Ownership and permissions */
               time_t          sem_otime; /* Last semop time */
               time_t          sem_ctime; /* Last change time */
               unsigned long   sem_nsems; /* No. of semaphores in set */
           };

			struct ipc_perm {
               key_t          __key; /* Key supplied to semget(2) */
               uid_t          uid;   /* Effective UID of owner */
               gid_t          gid;   /* Effective GID of owner */
               uid_t          cuid;  /* Effective UID of creator */
               gid_t          cgid;  /* Effective GID of creator */
               unsigned short mode;  /* Permissions */
               unsigned short __seq; /* Sequence number */
           };

不仅接口相似度高(尤其是获取和删除),而且采用了类似的抽象方式。这叫什么?

这就叫 “标准”,所谓SystemV标准下的IPC方案,就是这个意思。

我们还发现,它们都有一个结构:

			struct ipc_perm {
               key_t          __key; /* Key supplied to XXX(2) */
               uid_t          uid;   /* Effective UID of owner */
               gid_t          gid;   /* Effective GID of owner */
               uid_t          cuid;  /* Effective UID of creator */
               gid_t          cgid;  /* Effective GID of creator */
               unsigned short mode;  /* Permissions */
               unsigned short __seq; /* Sequence number */
           };

而且每个ipc_perm对象,都是共享内存这些结构的第一个字段,这有什么用?

在这里插入图片描述

通过类型转换实现一地址多用,就像多态一样。


今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!

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

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

相关文章

AI杀疯!2023上半年至今有趣的AI算法(内附视频)

公众号&#xff1a;算法一只狗 文章目录 第一个&#xff0c;一切都可以进行分割第二个&#xff0c;开源图文回答工具第三个&#xff0c;视频转换风格生成第四个&#xff0c;免费好用的文档对话工具文档对话能力文档联系功能 今年&#xff0c;我们见证了人工智能算法的起飞&…

java如何导入导出excel

在Java中&#xff0c;可以使用多种方式导入和导出Excel文件。下面将详细介绍几种常见的方法及其实现步骤&#xff1a; 1. Apache POI库&#xff1a; Apache POI是一个开源的Java库&#xff0c;提供了许多类和方法用于处理Microsoft Office格式的文档&#xff0c;包括Excel文件…

关于刷题时使用数组的小注意事项

&#x1f4af; 博客内容&#xff1a;关于刷题时使用数组的小技巧 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; &#x1f496; 欢迎大家&#…

我们做播客这些年的自我进化

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 姝琦 运营 / SandLiu 卷圈 监制 / 姝琦 封面 / 姝琦Midjourney 产品统筹 / bobo 场地支持 / 声湃轩北京录音间 这是一期荔枝播客十周年活动的特别节目&#xff0c;借这次机会&#xff0c;我们几位主播也借此机会沉淀下…

RHCE8 资料整理(二)

RHCE8 资料整理 第二篇 用户及权限管理第8章 用户管理8.1 基本概念8.2 管理用户8.2.1 创建用户8.2.2 修改用户属性 8.3 用户的密码策略8.4 用户授权8.5 重置root密码 第9章 权限管理9.1 所有者和所属组9.2 查看及修改权限9.3 数字权限9.4 默认权限9.5 特殊权限9.6 隐藏权限 第1…

C++前缀和算法应用:和至少为 K 的最短子数组的原理、源码及测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 题目 给你一个整数数组 nums 和一个整数 k &#xff0c;找出 nums 中和至少为 k 的 最短非空子数组 &#xff0c;并返回该子数组的长度。如果不存在这样的 子数组 &a…

配置公网和私网用户通过非公网口的IP地址访问内部服务器和Internet示例

组网需求 如配置公网和私网用户通过非公网口的IP地址访问内部服务器和Internet示例所示&#xff0c;某小型企业内网部署了一台路由器、一台FTP服务器和一台Web服务器。路由器作为接入网关&#xff0c;为下挂的内网用户提供上网服务&#xff0c;主要包括浏览网页、使用即时通信…

短视频矩阵系统源头开发

一、智能剪辑、矩阵分发、无人直播、爆款文案于一体独立应用开发 抖去推----主要针对本地生活的----移动端(小程序软件系统&#xff0c;目前是全国源头独立开发)&#xff0c;开发功能大拆解分享&#xff0c;功能大拆解&#xff1a; 7大模型剪辑法&#xff08;数学阶乘&#x…

Flink之输出算子Data Sink

Flink之输出算子Data Sink Data Sink常见输出算子print()printToErr()writeAsText()writeAsCsv()writeToSocket() 常用连接器File Sink连接器Kafka Sink连接器RabbitMQ Sink连接器JDBC Sink连接器Elasticsearch Sink连接器MongoDB Sink连接器 自定义SinkRichSinkFunctionSinkFu…

海外展预告 | 同立海源将参展美国圣地亚哥SITC 2023年会

第38届癌症免疫治疗学会&#xff08;Society for Immunotherapy of Cancer, SITC&#xff09;年会将于11月1日-5日在美国圣地亚哥举行。同立海源将携细胞分选磁珠试剂、真核/原核重组蛋白、免疫细胞培养基等CGT上游GMP级核心原料整体解决方案参加此次会议并设立展台&#xff0c…

4.5 互联网的路由器

思维导图&#xff1a; 4.5 互联网的路由选择协议 本节的核心内容是讨论如何确定路由表中的路由&#xff0c;具体通过何种路由选择协议实现。 --- **4.5.1 有关路由选择协议的几个基本概念** - **理想的路由算法:** 路由选择协议的关键是路由算法。一个理想的路由算法应具…

如何打造独立站?这4个要点必须做到!

“什么是独立站”独立站指的是个人或小团队独立创建和管理的网站&#xff0c;与依赖于第三方平台的博客、社交媒体或在线商店不同。独立站的所有权和控制权完全归个人或小团队所有&#xff0c;因此具有更大的自主性和独立性&#xff0c;不受第三方平台的限制。 独立站是由个人…

Lua-http库写一个爬虫程序怎么样 ?

以下是一个使用Lua-http库编写的一个爬虫程序&#xff0c;该爬虫使用Lua语言来抓取www.snapchat.com的内容。 代码必须使用以下代码&#xff1a;get_proxy -- 导入所需的库 local http require("http") local json require("json")-- 定义爬虫IP服务器 …

必示科技发布“早准快全易”智能运维产品,与生态伙伴共谋增长

2023年10月13日&#xff0c;“因智而聚 共谋增长”必示科技产品发布活动在北京中关村智造大街圆满召开&#xff0c;来自智能运维行业领域共40多家企业高层代表出席了本次闭门交流活动。 必示科技发布了三款智能运维产品&#xff1a;应用监控预警系统&#xff08;RiskSeer-App&…

[SQL开发笔记]创建SQL数据库

一、引言 在计算机软件开发以及业务流程中&#xff0c;大量数据不断产生&#xff0c;如何安全有效地存储、检索和管理它们已成为信息时代一个至关重要的问题。解决这个问题的关键在于使用数据库&#xff0c;数据库能够高效且条理分明地存储数据&#xff0c;方便用户进行迅速和…

TikTok Shop新结算政策:卖家选择权加强,电商市场蓄势待发

据悉&#xff0c;从2023年11月1日开始&#xff0c;TikTok Shop将根据卖家的店铺表现来应用3种不同类型的结算期&#xff0c;其中&#xff0c;标准结算期&#xff1a;资金交收期为8个日历日&#xff1b;快速结算期&#xff1a;资金交收期为3个日历日&#xff1b;延长结算期&…

HarmonyOS开发:Log工具类源码分析

前言 一转眼就十月中旬了&#xff0c;国庆的劲真大&#xff0c;到现在还未缓过来&#xff0c;以至于要更新的文章迟迟未发布&#xff0c;大家可以看到&#xff0c;最近一段时间的文章&#xff0c;都是关于HarmonyOS相关的&#xff0c;两个原因吧&#xff0c;一是我司有这样的任…

《数据结构、算法与应用C++语言描述》使用C++语言实现数组双端队列

《数据结构、算法与应用C语言描述》使用C语言实现数组双端队列 定义 队列的定义 队列&#xff08;queue&#xff09;是一个线性表&#xff0c;其插入和删除操作分别在表的不同端进行。插入元素的那一端称为队尾&#xff08;back或rear&#xff09;&#xff0c;删除元素的那一…

网站二级域名怎么部署SSL证书?

二级域名是在主域名下创建的子域名&#xff0c;常用于区分不同功能或部门的网站。随着互联网的发展&#xff0c;越来越多的网站开始采用二级域名来构建更灵活和个性化的网站结构&#xff0c;保护二级域名的数据安全也变得至关重要。为了确保二级域名的安全性&#xff0c;申请SS…

python爬虫requests.get乱码问题

爬取百度图片的时候res.text出现乱码&#xff1a; 解决&#xff1a; 删除请求头中的接受编码项