• 进程间的通信方式


    1.简单介绍

    计算机中,进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间交换数据或信息的机制。

    首先我们通过之前的学习知道进程是具有独立性的,其交互数据成本非常高,所以通信这一机制就诞生了。

    通信的本质其实就是由os参与,提供一份所有通信进程能看到的公共资源。

    进程间通信的方式非常多以下是对这些方式的梳理以及分类:

    进程间通信分类

    管道

    • 匿名管道pipe

    • 命名管道

      System V IPC

    • System V 消息队列

    • System V 共享内存

    • System V 信号量

      POSIX IPC

    • 消息队列

    • 共享内存

    • 信号量

    • 互斥量

    • 条件变量

    • 读写锁

    2.管道

    2.1管道的基础概念

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

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

    在这里插入图片描述

    管道读写规则:

    当没有数据可读时

    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

    当管道满的时候

    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

    如果所有管道写端对应的文件描述符被关闭,则read返回0

    如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程

    退出

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

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

    管道特点
    • 管道提供流式服务

    • 一般而言,进程退出,匿名管道释放,所以匿名管道的生命周期随进程

    • 一般而言,内核会对管道操作进行同步与互斥

    • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

    在这里插入图片描述

    2.2匿名管道
    #include 
    功能:创建一无名管道,管道是一种特殊的文件,它具有两个端点,一个用于写入数据,另一个用于读取数据。
    原型
    int pipe(int fd[2]);
    参数
    fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
        是一个包含两个整数的数组,用于接收管道的两个文件描述符
    返回值:成功返回0,失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    匿名管道父子进程间通信的经典案例:
    使用例子:从键盘读取数据,写入管道,读取管道,写到屏幕
        
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
        int pipe_fd[2] = {0};
    
        if(pipe(pipe_fd) < 0){
            perror("pipe");
            return 1;
        }
        printf("%d, %d\n", pipe_fd[0], pipe_fd[1]);
    
        pid_t id = fork();
        if(id < 0){
            perror("fork");
            return 2;
        }
        else if(id == 0) { //write
            //child
            close(pipe_fd[0]);
    
            //const char *msg = "hello parent, I am child";
    
            char c = 'x';
            int count = 0;
            //while(count){
            while(1){
                write(pipe_fd[1], &c, 1); //strlen(msg) + 1??
             //   sleep(1);
                count++;
                printf("write: %d\n", count);
            }
    
            close(pipe_fd[1]);
            exit(0);
        }
        else{              //read
            //parent
            close(pipe_fd[1]);
    
            char buffer[64];
            while(1){
                sleep(100);
                buffer[0] = 0;
                ssize_t size = read(pipe_fd[0], buffer, sizeof(buffer)-1);
                if(size > 0){
                    buffer[size] = 0;
                    printf("parent get messge from child# %s\n", buffer);
                }
                else if(size == 0){
                    printf("pipe file close, child quit!\n");
                    break;
                }
                else{
                    //TODO
                    break;
                }
            }
    
            int status = 0;
            if(waitpid(id, &status,0) > 0){
                printf("child quit, wait success!, sig: %d\n", status&0x7F);
            }
            close(pipe_fd[0]);
        }
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    对上端代码用fork来共享管道的原理

    在这里插入图片描述

    2.3命名管道
    基本概念:

    匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

    如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

    命名管道是一种特殊类型的文件

    命名管道的创建:
    //命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
    $ mkfifo filename
    //命名管道也可以从程序里创建,相关函数有:
        int mkfifo(const char *filename,mode_t mode);
    //filename:一个指向要创建的命名管道的路径和名称的字符串。它是一个以 null 结尾的字符数组。
    
    //mode:用于指定创建的命名管道的权限(访问权限和文件类型)。mode 是一个 mode_t 类型的参数,通常使用八进制表示的权限值。可以使用一些预定义的常量(如 S_IRUSR、S_IWUSR、S_IRGRP 等)来设置权限。
    
    //创建命名管道
    int main(int argc, char *argv[])
    {
     mkfifo("p2", 0644);
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    命名管道的打开规则:

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

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

    O_NONBLOCK enable:立刻返回成功

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

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

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

    匿名管道与普通管道的区别

    匿名管道由pipe函数创建并打开(使用了pipe函数默认该描述符对应的文件打开了)。

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

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

    成之后,它们具有相同的语义。

    tips:

    通过 mkfifo 函数创建的命名管道在文件系统中持久存在,与创建它的进程无关。

    使用 pipe 函数创建的管道是进程特有的,与创建它的进程相关联。当创建管道的进程结束时,管道会被自动关闭和销毁。而mkfifo函数所创建的管道是不会随着进程结束而销毁的,我们需要手动销毁。

    //销毁命名管道所使用的函数
    
    int unlink(const char *pathname);
    pathname 是一个指向要删除的文件的路径和名称的字符串。它是一个以 null 结尾的字符数组。
        
    //unlink 函数执行以下操作:
    
    1.首先,它会检查指定路径的文件是否存在。如果文件不存在,unlink 函数会返回一个错误码,并不执行任何其他操作。
    
    2.如果文件存在,并且调用进程对该文件具有足够的权限,unlink 函数将从文件系统中删除该文件。删除文件会将文件从目录中删除,并释放文件所占用的磁盘空间。
    
    3.注意,删除文件并不会关闭已打开的文件描述符。如果有进程仍然持有对该文件的打开描述符,文件将继续存在于文件系统中,直到所有打开的描述符关闭。
    
    4.当调用进程成功删除文件时,unlink 函数返回 0。如果出现错误,比如权限不足或指定的文件路径无效,unlink 函数返回 -1,并设置相应的错误码,可以通过 errno 全局变量获取错误信息。
    
        //删除命名管道文件并不会自动关闭已打开的管道文件描述符。在删除命名管道文件之前,需要确保所有打开的文件描述符都已关闭,以免导致资源泄漏或其他问题。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    例子:用命名管道实现server&client通信

    例子中使用的一些函数的解析:

    1.
        mode_t umask(mode_t mask);
    umask 函数接受一个参数 mask,它是一个无符号整数类型 mode_t 的值,表示要设置的权限掩码。mode_t 是一个用于表示文件权限和文件类型的数据类型。
    
    2.
        key_t ftok(const char *pathname, int proj_id);
        用于生成一个唯一的键值(key)用于标识一个共享资源,例如共享内存、信号量或消息队列。
        pathname:一个存在的文件路径名,可以是任意合法的文件路径。
        proj_id:一个用户定义的整数,用于区分不同的共享资源。通常取值为一个非负整数。  
        生成的键值可以用于创建或访问共享资源,例如在调用 shmget 函数创建共享内存时使用。
     3.
        int shmdt(const void *shmaddr);      
        shmdt 函数用于将共享内存从当前进程的地址空间中分离,即解除共享内存的挂载。
        shmdt 函数并不会删除共享内存段,只是将其与当前进程分离。
        在调用 shmdt 函数之后,当前进程将无法再直接访问共享内存中的数据。如果需要重新访问共享内存,必须使用 shmat 函数重新将共享内存段挂载到当前进程的地址空间。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    Log.hpp(一个简单的日志记录功能的实现)
        
    #ifndef _LOG_H_
    #define _LOG_H_
    
    #include 
    #include 
    
    #define Debug   0
    #define Notice  1
    #define Warning 2
    #define Error   3
    
    
    const std::string msg[] = {
        "Debug",
        "Notice",
        "Warning",
        "Error"
    };
    
    std::ostream &Log(std::string message, int level)
    {
        std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
        return std::cout;
    }
    
    
    #endif   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    Makefile
        
    .PHONY:all
    all:shmClient shmServer
    
    shmClient:shmClient.cc
    	g++ -o $@ $^ -std=c++11
    shmServer:shmServer.cc
    	g++ -o $@ $^ -std=c++11
    
    .PHONY:clean
    clean:
    	rm -f shmClient shmServer
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    comm.hpp
    
    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "Log.hpp"
    
    using namespace std; //不推荐
    
    #define PATH_NAME "/home/whb"
    #define PROJ_ID 0x66
    #define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
    
    #define FIFO_NAME "./fifo"
    
    class Init
    {
    public:
        Init()
        {
            umask(0);//表示没有遮掩任何权限
            int n = mkfifo(FIFO_NAME, 0666);
            assert(n == 0);
            (void)n;
            Log("create fifo success",Notice) << "\n";
        }
        ~Init()
        {
            unlink(FIFO_NAME);
            Log("remove fifo success",Notice) << "\n";
        }
    };
    
    #define READ O_RDONLY
    #define WRITE O_WRONLY
    
    int OpenFIFO(std::string pathname, int flags)
    {
        int fd = open(pathname.c_str(), flags);
        assert(fd >= 0);
        return fd;
    }
    
    void Wait(int fd)
    {
        Log("等待中....", Notice) << "\n";
        uint32_t temp = 0;//为了确保读取的值可以正确地存储和比较
        ssize_t s = read(fd, &temp, sizeof(uint32_t));
        assert(s == sizeof(uint32_t));
        (void)s;
    }
    
    void Signal(int fd)
    {
        uint32_t temp = 1;   //为了确保读取的值可以正确地存储和比较
        ssize_t s = write(fd, &temp, sizeof(uint32_t));
        assert(s == sizeof(uint32_t));
        (void)s;
        Log("唤醒中....", Notice) << "\n";
    }
    
    void CloseFifo(int fd)
    {
        close(fd);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    shmClient.cc
    
    #include "comm.hpp"
    
    int main()
    {
        Log("child pid is : ", Debug) << getpid() << endl;
        key_t k = ftok(PATH_NAME, PROJ_ID);
        if (k < 0)
        {
            Log("create key failed", Error) << " client key : " << k << endl;
            exit(1);
        }
        Log("create key done", Debug) << " client key : " << k << endl;
    
        // 获取共享内存
        int shmid = shmget(k, SHM_SIZE, 0);
        if(shmid < 0)
        {
            Log("create shm failed", Error) << " client key : " << k << endl;
            exit(2);
        }
        Log("create shm success", Error) << " client key : " << k << endl;
    
        // sleep(10);
    
        char *shmaddr = (char *)shmat(shmid, nullptr, 0);
        if(shmaddr == nullptr)
        {
            Log("attach shm failed", Error) << " client key : " << k << endl;
            exit(3);
        }
        Log("attach shm success", Error) << " client key : " << k << endl;
        // sleep(10);
    
        int fd = OpenFIFO(FIFO_NAME, WRITE);
        // 使用
        // client将共享内存看做一个char 类型的buffer
        while(true)
        {
            ssize_t s = read(0, shmaddr, SHM_SIZE-1);
            if(s > 0)
            {
                shmaddr[s-1] = 0;
                Signal(fd);
                if(strcmp(shmaddr,"quit") == 0) break;
            }
        }
    
        CloseFifo(fd);
        // char a = 'a';
        // for(; a <= 'z'; a++)
        // {
        //     shmaddr[a-'a'] = a;
        //     // 我们是每一次都向shmaddr[共享内存的起始地址]写入
        //     // snprintf(shmaddr, SHM_SIZE - 1,\
        //     //     "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
        //     //     getpid(), a);
        //     sleep(5);
        // }
    
        // strcpy(shmaddr, "quit");
    
        // 去关联
        int n = shmdt(shmaddr);
        assert(n != -1);
        Log("detach shm success", Error) << " client key : " << k << endl;
        // sleep(10);
    
        // client 要不要chmctl删除呢?不需要!!
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    shmServer.cc
     
    #include "comm.hpp"
    
    // 是不是对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
    // 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
    Init init; 
    
    string TransToHex(key_t k)
    {
        char buffer[32];
        snprintf(buffer, sizeof buffer, "0x%x", k);
        return buffer;
    }
    
    int main()
    {
        // 我们之前为了通信,所做的所有的工作,属于什么工作呢:让不同的进程看到了同一份资源(内存)
        // 1. 创建公共的Key值
        key_t k = ftok(PATH_NAME, PROJ_ID);
        assert(k != -1);
    
        Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
    
        // 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
        int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //
        if (shmid == -1)
        {
            perror("shmget");
            exit(1);
        }
        Log("create shm done", Debug) << " shmid : " << shmid << endl;
    
        // sleep(10);
        // 3. 将指定的共享内存,挂接到自己的地址空间
        char *shmaddr = (char *)shmat(shmid, nullptr, 0);
        Log("attach shm done", Debug) << " shmid : " << shmid << endl;
    
        // sleep(10);
    
        // 这里就是通信的逻辑了
        // 将共享内存当成一个大字符串
        // char buffer[SHM_SIZE];
        // 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
        //         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
        // 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】
        
        int fd = OpenFIFO(FIFO_NAME, READ);
        for(;;)
        {
            Wait(fd);
    
            // 临界区
            printf("%s\n", shmaddr);
            if(strcmp(shmaddr, "quit") == 0) break;
            // sleep(1);
        }
        // 4. 将指定的共享内存,从自己的地址空间中去关联
        int n = shmdt(shmaddr);
        assert(n != -1);
        (void)n;
        Log("detach shm done", Debug) << " shmid : " << shmid << endl;
        // sleep(10);
    
        // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
        n = shmctl(shmid, IPC_RMID, nullptr);
        assert(n != -1);
        (void)n;
        Log("delete shm done", Debug) << " shmid : " << shmid << endl;
    
        CloseFifo(fd);
        return 0;
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    3.system v ipc

    3.1system v 共享内存 基础介绍

    在这里插入图片描述

    实际上,system v本质也是创建虚拟内存进而映射到实际内存上,不过跟上面管道不同的是,进程间信息交互的处理不在由内核系统那一套来处理,而是由操作系统直接处理,因此其是最快的ipc模式。

    3.2system v 共享内存常用接口介绍
    功能:用来创建共享内存
    原型
        int shmget(key_t key, size_t size, int shmflg);
    参数
        key:这个共享内存段名字
        size:共享内存大小
        shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    功能:将共享内存段连接到进程地址空间
    原型
        void *shmat(int shmid, const void *shmaddr, int shmflg);参数
        shmid: 共享内存标识
        shmaddr:指定连接的地址
        shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
    返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    shmaddr为NULL,核心自动选择一个地址
    shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
    shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
    shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
    
    • 1
    • 2
    • 3
    • 4
    功能:将共享内存段与当前进程脱离
    原型
        int shmdt(const void *shmaddr);
    参数
        shmaddr: 由shmat所返回的指针
    返回值:成功返回0;失败返回-1
    注意:将共享内存段与当前进程脱离不等于删除共享内存段
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    功能:用于控制共享内存
    原型
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数
        shmid:由shmget返回的共享内存标识码
        cmd:将要采取的动作(有三个可取值)
        buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
    返回值:成功返回0;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    3.3system v 信息队列 以及信号量

    这一块学有余力的同学再去详细了解,这里便不在一一呈现了。

    4.一些基础概念的讲解

    为了之后获取更好的阅读文章体验,我将提前把一些经常出现的基础概念在此总结一下,强烈介意第一次打基础的同学阅读。

    当多个进程对同一份资源进行来回操作时,因为时序问题,可能会造成数据不一致的问题,为了解决这一问题,我们将定制一套基础方案,下面先对其中涉及到的专有名词的含义进行讲解。

    临界资源:多个进程执行流看到的公用的一份资源

    临界区:进程访问临界资源的代码

    互斥性:为了更好的临界区的保护,可以让各执行流在同一时刻只有一个进程进入临界区

    原子性:一件事情要么不做,要么做完,没有中间状态

    信号量:信号量是一种用于实现进程间同步和互斥的机制。可以将信号量看作是一个计数器,它的值可以被多个进程或线程修改和读取。信号量的值表示可用的资源数量或某种条件的状态。

    主要有两种类型的信号量:

    1. 二进制信号量(Binary Semaphore):也称为互斥信号量,它的值只能为0或1。用于实现互斥访问共享资源,只允许一个进程或线程访问资源。
    2. 计数信号量(Counting Semaphore):它的值可以是任意非负整数。用于控制一定数量的资源,多个进程或线程可以同时访问资源,但受限于信号量的计数值。
  • 相关阅读:
    Vue+elementui 纯前端实现Excel导入导出功能(区分表头标题)
    java基于springboot的电子病历开处方选药管理系统
    Leetcode 720. 词典中最长的单词(为啥感觉这道题很难?)
    Redis 6.0学习指南
    使用扩展卡尔曼滤波器进行包裹测量的状态估计
    Upload-labs(1-21关详细教程)【简单易懂】【万字教程】
    UI 自动化测试框架:PO 模式+数据驱动!
    主存储器与CPU的连接
    盘一盘那些高性能设计的点(一)
    算法 | A*算法实现最优路径规划
  • 原文地址:https://blog.csdn.net/midslucky/article/details/133012548