Linux:进程间通信

目录

一、关于进程间通信

二、管道

pipe函数

管道的特点

匿名管道

命名管道

mkfifo

三、system v共享内存

shmget函数(创建)

ftok函数(生成key)

shmctl函数(删除)

shmat/dt函数(挂接/去关联)

四、初识信号量


一、关于进程间通信

首先我们都知道,进程运行是具有独立性的,所以两个进程想通信的话,难度较大

进程间通信的本质:

让不同的进程看到同一份资源(内存空间)

这里的能看到的同一块内存,不能属于任何一个进程,而应该是共享的

进程间通信的目的:

为了交互数据、控制、通知等目的

进程间通信的发展:

管道

system V进程间通信

POSIX进程间通信

进程间通信分类:

管道:匿名管道pipe、命名管道

system V IPC:system V 消息队列、共享内存、信号量

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


二、管道

管道是非常古老的进程间通信的形式

管道是计算机通信领域的设计者设计得一种单向通信的方式

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

管道通信的背后是进程间通过管道进行通信的


下面举个管道的例子,方便理解:

pipe函数

需要用到pipe创建管道,man查看pipe,pipe是系统调用接口

需要的头文件是unistd.h

作用是创建管道

参数是pipefd[2],是输出型参数,希望通过调用它,得到被打开的文件fd

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

代码如下:

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

using namespace std;

int main()
{
    // 创建管道
    int pipefd[2] = {0};
    // 文件描述符0/1/2分别是stdin/stdout/stderr
    // 这里的pipefd[0]为3,表示读端
    // 这里的pipefd[1]为4,表示写端
    int n = pipe(pipefd);
    assert(n != -1);
    // debug下assert起作用,release下不起作用
    // 所以需要(void)n,表示n被使用过
    // 如果不(void)n,在release下会被认为n没有被使用
    (void)n;

    // 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        // 子进程 -> 读
        // 构建单向通信的信道,父进程写入,子进程读取
        // 关闭子进程不需要的fd,关闭写
        close(pipefd[1]);
        char buffer[1024]; // 缓冲区,用于读数据
        while (true)       // 一直循环
        {
            // ssize_t类型是long int
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0) // 读成功
            {
                // read是系统调用,所以读取完结尾不会有\0,自己加
                buffer[s] = 0;
                cout << "子进程[" << getpid() << "]获得父进程的message," 
                     << buffer << endl;
            }
        }
        // 最后关闭读
        close(pipefd[0]);
        exit(0);
    }
    // 父进程 -> 写
    // 构建单向通信的信道,父进程写入,子进程读取
    // 关闭父进程不需要的fd,关闭读
    close(pipefd[0]);
    int count = 0; // 发送消息的条数
    string message = "父进程正在发送消息";
    char send_buffer[1024];
    while (true)
    {
        // 构建变化的字符串
        // 往send_buffer中写入
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
                 message.c_str(), getpid(), count++);
        // 写入
        write(pipefd[1], send_buffer, strlen(send_buffer));
        // 每次sleep方便观察
        sleep(1);
    }
    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret > 0);
    (void)ret;
    // 最后关闭写
    close(pipefd[1]);

    return 0;
}

观察运行结果:

通过结果可以发现,父进程每次写入后,子进程都能读取到,以管道的方式实现了进程间通信


管道的特点

1、管道是用来进行具有血缘关系的进程进行进程间通信的,常用于父子进程

2、管道通过让进程间协同,提供了访问控制

访问控制就是指:如果父进程写的慢,例如上述代码,每隔一秒写一次,子进程即使没有sleep,也只能每隔一秒再读;而如果写的很快,当把缓冲区写满了,在读取前也就不能再继续写了

3、管道提供的是面向流式的通信服务(面向字节流,需要定制协议进行数据区分,后面博客会说到)

流式服务:如果写的很快,读的很慢,读的时候一次就可以读一批消息

4、管道是基于文件的,文件的生命周期是随进程的,所以管道的生命周期也是随进程的

写入的一方的fd如果没有关闭,读取的一方有数据就读,没有数据就等

写入的一方的fd如果关闭了,读取的一方read返回0,表示读到了文件的结尾

5、管道是单向通信的,就是半双工通信的一种特殊情况

半双工通信就是指两个人通信,我发你就不能发,你发我就不能发

下面总结四种情况:

①写端快,读端慢,写满就不能再写了

②写端慢,读端快,管道没有数据时,读端必须等待

③写端关闭fd,读端read返回0,表示读到了文件结尾

④读端关闭fd,写端继续写,OS会自动终止写端

前两种也就是上面提到的访问控制


匿名管道

下面引入一个例子了解匿名管道的使用,一个父进程有5个子进程,父进程与每一个子进程都建立对应的管道,每个子进程内部都有处理任务的方法,如果用户给了一个任务,父进程可以给子进程派发该任务,让子进程完成该任务,也就是实现一个小型的进程池

首先实现Makefile:

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

ProcPool.cc(.cc和.cpp一样)代码:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cassert>
#include <vector>

#include "task.hpp"

using namespace std;

#define PROCESS_NUM 5

int waitcommand(int waitfd, bool& q)
{
    //uint32_t是4字节
    uint32_t command = 0;
    //从文件描述符waitfd中读取,读取的内容写到command里
    ssize_t s = read(waitfd,&command,sizeof(command));
    if(s == 0)
    {
        q = true;
        return -1;
    }
    assert(s == sizeof(uint32_t));
    return command;
}

//给一个进程id,通过文件描述符fd,发送命令command
void sendAndExec(pid_t id,int fd, uint32_t command)
{
    write(fd,&command,sizeof(command));
    cout << "调用进程[" << id << "] 执行:`" << desc[command] << endl;
}


int main()
{
    //将task中的方法装载进来
    load();
    //pair键值对:pid、pipefd
    vector<pair<pid_t, int>> slots;
    //创建多个进程
    for(int i= 0; i < PROCESS_NUM; ++i)
    {
        //创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        //这里的(void)n与前一个例子一样的作用
        //release下,assert就没用了,n会被认为没有使用
        (void)n;

        pid_t id = fork();
        assert(id != -1);
        //子进程进行读取
        if(id == 0)
        {
            //子进程关闭写端
            close(pipefd[1]);
            while(true)
            {
                //false表示不退出
                bool q = false;
                //子进程等命令,不发命令,就阻塞
                int command = waitcommand(pipefd[0], q);
                //执行对应的命令
                if(command >= 0 && command < tasksize())
                {
                    //执行call中command对应的方法
                    call[command]();
                }
                else
                {
                    cout << "非法command: " << command << endl;
                }
            }

            exit(1);
        }
        //父进程关闭读端
        close(pipefd[0]);
        //存每个子进程的pid
        slots.push_back(pair<pid_t,int>(id,pipefd[1]));
    }
    //父进程派发任务
    //让数据源更加随机
    srand((unsigned long)time(nullptr) ^ getpid() ^ 89745213L);
    while(true)
    {
        //下面屏蔽的部分是自动选择任务,不需要人为输入
        //选择任务
        //int command = rand() % tasksize();
        //选择进程
        //int selectID = rand() % slots.size();
        //将任务交给子进程
        //sendAndExec(slots[selectID].first,slots[selectID].second,command);
        //sleep(1);

        int num;
        int command;
        cout << "##########################################" << endl;
        cout << "#####    1.展示功能    2.发送命令    #####" << endl;
        cout << "##########################################" << endl;
        cout << "请输入你的选择:";
        cin >> num;
        if(num == 1)
            show();
        else if(num == 2)
        {
            cout << "请选择你的命令:";
            //选择任务
            cin >> command;
            //选择进程
            int selectID = rand()%slots.size();
            //将任务交给子进程
            sendAndExec(slots[selectID].first,slots[selectID].second,command);
        }
    }
    //关闭fd,子进程会退出
    for(const auto& e : slots)
    {
        close(e.second);
    }

    //等待子进程退出,回收子进程信息
    for(const auto& e : slots)
    {
        //默认在阻塞状态去等待子进程
        waitpid(e.first,nullptr,0);
    }

    return 0;
}

Task.hpp

(以hpp结尾,.cc的实现代码混入.h头文件当中,定义与实现都包含在同一文件)

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <functional>
#include <unordered_map>

//这里表示func这个函数类型,返回值都是void,没有参数
typedef std::function<void()> func;
//call中存各种func类型的函数
std::vector<func> call;
//desc是描述各个命令编号,所对应的任务名称的
std::unordered_map<int, std::string> desc;

void execadd()
{
    std::cout << "程序[" << getpid() << "] 执行加法操作的任务" << std::endl;
}

void execsub()
{
    std::cout << "程序[" << getpid() << "] 执行减法操作的任务" << std::endl;
}

void execmul()
{
    std::cout << "程序[" << getpid() << "] 执行乘法操作的任务" << std::endl;
}

void execdiv()
{
    std::cout << "程序[" << getpid() << "] 执行除法操作的任务" << std::endl;
}

void load()
{
    //即0号编号的任务是execadd
    desc.insert({call.size(),"execadd : 加法"});
    call.push_back(execadd);

    desc.insert({call.size(),"execsub : 减法"});
    call.push_back(execsub);

    desc.insert({call.size(),"execmul : 乘法"});
    call.push_back(execmul);

    desc.insert({call.size(),"execdiv : 除法"});
    call.push_back(execdiv);
}

void show()
{
    for(const auto& e : desc)
    {
        std::cout << e.first << "->" << e.second << std::endl;
    }
}

int tasksize()
{
    return call.size();
}

人为输入的结果如下:

自动选择任务的结果如下:

小型的进程池实现完毕


命名管道

关于匿名管道和命名管道,上面所举的例子都是匿名管道,匿名管道缺点是只能由有亲缘关系的进程进行通信,如果想要两个毫不相关的进程进行通信,就需要用到命名管道了

匿名管道和命名管道一样,都是两个进程通过同一份文件,进行通信,只不过看到同一份文件的手段、途径是不一样的

匿名管道是子进程通过继承父进程的方式,打开同一份文件

命名管道是创建一个管道文件,并且让两个不相关的进程打开同一个文件


创建管道文件需要用到mkfifo,可以在指定路径下创建命名管道

man查看:

下面就是创建一个name_pipe的管道文件:

圈中的p就是指管道文件

下面具体演示现象:

首先复制SSH渠道,形成左右两个窗口(即有两个毫不相关的进程),在同一个路径下,:

左边的窗口,先往name_pipe中重定向一句话hello,写到name_pipe中:

这时由于左边的窗口写了内容,但是右边窗口并没有打开,所以此时处于阻塞状态

所以此时右边窗口cat从管道中把数据读取出来:

此时完成了一个进程向另一个进程通过管道的方式写入消息的过程

如果想删除管道文件,可以rm,也可以unlink


mkfifo

man 3 mkfifo查看mkfifo函数

头文件:

sys/types.h和sys/stat.h

参数:

第一个参数pathname,表示特定的路径

第二个参数mode,open时也见过,表示需要指定权限(例如0666,6表示rw-)

返回值:

mkfifo成功了返回0,小于0表示创建失败 


下面用样例更清楚的理解命名管道的知识:

创建两个.cc(.cc、.cxx、.cpp是一样的)文件,分别是client.cc和server.cc,分别表示服务器端和顾客端

common.hpp表示client.cc和server.cc必须包含的文件

log.hpp表示每次执行完操作,打印提示信息

makefile当然也是有的,方便操作

下面看具体演示,首先复制一个ssh渠道,让两个毫不相干的进程都在一个路径下(具体代码在演示结果的下面)

左边窗口当做server端,右边窗口当做client端:

左边先运行server:

这时打印显示创建管道文件成功,再在右边窗口运行client:

这时server端会显示打开管道文件成功

接着在client端输入信息,分别输入你好,再见:

这时serve端也会显示对应输入的信息,最后client端Ctrl + c 退出进程:

server端会显示左边红框的信息,整个过程结束


下面是具体代码:

Makefile:

.PHONY:all
all:client server

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

.PHONY:clean
clean:
	rm -f client server

common.hpp:

//如果_COMMON_H_不存在就define
#ifndef _COMMON_H_ 
#define _COMMON_H_

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"

using namespace std;

#define SIZE 128
//设置MODE权限为0666
#define MODE 0666
//设置路劲path为当前路径的fifo.ipc文件
string path = "./fifo.ipc";

#endif

log.hpp:

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

std::ostream &log(std::string message)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << message;
    return std::cout;
}

#endif

client.cc:

#include "common.hpp"

int main()
{
    //获取管道文件
    int fd = open(path.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }
    //通信过程
    string buffer; 
    while(true)
    {
        cout << "请输入信息:";
        getline(cin, buffer);
        write(fd,buffer.c_str(),buffer.size());
    }

    //关闭fd
    close(fd);

    return 0;
}

server.cc

#include "common.hpp"

int main()
{
    //创建管道文件
    if(mkfifo(path.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }
    log("创建管道文件成功!") << endl;
    //文件操作
    int fd = open(path.c_str(), O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        exit(2);
    }
    log("打开管道文件成功!") << endl;
    //编写通信代码
    char buffer[SIZE];
    while(true)
    {
        //清空buffer
        memset(buffer,'\0',sizeof(buffer));
        //从管道文件中读取
        //sizeof(buffer)-1是因为系统接口不考虑\0结尾
        ssize_t s = read(fd,buffer,sizeof(buffer) - 1);
        if(s > 0)
        {
            //读取成功
            cout << "client :" << buffer << endl;
        }
        else if(s == 0)
        {
            //读到了文件结尾
            cerr << "读到文件结尾,client和server都退出" << endl;
            break;
        }
        else
        {
            //读取失败
            perror("read");
            break;
        }
    }

    //关闭文件
    close(fd);
    log("关闭管道文件成功!") << endl;
    //通信完毕就删除文件
    unlink(path.c_str());
    log("删除管道文件成功!") << endl;
    return 0;
}

三、system v共享内存

操作系统提供了共享内存,共享内存的本质:

共享内存 = 共享内存块 + 共享内存对应的内核数据结构

共享内存映射到各自进程的地址空间后,不用经过系统调用,直接可以访问,意味着双方进程要通信,直接进行内存级的读和写即可

shmget函数(创建)

首先共享内存需要用到shmget函数,作用是创建并获取共享内存

man 2 shmget查看:

包含了两个头文件:

sys/ipc.h和sys/shm.h

函数参数:

第一个参数key(key_t其实就是32位的整数类型),是用于保证通信的双方进程看到的是同一份共享内存,key在系统是唯一的,通信双方使用同一个key,只要key值相同,通信双方就是看到了同一个共享内存

第二个参数size,表示要创建的共享内存有多大

第三个参数shmflg,一般由两种选项(IPC_CREAT、IPC_EXCL)

IPC_CREAT:单独使用时,如果底层已经存在,就获取并返回它;如果底层不存在,就创建并返回它

IPC_EXCL:单独使用是无意义的

IPC_CREAT、IPC_EXCL共同使用:如果底层不存在,创建并返回;如果底层已经存在,出错返回

创建共享内存要有权限,还需 | 0666

函数返回值:

成功则返回一个合法的标识符,失败就返回-1


下面具体讲解关于第一个参数key的相关知识

ftok函数(生成key)

为了能够形成唯一的key值,需要用到函数ftok

ftok其实是一套算法,只是为了将pathname和proj_id联合起来,形成一个唯一值

ftok包含头文件sys/types.h和sys/ipc.h

参数pathname是路径,proj_id是项目id(一般是0~255之间)

ftok的返回值成功就返回key,失败返回-1

下面是ftok的使用:

shmserver.cc:

shmclient.cc:

common.hpp:

此时运行代码,观察shmserver.cc和shmclient.cc中创建出来的key值:

通过结果发现,key值相同


共享内存的声明周期是随内核的

查看共享内存:ipcs -m

上图的perm就是权限,nattch与该共享内存关联的进程个数,status是状态 

手动删除共享内存:ipcrm -m [shmid]

删除后shmid为0的共享内存就被删除了

这是手动删除共享内存,比较麻烦,也有自动删除共享内存的函数

shmctl函数(删除)

man查看shmctl:

包含头文件sys/ipc.h和sys/shm.h

函数参数:

第一个参数shmid,就是创建的共享内存用户管理的id

第二个参数cmd,有不同的选项,对这个共享内存有不同的操作方案(删除一般用IPC_RMID)

IPC_RMID即使有进程与当前共享内存的shm挂接,依旧删除共享内存

第三个参数通常用于获取共享内存的属性,一般只进行删除操作时,设为空即可

函数返回值:

失败会返回-1,成功会返回0

使用如下(shmid是自己创建的):


shmat/dt函数(挂接/去关联)

at就是attch的意思

man shmat:

包含头文件sys/types.h和sys/shm.h

shmat函数参数

第一个参数shmid,就是要挂接的共享内存的用户管理的id

第二个参数shmaddr,就是要挂接这个共享内存的虚拟地址(由于虚拟地址我们并不清楚,所以一般设为空,让OS帮我们自由挂接)

第三个参数shmflg,就是挂接共享内存的挂接方式(例如SHM_RDONLY只读,如果是0表示默认)

shmat函数返回值:

成功时会返回挂接成功的共享内存的地址(虚拟地址),失败返回-1

shmdt函数参数:

shmaddr就是刚刚shmat函数成功后的返回值

shmdt函数返回值:

成功返回0,失败返回-1


共享内存总结的结论:

只要通信双方使用共享内存,一方直接向共享内存中写入,另一方马上就能看到对方写入的数据,所以共享内存是所以进程间通信中速度最快的,因为不需要过多的拷贝

与管道不同,共享内存缺乏访问控制,所以会带来并发问题


由于共享内存缺乏访问控制,而管道是有访问控制的,所以我们可以借助管道使得共享内存拥有访问控制

下面例子具体演示(代码下演示结果的下方)

有以下文件:

首先复制SSH渠道,使得两个窗口都处于同一路径下,一端作为server端,一端作为client端:

接着make,创建两个可执行文件:

左边窗口当做server端,右边当做client端:

左边窗口server端显示没有数据,等待中,等待右边窗口client端输入数据

右边输入数据后结果如下:

最后想退出时,client端输入quit即可:


下面是上述例子的代码部分:

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

log.hpp:

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

std::ostream &log(std::string message)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << message;
    return std::cout;
}

#endif

common.hpp:

#pragma once

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cassert>
#include <fcntl.h>

#include "log.hpp"

using namespace std;

#define PATH_NAME "/home/fcy"
#define PROJ_ID 0x55
//共享内存的大小最好是页的整数倍(4096)
#define SHM_SIZE 4096
#define FIFO_NAME "./fifo"

//由于共享内存没有访问控制
//所以借助管道,实现访问控制
class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME,0666);
        assert(n == 0);
        (void)n;
        log("创建管道文件成功!");
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        log("删除管道文件成功!");
    }
};


#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("没有数据,等待中") << endl;
    uint32_t str = 0;
    ssize_t s = read(fd,&str,sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}
//写入信息
int Signal(int fd)
{
    uint32_t str = 1;
    ssize_t s = write(fd,&str,sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    log("正在等待输入数据") << endl;

}
//关闭该文件描述符
void CloseFIFO(int fd)
{
    close(fd);
}

shmserver.cc:

#include "common.hpp"

//程序加载时自动构建全局变量,自动调用该类的构造函数
//程序退出时,全局变量被析构,自动调用析构函数删除管道文件
Init init;


//将10进制数转化为16进制,方便查看共享内存时观察
string Trans(key_t k)
{
    char buffer[32];
    snprintf(buffer, sizeof(buffer), "0x%x", k);
    return buffer;
}

int main()
{
    //创建公共的key
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);
    log("创建key成功") << "server key->" << Trans(k) << endl;

    //创建共享内存,建议创建全新的共享内存
    int shmid = shmget(k,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
    if(shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    log("创建共享内存成功") << "shmid->" << shmid << endl;

    //将指定的共享内存,挂接到自己的地址空间
    char* shmaddr = (char*)shmat(shmid,nullptr,0);
    log("挂接共享内存成功") << "shmid->" << shmid << endl;

    int fd = OpenFIFO(FIFO_NAME,READ);
    //将共享内存当做字符串使用
    for(;;)
    {
        //刚开始先wait,数据为空,先不读取
        Wait(fd);
        printf("%s\n",shmaddr);
        //当client端不写入了,server端就停止读取共享内存的数据
        if(strcmp(shmaddr, "quit") == 0)
            break;
    }

    //将指定的共享内存,从自己的地址空间中去除关联
    int ret = shmdt(shmaddr);
    assert(ret != -1);
    (void)ret;
    log("去关联共享内存成功") << "shmid->" << shmid << endl;

    //删除共享内存
    ret = shmctl(shmid,IPC_RMID,nullptr);
    assert(ret != -1);
    (void)ret;
    log("删除共享内存成功") << "shmid->" << shmid << endl;

    //关闭文件描述符
    CloseFIFO(fd);
    return 0;
}

shmclient.cc:

#include "common.hpp"

int main()
{
    //创建公共的key
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if(k < 0)
    {
        perror("ftok");
        exit(1);
    }
    log("创建key成功") << "client key->" << k << endl;

    //获取共享内存
    int shmid = shmget(k,SHM_SIZE,0);
    if(shmid < 0)
    {
        perror("ftok");
        exit(2);
    }
    log("创建共享内存成功") << "shmid->" << shmid << endl;

    //挂接共享内存
    char* shmaddr = (char*)shmat(shmid,nullptr,0);
    if(shmaddr == nullptr)
    {
        perror("ftok");
        exit(3);
    }
    log("挂接共享内存成功") << "shmid->" << shmid << endl;


    int fd = OpenFIFO(FIFO_NAME,WRITE);
    //将共享内存当做char类型的buffer
    while(true)
    {
        ssize_t s = read(0,shmaddr,SHM_SIZE-1);
        if(s > 0)
        {
            //将quit后面的回车(\n)变为\0
            //否则无法判断是quit这个字符串
            shmaddr[s-1] = 0;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0)
                break;
        }
    }
    //关闭
    CloseFIFO(fd);

    //将指定的共享内存,从自己的地址空间中去除关联
    int ret = shmdt(shmaddr);
    assert(ret != -1);
    (void)ret;
    log("去关联共享内存成功") << "shmid->" << shmid << endl;

    //client不需要删除共享内存
    return 0;
}

四、初识信号量

我们之前所学的,为了进程间通信,都是要让不同的进程看到同一份资源,而不同的进程的进程看到同一份资源,会带来时序问题,造成数据不一致的问题

多个执行流互相运行是相互干扰,主要是访问临界资源时不加以保护,而在非临界区是不会互相干扰的

下面引入几个概念:

临界资源多个进程(执行流)看到的公共的一份资源称为临界资源

临界区:自己的进程访问临界资源的代码称为临界区

互斥为了更好的进行临界区的保护,可以让多个执行流在任意时刻只能有一个进程进入临界区,这种方式称为互斥

原子性:要么不做,要么做完,没有中间状态,称之为原子性


每一个进程想要进入临界资源,访问临界资源的一部分,需要先申请信号量,才能使用临界资源

信号量就像一个计数器一样,申请信号量的本质是让信号量计数器--

申请信号量成功后,临界资源内部就会预留你想要的资源,是对临界资源的预定机制

所以申请信号量,信号量计数器--,称之为信号量的P操作,必须是原子的

释放信号量,信号量计数器会++,称之为信号量的V操作,也必须是原子的

访问临界资源也就是进程执行自己的临界区代码


剩下详细的信号量知识在多线程部分会说到

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于视觉传感器的自主扫雷机器人设计与实现

摘要&#xff1a; 在当今的世界安全形势下&#xff0c;扫雷小车的出现可以减少各国人员在扫雷过程中的人员伤亡&#xff0c;扫雷小车实用性能强更适合在军事化领域或者是民用领域上应用。让它具有光明的发展前景。针对这一情况&#xff0c;本毕业设计就对自主扫雷小车进行研究…

C语言链表学习实例,链表初始化,利用尾指针将两个链表链接在一起。

C语言链表学习实例&#xff0c;链表初始化&#xff0c;利用尾指针将两个链表链接在一起。 这个实例中&#xff0c;讲解了如何使用两个单循环链表利用尾指针连接&#xff0c;代码如下&#xff1a; #include<stdio.h> #include<stdlib.h> typedef struct CLinkList {…

设计模式详解(三):工厂方法

目录导航 抽象工厂及其作用工厂方法的好处工厂方法的实现关系图实现步骤 工厂方法的适用场景工厂方法举例 抽象工厂及其作用 工厂方法是一种创建型设计模式。所谓创建型设计模式是说针对创建对象方面的设计模式。在面向对象的编程语言里&#xff0c;我们通过对象间的相互协作&…

同旺科技 分布式数字温度传感器 -- Modbus Poll测试

内附链接 1、数字温度传感器 主要特性有&#xff1a; ● 支持PT100 / PT1000 两种铂电阻&#xff1b; ● 支持 2线 / 3线 / 4线 制接线方式&#xff1b; ● 支持5V&#xff5e;17V DC电源供电&#xff1b; ● 支持电源反接保护&#xff1b; ● 支持通讯波特率1200bps、2…

Linux常用命令——vi命令

文章目录 vi的工作模式常用快捷键提示和技巧结论 Linux环境下的vi编辑器不仅以其强大的功能著称&#xff0c;也因其快捷键而闻名。这些快捷键可以显著提高编辑效率&#xff0c;是每个使用vi的人必须掌握的。下面将扩展介绍vi的一些常用快捷键。 vi的工作模式 vi主要有两种模式…

计算机网络HTTP篇

目录 一、HTTP基本概念 二、GET 与 POST 2.1、GET 与 POST 有什么区别&#xff1f; 2.2、GET 和 POST 方法都是安全和幂等的吗&#xff1f; 三、HTTP 缓存 3.1、强制缓存&#xff1a; 3.2、协商缓存 四、HTTP 特性 4.1、HTTP/1.1 4.1.1、HTTP/1.1 的优点 4.1.2、HTT…

mysql mybatis分页查询 大数据量 非常慢

查阅了很多博客和资料&#xff0c;这篇文章以思路为准&#xff0c;详细代码不细说&#xff0c;都是非常简单的方法&#xff0c;一看就明白。具体实现稍微百度一下就能出来。仅供参考。 如题&#xff1a;单表数据已经达到4千万条数据&#xff0c;通过mybatis的分页查询效率非常低…

1、底层世界单片机

一、单片机简介 单片机是单片微型计算机的简称&#xff0c;MCU是Microcontroller的简称&#xff0c;也就是嵌入式微控制器。采用集成电路技术将具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、定时器/计时器、多种I/O口和中断系统等功能集成到一块硅片上。可…

漏洞扫描服务是什么

漏洞扫描服务是维护网络安全的重要一环。通过定期或实时的漏洞扫描&#xff0c;组织可以及时发现并修复可能存在的安全威胁&#xff0c;增强自身网络的安全性。在选择漏洞扫描服务时&#xff0c;需要明确自身的需求和目标&#xff0c;并选择合适的工具和服务提供商。只有这样&a…

生成对抗网络(DCGAN)手写数字生成

文章目录 一、前言二、前期工作1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09; 二、什么是生成对抗网络1. 简单介绍2. 应用领域 三、创建模型1. 生成器2. 判别器 四、定义损失函数和优化器1. 判别器损失2. 生成器损失 五、定义训练循环六、训练模型七、创建 G…

【新品上市】启扬储能管理平板,打造储能管理新模式,助力全场景储能数智化升级!

随着可再生能源的快速发展&#xff0c;储能技术的应用日益广泛&#xff0c;储能系统成为解决可再生能源波动性和不可控制性的关键环节。储能系统通过实时监测、数据分析、远程控制等智能化功能&#xff0c;实现能量的高效利用和系统的稳定运行。 启扬智能推出 工业级储能管理平…

自定义 element DatePicker组件指令 使选择器呈现为只读状态,用户无法直接编辑,但可以查看和选择日期

1.问题 现实中遇到列表的搜索条件使用DatePicker 组件进行开始结束时间筛选&#xff0c;但是手动修改input中的值&#xff0c;导致请求参数异常。比如讲clearable设置为false之后还是能手动清空输入框中的值。虽然组件提供了readonly 属性&#xff0c;但是也会让日期选择也无法…

IDEA中springboot 提示 java: 找不到符号 符号: 变量 log

在以下位置加上该配置"-Djps.track.ap.dependenciesfalse" 然后重新启动项目&#xff0c;到此问题解决&#xff01;&#xff01;&#xff01;

近期知识点

aop (1) 要求利用AOP记录用户操作日志。内容包含以下信息&#xff1a;ip地址、用户名、请求的地址&#xff0c;请求的时间 &#xff08; 4 分&#xff09; &#xff08;2&#xff09;要求利用AOP记录用户操作日志&#xff0c;日志记录到文本文件。内容包含以下信息&#xff…

如何用量化交易“做空”来获取收益

最近的市场环境相当不好&#xff0c;今年一年都没有任何主线的模式情况下去交易。更多的都是题材热点聚焦&#xff0c;而且板块轮动过快。市场环境不好的情况下&#xff0c;我们如何通过“做空”来获取收益&#xff01;量化做空是指利用计算机模型和算法&#xff0c;通过分析市…

FPGA falsh相关知识总结

1.存储容量是128M/8 Mb16MB 2.有256个sector扇区*每个扇区64KB16MB 3.一页256Byte 4.页编程地址0256 5&#xff1a;在调试SPI时序的时候一定注意&#xff0c;miso和mosi两个管脚只要没发送数据就一定要悬空&#xff08;处于高组态&#xff09;&#xff0c;不然指令会通过两…

数据爬取+数据可视化实战_哪里只得我共你(Dear Jane)_词云展示----网易云

一、前言 歌词上做文本分析&#xff0c;数据存储在网页上&#xff0c;需要爬取数据下来&#xff0c;词云展示在工作中也变得日益重要&#xff0c;接下来将数据爬虫与可视化结合起来&#xff0c;做个词云展示案例。 二、操作步骤 代码如下&#xff1a; # -*- coding:utf-8 -*-…

专家解读金融科技大势《金融科技行业2023年专利分析白皮书》发布

文/ 张翼翔 China IP 金融是国民经济的血脉&#xff0c;也是国家核心竞争力的重要组成部分。今年10月30日至31日举行的中央金融工作会议指出&#xff0c;金融要为经济社会发展提供高质量服务&#xff0c;坚持在市场化、法治化轨道上推进金融创新发展。近年来&#xff0c;人工智…

Course1-Week3-分类问题

Course1-Week3-分类问题 文章目录 Course1-Week3-分类问题1. 逻辑回归1.1 线性回归不适用于分类问题1.2 逻辑回归模型1.3 决策边界 2. 逻辑回归的代价函数3. 实现梯度下降4. 过拟合与正则化4.1 线性回归和逻辑回归中的过拟合4.2 解决过拟合的三种方法4.3 正则化4.4 用于线性回归…

知识图谱最简单的demo实现——基于pyvis

1、前言 我们在上篇文章中介绍了知识图谱的简单实现&#xff0c;最后使用neo4j进行了展示&#xff0c;对于有些情况我们可能并不想为了查看知识图的结果再去安装一个软件去实现&#xff0c;那么我们能不能直接将三元组画出来呢/ 接下来我们就介绍一个可视化的工具pyvis&#…