Linux进程间通信

目录

进程间通信介绍

进程间通信的目的

进程间通信发展

进程间通信分类

管道

什么是管道

匿名管道

匿名管道的通信原理:

关于匿名管道的特征:

管道的读写规则

管道的应用场景(匿名管道)

命名管道

创建命名管道

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

[System V共享内存](#System V共享内存)

共享内存示意图

共享内存的原理:

共享内存数据结构

共享内存函数

shmget函数

shmat函数

shmdt函数

shmctl函数

关于共享内存的特性:

[简单谈一谈System V的消息队列](#简单谈一谈System V的消息队列)

关于信号量

引入

理解信号量

介绍信号量


进程间通信介绍

进程间通信的目的

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

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

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

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

进程间通信发展

1.管道

2.System V进程通信

3.POSIX进程间通信

进程间通信分类

1.管道

a.匿名管道pipe。

b.命名管道。

2.System V IPC

a.System V消息队列。

b.System V共享内存。

c.System V信号量。

3.POSIX IPC

a.消息队列。

b.共享内存。

c.信号量。

d.互斥量。

e.条件变量。

f.读写锁。

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

a.进程间通信的本质:必须让不同的进程看到同一份"资源"。

b."资源"是啥?-----特定形式的内存空间。

c.这个"资源"是谁提供的?----一般是由操作系统提供。为什么不是两个进程中的一个呢?假设由其中一个进程提供,这个"资源"属于谁?这个进程独有,破坏了进程的独立性。

d.我们进程访问这个空间,进行通信,本质就是访问操作系统!通过操作系统提供的系统调用接口!(一般操作系统都有一个独立的通信模块-----隶属于文件系统-----IPC通信模块定制标准-----进程间通信是有标准的------System V (主要进行本机交互)&& POSIX(主要用于网络之间))。

e.基于文件级别的通信方式----管道。

管道

什么是管道

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

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

匿名管道

#include <unistd.h>

功能:创建---无名管道

原型:int pipe(int fd [ 2 ]);

参数:

fd:文件描述符数组,其中fd [ 0 ]表示读端,fd[ 1 ]表示写端。

返回值:成功返回0,失败返回错误代码。

匿名管道的通信原理:

要进行通信的进程之间必须有血缘关系,只能进行单向通信。

上图建立好了父子进程之间的通信的信道。为啥建立通信信道这么麻烦呢?------因为进程之间具有独立性!!!

下面写一段测试代码

cpp 复制代码
testPipe.cc
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;
#define N 2
#define NUM 1024

void Writer(int wfd)
{
    // 写入
    string s = "hello,I am child process";
    char buffer[1024];
    pid_t pid = getpid();
    int number = 0;
    while (true)
    {
        buffer[0] = 0; // 清空字符串,把这个数组当成字符串使用
        snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), pid, number++);

        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}

void Reader(int rfd)
{
    // 读取
    char buffer[NUM];
    while (true)
    {
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if (n > 0)
        {
            // 读取成功了
            buffer[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if (0 == n)
        {
            // 读到了文件末尾
            printf("read end of file!\n");
            break;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    // 我们自己规定:child->w ,father->r
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if (n < 0)
        return 1;
    pid_t id = fork();
    if (id < 0)
        return 2;
    else if (0 == id)
    {
        // child--->w
        close(pipefd[0]); // child进行写入,先把r端的文件描述符关闭.

        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    // father--->w
    close(pipefd[1]); // father进行读取,先把w端的文件描述符给关闭.

    Reader(pipefd[0]);

    // 进程等待
    pid_t ret = waitpid(id, nullptr, 0);
    if (ret < 0)
        return 3;

    close(pipefd[0]);

    return 0;
}
cpp 复制代码
Makefile文件


testpipe:testPipe.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f testpipe

上述代码编译好运行之后,我们发现父子进程是可以进行通信的。这里使用ulimit -a 命令可以查看操作系统下的一些极限大小。下面可以看到,管道是有固定大小的!不同的内核可能大小不一样。

关于匿名管道的特征:

1.具有血缘关系的进程进行进程间通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。

2.管道只能单向通信(半双工的)。如果需要双方通信时,需要建立两个管道。

3.父子进程是会进行进程协同的,同步与互斥---保护管道文件的数据安全。

4.管道是面向字节流的。

5.管道是基于文件的,而文件的生命周期是随进程的!!!(一般而言,进程退出,管道释放)

对于上述的第三点,父子进程会进程同步与互斥 ---保护管道文件的数据安全 ,在管道中会有四种情况:

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

2.读写端正常,管道如果被写满,写端就要阻塞。

3.读端正常读,写端关闭。读端就会读到0,表明读到了文件(pipe)的末尾,不会被阻塞。(用户可以在读端里自行判断)

4.写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。(如何干掉呢?---通过信号杀掉。)

下面来验证一下4的说法,OS会把子进程杀掉,并看看使用几号信号把子进程杀掉的。

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;
#define N 2
#define NUM 1024

void Writer(int wfd)
{
    // 写入
    string s = "hello,I am child process";
    char buffer[1024];
    pid_t pid = getpid();
    int number = 0;
    while (true)
    {
        buffer[0] = 0; // 清空字符串,把这个数组当成字符串使用
        snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), pid, number++);

        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}

void Reader(int rfd)
{
    // 读取
    char buffer[NUM];
    int cnt = 0;
    while (true)
    {
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if (n > 0)
        {
            // 读取成功了
            buffer[n] = 0;
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if (0 == n)
        {
            // 读到了文件末尾
            printf("read end of file!\n");
            break;
        }
        else
        {
            break;
        }
        cnt++;
        if (cnt == 5)
            break;
        // sleep(5);
        sleep(1);
    }
}

int main()
{
    // 我们自己规定:child->w ,father->r
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if (n < 0)
        return 1;
    pid_t id = fork();
    if (id < 0)
        return 2;
    else if (0 == id)
    {
        // child--->w
        close(pipefd[0]); // child进行写入,先把r端的文件描述符关闭.

        Writer(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    // father--->w
    close(pipefd[1]); // father进行读取,先把w端的文件描述符给关闭.

    Reader(pipefd[0]);
    close(pipefd[0]);
    cout<<"father close read fd:"<<pipefd[0]<<endl;


    sleep(5); // 观察子进程的僵尸状态

    // 进程等待
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret < 0)
        return 3;

    cout << "wait child success: " << ret << " exit code:" << ((status >> 8) & 0xFF)
         << " exit signal:" << (status & 0x7F) << endl;


    return 0;
}

可以看出上述,父进程进行读取,子进程进程写入,当读取端关闭时,操作系统会给子进程发送13号信号(SIGPIPE),把子进程给杀掉。

管道的读写规则

除了上述四种情况外,当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。

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

管道的应用场景(匿名管道)

我们之前使用的 | 也是属于匿名管道。可以利用匿名管道实现一个简易版的进程池!

cpp 复制代码
ProcessPool.cc


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

const int processnum = 10;
std::vector<task_t> tasks;

// 先描述进程对象
class channel
{
public:
    channel(int cmdfd, pid_t slaverid, std::string &processname)
        : _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {
    }

public:
    int _cmdfd;               // 要写入任务的文件描述符
    pid_t _slaverid;          // 子进程的pid
    std::string _processname; // 子进程的名字
};
void Slaver()
{
    // read(0)----因为在创建进程池的时候,把0号文件描述符重定向到读端的文件了
    while (true)
    {
        int cmdcode = 0;
        ssize_t n = read(0, &cmdcode, sizeof(cmdcode));
        if (n > 0)
        {
            // 读取成功
            std::cout << "slaver say@ get a command: " << getpid()
                      << " :cmdcode:" << cmdcode << std::endl;
            if (cmdcode >= 0 && cmdcode < tasks.size())
                tasks[cmdcode]();
        }
        else if (0 == n)
            break;
    }
}

void InitProcessPool(std::vector<channel> *channels)
{
    // 进行初始化进程池
    // 确保每一个子进程都只有一个写端!!!
    std::vector<int> oldfds;
    for (size_t i = 0; i < processnum; i++)
    {
        int pipefd[2] = {0};
        int n = pipe(pipefd); // 临时的
        if (n < 0)
            assert(n);
        pid_t id = fork();
        if (id == 0)
        {
            // child---r
            for (auto &fd : oldfds)
                close(fd);

            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将0号文件描述符指向pipefd[0]指向的文件。
            close(pipefd[0]);   // 避免浪费文件描述符数组资源
            Slaver();
            exit(0); // 进程退出
        }
        // father
        close(pipefd[0]);
        // 向进程池中插入进程信息
        std::string name = "process: " + std::to_string(i);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);
    }
}

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

void CtrlProcessPool(std::vector<channel> &channels)
{
    // 分配任务码
    for (size_t i = 0; i < 5; i++)
    {
        // 任务码
        int cmdcode = rand() % tasks.size();
        // 选择某个进程
        int processid = rand() % channels.size();

        std::cout << "father say: " << "cmdcode: "
                  << cmdcode << "already sendto " << channels[processid]._slaverid
                  << " processname:" << channels[processid]._processname << std::endl;

        // 写入任务码
        write(channels[processid]._cmdfd, &cmdcode, sizeof(cmdcode));
        sleep(1);
    }
}

void QuitProcess(std::vector<channel> &channels)
{
    for (auto &c : channels)
    {
        close(c._cmdfd);
        pid_t ret = waitpid(c._slaverid, nullptr, 0);
        if (ret > 0)
            std::cout << "wait slaverid success,slaverid:" << ret << std::endl;
    }
}

int main()
{
    // 装载任务
    LoadTask(&tasks);

    srand(time(nullptr) ^ getpid() ^ 1023); // 种一颗随机数的种子
    // 再组织进程对象
    std::vector<channel> channels;
    // 初始化进程池
    InitProcessPool(&channels); // 注意这里创建多进程的时候,子进程会继承父进程的写端
    // 打印进程池
    Debug(channels);

    // 派发任务
    CtrlProcessPool(channels);

    // 清理进程池
    QuitProcess(channels);

    std::cout << "safed quit" << std::endl;
    // sleep(1000);
    return 0;
}
cpp 复制代码
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);
}
cpp 复制代码
Makefile



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

.PHONY:clean
clean:
	rm -f processpool

注意上面的小细节:在创建多个进程的时候,子进程会继承父进程的写端。所以要保证一个子进程只有一个写端,一定要记得及时close掉上一次继承的写端!!!

命名管道

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

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

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

创建命名管道

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

mkfifo filename

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

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

创建命名管道:

int main()

{

mkfifo("p1", 0644);

return 0;

}

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

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

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

3.命名管道删除使用unlink()函数。

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

下面来演示一下命名管道如何使用

cpp 复制代码
server.cc


#include "comm.hpp"

int main()
{
    InitMyPipe init;

    // 打开管道
    int fd = open(MYFIFO_FILENAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(OPEN_ERR);
    }

    // read
    while (true)
    {
        char buffer[1024] = {0};
        ssize_t k = read(fd, buffer, sizeof(buffer)); 
        // 如果client没有发送消息,则阻塞式等待
        if (k > 0)
        {
            buffer[k] = 0;
            std::cout << "client say#" << buffer << std::endl;
        }
        else if (0 == k)
        {
            std::cout << "client quit,me too!" << std::endl;
            break;
        }
        else
            break;
    }

    close(fd);

    return 0;
}
cpp 复制代码
client.cc


#include "comm.hpp"

int main()
{
    // client发送
    // 打开命名管道文件
    int fd = open(MYFIFO_FILENAME, O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        exit(OPEN_ERR);
    }

    std::string line;
    while (true)
    {
        std::cout << "Please Enter@";
        std::cin >> line;

        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}
cpp 复制代码
comm.hpp



#pragma once

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

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

enum
{
    MKFIFO_ERR = 1,
    MKFIFO_DEL_ERR,
    OPEN_ERR,
};

class InitMyPipe
{
public:
    InitMyPipe()
    {
        // 先创建命名管道
        int n = mkfifo(MYFIFO_FILENAME, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(MKFIFO_ERR);
        }
    }
    ~InitMyPipe()
    {
        // 删除管道
        int m = unlink(MYFIFO_FILENAME);
        if (m == -1)
        {
            perror("unlink");
            exit(MKFIFO_DEL_ERR);
        }
    }
};
cpp 复制代码
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和client之间进程通信:

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

};

共享内存函数

shmget函数

功能:用来创建共享内存。

原型:

int shmget(key_t key,size_t size,int shmflg);

参数:

key:这个共享内存段名字。

size:共享内存大小,单位是字节。共享内存的size一般建议是4096的整数倍,如果你设置4097,实际上操作系统会给你分4096*2的大小,但是你却只能用其中的4097个字节!

shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。

返回值:成功返回一个非负整数,即该共享内存段的标识码。失败则返回-1.

关于shmget函数的shmflg说明:

IPC_CREAT(单独 ):如果你申请的共享内存不存在,就创建。存在,就获取并返回

IPC_CREAT | IPC_EXCL如果你申请的共享内存不存在,就创建。存在,就出错返回。---确保,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的!

那么问题来了,shmget函数是如何保证让不同的进程看到同一个共享内存呢?

是通过key_t key来确定的,通过key值!-----其实key_t 本质就是int

谈一谈key:

1.key是一个数字,这个数字是几,并不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识

2.第一个进程可以通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一个共享内存了!

3.key在共享内存的描述对象中!

4.怎么创建key值呢?----使用key_t ftok(const char* pathname,int proj_id);函数。
**pathname和proj_id都是由用户来自由指定的~**这个ftok函数其实是一套算法,把pathname和proj_id进行了数值计算。

shmat函数

功能:将共享内存段连接到进程地址空间。

原型

void* shmat(int shmid,const void* shmaddr,int shmflg);

参数:

shmid:共享内存标识。

shmaddr:指定连接的地址。设置为nullptr。

shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY。设置为0即可。

返回值:成功返回一个指针,指向共享内存第一个字节。失败返回-1.

【说明】

shmaddr为NULL,核心自动选择一个地址。

shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。

shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)。

shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。

shmdt函数

功能:将共享内存段与当前进程脱离。

原型:

int shmdt(const void* shmaddr);

参数:

shmaddr:由shmat所返回的指针。

返回值:成功返回0。失败返回-1.

注意:将共享内存段与当前进程脱离不等于删除共享内存段。

shmctl函数

功能:用于控制共享内存。

原型:

int shmctl(int shmid,int cmd,struct shmid_ds* buf);

参数:

shmid:由shmget返回的共享内存标识码。

cmd:将要采取的动作(有三个可取值)。

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。设置为nullptr即可。

返回值:成功返回0,失败返回-1.

cmd的选项说明:

|----------|-----------------------------------------------|
| 命令 | 说明 |
| IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值。 |
| IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值。 |
| IPC_RMID | 删除共享内存段。 |

**一旦有了共享内存,挂接到自己的地址空间中,可直接把它当做你的内存空间来使用即可!**不需要调用系统调用了!

共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在!除非内核重启(用户释放)。

ipcs -m 命令,可查看当前的共享内存的信息。
**ipcrm -m 【shmid】**命令,用来释放当前的共享内存的!

其中key是操作系统内标定唯一性的 ,shmid只在你的进程中用来表示资源的唯一性的!!!

下面写一段使用共享内存的样例:

cpp 复制代码
comm.hpp




#ifndef __COMM_HPP__
#define __COMM_HPP__

#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

extern Log lg;

const int size = 4096;
std::string pathname = "/home/hxh";
const int proj_id = 9999;

key_t Getkey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if (key < 0)
    {
        lg(Error, "ftok error:%s\n", strerror(errno));
        return -1;
    }
    lg(Info, "create key success,key:%d\n", key);
    return key;
}

int GetShmHelper(int flag)
{
    int key = Getkey();
    int shmid = shmget(key, size, flag);
    if (shmid < 0)
    {
        lg(Error, "create share memory error:%s\n", strerror(errno));
        return -1;
    }
    return shmid;
}

int CreateShm()
{
    return GetShmHelper(IPC_CREAT | IPC_EXCL | 0664); // 可带上权限
}

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

#endif
cpp 复制代码
processa.cc




#include "comm.hpp"
#include "Log.hpp"

int main()
{
    // 创建共享内存
    int shmid = CreateShm();
    // 挂载到进程地址空间
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    // IPC code

    while (true)
    {
        std::cout << "client say# " << shmaddr << std::endl; // 直接访问共享内存
        sleep(1);
    }

    // 去关联
    shmdt(shmaddr);
    // 释放共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    return 0;
}
cpp 复制代码
processb.cc




#include "comm.hpp"
#include "Log.hpp"

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

    // IPC code

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

    shmdt(shmaddr);
    return 0;
}

关于共享内存的特性:

1.共享内存没有同步互斥之类的保护机制!

2.共享内存是所有的进程间通信中,速度最快的。----因为拷贝少。

3.共享内存内部的数据,由用户自己维护!

简单谈一谈System V的消息队列

关于信号量

引入

由上述共享内存可知,共享内存是没有任何保护机制的。

1.当我们的A正在写入,写入一部分,就被B拿走了,导致双方发和收的数据不完整,A和B看到同一份资源,是共享资源。------数据不一致问题。

2.任何时刻,只允许一个执行流访问共享资源-----互斥

3.共享的,任何时刻只允许一份执行流访问(就是执行访问代码)的资源------临界资源。(一般是内存空间。)

4.访问临界资源的那段代码。------临界区

理解信号量

信号量/信号灯的本质就是一把计数器,类似但不是等于计数器!用来描述临界资源中资源数量的多少!

1.申请计数器成功,就表示我具有访问资源的权限了!

2.申请了计数器资源,我当前访问了我要的资源了吗?----并没有,申请了计数器资源是对资源的预订机制。

3.计数器可以有效保证进入共享资源的执行流的数量。

4.所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。

程序员把这个"计数器"叫做信号量!把值只能为0,1两态的计数器叫做二元信号量-----本质也就是一个锁!

但是问题来了:我们要访问临界资源,先要申请信号量计数器资源------->信号量计数器不也就是共享资源吗?这不就演化成是鸡生蛋还是蛋生鸡的问题了???

所以信号量要想保护临界资源的安全,首先先保证自己是安全的!

介绍信号量

申请信号量,本质是对计数器减减P操作

释放信号量,本质是对计数器加加V操作

申请和释放PV操作 ------>原子的

什么叫做原子的 ?------>就是只有两种状态:要么不做,要做就做完!没有"正在做"这样的概念!

总结一下:

信号量的本质就是一把计数器,PV操作,原子的!

执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源!

信号量值为0,1两态的,叫二元信号量,就是互斥机制。

申请信号量的本质:是对临界资源的预订机制!!!

信号量凭什么是进程间通信的一种?

1.通信不仅仅是通信数据,互相协同也是。

2.要协同,本质也是通信,信号量首先要被所有的通信进程看到!!!

相关推荐
ONE_SIX_MIX2 小时前
debian 13 安装 nvidia-driver 后,登录后黑屏,只能看到左上角光标 或 一个鼠标 的问题解决
运维·debian
虹科数字化与AR2 小时前
安宝特方案丨AR电力·变电篇:筑牢变电站安全运维
运维·安全·ar
Wang15302 小时前
c++与Java谁的性能更胜一筹
java·c++
代码游侠2 小时前
应用——Linux Socket编程
运维·服务器·开发语言·笔记·网络协议·学习
SunkingYang2 小时前
使用TortoiseGit工具推送(push)时,忘了先拉取(Pull),提示冲突,后续怎么处理冲突?
git·tortoisegit·pull·push·推送·冲突·拉取
张火火isgudi2 小时前
VMware Debian 挂载 Windows 文件夹至 Debian 目录
linux·运维·windows·debian
石榴花上2 小时前
银河麒麟上的9090端口被占用问题处理
linux
Tipriest_2 小时前
C++ 中 std::move 的使用方法与注意事项
c++·move
神算大模型APi--天枢6462 小时前
2025 国产算力破局后,大模型训练数据集如何实现 “合规采集 + 高效清洗”?
运维·服务器·人工智能·架构·硬件架构
yuuki2332332 小时前
【C++】vector底层实现全解析
c++·后端·算法