Linux进程间通信

1、前置知识

进程间通信是什么?——两个或者多个进程实现数据层面的交互。因为进程独立性的存在,所以进程间通信的成本会比较高。

为什么要进行进程间通信?——因为进程之间可能需要:1、发送基本数据。2、发送命令。3、某种协同。4、通知另一个进程。…

怎么办?
a、进程间通信的本质:让不同进程看到同一份资源。

b、资源是什么?——特定形式的内存空间。

c、这个资源谁提供?——一般是操作系统。为什么不是两个进程中的一个呢?——如果某一个进程提供,那么这个资源属于谁?本质上属于该进程,破坏进程的独立性。所以需要第三方空间。

d、进程访问这个空间进行通信,本质就是访问操作系统!进程代表的是用户,所以这个空间从创建、使用、释放需要通过系统调用来完成。
所以从底层、接口设计都要由操作系统独立设计。所以一般操作系统有一个通信模块,这个通信模块隶属于文件系统——IPC通信模块。
另外进行进程间通信的方式很多,所以还需要定制标准,有两套标准:system V和posix。system V用于本机内部通信,posix用于网络通信。
System V IPC包括:System V消息队列、System V共享内存、System V信号量。
Posix IPC包括:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。

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


2、匿名管道

2.1、管道的原理

在这里插入图片描述
进程在操作系统中有一个task_struct对象,这个对象里面有struct files_struct* flies的指针,指向了struct files_struct,而在struct files_struct里面有文件描述符表-fd_array。fd_array是struct file*的指针数组,指向一个一个的struct file对象。进程默认会打开三个文件,0对应键盘文件,1、2对应显示器文件。

过去我们打开磁盘中的一个文件,系统会创建一个struct file对象,根据文件描述符的分配规则,寻找最小的没有被使用的数组下标,所以fd_array下标为3的指针指向了我们打开的文件。struct file里面有指向inode对象的指针,inode里面保存了文件的属性信息。也有指向file_operators的指针,file_operators里面是函数指针,对应文件的读写方法,实现一切皆文件。并且还有一个文件的页缓冲区。
那么其实我们可以让两个文件都打开磁盘中的同一个文件,然后一个进行写入,另一个进行读取,从技术的角度来说是可以做到的。但是这样要跟外设(磁盘)打交道,那么效率就会比较低。所以我们是不是可以直接在系统中创建一个struct file对象,这个对象也有对应的读写方法、属性、缓冲区。但是这个文件并不是磁盘上的文件,而是内存级别的文件。我们进程向这个文件写入数据,写入到缓冲区中,然后其他进程就通过缓冲区读取数据。


创建子进程后:
在这里插入图片描述
父进程以r方式打开这个文件,然后创建子进程,子进程会以父进程pcb为模板初始化自己的pcb。现在问题是struct files_struct这个对象会拷贝吗?答案是会的,直接继承父进程的,并且它们指向的struct file对象也是一样的。
通信本质需要先让不同的进程看到同一份资源!管道就是文件!
现在父子进程不就看到同一份资源了吗?父子进程看到同一个struct file对象,那么就可以让一个进程往里面写,另一个进程从里面读。但是现在问题是:父进程以读方式打开,子进程继承后也是只读的,如何实现通信呢?所以父进程就不能这么草率的打开文件了。


在这里插入图片描述
现在父进程打开两个文件,一个struct file用来读取数据,另一个用来写入数据。对于父进程来说,返回的fd=3就是读,4就是写。然后创建子进程后,子进程继承,所以子进程文件描述符3也是读,4也是写。
但是,管道只能进行单向通信。
所以我们需要根据情况让父子进程关闭读写文件。例如我想让子进程写父进程读,那么对于父进程来说就需要close(4),对于子进程就需要close(3)。虽然这不是强制规定的,但是我们建议这么做。
那么为什么不直接用一个struct file同时表示读写呢,因为读写文件会有偏移量的概念,假设现在向文件写入一部分数据,那么当前就会偏移到写入的位置,但是你读取要从头开始读入,所以你就读不到数据。

关闭后:
在这里插入图片描述

如图,这样就实现了单向通信。
如果我想进行双向通信呢?——多个管道。
如果两个进程没有任何关系,能否用上面的原理进行通信?——不能,必须是父子关系、兄弟关系,爷孙关系…。使用管道进行通信两个进程之间需要具有血缘关系,常用于父子。

这个文件有名字吗?没有,所以是匿名管道。

至此,通信了吗?——没有,这只是建立了通信信道,为什么如此费劲?——因为进程具有独立性,通信是有成本的。


2.2、编码实现

在这里插入图片描述
在这里插入图片描述

使用系统调用接口pipe创建管道,pipe有个参数为pipefd的数组。这个pipefd是一个输出型参数,它会将文件描述符数字带出来,让用户使用。pipefd[0]:读下标。pipefd[1]:写下表。
pipe成功返回0,失败返回-1并且错误码被设置。

编码实现进程间通信:我们让子进程进行写入,父进程进行读取。

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

using namespace std;

#define SIZE 1024

void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;
    char buff[SIZE] = {0};
    while (true)
    {
        buff[0] = 0;
        snprintf(buff, sizeof(buff), "%s-%d-%d", s.c_str(), self, number++);
        write(wfd, buff, strlen(buff));
        sleep(1);
    }
}

void Reader(int rfd)
{
    char buff[SIZE] = {0};
    while (true)
    {
        buff[0] = 0;
        ssize_t n = read(rfd, buff, sizeof(buff)-1);
        if (n > 0)
        {
            buff[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buff << endl;
        }
    }
}

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n < 0) return 1;

    pid_t id = fork();
    if (id < 0) return 2;
    if (id == 0)
    {
        close(pipefd[0]);
        Writer(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    
    close(pipefd[1]);
    Reader(pipefd[0]);

    pid_t rid = waitpid(id, NULL, 0);
    if (rid < 0) return 3;
    close(pipefd[0]);
    return 0;
}

在这里插入图片描述
注意到,我们代码中让子进程每隔一秒写入,父进程并没有休眠,但是父进程并不会读到垃圾数据。

结论1:读写端正常,管道如果为空,读端就要阻塞。


修改代码,让父进程每次都先休眠3秒再读取数据,而子进程一直向管道写入数据。通过子进程打印的number我们就可以观察子进程正在写入数据。

void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;
    char buff[SIZE] = {0};
    while (true)
    {
        buff[0] = 0;
        snprintf(buff, sizeof(buff), "%s-%d-%d", s.c_str(), self, number++);
        write(wfd, buff, strlen(buff));
        cout << number << endl;
        //sleep(1);
    }
}

void Reader(int rfd)
{
    char buff[SIZE] = {0};
    while (true)
    {
        sleep(3);
        buff[0] = 0;
        ssize_t n = read(rfd, buff, sizeof(buff)-1);
        if (n > 0)
        {
            buff[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buff << endl;
        }
    }
}

在这里插入图片描述
结果为:子进程一下子写入了很多数据,然后就停止了写入,父进程一次读取buff大小的数据。那么说明管道是有固定大小的,我们通过修改代码来看看管道有多大

在这里插入图片描述
在这里插入图片描述
我们让子进程每次只写入一个字符,然后让number++,并且输出。最后运行结果发现管道的大小为65536字节,也就是64KB。

结论2:读写端正常,管道如果被写满了,写端就要阻塞。
通过结论1、2我们知道,管道是自带同步与互斥机制的,保护了管道文件的数据安全。
另外我们可以看到管道是面向字节流的。


修改代码,子进程写入5次后break退出循环,关闭写端文件。父进程读取数据,并不断打印read返回值。

void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;
    char buff[SIZE] = {0};
    while (true)
    {
        buff[0] = 0;
        snprintf(buff, sizeof(buff), "%s-%d-%d", s.c_str(), self, number++);
        write(wfd, buff, strlen(buff));
        // cout << number << endl;
        if (number >= 5) break;
        sleep(1);
    }
}

void Reader(int rfd)
{
    char buff[SIZE] = {0};
    while (true)
    {
        //sleep(3);
        buff[0] = 0;
        ssize_t n = read(rfd, buff, sizeof(buff)-1);
        if (n > 0)
        {
            buff[n] = 0;
            //cout << "father get a message[" << getpid() << "]# " << buff << endl;
        }
        cout << n << endl;
    }
}

在这里插入图片描述
结果:前五次父进程都读到长度为25的字符串,子进程退出后,read返回值为0,并且不会阻塞。

在这里插入图片描述
read函数返回值:如果成功,返回读取字符的个数。返回0表示读取到文件的结尾。如果失败返回-1,并且错误码被设置。

所以在父进程的代码中,需要对read的返回值进行判断。

void Reader(int rfd)
{
    char buff[SIZE] = {0};
    while (true)
    {
        //sleep(3);
        buff[0] = 0;
        ssize_t n = read(rfd, buff, sizeof(buff)-1);
        if (n > 0)
        {
            buff[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buff << endl;
        }
        else if (n == 0)
        {
            cout << "father read file done!" << endl;
            break;
        }
        else break;
    }
}

在这里插入图片描述

结论3:读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞。
那么如果子进程break之后,不手动close(pipefd[1]),如果子进程没有手动关闭写端,那子进程退出的时候对应的文件也会被关闭,操作系统会释放。
所以管道是基于文件的,文件的生命周期是随进程的。


如果读端关闭了,那么对于写端来说,再写入就没有什么意义,因为写入也没有进程读取。 对于操作系统来说,操作系统是不会做低效、浪费等类似工作的,所以操作系统要杀掉正在写入的进程,如何杀掉呢?——通过信号杀掉。
下面修改代码让子进程不断写入,父进程读取5次后关闭读端,然后操作系统会发送信号杀掉子进程,父进程等待子进程并提取子进程的status信息,这样我们就能看到子进程是被哪个信号所杀。

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

using namespace std;

#define SIZE 1024

void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;
    char buff[SIZE] = {0};
    while (true)
    {
        buff[0] = 0;
        snprintf(buff, sizeof(buff), "%s-%d-%d", s.c_str(), self, number++);
        write(wfd, buff, strlen(buff));
        // cout << number << endl;
        // if (number >= 5) break;
        sleep(1);
    }
}

void Reader(int rfd)
{
    char buff[SIZE] = {0};
    int cnt = 5;
    while (cnt--)
    {
        //sleep(3);
        buff[0] = 0;
        ssize_t n = read(rfd, buff, sizeof(buff)-1);
        if (n > 0)
        {
            buff[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buff << endl;
        }
        else if (n == 0)
        {
            cout << "father read file done!" << endl;
            break;
        }
        else break;
    }
}
int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n < 0) return 1;

    pid_t id = fork();
    if (id < 0) return 2;
    if (id == 0)
    {
        close(pipefd[0]);
        Writer(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }
    
    close(pipefd[1]);
    Reader(pipefd[0]);
    close(pipefd[0]); // 父进程关闭读端

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid < 0) return 3;
    printf("father wait %d success, exit code: %d, exit signal: %d\n", rid, (status>>8)&0xff, status&0x7f);
    close(pipefd[0]);
    return 0;
}

在这里插入图片描述
在这里插入图片描述
从结果来看,子进程会受到13号信号,使用kill- l查看所有信号,13号信号对应的是SIGPIPE。

结论4:写端正常写,读端关闭,操作系统就要杀掉正在写入的进程,通过13号信号SIGPIPE杀掉正在写入的进程。


2.3、管道的特征和四种情况

管道的特征:
1、具有血缘关系的进程进行进程间通信。
2、管道只能单向通信。
3、父子进程是会进程协同,同步与互斥的。——保护管道文件的数据安全
4、管道是面向字节流的。
5、管道是基于文件的,而文件的生命周期是随进程的。

管道的4种情况:
1、读写端正常,管道如果为空,读端就要阻塞。
2、读写端正常,管道如果被写满了,写端就要阻塞。
3、读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞。
4、写端正常写,读端关闭,操作系统就要杀掉正在写入的进程,通过13号信号SIGPIPE杀掉正在写入的进程。


2.4、匿名管道的应用场景

2.4.1、匿名管道和命令行的联系

在这里插入图片描述
我们在命令行使用管道执行三个指令,所以bash需要创建三个进程来执行,并且我们发现执行这三个指令的进程具有血缘关系,因为使用管道必须是具有血缘关系的两个进程。

那么我们之前使用cat命令用过管道,如:cat file.txt | head -10 | tail -5。这里的管道就是匿名管道。


2.4.2、实现简易进程池

使用匿名管道实现一个简易版本的进程池。
我们可以创建一批进程出来,这样当有任务需要执行时,直接在这批进程中挑选一个进程去完成任务。因为创建一个进程需要创建PCB、进程地址空间、页表,并且还需要建立映射,这是一个很重的过程。所以我们可以实现一个进程池,提高效率。
在这里插入图片描述
如图:主进程master创建出一批子进程slavor,同时创建一批匿名管道,父进程向管道里面写,子进程从管道里面获取任务,然后子进程执行任务。
我们规定,父子进程写入和读取都是4个字节,父进程通过给子进程写入任务码,然后子进程读取任务码然后执行对应的任务。

// Task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>

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 InitTasks(std::vector<std::function<void()>>* tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}
// ProcessPool.cc
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

#define N 5

std::vector<std::function<void()>> tasks;

class channel
{
public:
    channel(int cmdfd, int slavorid, std::string name)
    :_cmdfd(cmdfd)
    ,_slavorid(slavorid)
    ,_name(name)
    {}
public:
    int _cmdfd;         // 发送任务的文件描述符
    pid_t _slavorid;    // 子进程PID
    std::string _name;  // 子进程名称 
};

void slavor()
{
    while (true)
    {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int));
        if (n == sizeof(int))
        {
            //printf("child process receive, cmdcode: %d, pid: %d\n", cmdcode, getpid()); 
            if (cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
        }
        else if (n == 0) break;
        // sleep(1);
    }
}

void InitProcessPool(std::vector<channel>* channels)
{
    std::vector<int> oldfds;
    for (int i = 0; i < N; i++)
    {
        int pipefd[2];
        int n = pipe(pipefd);    // 创建匿名管道
        assert(!n);
        (void)n;
        pid_t id = fork();
        if (id == 0)
        {
            for (auto& e : oldfds)
            {
                close(e);
            }
            dup2(pipefd[0], 0);  // 重定向,子进程从0读取。
            close(pipefd[0]);
            close(pipefd[1]);
 
            slavor();
            exit(0); 
        }
        close(pipefd[0]);
        std::string name = "process-" + std::to_string(i + 1);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);
    }
}

void Debug(const std::vector<channel>& channels)
{  
    for (const auto& iter : channels)
    {
        printf("create a process, cmdfd: %d, slavorid: %d, name: %s\n",
        iter._cmdfd, iter._slavorid, iter._name.c_str());
    }
}

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 CtrlProcess(const std::vector<channel>& channels)
{
    int num = 0;
    while (true)
    {
        Menu();
        int select = 0;
        std::cout << "Please Enter@ ";
        std::cin >> select;
        if (select <= 0 || select > tasks.size())  break;

        int cmdcode = select - 1;

        //int cmdcode = rand() % 20;
        // int cmdcode = rand() % tasks.size();
        //int n = rand() % 5;
        int n = num;
        num++;
        num %= channels.size();
        write(channels[n]._cmdfd, &cmdcode, sizeof(int));

        //printf("father process send, cmdcode: %d, slavorid: %d, name: %s\n",
        //cmdcode, channels[n]._slavorid, channels[n]._name.c_str());
        // sleep(1);
    }

}

void QuitProcess(const std::vector<channel>& channels)
{
    for (const auto& e : channels)
    {
        close(e._cmdfd);
        waitpid(e._slavorid, nullptr, 0);
    }

    // for (const auto& e : channels)
    //     close(e._cmdfd);
    // for (const auto& e : channels)
    // {
    //     waitpid(e._slavorid, nullptr, 0);
    // }
}

int main()
{
    srand(time(nullptr)^getpid()^1023);  // 种一颗随机数种子
    std::vector<channel> channels;
    // 初始化任务
    InitTasks(&tasks);
    // 创建一批子进程
    InitProcessPool(&channels);
    //Debug(channels);
    // 选择一个子进程发送任务
    CtrlProcess(channels);
    // 收尾工作
    QuitProcess(channels);
    return 0;
}

1、父进程给子进程发送任务,我们可以使用rand()函数随机选取一个任务码,也可以让用户手动输入。
2、父进程选择子进程我们可以使用rand()函数随机选取一个子进程,或者采用轮转的方式,让它们负载均衡。
3、每个子进程都会继承父进程之前打开的管道写端,所以在进程中我们要把这些继承下来的写端都关掉。在代码中我们用一个oldfds来保存之前打开的写端,然后每次创建子进程让子进程继承下去,每次循环oldfds都会多一个,并且父子进程具有独立性不会相互影响。


3、命名管道

3.1、命名管道的理解

上面我们讲的都是两个具有血缘关系的进程进行进程间通信,那如果毫不相关的进程进行进程间通信呢?那就需要命名管道了。

可以使用mkfifo命令创建命名管道:
在这里插入图片描述
如图,我们创建了myfifo管道文件,并且发现它的文件标识为p,标识的就是命名管道。然后我们输出重定向到命名管道中,发现阻塞住了。
在这里插入图片描述
我们在右边再次查看myfifo文件信息,发现文件大小为0,所以这是一个内存级文件,然后我们输入重定向后左边就不再阻塞。

如果两个不同的进程打开同一个文件,在内核中,操作系统会打开几个文件?
操作系统会创建两个struct file对象,但是文件的inode、operator方法、文件缓冲区是只有一个的。如下图:
在这里插入图片描述
进程间通信的前提:让不同的进程看到同一份资源。只不过过去我们打开的是磁盘上的文件,现在这个是内存级文件,不需要进行刷盘。所以命名管道原理和匿名管道是一样的,也只能进行单向通信,只不过命名管道是有名字的。

那么怎么知道两个进程打开的是同一个文件呢?——同路径下同一个文件名=路径+文件名。
在这里插入图片描述
在这里插入图片描述
系统调用mkfifo用于创建命名管道,pathname表示路径+文件名,mode就是创建管道的权限。
调用成功返回0,失败返回-1错误码被设置。

在这里插入图片描述
在这里插入图片描述
删除命名管道我们可以使用unlink函数。


3.2、编码实现

// comm.hpp
#pragma once

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

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

enum{
    FIFO_CREAT_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR,
};

class Init
{
public:
    Init()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n < 0)
        {
            perror("mkfifo");
            exit(FIFO_CREAT_ERR);
        }
    }
    ~Init()
    {
        int n = unlink(FIFO_FILE);
        if (n < 0)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};
// server.cc
#include "comm.hpp"

using namespace std;

int main()
{
    Init init;

    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }
    cout << "server open file done!\n";

    char buffer[SIZE];
    while (true)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)    
        {
            buffer[s] = 0;
            printf("client say# %s\n", buffer);
        }
        else if (s == 0)
        {
            printf("client quit, me too!\n");
            break;
        }
        else break;
    }

    close(fd);

    return 0;
}
// client.cc
#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 << "clinet open file done!\n";

    std::string str;
    while (true)
    {
        printf("Please Enter@ ");
        getline(cin, str);
        write(fd, str.c_str(), str.size());
    }

    close(fd);
    return 0;
}

在这里插入图片描述
在这里插入图片描述
刚开始server先启动,server创建管道文件,然后open打开管道文件的时候阻塞住了,等待client也打开管道文件,两边才会继续往下走。


3.3、实现日志组件

实现一个日志组件,可以帮助我们打印输出日志信息。
一般要有:日志时间、日志的等级、日志内容、文件的名称和行号。
日志等级我们分为: Info:常规消息,Warning:报警信息,Error:比较严重可能需要立即处理,Fatal:致命的,Debug:调试信息。

3.1、可变参数相关知识

在这里插入图片描述
在这里插入图片描述
我们知道函数参数压栈是从右向左压栈的,并且栈帧是向下增长的,所以sum函数最底下的参数就是n,va_list相当于定义一个char*的指针,然后va_start(s,n)相当于让s指向第一个可变参数。va_arg(s,int)相当于把s指向的地址当做int类型数据获取,va_end相当于把s置空。


3.2、获取时间

在这里插入图片描述
使用time函数获取时间,time函数会返回time_t类型的一个时间戳。
在这里插入图片描述
使用localtime函数获取一个struct tm*的对象,传入参数为const time_t*,这个结构体里面有年月日时分秒等信息。注意年份是从1900年开始算的,所以最后获取年份需要加上1900,月份是0-11所以获取月份要加上1。


3.3、具体实现

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 1024

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

#define Screen    0
#define Onefile   1
#define Classfile 2

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = 0;   // 默认向显示器打印
        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, const char* format, ...)
    // {
    //     time_t t = time(nullptr); 
    //     struct tm* ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%02d-%02d %02d:%02d:%02d]",
    //     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);
    //     // std::cout << logtxt;
    //     printLog(level, logtxt);
    // }

    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-%02d-%02d %02d:%02d:%02d]",
        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);
        // std::cout << logtxt;
        printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
        switch(printMethod)
        {
            case Screen:
                std::cout << logtxt;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt);
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
        }
    }


    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_CREAT|O_WRONLY|O_APPEND, 0666);
        if (fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
        std::string logname = LogFile;
        logname += ".";
        logname += levelToString(level);
        printOneFile(logname, logtxt);
    }

    private:
    int printMethod;
    std::string path;
};



// int sum(int n, ...)
// {
//     va_list s;       // 定义一个char*指针
//     va_start(s, n);  // 让s指向第一个可变参数
//     int num = 0;
//     while (n--)
//     {
//         num += va_arg(s, int); // 获取s指向的数据
//     }
//     va_end(s);   // 将s置空
//     return num;
// }

4、共享内存

4.1、共享内存的原理

进程间通信的本质:让不同的进程看到同一份资源。
在这里插入图片描述

如图:首先在物理内存开辟一块空间,接着通过页表映射到进程地址空间中的共享区,然后虚拟地址空间的首地址返回,对于另一个进程也是这样,那么这两个进程就能看到物理内存中的同一块空间,以这块空间来实现进程间通信。
所以使用共享内存需要:1、申请内存。2、挂接到进程地址空间。
如果释放共享内存:1、去关联。2、释放共享内存。

这个内存能由进程申请吗?肯定是不行的,如果是进程申请的,那就属于申请的进程,而且进程间具有独立性,所以这块空间必须由操作系统申请,那么挂接操作也需要由操作系统来完成,所以必定是通过系统调用来实现的。

操作系统中可不只有这两个进程,其他进程之间也可能进行通信,所以可能会创建很多块共享内存。那么操作系统要不要管理共享内存呢?要,如何管理?——先描述,再组织。所以操作系统内必定要有描述共享内存的struct XXX结构体,里面包含共享内存的大小、唯一标识、谁创建的、哪些进程挂接了等等。


4.2、编写代码

在这里插入图片描述
在这里插入图片描述
shmget函数用来创建共享内存,size表示创建的共享内存空间大小,shmflg表示创建的选项。
shmflg我们关注两个:IPC_CREAT和IPC_EXCL
IPC_CREAT单独使用:如果申请的共享内存不存在就创建,存在就获取返回。
IPC_CREAT|IPC_EXCL:如果申请的共享内存不存在就创建,存在就出错返回。这两个搭配一起使用是为了保证创建出来的共享内存一定是新的。IPC_EXCL不单独使用。

shmget返回值为共享内存标识符,创建成功返回共享内存标识符,创建失败返回-1并且错误码被设置。

那么我们如何知道这个共享内存是否存在呢?这个问题实际上等同于如何让两个进程看到同一份资源。
这是通过key来实现的,下面再谈谈key。

key:
1、key是一个数字,这个数字是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
2、第一个进程可以拿着key创建共享内存,第二个之后的进程,只要拿着相同的key就可以获取第一个进程创建的共享内存,这样它们就能看到同一份共享内存了。
3、操作系统也要管理共享内存,所以也要描述共享内存,对于一个已经创建好的共享内存,它的key在哪里呢?必定是在描述共享内存的结构体里面。
4、第一创建的时候需要有key,这个key怎么来呢?——通过ftok函数。

在这里插入图片描述
在这里插入图片描述
fotk函数参数有路径pathname和项目id。返回值是一个key_t的类型,也就是一个整数。这个函数是一套算法,通过pathname和proj_id进行数值计算,然后将结果返回。那么pathname和proj_id就由用户指定。
ftok成功返回计算数值,失败返回-1并且错误码被设置。
5、key类似创建命名管道那里传的路径,它们都具有唯一性。

ftok计算出的key有没有可能冲突呢?当然是有可能的,但是系统的算法计算出来的冲突会尽可能小,但是也并不代表不会出现冲突,那如果出现冲突怎么办?修改pathname和proj_id。

为什么操作系统不直接生成一个然后返回给进程呢?理论上这是可以做到的,但是如果是操作系统生成返回的key,那么你这个进程可以用来创建共享内存,问题是其他进程如何知道你这个key,然后通过这个key去获取共享内存。其他进程无法得知这个key,所以不能这么做。有人会说那就用管道,利用管道把key传过去,那这样共享内存就不是一个独立的通信模块了,所以这样是行不通的。


先编码实现创建共享内存:

// comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"

using namespace std;

Log log;
const int size = 4096;
const char* pathname = "/home/zzy";
const int proj_id = 0x6666;

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

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

int CreateShm()
{
    return GetShmHelper(IPC_CREAT|IPC_EXCL);
}

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

#endif
#include "comm.hpp"

int main()
{
    int shmid = CreateShm();
    sleep(10);

    return 0;
}

在这里插入图片描述
左侧运行程序后创建共享内存成功,打印出shmid——共享内存标识符为2。
右侧我们使用命令:ipcs -m查看创建的所有共享内存。

我们发现有个key,这个key是十六进制的,也就是之前调用shmget传入的key。shmid为2,跟左侧打印信息一致。并且左边进程退出后,右侧查看共享内存发现共享内存还是存在的,所以共享内存需要我们手动释放。owner表示作者,perms表示权限,因为我们创建没有设置权限,所以默认权限为0,然后bytes表示共享内存的大小,nattch表示有多少个进程挂接到共享内存中。
共享内存的生命周期是随内核的!用户不主动关闭,共享内存就会一直存在。除非内核重启或用户释放。

使用ipcrm -m shmid可以删除共享内存:
在这里插入图片描述

key VS shmid,key用于操作系统内标定唯一性。shmid在你的进程内表示资源的唯一性。

所以创建共享内存,我们还需要传入权限:
在这里插入图片描述
另外共享内存创建的大小最好为4096的整数倍,因为page页的大小为4KB,操作系统会以页为单位给你。假设你创建大小为4097,实际上操作系统会给你两页,但是你还是只能使用4097,所以就会造成空间浪费。


在这里插入图片描述
在这里插入图片描述
shmat函数用来挂接共享内存,shmid就是创建的或获取的共享内存标识符,shmaddr表示你要挂接到进程地址空间共享区的哪个位置,但是一般我们也不知道,所以直接设置为nullptr即可,shmflg可以设置只读等权限,我们直接设置为0表示默认创建的权限即可。挂接成功返回进程地址空间的首地址,挂接失败返回-1这个无类型地址,并设置错误码。

shmdt函数用来取消挂接,参数为挂接返回的地址。成功返回0,失败返回-1错误码被设置。

添加代码挂接和去挂接:

#include "comm.hpp"
int main()
{
    int shmid = CreateShm();
    sleep(2);
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    sleep(2);
    shmdt(shmaddr);
    sleep(2);
    return 0;
}

在这里插入图片描述
刚开始创建了共享内存后还没有进程挂接,nattch为0,创建两秒后进程挂接了,所以nattch为1,然后两秒后进程去挂接了,所以naatch又变为0,最后进程退出。


在这里插入图片描述
shmctl用来删除共享内存或获取共享内存信息。第一个参数是共享内存标识符,第二个参数是选项。第二个参数我们关注两个:IPC_RMID表示删除共享内存。IPC_STAT表示获取共享内存的信息。
第三个参数是用户级的共享内存对象信息,右侧是它的成员变量。如果使用IPC_RMID,那么第三个参数设置为nullptr即可。如果使用IPC_STAT获取信息,需要传入struct shmid_ds的指针,这是给输出型参数,会将共享内存信息带出。

观察shmid_ds结构,里面有shm_segsz:共享内存的大小。shm_atime最近挂接时间。shm_dtime最近去挂接时间。shm_cpid:创建进程的PID。shm_nattch:挂接进程数量。而shmid_ds里面还有个结构struct ipc_perm,struct ipc_perm里面就存储了key和mode。

下面实现两个进程间通信:

// processa.cc
#include "comm.hpp"

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

    struct shmid_ds shmds;
    while (true)
    {
        cout << "processb say# " << shmaddr << endl; 

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

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);
    return 0;
}
// processb.cc
#include "comm.hpp"

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

    while (true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);
    }

    shmdt(shmaddr);
    return 0;
}

在这里插入图片描述
可以看到我们通过shmctl中cmd设置为IPC_STAT可以获取共享内存的信息。


4.3、共享内存的特性

1、共享内存没有同步互斥之类的保护机制。
2、共享内存是所有进程间通信中速度最快的。为什么?——拷贝少。共享内存只需要拷贝一次,将数据拷贝到内存中,另一个进程直接从内存中读取即可。使用管道需要发生两次拷贝:调用write时将数据拷贝到文件缓冲区,调用read时将数据从文件缓冲区拷贝到用户层。
3、共享内存内部的数据由用户自己维护。

下面使用管道实现共享内存的同步机制:

// comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"

using namespace std;

Log log;
const int size = 4096;
const char* pathname = "/home/zzy";
const int proj_id = 0x6666;

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

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

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

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

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

enum{
    FIFO_CREAT_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR,
};

class Init
{
public:
    Init()
    {
        int n = mkfifo(FIFO_FILE, MODE);
        if (n < 0)
        {
            log(Fatal, "mkfifo error: %s", strerror(errno));
            exit(FIFO_CREAT_ERR);
        }
    }

    ~Init()
    {
        int n = unlink(FIFO_FILE);
        if (n < 0)
        {
            log(Fatal, "unlink error: %s", strerror(errno));
            exit(FIFO_DELETE_ERR);
        }
    }
};
#endif
// processa.cc
#include "comm.hpp"

int main()
{
    Init init;
    int shmid = CreateShm();
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);

    // 打开管道文件
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        log(Fatal, "open error: %s", strerror(errno));
        exit(FIFO_OPEN_ERR);
    }

    struct shmid_ds shmds;
    while (true)
    {
        char ch;
        ssize_t n = read(fd, &ch, sizeof(ch));
        if (n <= 0) break;
        cout << "processb say# " << shmaddr; 

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

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr);
    close(fd);
    return 0;
}
// processb.cc
#include "comm.hpp"

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

    int fd = open(FIFO_FILE, O_WRONLY|O_APPEND);
    if (fd < 0)
    {
        log(Fatal, "open error: %s", strerror(errno));
        exit(FIFO_OPEN_ERR);
    }

    while (true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);
        write(fd, "c", 1);
    }

    shmdt(shmaddr);
    close(fd);
    return 0;
}

在这里插入图片描述
当进程a运行起来后,创建了管道文件,但是会在open处阻塞,然后进程b起来后调用open,然后双方一起往后走,进程a会阻塞在read函数处等待进程b发送消息。当进程b输入数据后调用fgets读取到共享内存中,然后调用write函数向管道写入一个字符,这时候进程a就会读到管道中的数据,然后获取到共享内存的数据并输出,下次循环再次阻塞在read函数处等待进程b发送消息。
当进程b退出时,写进程退出,读进程就会读到0,我们进行判断直接break。然后进程a释放共享内存退出。


5、消息队列

5.1、消息队列原理

在这里插入图片描述
进程间通信的本质是:让不同的进程看到同一份资源。
消息队列的原理就是先在操作系统创建一个message queue,然后对于进程A和进程B都要看到同一个队列。然后进程AB要进行通信,可以把带有类型的数据块发送到内核中,在队列中链接起来。
那么进程从消息队列获取数据如何获取呢?因为进程A、B发送的数据块都会在队列中链接起来,所以发送的是带有类型的数据块,通过类型来判断。


5.2、消息队列接口介绍

在这里插入图片描述
在这里插入图片描述
msgget函数用来获取消息队列,key跟共享内存一样用来在操作系统内表示消息队列的唯一性,可以通过ftok函数来获取。msgflg有两个选项:IPC_CREAT 和 IPC_EXCL,使用同共享内存,IPC_CREAT单独使用如果消息队列不存在就创建,如果存在就获取返回。IPC_CREAT | IPC_EXCL一起使用如果消息队列不存在就创建,存在就出错返回,保证创建的消息队列一定是新的。
返回值:成功返回消息队列标识符,失败返回-1,错误码被设置。

在这里插入图片描述
msgctl用俩获取消息队列信息或删除消息队列,msgid表示消息队列标识符,cmd可以传:IPC_RMID表示删除消息队列,IPC_STAT表示获取消息队列信息。
在这里插入图片描述
注意到struct msgid_ds里面的有消息队列的数量、大小、时间信息等。但是msgid_ds和shmid_ds第一个成员都是struct ipc_perm,它们有共同的成员。

在这里插入图片描述
msgsnd用来发送数据块,msgp表示数据块的地址,msgsz表示数据块的大小,msgflg设置为0表示阻塞发送。
msgrcv用来获取数据块。

在这里插入图片描述
数据块类似这样的结构,第一个字段表示数据块的类型,然后第二个表示数据。


5.3、IPC在内核中的数据结构设计

在这里插入图片描述
我们把共享内存、消息队列、信号量的接口都截出来,发现它们对应的ctl函数,都有对应的结构。共享内存:shmid_ds,消息队列:msgid_ds,信号量:semid_ds,里面保存了它们的属性,并且XXXid_ds里面的第一个成员变量都是struct ipc_perm。然后ipc_perm里面有key和mode,所以对于共享内存、消息队列、信号量都可以实现让不同进程看到同一份资源。

在操作系统中,所有的IPC资源都是整合进操作系统的IPC模块的。
在这里插入图片描述

操作系统内有一个struct ipc_perm*的指针数组,里面存储的都是ipc_perm对象的地址。当我们创建了共享内存/消息队列/信号量,由于XXXid_ds都有struct ipc_perm对象,所以也会创建一个strcut ipc_perm对象,那么就会把这个对象的地址填到struct ipc_perm* array[]数组中,那么这个数据里面的元素就指向了一个一个的struct ipc_perm对象。而我们获取资源标识符shmid、msgid、semid就是这个数组的下标。这个数组是一个变长数组,然后我们每次获取的id值都会++,所以它是线性递增的,但是最后它会回绕然后从头开始。

那么我们如果获取XXXid_ds对象呢,因为每个XXXid_ds第一个对象都是ipc_perm,所以我们只要将对应的ipc_perm地址进行强转即可。比如数组中的地址addr,强转成struct XXXid_ds的指针,那么就可以获取所有数据了。那么如何知道要强转成哪个类型呢,你这不是有三个类型吗,在ipc_perm内部可以标记类型。总之,操作系统能区分指针指向的对象的类型。


6、信号量

6.1、信号量原理

对于共享内存来说,操作系统在物理内存开辟一块空间,然后两个进程挂接到共享内存,将其映射在虚拟地址空间的首地址返回,实现了两个进程间通信。
现在如果进程A正在写入,假设进程A要写入100个字节的数据,这100字节数据是作为一个整体要发送给进程B的。进程B读取数据,但是进程B在进程A写入了50个字节的时候就把这50字节的数据读走了,这就导致双方发和收的数据不完整,我们称为数据不一致问题。

1、进程AB看到的同一份资源,我们称之为共享资源。共享资源如果不加以保护,就会导致数据不一致问题。
2、所以需要加锁,加锁就是互斥访问。保证在任何时刻,只允许一个执行流访问共享资源,我们称之为互斥。
3、共享的,任何时刻只允许一个执行流访问的资源(本质就是执行访问代码),我们称之为临界资源,一般是内存空间。
4、程序有100行代码,并不是所有代码都在访问临界资源,可能只有5-10行才会访问临界资源。我们把访问临界资源的代码称之为临界区。

我们上面学习的共享内存是没有任何保护机制的,而管道是有同步互斥保护机制的。
为什么多进程、多线程打印,显示器上显示的消息是混乱的?因为多进程、多线程打印就是访问显示器文件资源,显示器文件是共享资源,如果不加以保护就会出现这种情况。


理解信号量:
信号量/信号灯本质是一把计数器,类似但不敢等同:int cnt = n。它用来描述临界资源中资源数量的多少。

假设你去看电影,放映厅中有100个座位,那就有100张票,维护一个票数计数器cnt=100。当我们准备看电影的时候,我们还没去看电影,而是先买票,买票的本质就是对资源的预定机制。当我买好票选好了位置了,这个位置就被我预定了,在特定的时间内这个位置就是我的,别人不能坐,哪怕我没去。每卖一张票,票数的计数器就要减1,放映厅里面的资源就少了一个。当票数计数器减到0,资源就被申请完毕了。

对于临界资源来说,我们可以把它划分成很多份,假设划分成15份。我们最担心的就是多个执行流访问同一个资源,比如15份资源,但是进来了16个执行流,那么一定会有两个执行流访问同一份资源,这样就会出问题。所以我们引入一个计数器,int cnt = 5,每个执行流访问临界资源先申请计数器:int number = cnt–,number就是你这个执行流分配到的资源,然后让计数器减1,那么当计数器减到0就说明临界资源已经被申请完了,后面再有执行流来申请也不给你了。

1、申请计数器成功就表示我具有访问资源的权限。
2、申请了计数器资源,我访问了我要的资源吗?没有,申请计数器资源是对资源的预定机制。
3、计数器可以有效保证进入共享资源的执行流的数量。
4、所以每个执行流要访问共享资源中的一部分,不是直接访问,要先申请计数器资源。如看电影的先买票。

程序员把这个"计数器"叫做信号量。

如果电影院只有一个座位,超级VVVIP电影院。那么我们只需要一个值为1的计数器。
只有一个人可以抢到这个资源,只有一个人能进放映厅看电影,看电影期间只有一个执行流在访问临界资源。这就是互斥。

我们把值只能为1、0两态的计数器叫做二元信号量——本质就是一个锁。

为什么让计数器为1?计数器为1的本质就是:不再将临界资源分成很多块,而是当作一个整体。整体申请,整体释放。


思考:要访问临界资源,要先申请信号量计数器资源,申请信号量资源要对计数器进行--。那么信号量计数器不也是共享资源吗?要保护别人的安全你得先保证自己的安全。

申请信号量,本质是对计数器--,称之为P操作。
释放资源,释放信号量,本质是对计数器进行++,称之为V操作。
申请和释放统称为PV操作。它们是原子的。

什么叫做原子的?一件事要么不做,要么做完——两态的,没有正在做的概念。

在C语言中,cnt--\是一条语句,编程汇编,一般是三条语句。
1、将cnt变量的值放入CPU寄存器中。
2、CPU内进行--操作。
3、将计算结果写回cnt变量的内存位置。
我们认为一条汇编语句是原子的。但是cnt--被转换成了三条汇编,而多线程、多进程在执行这三条汇编的时候都可能被切换。所以是有问题的,具体我们到多线程部分再做讲解。

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值