Linux篇:进程间通信

news2024/12/27 12:32:07

一、进程间通信原理:

1、通信是有成本的:两个或者多个进程,实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高。

2、进程间通信的方式:
①基本数据
②发送命令
③某种协同
④通知
......

3、进程间通信的本质:必须让不同的进程,看到同一份资源——特定形式的内存空间。这个“资源”一般是操作系统提供(第三方空间)(为什么不是我们两个进程中的一个呢?因为这样会破坏进程独立性)。进程访问这个空间进行通信,本身就是访问操作系统!而进程代表的就是用户。“资源”从创建,使用(一般)到释放,都要使用系统调用接口。所以从底层设计,从接口设计,都要从操作系统独立设计。

4、一般操作系统会有一个独立的通信模块,它隶属于文件系统,称为IPC通信模块。进程间通信是有标准的——system V &&possix。

5、基于文件级别的通信方式——管道。

二、(匿名)管道(本质是文件):让不同的进程,看到同一份资源。

1. 原理:
 · 父进程fork创建出子进程,子进程会拷贝父进程的文件描述符表,此时,父进程与子进程都会有相应的读写端指向同一个文件。此时根据要求关闭父进程与子进程相应的读写端,来形成单向通信的信道。

 · 同一个文件是内存级的,每个文件都存在自己的缓冲区,如果双方想向自己的缓冲区中写入,子进程就可以通过缓冲区读取,实现进程间通信。正因为其只能进行单向通信,故称其为管道。

 · 若进行双向通信用多个管道即可。

 · 必须有血缘关系的进程才能通信,常用于父子关系,兄弟关系和爷孙关系也可。

 · 管道是有固定大小的,在不同内核里,大小可能有差别。

2、接口:

输出型参数:将文件的文件描述符数字带出来,让用户使用。
pipefd[0]:读下标。
pipefd[1]:写下标。

//testPipe.cc

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

#define N 2
#define NUM 1024

using namespace std;

//child:用户级缓冲区拷贝到文件级缓冲区
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;

    char buffer[NUM];
    while(true)
    {
        //构建发送字符串
        buffer[0] = 0;//字符串清空,只是为了提醒阅读代码的人,我把这个数组当做字符串了
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);//int snprintf(char* str, size_t size, const char *format, ...);
        cout << buffer << endl;
        //发送/写入给父进程,system call
        write(wfd, buffer, strlen(buffer));//ssize_t write(int fd, const void * buf, size_t count);//向文件写入不需要+1
        sleep(1);
    }
}

//father:内核级缓冲区拷贝到应用层缓冲区
void Reader(int rfd)
{
    char buffer[NUM];

    while(true)
    {
        buffer[0] = 0;
        //system call
        ssize_t n = read(rfd, buffer, sizeof(buffer));//ssize_t read(int fd, void *buf, size_t count);//sizeof != strlen, 代表buffer缓冲区大小
        if(n > 0)
        {
            buffer[n] = 0;// 0 == '\0'
            cout << "father get a message[" << getpid() << "]#" << buffer << endl;
        }
        //TODO
    }
}

int main()
{
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if(n < 0) return 1;
    
    //cout << "pipefd[0]:" << pipefd[0] << " , pipefd[1]: " << pipefd[1] << endl;

    //child -> w, father -> r
    pid_t id = fork();
    if(id < 0) return 2;
    if(id == 0)
    {
        //child
        close(pipefd[0]);

        //IPC code
        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }

    //father
    close(pipefd[1]);

    //IPC code
    Reader(pipefd[0]);

    pid_t rid = waitpid(id, nullptr, 0);
    if(rid < 0) return 3;

    close(pipefd[0]);
    return 0;
}
//Makefile

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

 3、编码实现:

管道的五大特征:
①具有血缘关系的进程进行进程间通信。
②管道只能单向通信。
③多执行流共享的难免出现访问冲突的问题。临界资源竞争的问题。所以父子进程是会进程协同,同步与互斥,保护管道文件的数据安全。
④管道是面向字节流的
⑤那是基于文件的,而对象的生命周期是随进程的

管道四种的情况:
①读写端正常管道,如果为空,读端就要堵塞
②读写端正常,管道如果被写满,写端就要阻塞
③读端正常读写端关闭,读端就会读到零,表明读到的文件(pipe)结尾不会被阻塞。
④写端是正常写入,读端关闭了。操作系统就要通过信号杀掉正在写入的进程。转为③(所以子进程写入父进程读取)(操作系统是不会做低效浪费等类似的工作的,如果做了,就是操作系统的bug)。

4、管道的应用场景:

①自定义shell:指令的判断
a.分析输入的命令行字符串,获取有多少个|命令,打散多个子命令字符串。
b.malloc申请空间,pipe先申请多个管道。
c.循环创建多个子进程,每一个子进程的重定向情况。最开始:输出重定向,1->指定的一个管道的写端。中间:输入输出重定向,0标准输入重定向到上一个管道的读端,标准输出重定向到下一个管道的写端。最后一个:将输入重定向将标准输入重定向到最后一个管道的读端。
d.分别让不同的子进程执行不同的命令---exec*(exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向)。

②简易版本的进程池:降低系统调用和成本。

ProcessPool.cc

#include "Task.hpp"
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>

const int processnum = 10;

//void LoadTask(std::vector<task_t> *tasks);

std::vector<task_t> tasks;

// 先描述
class channel
{
public:
    channel(int cmdfd, pid_t slaverid, const std::string &processname)
    :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {}
public:
    int _cmdfd;              // 发送任务的文件描述符
    pid_t _slaverid;         // 子进程PID
    std::string _processname;// 子进程的名字————方便我们打印日志
};

void slaver()
{
    // read(0)
    while(true)
    {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int)); // 如果父进程不给子进程发送数据呢?阻塞等待!
        if(n == sizeof(int))
        {
            //执行cmdcode对应的任务列表
            std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode: " << cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
        }
        if(n == 0) break;
    }
}

//编码规范:
//输入: const &
//输出: *
//输入输出:&
void InitProcessPool(std::vector<channel> *channels)
{
    // version 2 : 确保每一个子进程都只有一个写端
    std::vector<int> oldfds;
    for(int i =0; i < processnum; i++)
    {
        int pipefd[2]; // 临时空间
        int n = pipe(pipefd);
        assert(!n);
        (void)n;

        pid_t id = fork();
        if(id == 0) // child
        {
            std::cout << " child: " << getpid() << " close history fd: ";
            for(auto fd : oldfds) {
                std::cout <<  fd << " ";
                close(fd); 
            }
            std::cout << "\n";
            close(pipefd[1]);
            dup2(pipefd[0], 0);
            close(pipefd[0]);
            slaver();
            std::cout << "process : " << getpid() << "quit" << std::endl;
            // slaver(pipefd[0]);
            exit(0);
        }
        // father
        close(pipefd[0]);

        // 添加channel字段了
        std::string name = "process-" + std::to_string(i);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);

        sleep(1);
    }
}

void Debug(const std::vector<channel> &channels)
{
    // test
    for(const auto &c : channels)
    {
        std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
    }
}

void Menu()
{
    std::cout << "#####################################" << std::endl;
    std::cout << "##### 1、刷新日志   2、刷新野怪 #######" << std::endl;
    std::cout << "##3、检测软件是否更新  4、更新血量蓝量##" << std::endl;
    std::cout << "############## 0、退出 ###############" << std::endl;
    std::cout << "#####################################" << std::endl;
}

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    //int cnt = 5;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;

        //1、选择任务
        //int cmdcode = rand()%tasks.size();
        int cmdcode = select - 1;
        
        //2、选择进程:负载均衡(随机数,轮转)
        //int processpos = rand()%channels.size();
        std::cout << " father say: " << " cmdcode: " << cmdcode << " already sendto " << channels[which]._slaverid << " process name: " << channels[which]._processname << std::endl;

        //3、发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;
        which %= channels.size();

        //cnt--;
        //sleep(1);
    }
}

void QuitProcess(const std::vector<channel> &channels)
{
    // version1
    int last = channels.size()-1;
    for(int i = last; i >= 0; i--)
    {
        close(channels[i]._cmdfd);
        waitpid(channels[i]._slaverid, nullptr, 0);
    }
    // for(const auto &c : channels) close(c._cmdfd);
    // //sleep(5);
    // for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0);
    // //sleep(5);
}

int main()
{
    LoadTask(&tasks);
    srand(time(nullptr)^getpid()^1023); // 种一颗随机数种子
    // 再组织
    std::vector<channel> channels;
    // 1、初始化
    InitProcessPool(&channels);
    Debug(channels);

    // 2、开始控制子进程
    ctrlSlaver(channels);

    // 3、清理收尾
    QuitProcess(channels);

    return 0;
}
//Task.hpp

#pragma once

#include <iostream>
#include <vector>

typedef void (*task_t)();

void task1()
{
    std::cout << "lol 刷新日志" << std::endl;
}

void task2()
{
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}

void task3()
{
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}

void task4()
{
    std::cout << "lol 用户释放技能,更新血量蓝量" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}
##Makefile

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

以上为具有血缘关系的进程进行进程间通信
如果毫不相关的进程进行进程间通信呢?

三、命名管道:

1、 理解:如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开同一个文件。

 · 进程间通信的前提:先让不同进程看到同一份资源。

 · 管道文件不需要刷盘,只是内存级文件。

 · 管道怎么打开同一个文件?为什么要这么做?
通过同路径下的同一个文件名(路径+文件名具有唯一性)的方式,让不同进程看到同一份资源,进而实现不同进程间通信,所以叫命名管道。

2、编码:

 ①模拟实现命名管道的应用场景:

//server.cc

#include "comm.hpp"

using namespace std;

int main()
{
    Init init;

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

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

    //开始通信
    while(true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            cout << "client quit, me too!\n" << endl;
            break;
        }
        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;
}
//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"
#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);
        }
    }
};
##Makefile

.PHONY:all
all:server client

server:server.cc
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f server client

②模拟实现日志:输出时间,日志的等级,日志内容,文件的名称和行号。

//server.cc

#include "comm.hpp"
#include "log.hpp"

using namespace std;

int main()
{
    Init init;
    Log log;
    log.Enable(Classfile);

    //打开管道
    int fd = open(FIFO_FILE, O_RDONLY);// 等待写入方打开之后,自己才会打开文件,向后执行,open 阻塞了!
    if(fd < 0)
    {
        // log.logmessage(Fatal, "error string: %s, error code: %d",strerror(errno), errno);
         log(Fatal, "error string: %s, error code: %d",strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    // log.logmessage(Info, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    // log.logmessage(Warning, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    // log.logmessage(Fatal, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    // log.logmessage(Debug, "server open file done, error string: %s, error code: %d",strerror(errno), errno);

    log(Info, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    log(Warning, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    log(Fatal, "server open file done, error string: %s, error code: %d",strerror(errno), errno);
    log(Debug, "server open file done, error string: %s, error code: %d",strerror(errno), errno);

    //开始通信
    while(true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            // log.logmessage(Debug, "client quit, me too, error string: %s, error code: %d",strerror(errno), errno);
            log(Debug, "client quit, me too, error string: %s, error code: %d",strerror(errno), errno);
            break;
        }
        else break;
    }
    close(fd);

    return 0;
}
//client

#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;
}
//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"
#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);
        }
    }
};
//log.hpp

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method)
    {
        printMethod = method;
    }

    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info: return "Info";
            case Debug: return "Debug";
            case Warning: return "Warning";
            case Error: return "Error";
            case Fatal: return "Fatal";
            default: return "None";
        }
    }

    // void logmessage(int level, 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);
    // }

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


// 拓展:可变参数(可变参数必须要至少一个实参)
// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int);
//         n--;
//     }

//     va_end(s);
//     return sum;
// }
//Makefile

.PHONY:all
all:server client

server:server.cc
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f server client

四、System共享内存

1、原理:每个进程都要有自己对应的地址空间,有自己对应的tast_struct,通过页表将自己地址空间中的内容映射到物理内存中。在物理内存中创建一块共享空间,共享空间通过页表映射到,进程的共享区当中,并给应用层返回一个所对应连续内存空间的起始虚拟地址。从此两个进程就可以通过各自页表访问到同一块物理内存了。

2、申请共享内存步骤:
①申请内存
②挂接到进程地址空间
③返回首地址

释放共享内存步骤:去关联,释放共享内存

3、上面的操作不是进程直接做的(进程具有独立性),而是直接由操作系统来做(系统调用)。操作系统中内核结构体描述共享内存,再组织,从而管理所有内存。

4、接口:

创建共享内存(返回消息队列标识符):

获取key: 

让当前进程和指定共享内存链接起来和去关联:

 删除共享内存:

 

问题1:共享内存标识符shmflg
IPC_CREAT(单独):如果你申请的共享内存不存在就创建,存在,就获取并返回。
IPC_CREAT|IPC_EXCL:你申请的共享内存不存在,就创建,存在,就出错并返回。确保如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。(IPC_EXCL不单独使用)

问题2:不过你怎么保证让不同的进程看到同一个内存资源呢?你怎么知道这个内存资源存在还是不存在呢?
谈谈key
①key是一个数字,这个数字是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
②第一个进程可以通过key创建共享进程。第二个之后的进程,只要拿着同一个key,就可以和第一个进程看到同一个共享内存了。
③对于一个已经创建好的共享内存,key在哪?key在共享内存的描述对象中。
④第一次创建的时候,必须有一个key了。怎么有?ftok,它是一套算法——通过pathname和proj_id进行数值计算即可!(pathname和proj_id由用户自由指定。)
⑤key和路径都是唯一的。

问题三:key和shmid区别:
key:操作系统的标定唯一型(只在创建管理内存时使用)。
shmid:只在进程内表示资源的唯一性。

问题四:
①共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)。
②接口:
查看所有的共享内存:ipcs -m
删除管理内存:ipcrm -m shmid

5、共享内存的特性:
①共享内存没有同步互斥之类的保护机制。
②共享内存是所有的进程间通信中速度最快的(由地址空间映射的方式,拷贝少,速度快)。
③共享内存内部的数据由用户自己维护。
④共享内存没有同步机制。

6、共享内存的属性:

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

7、用共享内存实现进程间通信:

// 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 "log.hpp"

using namespace std;

Log log;
//共享内存大小一般的大小一般建议是4096的整数倍
//4097,实际上操作系统给你的是4096*2的大小
const int size = 4096;
const string pathname = "/home/zsx";
const int proj_id = 0x6666;

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 sucess, key is : 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag)
{
    key_t k = GetKey();
    // int shmid = shmget(k, size, IPC_CREAT | IPC_EXCL | 0666);
    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);
}

#endif
// log.hpp

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method)
    {
        printMethod = method;
    }

    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info: return "Info";
            case Debug: return "Debug";
            case Warning: return "Warning";
            case Error: return "Error";
            case Fatal: return "Fatal";
            default: return "None";
        }
    }

    // void logmessage(int level, 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);
    // }

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


// 拓展:可变参数
// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int);
//         n--;
//     }

//     va_end(s);
//     return sum;
// }
//processa.cc

#include "comm.hpp"

extern Log log;

int main()
{
    // sleep(3);

    int shmid = CreateShm();
    // log(Debug, "create shm done");
    // sleep(5);

    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    // log(Debug, "attach shm done, shmaddr: 0x%x", shmaddr);
    // sleep(5);

    struct shmid_ds shmds;

    // ipc code
    // 一旦有人把数据写入共享内存,不需要经过系统调用,直接就能看到数据了。
    while(true)
    {
        cout << "client say@ " << shmaddr << endl; // 直接访问共享内存
        sleep(1);

        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        printf("0x%x\n", shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode<< endl;
    }

    shmdt(shmaddr);
    // log(Debug, "detach shm done, shmaddr: 0x%x", shmaddr);
    // sleep(5);

    shmctl(shmid, IPC_RMID, nullptr);
    // log(Debug, "destory shm done, shmaddr: 0x%x", shmaddr);
    // sleep(5);

    return 0;
}
//processb.cc

#include "comm.hpp"

int main()
{
    // sleep(3);

    int shmid = GetShm();
    // log(Debug, "create shm done");
    // sleep(5);

    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    // log(Debug, "attach shm done, shmaddr: 0x%x", shmaddr);
    // sleep(5);

    // ipc code
    //一旦有了共享内存,挂接到自己的地址空间中,直接把它当成自己的内存空间来用即可。
    //不需要调用系统调用
    while(true)
    {
        cout << "Please Enter@ ";

        // char buffer[1024]; // 缓冲区(此处没必要),因为有内存
        // fgets(buffer, sizeof(buffer), stdin);
        // memcpy(shmaddr, buffer, strlen(buffer)+1); // 当作字符串

        fgets(shmaddr, 4096, stdin);
    }

    shmdt(shmaddr);
    // log(Debug, "detach shm done, shmaddr: 0x%x", shmaddr);
    // sleep(5);

    return 0;
}
##Makefile

.PHONY:all
all:processa processb

processa:processa.cc
	g++ -o $@ $^ -g -std=c++11
processb:processb.cc
	g++ -o $@ $^ -g -std=c++11

.PHONY:clean
clean:
	rm -f processa processb

当然,我们也可以利用命名管道实现共享内存的同步,有兴趣的同学可以尝试一下!

8、mmap函数:也是共享内存的一种。(选学)

五、消息队列(了解):

1、原理:
①必须让不同的进程看到同一个队列。
②允许不同的进程向内核中发送带类型的数据块。

2、接口:

①创建共享内存(返回消息队列标识符):

②释放共享内存:

 

③发送/接收消息队列: 

④查看所有的共享内存:ipcs -q
删除管理内存:ipcrm -m msgid 

六、IPC内核中的数据结构设计:
①在操作系统中,所有的IPC资源,都是整合进操作系统里的IPC模块中的!
②创建共享内存/消息队列就要创建对应的数据结构。这些数据结构的第一个字段类型都是ipc_perm,管理这些数据结构是通过数组(struct ipc_perm *array[])来管理的。创建共享内存/消息队列时,操作系统就要创建对应的数据结构,并将第一个字段的地址填入数组(结构体数据类型可以不一样,因为第一个字段的类型都一样)。
③从此往后,管理操纵系统中所有的Ipc资源,只要先描述,然后对所有资源进行的增删查改转化成对该数组的增删查改。
④当然,也可通过用户输入的key,找到每一个对应的ipc资源,通过比较第一个字段的ipc_perm中的key,确认进程是否已经被创建(新旧)。
⑤其中该数组的数组下标为xxxid(shmid/msgid……)(是线性递增的)
⑥当用户未来尝试访问某种资源的时候,只要将对应的地址强转成指定的类型,就可以自由访问整个结构中的任意类型。
⑦而操作系统为什么能区分指针指向的对象的类型呢?
ipc_perm是操作系统在应用层上的结构体,而在内核结构中,它的数据结构为kern_ipc_perm。它在第一个字段中增加了一种类型标志位,来让代码区分它自己是哪种ipc资源。
⑧这实际上是多态技术的一种体现。其中ipc_perm就是基类,shm_perm/msg_perm就是子类。

七、信号量:(了解)

1、原理概念

1、)当我们的进程a正在向共享内存写入时,写入了一部分就被进程b拿走了,就会导致双方发和收的数据不完整——数据不一致问题(因为共享内存没有保护机制)。
而管道不会(因为管道在通信过程中有原子性保证和同步互斥)。

2、)
①AB看到的同一份资源共享资源,如果不加保护,会导致数据不一致问题
②加锁--互斥访问--任何时刻,只允许一个执行流访问(就是执行访问)共享资源——互斥
③共享的,任何时刻只允许一个执行流访问的资源称为临界资源---一般是内存空间。
④举例:100行代码,5到10行代码才在访问临界资源。那我们访问临界资源的代码称为临界区

3、)解释一个现象:
多进程、多线程、并发打印时,显示器上的信息是错乱的、混乱的、和命令行混在一起的。这就是数据不一致问题。

4、)理解信号量:信号量/信号灯的本质是一把计数器,用来描述临界资源中资源数量的多少。
①申请计数器成功就表示,我具有访问资源的权限了。
②申请了计数器资源。我当前访问我要的资源了吗?
没有。申请了计数器资源是对资源的预定机制。
③计数器可以有效保证进入共享资源的执行流的数量。
④所以每一个,执行刘翔访问共享资源中的一部分的时候不是直接访问,而是先申请计数器资源。这个计数器就叫做信号量。
⑤我们把值只能为01两态的计数器叫做二元信号量,本质就是一个锁。
⑥让计数器为1,资源为1的本质,其实就是不将临界资源分成很多块了,而是当做一个整体整体申请,整体释放(比如管道)。
⑦要访问临界资源,需要申请信号量计数器资源。而信号量计数器也是共享资源。它要先保证自己的安全。
cnt--:C语言一条语句变成汇编,多条(3)汇编语句。
a.cnt变量的内容,内存->CPU寄存器
b.cpu内进行--操作
c.将计算结果写回cnt变量的内存位置。
进程在运行的时候可以随时被切换,多执行流都访问这个变量时,可能会出错。(线程部分详细解释)
⑧申请信号量本质是对计数器--(P操作)。
释放资源,释放信号量,本质是对计数器进行++操作(V操作)。
申请和释放PV操作——原子的:一件事情要么不做,要做就做完(两态的),没有“正在做”这样的概念。用一条汇编语句即可实现。

总结:信号量本质是一把计数器。对计数器匹配的操作叫pv操作---原子的。执行流申请资源,必须先申请信号量资源,得到信号量之后才能访问临界资源。信息量值10两态的二元信号量就是互斥功能。申请信号量的本质是:对临界资源的预定机制。

2、接口:  

①申请信号量:

②控制信号量:

③设定信号量:

 

 注:system v的接口是最难的:多线程部分来进行操作说明。

3、信号量凭什么是进程间通信的一种?
①通信不仅仅是通信数据,也在于互相协同。
②要协同,本质也是通信信号量首先被所有的通信进程看到。

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

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

相关文章

2.HTML进阶

第2章-HTML进阶 Objective(本课目标) 了解表格标签的使用掌握列表标签的使用掌握表单标签的使用 1. 表格 table(会使用) 表格作用&#xff1a; 存在即是合理的。 表格的现在还是较为常用的一种标签&#xff0c;但不是用来布局&#xff0c;常见显示、展示表格式数据。 因为它…

Motion Plan之轨迹生成笔记 (2)

Motion Plan之搜索算法笔记 Motion Plan之基于采样的路径规划算法笔记 Motion Plan之带动力学约束路径搜索 什么是基于优化的轨迹生成 Optimization-Based Trajectory Planning&#xff08;基于优化的轨迹规划&#xff09;是一种常用的方法&#xff0c;用于生成自动化系统&am…

【Flutter】vs2022上开发flutter

在vs上开发flutter&#xff0c;结果扩展仓库上没办法找到Dart&#xff0c;Flutter。 在 这 搜索Dart时也无法找到插件。 最后发现是安装工具出错了 安装了 开发需要的是

电商类直播介绍

电商直播是一种购物方式&#xff0c;通过直播技术向消费者展示商品&#xff0c;并引导其进行购买。在法律上&#xff0c;电商直播属于商业广告活动&#xff0c;主播需要根据具体行为承担“广告代言人"“广告发布者"或“广告主"的责任。 电商直播的特点在于其更…

网络机房的功能有哪些?

网络机房的功能主要包括&#xff1a; 信息存储和管理&#xff1a;机房作为信息系统的核心&#xff0c;需要提供可靠的存储和管理能力&#xff0c;包括服务器、存储设备、备份系统等硬件设备&#xff0c;以及数据备份、数据迁移、容灾等管理方法和技术。网络连接和通信&#xf…

字符串经典基础面试题

关卡名 字符串经典基础面试题 我会了✔️ 内容 1.理解字符串反转的处理方法 ✔️ 2.熟练掌握回文串的判断方法 ✔️ 3.掌握字符串中搜索第一个唯一字符的方法 ✔️ 4.掌握判断是否互为字符串重排的处理技巧 ✔️ 1 反转的问题 我们知道反转是链表的一个重要考点&#xf…

03_W5500TCP_Client

上一节我们完成了W5500网络的初始化过程&#xff0c;这节我们进行TCP通信&#xff0c;w5500作为TCP客户端与电脑端的TCP_Server进行通信。 目录 1.TCP通信流程图&#xff1a; tcp的三次握手&#xff1a; tcp四次挥手&#xff1a; 2.代码分析&#xff1a; 3.测试&#xff1a…

Mysql综合案例练习<1>

MySql综合案例练习<1> 题目一题目二题目三题目四题目五题目六题目七题目八题目九题目十题目十一题目十二题目十三题目十四题目十五题目十六题目十七题目十八题目十九 题目一 创建数据库test01_library 创建表 books&#xff0c;表结构如下&#xff1a; CREATE DATABASE …

Linux操作系统 3.Linux用户和权限

一、认知root用户&#xff08;超级管理员&#xff09; Windows、MacOS、Linux均采用多用户的管理模式进行权限管理 在Linux系统中&#xff0c;拥有最大权限的账户名为&#xff1a;root&#xff08;超级管理员&#xff09; 之前我们一直使用的是普通的用户 root用户拥有最大的系…

【STM32F103】USART通用同步异步收发器

串行通信 通信分为串行通信和并行通信&#xff0c;区别如下&#xff0c;同样是发送0101的数据&#xff1a; 可以看的出来&#xff0c;串行通信的优点是消耗的数据线会小一些。 而并行通信的优点是传输的速度快。 通常我们会选择使用串行通信来进行设备间的通信&#xff0c;这…

参考信号速度变化存在跳跃时容易发生不稳定的阻抗调节

问题描述 当参考信号速度存在跳跃变化时&#xff0c;阻抗调节系统容易发生不稳定。这是因为阻抗调节系统需要根据参考信号的速度来调整其输出阻抗&#xff0c;以匹配负载阻抗&#xff0c;从而保持系统的稳定性。 当参考信号速度突然变化时&#xff0c;阻抗调节系统可能无法及…

智能安防无人机——一种安防巡检新方案

在高新技术的推动下&#xff0c;安防无人机在监控、巡逻等领域的使用频率越来越高&#xff0c;逐渐成为安防救援的重要帮手。安防无人机作为城市安全应急保障体系的重要组成部分&#xff0c;在未来将变得不可或缺。 一、安防无人机的定义及构成 复亚智能无人机全自主巡飞系统由…

Redis——简单动态字符串(Simple Dynamic Strings,SDS)

简单动态字符串&#xff08;Simple Dynamic Strings,SDS&#xff09;是Redis的基本数据结构之一&#xff0c;用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数&#xff0c;且在此基础上保证了二进制安全。 1、数据结构 在了解SDS源码前&#xff0c;我们先思考一个问…

Android平板还能编程?Ubuntu本地安装code-server远程编程写代码

文章目录 1.ubuntu本地安装code-server2. 安装cpolar内网穿透3. 创建隧道映射本地端口4. 安卓平板测试访问5.固定域名公网地址6.结语 1.ubuntu本地安装code-server 准备一台虚拟机,Ubuntu或者centos都可以&#xff0c;这里以VMwhere ubuntu系统为例 下载code server服务,浏览器…

PMSM转速电流双闭环调速仿真simulink

威♥关注“电击小子程高兴的MATLAB小屋”获取专享优惠 PMSM永磁同步电机PI双闭环&#xff08;速度&#xff0c;电流调节&#xff09;SVPWM矢量matlab-simulink仿真 1.模型简介 本仿真模型基于MATLAB/Simulink&#xff08;版本MATLAB 2017Rb&#xff09;软件。建议采用matlab…

Large Language Models areVisual Reasoning Coordinators

目录 一、论文速读 1.1 摘要 1.2 论文概要总结 二、论文精度 2.1 论文试图解决什么问题&#xff1f; 2.2 论文中提到的解决方案之关键是什么&#xff1f; 2.3 用于定量评估的数据集是什么&#xff1f;代码有没有开源&#xff1f; 2.4 这篇论文到底有什么贡献&#xff1…

多路径传输(MPTCP MPQUIC)数据包调度研究总结

近些年来&#xff0c;以5G和Wifi6为代表的无线通信技术发展迅速&#xff0c;并已经在全世界实现了大规模部署。此外&#xff0c;智能手机等移动设备不断迭代更新&#xff0c;其网络通信能力也持续演进&#xff0c;使得应用同时利用多个不同网卡在多条不同物理链路上&#xff08…

从遍历到A星寻路算法

在游戏当中&#xff0c;经常需要找一个点到其它点的路径。在之前的一篇博文(地图编辑器开发&#xff08;三&#xff09;)中也有使用到到A*寻路算法。我们期望能找到最短的路径&#xff0c;同时也需要考虑到查找路径的时间消耗。游戏中的地图可以图的数据结构来表示&#xff0c;…

【科普】什么是电子印章? PS抠的印章能用吗?

各类扣章教程一搜一大堆&#xff0c;说明大家对于电子印章使用需求很高。不过要谨记&#xff0c;不要随便抠印章用于公文、证明书、合同协议、收据发票等电子文件&#xff0c;否则可能会吃牢饭。 单是一张电子化的图片是不具备合法性的。那有的人就要问了&#xff0c;我见到的…

采样率越高噪声越大?

ADC采样率指的是模拟到数字转换器&#xff08;ADC&#xff09;对模拟信号进行采样的速率。在数字信号处理系统中&#xff0c;模拟信号首先通过ADC转换为数字形式&#xff0c;以便计算机或其他数字设备能够处理它们。 ADC采样率通常以每秒采样的次数来表示&#xff0c;单位为赫…