Linux进程间通信

进程间通信目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信发展

管道

System V进程间通信

POSIX进程间通信

进程间通信分类

管道

匿名管道pipe
命名管道

System V IPC

System V 消息队列
System V 共享内存
System V 信号量

POSIX IPC

消息队列、共享内存、信号量、互斥量、条件变量、读写锁

进程通信的本质

进程间通信的本质:让不同的进程先看到同一份资源(通常都是由OS提供)

什么是管道

管道是Unix中最古老的进程间通信的形式

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

匿名管道

#include <unistd.h>
功能:创建一无名管道
int pipe(int fd[2]);

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

 站在文件描述符角度-深度理解管道

只允许单向通信

一个进程打开一个文件,在内核中是会形成两个内核数据结构对象struct file的,一个struct file是用来进行读取的,另一个是用来写入的,struct file里面由一个整型,类似引用计数,记录指向该struct file对象的指针的数量 

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n==0);
    (void)n;//防止编译器告警
    cout<<"pipefd[0]: "<<pipefd[0]<<",pipefd[1]: "<<pipefd[1]<<endl;
    return 0;
}

 

0下标:读端  (记忆成:一张嘴,用来读)1下标:写端 (记忆成:一支笔,用来写)

Makefile:

mypipe:mypipe.cc
	g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
	rm -f mypipe

 mypipe.cc:

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX 1024

using namespace std;

int main()
{
     // 第1步,建立管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n==0);
    (void)n;//防止编译器告警
    cout<<"pipefd[0]: "<<pipefd[0]<<",pipefd[1]: "<<pipefd[1]<<endl;

    // 第2步,创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 1;
    }
    //子写,父读
    // 第3步,父子关闭不需要的fd,形成单向通信的管道
    if(id==0)
    {
        //child
        close(pipefd[0]);
        // w - 只向管道写入,没有打印
        int cnt = 10;
        while(cnt)
        {
            char message[MAX];
            snprintf(message, sizeof(message), "hello father, I am child, pid: %d, cnt: %d", getpid(), cnt);
            cnt--;
            write(pipefd[1],message,strlen(message));
            sleep(1);
        }
        exit(0);
    }
    //父进程
    close(pipefd[1]);

    // r
    char buffer[MAX];
    while(true)
    {
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n] = 0;// '\0', 当做字符串
            cout << getpid() << ", " << "child say: " << buffer << " to me!" << endl;
        }
    }

    pid_t rid = waitpid(id,nullptr,0);
    if(rid==id)
    {
        cout<<"wait success"<<endl;
    }
    return 0;
}

管道读写规则

管道的4种情况:

 1. 正常情况(读写端都打开)如果管道没有数据了,读端必须等待,直到有数据为止(即写端写入数据了)

2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)

3.写端关闭,读端一直读取,读端会读到read返回值为0,表示读到文件结尾

4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止目标进程

(当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性,当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性)

管道的五种特性:

1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信(仅限于此),常用于父子

2. 匿名管道默认给读写端要提供同步机制

3. 匿名管道是面向字节流的

4. 管道的生命周期是随进程的(管道也是文件)

5. 管道是单向通信的,是半双工通信的一种特殊情况(半双工,可以简单形象理解为你说我听,我说你听)数据只能向一个方向流动,需要双方通信时,需要建立起两个管道

进程池

预先创建一批进程以待使用

代码:

Makefile:

processpool:ProcessPool.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f processpool

 ProcessPool.cc:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

const int num = 5;
static int number = 1;

class channel
{
public:
    channel(int fd, pid_t id) 
    : ctrlfd(fd)
    , workerid(id)
    {
        name = "channel-" + std::to_string(number++);
    }

public:
    int ctrlfd;
    pid_t workerid;
    std::string name;//管道名字
};

void PrintFd(const std::vector<int> &fds)//打印一个进程关闭的所有不属于自己的写端文件
{
    std::cout << getpid() << " close fds: ";
    for(auto fd : fds)
    {
        std::cout << fd << " ";
    }
    std::cout << std::endl;
}


void Work()//读取信道中的整型数据(任务编码)并根据该数据执行任务
{
    while (true)
    {
        int code = 0;
        ssize_t n = read(0, &code, sizeof(code));
        if (n == sizeof(code))
        {
            if (!init.CheckSafe(code))//写入的任务编码不合法,不执行任何任务
                continue;
            init.RunTask(code);
        }
        else if (n == 0)//没有写入数据
        {
            break;
        }
    }

    std::cout << "child quit" << std::endl;
}

void CreateChannels(std::vector<channel> *c)
{
    std::vector<int> old;//记录该进程继承父进程文件描述符表中原有的fd(指向之前的写端文件)
    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0) // child
        {
            if(!old.empty())
            {
                for(auto fd : old)
                {
                    close(fd);//关闭所有之前的(不属于自己的)写端文件
                }
                PrintFd(old);
            }
            close(pipefd[1]);//关闭写端
            dup2(pipefd[0], 0);//为从信道拿数据做重定向准备
            Work();
            exit(0); // 会自动关闭自己打开的所有的fd
        }

        // father
        close(pipefd[0]);//关闭读端
        c->push_back(channel(pipefd[1], id));//管理信道信息
        old.push_back(pipefd[1]);//记录打开的写端
    }
}

void SendCommand(const std::vector<channel> &c, bool flag, int num = -1)//缺省值-1+flag==true-->派发无限次任务
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务
        int command = init.SelectTask();

        // 2. 选择信道(进程)
        const auto &channel = c[pos++];
        pos %= c.size();

        std::cout << "send command " << init.ToDesc(command) << "[" << command << "]"
                  << " in "
                  << channel.name << " worker is : " << channel.workerid << std::endl;

        // 3. 发送任务
        write(channel.ctrlfd, &command, sizeof(command));

        // 4. 判断是否要退出(为有限次任务)
        if (!flag)
        {
            num--;
            if (num <= 0)
                break;
        }
        sleep(1);
    }

    std::cout << "SendCommand done..." << std::endl;
}

void ReleaseChannels(std::vector<channel> c)
{
    for (const auto &channel : c)
    {
        close(channel.ctrlfd);
        pid_t rid = waitpid(channel.workerid, nullptr, 0);
        if (rid == channel.workerid)
        {
            std::cout << "wait child: " << channel.workerid << " success" << std::endl;
        }
    }
}

int main()
{
     std::vector<channel> channels;

     // 1. 创建信道,创建进程
     CreateChannels(&channels);

     // 2. 开始发送任务
     const bool g_always_loop = true;//意味着将要派发无限次任务
     // SendCommand(channels, g_always_loop);//派发无限次任务
     SendCommand(channels, !g_always_loop, 10);//派发有限次任务

      // 3. 回收资源,想让子进程退出,并且释放管道,只要关闭写端
      ReleaseChannels(channels);

    return 0;
}

Task.hpp:

#pragma once

#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>


typedef std::function<void()> task_t;

void Download()
{
    std::cout << "我是一个下载任务"
              << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务"
              << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "这是一个推送视频流的任务"
              << " 处理者: " << getpid() << std::endl;
}

class Init
{
public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);
        srand(time(nullptr) ^ getpid());
    }

    bool CheckSafe(int code)
    {
        if (code >= 0 && code < tasks.size())
            return true;
        else
            return false;
    }


    void RunTask(int code)
    {
        tasks[code]();
    }

    int SelectTask()
    {
        return rand() % tasks.size();
    }

    std::string ToDesc(int code)
    {
        switch (code)
        {
        case g_download_code:
            return "Download";
        case g_printlog_code:
            return "PrintLog";
        case g_push_videostream_code:
            return "PushVideoStream";
        default:
            return "Unknow";
        }
    }

public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;
};

Init init; 

命名管道

 匿名管道只能让具有血缘关系的进程进程进程间通信,命名管道能让毫不相干的两个进程进行通信,可以使用FIFO文件来做这项工作,它经常被称为命名管道

echo 指令与cat指令运行起来是进程,从以上现象可以看到,这两个毫不相干的进程可以进行通信 

命令管道有文件名也有路径(如/home/xxx/fifo),它能让两个毫不相干的进程进行通信的原因在于:

进程间通信的本质:让不同的进程先看到同一份资源

因为路径是具有唯一性的,所以我们可以使用路径+文件名来唯一地让不同进程看到同一份资源

命名管道的创建 

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

mkfifo filename

命名管道也可以从程序里创建,相关函数有:

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

 创建命名管道:

 
int main(int argc, char *argv[])
{
     mkfifo("p2", 0644);
     return 0;
}

用命名管道实现server&client通信 :

命名管道代码:

comm.h

#pragma once


#define FILENAME "fifo"

server.cc

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.h"

bool MakeFifo()
{
    int n = mkfifo(FILENAME, 0666);
    if(n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }

    std::cout << "mkfifo success... read" << std::endl;
    return true;
}

int main()
{
Start:
    int rfd = open(FILENAME,O_RDONLY);
    if(rfd < 0)//没有创建管道文件,则创建
    {
        std::cerr<<"errno: "<<errno<<", errstring: "<<strerror(errno);
        if(MakeFifo()) //创建管道
        {
            goto Start;
        }
        else
        {
            return 1;
        }
    }

    std::cout << "open fifo success..." << std::endl;

    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s] = 0;
            std::cout << "Client say# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            std::cout << "client quit, server quit too!" << std::endl;
            break;
        }
    }

    close(rfd);
    std::cout << "close fifo success..." << std::endl;
    return 0;
}

client.cc

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.h"

int main()
{
    int wfd = open(FILENAME, O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    std::cout << "open fifo success... write" << std::endl;

    std::string message;
    while (true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        ssize_t s = write(wfd, message.c_str(), message.size());
        if (s < 0)
        {
            std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
            break;
        }
    }

    close(wfd);
    std::cout << "close fifo success..." << std::endl;
    return 0;
}

Makefile

.PHONY:all
all:server client

server:server.cc
	g++ -o $@ $^ -std=c++11
client:client.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f server client fifo

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

匿名管道由pipe函数创建并打开

命名管道由mkfifo函数创建,打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时:

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时:

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

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 */
};

OS一定会允许系统中同时存在多个共享内存,那么就要管理这些共享内存,即先描述再组织,因此每个共享内存会存在一个内核数据结构对象(struct结构体)

问题:如何保证第二个参与通信的进程与第一个参与通信的进程看到的是同一个共享内存?

答案:要求共享内存必须有唯一标识

那么两个即将通信的进程只要约定好一个数字key,这个key将在共享内存被创建时,写入该共享内存对应的struct结构体里

共享内存函数

shmget函数:

功能:用来创建共享内存
原型
 int shmget(key_t key, size_t size, int shmflg);
参数
 key:这个共享内存段名字
 size:共享内存大小
 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

 

ftok:

共享内存(IPC资源) 的生命周期是随内核的

comm.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <string>

const std::string pathname = "/home/badada/test/test3";
const int proj_id = 0x11223344;
const int size = 4096;

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(1);
    }
    std::cout<<"key : "<<key<<std::endl;
    return key;
}

server.cc

#include <iostream>
#include <cstring>
#include <sys/ipc.h> 
#include <sys/shm.h>
#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    int shmid = shmget(key,size,IPC_CREAT| IPC_EXCL);
    if(shmid < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    std::cout<<"shmid: "<<shmid<<std::endl;
    return 0;
}

ipcs - m:查看共享内存

ipcrm -m  shmid: 删除创建的共享内存

key vs shmid

key:不要在应用层使用,只用来在内核中标识shm的唯一性,类似fd

shmid:应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似FILE*

共享内存的大小,强烈建议设置为4096字节的整数倍,因为共享内存是按4096字节开辟的,即使我们将其设置为4097字节,但是还是会开辟4096*2 且我们只能使用4097字节 

shmat函数:

功能:将共享内存段连接到进程地址空间
原型
 void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
 shmid: 共享内存标识
 shmaddr:指定连接的地址
 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - 
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

 

将共享内存映射到进程的地址空间中

#include <iostream>
#include <cstring>
#include <sys/ipc.h> 
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    int shmid = shmget(key,size,IPC_CREAT| IPC_EXCL|0644);
    if(shmid < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    sleep(5);
    std::cout<<"shmid: "<<shmid<<std::endl;
    std::cout<<"开始将shm映射到进程的地址空间中"<<std::endl;
    char*s = (char*)shmat(shmid,nullptr,0);
    sleep(5);
    return 0;
}

使用shmat和使用malloc差不多 

shmdt函数:

功能:将共享内存段与当前进程脱离
原型
 int shmdt(const void *shmaddr);
参数
 shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
#include <iostream>
#include <cstring>
#include <sys/ipc.h> 
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    sleep(3);
    int shmid = shmget(key,size,IPC_CREAT| IPC_EXCL|0644);
    if(shmid < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    sleep(5);
    std::cout<<"shmid: "<<shmid<<std::endl;
    std::cout<<"开始将shm映射到进程的地址空间中"<<std::endl;
    char*s = (char*)shmat(shmid,nullptr,0);
    sleep(5);

    shmdt(s);
    std::cout<<"开始将shm从进程的地址空间中移除"<<std::endl;
    sleep(10);
    return 0;
}

shmctl函数:

功能:用于控制共享内存
原型
 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
 shmid:由shmget返回的共享内存标识码
 cmd:将要采取的动作(有三个可取值)
 buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

 

 

#include <iostream>
#include <cstring>
#include <sys/ipc.h> 
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    sleep(3);
    int shmid = shmget(key,size,IPC_CREAT| IPC_EXCL|0644);
    if(shmid < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    sleep(5);
    std::cout<<"shmid: "<<shmid<<std::endl;
    std::cout<<"开始将shm映射到进程的地址空间中"<<std::endl;
    char*s = (char*)shmat(shmid,nullptr,0);
    sleep(5);

    shmdt(s);
    std::cout<<"开始将shm从进程的地址空间中移除"<<std::endl;
    sleep(5);

    shmctl(shmid,IPC_RMID,nullptr);
    std::cout<<"开始将shm从OS中删除"<<std::endl;
    sleep(10);
    return 0;
}

 

 共享内存的特点

1 共享内存的通信方式,不会提供同步机制, 共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全问题

2 共享内存是所有进程间通信,速度最快的

3 共享内存可以提供较大的空间

进程通信之使用共享内存的进一步完善代码:

server.cc

class Init
{
public:
    Init()
    {
        bool r = MakeFifo();
        if(!r)
            return;
        key_t key = GetKey();
        std::cout << "key : " << ToHex(key) << std::endl;
        shmid = CreateShm(key);
        std::cout << "shmid: " << shmid << std::endl;
        std::cout << "开始将shm映射到进程的地址空间中" << std::endl;
        s = (char *)shmat(shmid, nullptr, 0);
        fd = open(filename.c_str(), O_RDONLY);
    }

     ~Init()
     {
        shmdt(s);
        std::cout << "开始将shm从进程的地址空间中移除" << std::endl;
        shmctl(shmid, IPC_RMID, nullptr);
        std::cout << "开始将shm从OS中删除" << std::endl;
        close(fd);
        unlink(filename.c_str());
     }
public:
    int shmid;
    int fd;
    char*s;
};

int main()
{
    Init init;
    while(true)
    {
        int code = 0;
        ssize_t n = read(init.fd, &code, sizeof(code));
        if (n > 0)
        {
            std::cout << "共享内存的内容: " << init.s << std::endl;
            sleep(1);
        }
        else if (n == 0)
        {
            break;
        }
    }
    return 0;
}

client.cc

int main()
{
    key_t key = GetKey();
    int shmid = GetShm(key);
    char *s = (char*)shmat(shmid, nullptr, 0);
    std::cout << "attach shm done" << std::endl;
    int fd = open(filename.c_str(), O_WRONLY);

    char c = 'a';
    for(; c <= 'z'; c++)
    {
        s[c-'a'] = c;
        std::cout << "write : " << c << " done" << std::endl;

        // 通知对方(实现同步机制)
        int code = 1;
        write(fd, &code, sizeof(4));
        sleep(1);
    }

    shmdt(s);
    std::cout << "detach shm done" << std::endl;
    close(fd);
    return 0;
}

comm.hpp

#pragma once

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

const std::string pathname = "/home/badada/test/test3";
const int proj_id = 0x11223344;
const int size = 4096;
const std::string filename = "fifo";

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(1);
    }
    std::cout<<"key : "<<key<<std::endl;
    return key;
}

bool MakeFifo()
{
    int n = mkfifo(filename.c_str(), 0666);
    if(n < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }

    std::cout << "mkfifo success... read" << std::endl;
    return true;
}

std::string ToHex(int id)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%x", id);
    return buffer;
}

int CreateShmHelper(key_t key, int flag)
{
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(2);
    }

    return shmid;
}

int CreateShm(key_t key)
{
    return CreateShmHelper(key, IPC_CREAT|IPC_EXCL|0644);
}

int GetShm(key_t key)
{
    return CreateShmHelper(key, IPC_CREAT/*0也可以*/);
}

之前我们说过系统中会存在许多共享内存,要把这些共享内存管理起来就要有对应的struct结构体,它里面会存储对应共享内存的属性,shmctl中的第三个参数就可以获取共享内存的属性 

 代码:

    Init init;
    struct shmid_ds ds;
    shmctl(init.shmid, IPC_STAT, &ds);
    std::cout << ToHex(ds.shm_perm.__key) << std::endl;
    std::cout << ds.shm_segsz << std::endl;
    std::cout << ds.shm_atime << std::endl;
    std::cout << ds.shm_nattch << std::endl;
    sleep(5);

结果:

能获取共享内存的属性,说明:共享内存=共享内存空间+共享内存属性 

system V消息队列(只做了解)

消息队列提供一个进程给另一个进程发送数据块的能力

每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

简单使用一下:

    key_t key = GetKey();
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL);
    std::cout << "msgid: " << msgid << std::endl;
    sleep(10);

 消息队列的生命周期也是随内核的

我们可以手动删除消息队列:ipcrm -q msqid

也可以使用msgctl来帮助我们自动删除消息队列

 msgctl(msgid,IPC_RMID,nullptr);

 系统中可以同时存在多个消息队列,消息队列也要在内核中被管理起来,所以就要先描述再组织

即每一个消息队列都会有一个数据结构对象struct结构体 

消息队列 = 队列+队列的属性

    key_t key = GetKey();
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL);
    std::cout << "msgid: " << msgid << std::endl;
    sleep(10);

    struct msqid_ds ds; 
    msgctl(msgid, IPC_STAT, &ds);
    std::cout << ds.msg_qbytes << std::endl;
    
    msgctl(msgid,IPC_RMID,nullptr);

 

    key_t key = GetKey();
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL);
    std::cout << "msgid: " << msgid << std::endl;


    struct msqid_ds ds; 
    msgctl(msgid, IPC_STAT, &ds);
    std::cout << ds.msg_qbytes << std::endl;
    std::cout << ToHex(ds.msg_perm.__key) << std::endl;
    
    sleep(10);
    msgctl(msgid,IPC_RMID,nullptr);

 system V信号量(只做了解)

信号量的本质:对公共资源进行保护机制的一种策略,就是一个计数器(保护共享资源的)

system V 允许创建一批信号量,一个也可以

    key_t key = GetKey();
    int semid = semget(key, 1, IPC_CREAT | IPC_EXCL);

 信号量的生命周期随内核,我们可以手动删:ipcrm -s semid

也可以使用semctl

信号量的创建与删除:

    key_t key = GetKey();
    int semid = semget(key, 1, IPC_CREAT | IPC_EXCL);
    std::cout << "semid: " << semid << std::endl;

    sleep(4);
    semctl(semid, 0, IPC_RMID);

 操作系统也要管理多个信号量集合,方法是先描述再组织

为了让进程间通信->多个执行流看到同一份资源(即公共资源)->存在并发访问问题->数据不一致问题->所以需要保护->互斥和同步策略(用户/OS提供)

互斥:任何一个时刻只允许一个执行流(进程)访问公共资源(由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥)

同步:多个执行流执行的时候,按照一定的顺序执行

被保护起来的公共资源,称为临界资源(系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。)

访问该临界资源的代码,成为临界区

维护临界资源其实就是在维护临界区

如何理解信号量

先来举一个小栗子:看电影

电影院和内部座位就是多个人共享的资源--公共资源,我们买票的本质是对资源的预订机制

现有int count = 100表示票的数量,也就是一个计数器,表示公共资源的个数(公共资源可能会被拆分成多份资源)买票成功则count--,那么本场电影该座位就属于购买者了(也就是说当购票成功的那一刻,你就已经拥有座位了,而不是当你真正坐下一个座位时,才拥有),由此:

信号量是表示资源数目的计数器,每一个执行流想要访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行--操作,本质上只要--成功,就完成了资源的预订机制

如果申请不成功,执行流被挂起阻塞

释放资源则计数器++

再来个栗子:超级vvvip放映厅,内部只有一个座位,计数器int sem = 1

那么只允许一个人进入,当此人退出后才能换别人进入,也就是互斥

此时的信号量就是二元信号量--互斥锁--完成互斥功能

下面来分析细节问题:

1 每个进程要访问公共资源,也就意味着每个进程都得先看到同一个信号量资源(只能由OS提供),那么信号量也要纳入到IPC体系中

2 信号量本质也是公共资源,信号量计数器的--操作,是原子性的(要么不做,要么做完)

   称之为P操作,释放资源的++操作,也是原子性的,称之为V操作

process.cc 

int main()
{
    while(true)
    {
        std::cout<<"running...."<<std::endl;
        sleep(1);
    }
    return 0;
}

让进程在后台运行:./process &

指令依然可以识别,但是ctrl+c无法终止进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值