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

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


是什么

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

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

为什么

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

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

怎么做

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

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

为什么?

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

通信方式/风格

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

主要有三种方式:

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

管道

是什么

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

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

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

分类:

  • 匿名管道
  • 命名管道

匿名管道

是什么

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

实现原理和过程

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

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

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

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第1张图片

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

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第2张图片

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

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第3张图片

相关接口

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!

应用:进程池

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第4张图片
设计思路:
父进程通过管道向子进程发送任务码,子进程获取并执行任务。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 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. 将物理内存空间映射给不同内存

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第5张图片

共享内存的抽象

不免有个问题:多条通信同时进行,每一组通信的进程都会有自己的共享内存,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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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的内核级队列。

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第6张图片

消息队列中的结点主要的字段有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对象,都是共享内存这些结构的第一个字段,这有什么用?

【Linux07-进程间通信】侧重 管道 和 SystemV 的进程间通信讲解_第7张图片

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


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

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

你可能感兴趣的:(linux,c++)