进程间通信

目录

进程间通信介绍

进程间通信的目的:

进程间通信的发展:

进程间通信的分类:

管道

什么是管道

pipe系统调用函数

匿名管道

管道的特点和4种场景:

给予管道,进行一个简单的设计(进程池)

命名管道

mkfifo指令

mkfifo函数

unlink函数

system V共享内存

shmget系统函数

ftok系统函数

什么是IPC

ipcs指令

ipcrm指令

shmctl系统函数

shmat系统函数

shmdt系统函数

共享内存的空间分配

共享内存通信和管道通信进行对比

共享内存 vs. 管道

使用场景:

system V信号量

互斥概念

信号量

基本概念:

类型:

基本操作:

semget函数

semctl函数

semop函数

进程间通信介绍

进程间通信的目的:

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的发展:

管道
System V进程间通信
POSIX进程间通信

进程间通信的分类:

1.管道

匿名管道pipe
命名管道

2.System V IPC
System V 消息队列(不介绍)
System V 共享内存
System V 信号量

3.POSIX IPC
消息队列(不介绍)
共享内存
信号量
互斥量
条件变量
读写锁

管道

什么是管道

管道(Pipe)是一种用于进程间通信的机制,用于在相关的进程之间传递数据。它是一个字节流的通信机制,具有先进先出(FIFO)的数据结构。

在Unix-like系统中,管道有两种类型:命名管道(Named Pipe)和匿名管道(Anonymous Pipe)。

  1. 命名管道(Named Pipe):命名管道允许不相关的进程进行通信,可以通过在文件系统中创建特殊文件来实现。命名管道使用FIFO文件(First In First Out)来作为通信的载体,进程通过打开和读写FIFO文件来进行通信。
  2. 匿名管道(Anonymous Pipe):匿名管道是一种特殊的管道,用于在相关的进程之间进行通信。它是无名字的,不对应任何文件系统中的文件,仅存在于内存中。匿名管道只能在具有父子关系的进程之间使用。

匿名管道的创建使用pipe函数,它创建一个无名的、半双工的管道。pipe函数返回两个文件描述符,一个用于读取管道数据,另一个用于写入管道数据。

匿名管道的特点包括:

  • 可以在具有父子关系的进程之间使用。
  • 是一个字节流的通信机制,没有固定的消息边界。
  • 数据以连续的字节流的形式传递。
  • 管道是先进先出(FIFO)的数据结构。
  • 管道的读写是阻塞的操作,如果写入时管道已满,写入操作将被阻塞,如果读取时没有可读取的数据,读取操作将被阻塞。

无论是命名管道还是匿名管道,都是一种常用的进程间通信方式,用于在相关的进程之间传递数据。它们都提供了简单有效的通信机制,但具体选择哪种方式要根据具体的需求和场景来决定

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

pipe系统调用函数

pipe函数是Linux系统提供的一个系统调用函数,用于创建一个匿名管道(pipe),用于进程间的通信。它的原型如下:

#include <unistd.h>

int pipe(int pipefd[2]);//int pipefd[2]输出型参数

输出型参数(Output Parameters): 输出型参数是一种用于从函数中返回结果的参数类型。
它用于将函数中计算或处理的结果传递给函数外部。输出型参数的值在函数调用之前不会被赋值,
而是在函数内部进行修改或赋值,函数执行完毕后,通过输出型参数将结果传递给调用者。
在函数定义时,输出型参数通常用指针或引用来声明,以便在函数内部修改参数的值能够在函数外部生效。

输入型参数(Input Parameters): 输入型参数是一种用于向函数传递数据的参数类型。
它用于将调用者提供的数据传递给函数内部进行计算或处理。输入型参数在函数调用之前需要进行
初始化或赋值,以便在函数内部使用。在函数定义时,输入型参数通常用值传递或常量引用的方式声明,
以防止函数内部对参数进行修改影响函数外部。
总结来说,输出型参数用于将函数内部计算或处理的结果传递给函数外部,而输入型参数用于
向函数传递数据供函数内部使用。输出型参数在函数调用之前不会被初始化或赋值,而输入型
参数需要在函数调用之前进行初始化或赋值。

pipe函数接受一个整型数组pipefd作为参数,该数组用于保存两个文件描述符,pipefd[0]用于读取管道数据,pipefd[1]用于写入管道数据。

调用pipe函数成功后,会创建一个无名管道,返回值为0。在一个进程中调用pipe函数后,可以使用pipefd[0]进行读操作,使用pipefd[1]进行写操作。

下面是一个使用pipe函数的示例:

#include <stdio.h>
#include <unistd.h>

int main() {
    int pipefd[2];
    char buffer[256];
    int result;

    // 创建管道
    result = pipe(pipefd);
    if (result == -1) {
        perror("pipe");
        return 1;
    }

    // 在子进程中向管道写入数据
    if (fork() == 0) {
        close(pipefd[0]);  // 关闭读端
        write(pipefd[1], "Hello, pipe!", 13);
        close(pipefd[1]);
        return 0;
    }

    // 在父进程中从管道读取数据
    close(pipefd[1]);  // 关闭写端
    read(pipefd[0], buffer, sizeof(buffer));
    close(pipefd[0]);

    printf("Received: %s\n", buffer);
    return 0;
}

运行结果:

Received: Hello, pipe!

在上述示例中,首先使用pipe函数创建一个管道,然后调用fork函数创建一个子进程。子进程向管道写入数据,父进程从管道读取数据,最后输出接收到的数据。

需要注意的是,管道是一个先进先出(FIFO)的数据结构,数据从管道的一端写入,从另一端读取。在使用管道进行进程间通信时,需要注意进程的读写顺序和关闭不需要的文件描述符,以避免死锁或其他错误。

pipe函数创建的管道文件实际上就是分配两个fd指向同一个文件?

是的,pipe函数创建的管道实际上是通过文件描述符(File Descriptor,简称FD)来引用的一个特殊文件。该特殊文件在内核中创建,不对应任何磁盘上的文件。

管道是一个字节流的通信机制,可以通过文件描述符进行读写操作。在调用pipe函数时,会创建一个特殊的文件,并返回两个文件描述符,一个用于读取管道数据,另一个用于写入管道数据。

这两个文件描述符的性质如下:

  1. pipefd[0]:管道的读取端。当从该文件描述符读取数据时,将返回管道中最早写入的数据,并将其从管道中删除。如果没有可读取的数据,读取操作将阻塞,直到有新的数据写入到管道中或根据设置非阻塞标志而返回错误。
  2. pipefd[1]:管道的写入端。通过该文件描述符可以将数据写入管道。写入的数据会被添加到管道的尾部,供读取端读取。如果管道已满,写入操作将阻塞,直到管道有足够的空间或根据设置非阻塞标志而返回错误。

这两个文件描述符实际上指向了同一个内核中的管道文件。当数据从写入端写入管道时,可以通过读取端进行读取,反之亦然。

需要注意的是,管道是半双工的,即同一时间只能进行读取或写入操作,不能同时进行读写操作。如果需要双向通信,可以创建两个管道,分别用于不同的方向。

综上所述,pipe函数创建的管道实际上是通过两个文件描述符指向同一个管道文件来实现进程间的通信。

当从管道读取数据时,已经被读取的数据会被清除。管道是一个先进先出的数据结构,数据从管道的一端写入,从另一端读取。

当从管道读取数据时,管道的读取指针会向后移动,指向下一个可读取的数据。已经读取的数据会从管道中删除,不再可见。如果管道中没有更多的数据可供读取,读取操作会被阻塞,直到有新的数据写入到管道中。

当一个进程向管道中写入数据后,如果没有其他进程读取数据,管道会存储数据直到被读取。如果管道已满且没有进程读取数据,写入操作会被阻塞,直到管道有足够的空间。

需要注意的是,对于无名管道,数据是在内核中缓存的,并没有存储在文件系统中。当管道不再被使用时,内核会自动释放管道所占用的资源。

因此,在使用管道进行进程间通信时,需要注意读取和写入的顺序,以免数据丢失或死锁的情况发生。

匿名管道

管道的特点和4种场景:

1.单向通信。

2.管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。

3.管道通信,通常用来进行具有"血缘"关系的进程 ,进行进程间通信。常用于父子通信 -- pipe打开管道,并不清楚管道的名字,成为匿名管道。

4.在管道通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数的多少没有强相关 --- 表现 --- 字节流

5.具有一定的协同力,让read和write能够按照一定的步骤进行通信 --- 自带同步机制,如下4种场景

1.如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待

2.如果我们writer端将管道写满了,我们还能写吗?不能

3.如果我关闭了写端,当一个进程尝试从一个已关闭的管道中读取数据时,read()函数将立即返回0,表明读 到了文件结尾。

4.写端一直写,读端关闭,会发生什么呢?没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。 OS会杀死一直在写入的进程! OS会通过信号来终止进程,13) SIGPIPE

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

///Makefile//
mypipe:mypipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf mypipe
///Makefile//

//mypipe.cc 注.cc也是c++的一种后缀形式
#include <iostream>
#include <cerrno>
#include <string.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    
    //缩写的这部分的代码是为了让不同的进程看到同一份资源!!!
    //因为任何一种进程间通信中,一定要先保证不同的进程之间看到同一份资源!

    int pipefd[2] = {0};
    //1.创建管道
    int n = pipe(pipefd);
    if(n < 0)
    {
        std::cout << "pipe error" << errno << ":" << strerror(errno) << std::endl;
        //如果pipe调用失败,会把错误码设置到errno里面,通过strerror(errno)输出错误原因,为什么pipe调用失败会把错误设置到erron里?他不是系统调用吗
        //因为系统调用接口也是C语言编写的
    }
    std::cout << "pipefd[0]" << pipefd[0] << std::endl;//读端 - 一般默认把0位置的作为读端
    std::cout << "pipefd[0]" << pipefd[1] << std::endl;//写端 - 一般默认把1位置的作为写端
    //2.创建子进程
    pid_t id= fork();
    assert(id != -1);//什么时候使用if,什么时候使用assert?意料之外使用if,意料之中使用assert

    //3.关闭不需要的fd
    if(id == 0)//子进程
    {
        //让父进程进行读取,让子进程进行写入,也可以反过来
        close(pipefd[0]);//关闭子进程的读取

        //4.开始通信 -- 结合某种场景
        const std::string namestr = "hello, 我是子进程";
        int cnt = 1;
        char buffer[1024];
        while(true)
        {
            snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
            write(pipefd[1], buffer, strlen(buffer));//输出数据
            sleep(1);
            //sleep(10);//10秒输入一次数据,验证场景1
        }

        close(pipefd[1]);//当程序要结束时关闭剩下的fd。(这一步可以不写,因为它的生命周期随进程的结束而结束)
        exit(0);
    }
    
    //父进程
    close(pipefd[1]);//关闭父进程的写入
    
    /

    //4.开始通信 -- 结合某种场景
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        //sleep(10);//10秒读取一次数据,验证场景2
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);//读取数据
        if(n > 0)
        {
            buffer[n] = '\0';
            cout << "我是父进程,child give me message: " << buffer << endl;
        }
        //验证场景3
        else if(n == 0)
        {
            cout << "我是父进程,读到了文件结尾" << endl;
            break;
        }
        else
        {
            cout << "我是父进程,读取异常了" << endl;
            break;
        }
        sleep(1);
        if(cnt++ > 5) break;
    }

    close(pipefd[0]);//当程序要结束时关闭剩下的fd。(这一步可以不写,因为它的生命周期随进程的结束而结束)
    
    //验证场景4
    int status = 0;
    waitpid(id, &status, 0);
    cout << "sig: " << (status & 0x7F) << endl;//输出是否接受到13信号

    return 0;
}

管道读写规则

1.当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2.当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,则read返回0。
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

注:原子性是指一个操作或者一个操作序列要么全都执行完毕,要么都不执行,不会出现部分执行的情况。

给予管道,进行一个简单的设计(进程池)

//Makefile
ctrlProcess:ctrlProcess.cc
	g++ -g -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf ctrlProcess
//task.hpp
#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>


using namespace std;

//typedef std::function<void ()> func_t;
typedef void(*fun_t)();//定义一个函数指针类型,类型名为fun_t

void PrintLog()
{
    cout << "pid: " << getpid() << ",打印日记任务,正在被执行..." << endl;
}

void InsertMySQL()
{
    cout << "执行数据库任务,正在被执行..." << endl;
}

void NetRequest()
{
    cout << "执行网络请求任务,正在被执行..." << endl;
}

//每一个command都是四字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
public:
    Task()
    {
        //把上面的函数地址存入到数组里
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }

    ~Task()
    {}

    void Execte(int command)
    {
        if(command >= 0 && command < funcs.size())
            funcs[command]();//调用数组里面的函数
    }
public:
    vector<fun_t> funcs;//函数指针数组 -- 存放函数的数组
};
//ctrlProcess.cc
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <vector>
#include <sys/wait.h>
#include <sys/types.h>
#include "task.hpp"

using namespace std;

const int gnum = 3;
Task t;
//父进程要管理自己创建的管道和进程,所以要先描述,在组织。
//EndPoint这个类的成员变量就是描述对象,成员函数就组织行为
class EndPoint
{
private:
    static int number;
public:
    pid_t _child_id;//子进程的pid
    int _write_fd;//父进程输出数据的fd
    string processname;
public:
    EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
    {
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    string name() const
    {
        return processname;
    }
    ~EndPoint()
    {}
}; 

int EndPoint::number = 0;

//子进程执行的方法
void waitCommand()
{
    while(true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(int));//等待父进程在标准输入,输入数据。如果没有数据子进程会自动等待,如博客中的4种场景。
        if(n == sizeof(int))
        {
            t.Execte(command);
        }
        else if(n == 0)
        {
            cout << "父进程让我提出,我就退出了" << getpid() << endl;
            break;
        }
        else
        {
            break;
        }
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    vector<int> fds;
    for(int i = 0; i < gnum; i++)
    {
        //1.1创建管道
        int pipefd[2] = {0};//循环创建一批管道
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        //1.2先进行构建控制结构
        pid_t id = fork();
        //printf("%d\n", id);
        assert(id != -1);

        if(id == 0)//子进程
        {
            for(auto& fd : fds)//避免从第二个子进程开始继承前面的子进程的写端口
                close(fd);
            //1.3关闭不要的fd
            close(pipefd[1]);
            //我们期望,所有的子进程读取“指令”的时候,都从标准输入读取
            //1.3.1输入重定向
            dup2(pipefd[0], 0);
            //1.3.2子进程开始等待获取命令
            waitCommand();//子进程会去自动等待并读取父进程在标准输入的数据

            close(pipefd[0]);
            exit(0);
        }
        //父进程
        //1.3 关闭不要的fd
        close(pipefd[0]);
        //1.4将新的子进程和父进程的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));

        fds.push_back(pipefd[1]);
    }
}

// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int showBoard()
{
    cout << "###################################" << endl;
    cout << "# 0. 执行日志任务  1.执行数据库任务 #" << endl;
    cout << "# 2. 执行请求任务  3.退出          #" << endl;
    cout << "###################################" << endl;
    cout << "请选择: " ;
    int command = 0;
    cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint>& end_points)
{
    int num = 0;
    int cnt = 0;
    while(true)
    {
        //1.选择任务
        int command = showBoard();
        if(command == 3)
            break;
        if(command < 0 || command > 2)
            continue;
        //2.选择进程
        int index = cnt++;
        cnt %= end_points.size();
        string name = end_points[index].name();
        cout << "选择了进程: " << name << " | 处理任务:" << command << endl;
        //3.下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }

}

void waitprocess(const vector<EndPoint>& end_points)
{
    //1.我们需要让子进程全部退出 --- 让父进程关闭所有的write fd即可
    for(const auto& ep : end_points)
        close(ep._write_fd);
    cout << "父进程让所有的子进程退出" << endl;
    sleep(10);

    //2.父进程要回收子进程的僵尸状态
    for(const auto& ep : end_points)
        waitpid(ep._child_id, nullptr, 0);
    cout << "父进程回收了所有的子进程" << endl;
    sleep(10);
}

int main()
{
    //1.先进行构建控制结构,父进程写入,子进程读取    
    vector<EndPoint> end_points;
    createProcesses(&end_points);
    
    //2.我们得到了什么?end_points
    ctrlProcess(end_points);

    //3.处理所有的退出问题
    waitprocess(end_points);

    return 0;
}

命名管道

为什么叫做命名管道?因为该文件是有文件名称的,而且必须得有,因为是创建并存在磁盘中的文件!而匿名管道是一个内存级的文件,是在内存中开辟的一段空间(缓冲区)用来保存临时数据的,以方便对其进行写入和读取,它存在于内核空间中,并不对应于磁盘上的文件。数据通过匿名管道传输时,直接从一个进程的缓冲区复制到另一个进程的缓冲区,没有涉及磁盘的读写操作。匿名管道和命名管道都是在进程之间进行通信的机制,但它们有一些区别。

  1. 命名管道(Named Pipes):
    • 使用命名管道,可以支持两个不相关的进程之间进行通信。
    • 命名管道在文件系统中有一个路径名(唯一性),可以通过这个路径名进行访问。
    • 可以永久存在,可以在进程之间启动和关闭。
  1. 匿名管道(Anonymous Pipes):
    • 匿名管道只能在具有父子关系的进程之间进行通信。它们是在创建进程时自动创建的。
    • 没有路径名,只能通过被创建的进程之间的句柄进行访问。
    • 只能在创建它们的进程存在的时候使用,当创建的进程终止后,管道也会被自动关闭。

总结来说,命名管道适用于不相关的进程之间的通信,并且可以在进程之间启动和关闭。匿名管道适用于具有父子关系的进程之间的通信,并且只能在创建它们的进程存在的时候使用。

mkfifo指令

在Linux中,mkfifo也是一个用于创建命名管道的命令行指令。

命令的语法如下:

mkfifo [OPTION]... NAME...

其中,OPTION是可选参数,用于指定一些选项,比如设置管道的权限、修改时间戳等。NAME是一个或多个命名管道的名称。

可选参数:

  • -m, --mode=MODE:指定管道的权限模式,默认为0666。
  • -Z, --context[=CTX]:设置安全上下文。

例如,以下命令创建了一个名为mypipe的命名管道:

mkfifo mypipe

创建成功后,可以通过文件读写的方式操作管道。例如,可以使用重定向运算符>向管道写入数据:

echo "Hello, World!" > mypipe

然后,可以使用cat命令从管道中读取数据:

cat mypipe

需要注意的是,当没有进程打开写端或读端时,对管道的写入和读取操作会被阻塞,直到有进程打开对应的端口

通过命名管道,不同的进程可以通过读写同一个管道文件来进行通信,从而实现进程间的数据交换。这在多个进程需要进行数据传输的场景中非常有用。

mkfifo函数

在Linux系统中,mkfifo系统调用函数用于创建一个命名管道(Named Pipe),也称为FIFO(First In, First Out)。

注:这个是函数,上一个是指令。

函数的原型如下:

#include <sys/types.h>
#include <sys/stat.h>

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

参数说明:

  • pathname:指定创建的命名管道的路径和名称。
  • mode:指定创建的命名管道的访问权限。

函数的返回值为0表示成功创建命名管道,返回值为-1表示创建失败,具体的错误信息可以通过errno来获取。

例如,下面是一个使用mkfifo函数创建命名管道的示例:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

int main() {
    const char *fifoPath = "/tmp/myfifo";
    mode_t fifoMode = 0666;

    if (mkfifo(fifoPath, fifoMode) != 0) {
        perror("Failed to create fifo");
        return -1;
    }

    printf("Fifo created successfully\n");

    return 0;
}

上述示例中,通过调用mkfifo函数创建了一个名为/tmp/myfifo的命名管道,并设置了访问权限为0666。如果创建成功,则输出"Fifo created successfully"。如果创建失败,调用perror函数可以打印出具体的错误信息。

unlink函数

在Linux系统中,unlink函数用于删除一个文件。

函数的原型如下:

#include <unistd.h>

int unlink(const char *pathname);

参数说明:

  • pathname:指定要删除的文件的路径和名称。

函数的返回值为0表示成功删除文件,返回值为-1表示删除失败,具体的错误信息可以通过errno来获取。

以下是一个使用unlink函数删除文件的示例:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {
    const char *filePath = "/tmp/test.txt";

    if (unlink(filePath) != 0) {
        perror("Failed to unlink file");
        return -1;
    }

    printf("File unlinked successfully\n");

    return 0;
}

上述示例中,通过调用unlink函数删除了名为/tmp/test.txt的文件。如果删除成功,则输出"File unlinked successfully"。如果删除失败,调用perror函数可以打印出具体的错误信息。

使用命名管道设计一个不相关的进程进行通信:

//Makefile
.PHONT:all
all:server client

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

.PHONY:clean
clean:
	rm -rf server client
//comm.hpp
#pragma once

#include <iostream>
#include <string>

#define NUM 1024

const std::string fifoname = "./fifo";//路径+文件名
uint32_t mode = 0666;//权限
//server.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "comm.hpp"

using namespace std;

int main()
{
    //1.创建管道文件
    umask(0);//这个设置并不影响系统的默认配置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(), mode);//创建管道文件
    if(n != 0)
    {
        cout << errno << " : " << strerror(errno) << endl;
        return 1;
    }

    //2.让服务端直接开启管道文件
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if(rfd <= 0)
    {
        cout << errno << " : " << strerror(errno) << endl;
        return 2;
    }

    //3.正常通信
    char buffer[NUM];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "client# " << buffer << endl;
        }
        else if(n == 0)
        {
            cout << "client quit, me too" << endl;
            break;
        }
        else
        {
            cout << errno << " : " << strerror(errno) << endl;
            break;
        }
    }

    //关闭fd
    close(rfd);

    unlink(fifoname.c_str());//删除管道文件

    return 0;
}
//client.cc
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <assert.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "comm.hpp"
using namespace std;

int main()
{
    //1.不需要创建管道文件,只需要打开对应的管道文件即可!
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
        cout << errno << " : " << strerror(errno) << endl;
        return 1;
    }

    //2.开始进行常规通信
    char buffer[NUM];
    while(true)
    {
        cout << "请输入你的消息# ";
        char *msg = fgets(buffer, sizeof(buffer), stdin);//从标准输出读取数据,存放到buffer里
        assert(msg);
        (void)msg;

        buffer[strlen(buffer) - 1] = 0;//去掉输入的回车

        if(strcasecmp(buffer, "quit") == 0)//strcasecmp()函数用于比较两个字符串的内容,不区分大小写。
            break;

        ssize_t n = write(wfd, buffer, strlen(buffer));//把buffer里的数据,输入到管道文件里
        assert(n >= 0);
        (void)n;
    }

    close(wfd);
    
    return 0;
}

system V共享内存

在Linux中,System V共享内存是一种用于实现进程间通信的机制。它可以让多个进程共享同一块内存区域,从而实现进程之间高效地交换数据(目的就是让不同的进程,先看到同一份资源)。

System V共享内存使用三个主要的系统调用来创建、附加和控制共享内存段:

  1. shmget:用于创建或获取已存在的共享内存段。
  2. shmat:用于将共享内存段附加到进程的地址空间。
  3. shmctl:用于控制和操作共享内存段,例如删除共享内存段、修改权限等。

shmget系统函数

在Linux系统中,shmget系统调用函数用于创建和访问共享内存段。

函数的原型如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);//shmflg是位图结构

参数说明:

  • key:用于标识共享内存段的关键字,可以使用ftok函数生成。
  • size:指定要创建的共享内存段的大小(以字节为单位)。
  • shmflg:用于指定共享内存段的访问权限和创建标志,可以使用IPC_CREAT来创建共享内存段,还可以使用其他标志进行权限设置和控制。(shmflg是用来指定共享内存的权限和操作方式的参数,它可以是以下常量之一或它们的组合:
    • IPC_CREAT:如果共享内存不存在,则创建一个新的共享内存。如果共享内存已经存在,则获取其标识符(常用)。
    • IPC_EXCL:与IPC_CREAT一起使用,用于创建一个新的共享内存。如果共享内存已经存在,则返回错误(常用)。
    • IPC_NOWAIT:如果无法获取共享内存,立即返回错误,而不是等待。
    • SHM_R / SHM_RDONLY:设置共享内存的读权限,使其只读。只有一个进程可以将写权限与读权限组合在一起。
    • SHM_W:设置共享内存的写权限,使其可写。

除了以上可选参数之外,shmflg参数还可以与以下访问权限标志进行位或操作:

    • 0666:权限位,指定共享内存的访问权限,表示所有用户(包括拥有者、同组用户和其他用户)都有读写权限。
    • 0600:权限位,表示只有创建共享内存的进程才有读写权限。

这些可选参数可以灵活地配置共享内存的创建和访问权限,以满足不同的应用需求。)

shmflg函数的返回值为共享内存段的标识符。如果返回值为-1,则表示创建或访问共享内存段失败,具体的错误信息可以通过errno来获取。

以下是一个使用shmget函数创建或访问共享内存段的示例:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>

int main() {
    key_t shmKey = ftok("/tmp", 'M');
    size_t shmSize = 1024;
    int shmflg = IPC_CREAT | 0666;

    int shmid = shmget(shmKey, shmSize, shmflg);
    if (shmid == -1) {
        perror("Failed to create or access shared memory segment");
        return -1;
    }

    printf("Shared memory segment created or accessed successfully (shmid: %d)\n", shmid);

    return 0;
}

上述示例中,通过调用ftok函数生成一个关键字(使用/tmp目录和字符'M'),然后调用shmget函数创建或访问一个大小为1024字节的共享内存段。如果成功创建或访问共享内存段,则输出"Shared memory segment created or accessed successfully",并打印出共享内存段的标识符shmid。如果创建或访问失败,调用perror函数可以打印出具体的错误信息。

ftok系统函数

ftok函数是一个用于生成键值的函数,在Linux系统中用于创建共享内存、信号量、消息队列等可用于进程间通信的IPC(Inter-process Communication)对象的函数。它的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

其中,pathname是一个路径名参数,proj_id是一个用户自定义的整数参数。ftok函数根据pathname和proj_id生成一个唯一的键值,该键值可用于创建或访问IPC对象。生成的键值是通过计算pathname的索引节点号和proj_id的低8位组合而成的。

通过使用ftok函数生成的键值,可以在不同的进程中进行共享内存的创建、访问和销毁,实现进程间的数据共享。在使用IPC对象进行进程间通信时,由于不同进程可以通过相同的键值访问相同的IPC对象,因此需要确保键值的唯一性,一般可以通过使用不同的pathname和proj_id来实现。

返回值:

  • 如果成功,返回一个唯一的键值。
  • 如果出错,返回-1,并设置errno来指示错误的原因。

需要注意的是,ftok函数返回的键值类型为key_t,这是一个整数类型。在使用ftok函数生成键值之后,可以将其作为参数传递给其他的IPC对象创建函数进行操作。

什么是IPC

IPC是Inter-Process Communication(进程间通信)的缩写,指的是在操作系统中用于实现不同进程之间进行数据交换和通信的机制和技术。

在多进程的操作系统中,不同的进程可能需要相互通信、共享数据或进行协调合作。为了实现这种进程间的通信,操作系统提供了IPC机制,使得不同进程能够安全地共享和交换数据,以及进行进程间的协调和同步。

常见的IPC机制包括:

  • 消息队列(Message Queue):允许进程通过发送和接收消息进行通信。
  • 信号量(Semaphore):用于进程之间的互斥和同步。
  • 共享内存(Shared Memory):允许多个进程访问同一块内存区域,实现高效的数据共享。
  • 管道(Pipe):用于一个进程的标准输出连接到另一个进程的标准输入,实现单向的进程间通信。
  • 套接字(Socket):用于网络编程中不同主机上进程间的通信。

IPC是操作系统中非常重要的概念和技术,它使得不同进程之间能够协同工作、共享资源和交换数据,提高了系统的灵活性、效率和可靠性。

ipcs指令

在Linux中,ipcs是一个命令行工具,用于显示系统中的IPC(Inter-Process Communication)资源信息,包括消息队列、信号量和共享内存。

使用ipcs命令时,可以给它传递多个选项,以获取所需的IPC资源信息。常用的选项如下:

  • -q:显示消息队列的信息。
  • -m:显示共享内存的信息。
  • -s:显示信号量的信息。
  • -a:显示所有类型的IPC资源信息。

示例用法:

ipcs -q
ipcs -m
ipcs -s
ipcs -a

执行这些命令后,将会显示系统中相应IPC资源的详细信息,包括ID、权限、创建者、大小等。

注意,ipcs命令需要超级用户权限(root用户)或者相应的权限才能查看所有IPC资源信息。如果没有足够的权限,只能查看当前用户自己创建的IPC资源。

ipcrm指令

在Linux中,ipcrm是一个命令行工具,用于从系统中删除IPC(Inter-Process Communication)资源,包括消息队列、信号量和共享内存。

使用ipcrm命令时,需要指定要删除的IPC资源的标识符(shmid)。常用的选项如下:

  • -q 标识符:删除指定ID的消息队列。
  • -m 标识符:删除指定ID的共享内存。
  • -s 标识符:删除指定ID的信号量。
  • -a:删除当前用户创建的所有IPC资源。

示例用法:

ipcrm -q [id]
ipcrm -m [id]
ipcrm -s [id]
ipcrm -a

执行这些命令后,将会删除指定的IPC资源。注意,只有具有足够权限的用户(通常是拥有IPC资源的创建者或超级用户)才能删除IPC资源。

shmctl系统函数

shmctl系统函数用于控制共享内存段的操作,具体语法如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//注:struct shmid_ds 这是一个内置定义类型,可以直接定义变量使用即可

该函数的参数解释如下:

  • shmid:共享内存标识符,由shmget函数返回。
  • cmd:指定需要进行的操作,可以是以下常量之一:
    • IPC_STAT:获取共享内存的状态信息,并将其存储在buf结构中。
    • IPC_SET:设置共享内存的状态信息,buf结构中包含要设置的新值。
    • IPC_RMID:删除共享内存段。
  • buf:指向shmid_ds结构的指针,用于传递共享内存的状态信息。

该函数的返回值为成功时返回0,失败时返回-1,并设置errno变量来指示错误类型。

以下是shmctl函数的常用操作:

  1. 获取共享内存的状态信息:
struct shmid_ds shm_info;
int ret = shmctl(shmid, IPC_STAT, &shm_info);
  1. 设置共享内存的状态信息:
struct shmid_ds new_shm_info;
// 设置新值到new_shm_info结构中
int ret = shmctl(shmid, IPC_SET, &new_shm_info);
  1. 删除共享内存段:
int ret = shmctl(shmid, IPC_RMID, nullptr);

注意:使用shmctl函数时,获取共享内存段的状态信息时需要有足够的权限(root),并且在操作共享内存之前需要先使用shmget函数创建共享内存段并设置权限。

shmat系统函数

shmat(shared memory attach)系统函数用于将进程与共享内存段关联起来,使进程可以访问该共享内存段中的数据。具体语法如下:

#include <sys/types.h>
#include <sys/shm.h>

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

该函数的参数解释如下:

  • shmid:共享内存标识符,由shmget函数返回。
  • shmaddr:指定共享内存段的连接地址,通常设置为NULL,表示让系统自动选择一个地址。
  • shmflg:指定共享内存的访问权限,可以使用IPC_CREAT标志来创建共享内存段,以及使用SHM_RDONLY标志来将共享内存段设置为只读模式。

该函数的返回值为共享内存空间的起始地址,如果出错则返回-1,并设置errno变量来指示错误类型。

注:返回值为 void * 类型,不是指没有返回值,而是返回的起始地址是void *类型的,因为地址没有类型,所以在使用时需要强制转换类型。

以下是shmat函数的常用操作:

  1. 连接到共享内存段:
void *shm_ptr = shmat(shmid, nullptr, 0);
  1. 连接到共享内存段的指定地址:
void *shm_ptr = shmat(shmid, (void *)shm_address, 0);
  1. 连接到只读模式的共享内存段:
void *shm_ptr = shmat(shmid, nullptr, SHM_RDONLY);

注意:使用shmat函数需要对共享内存段有足够的权限,并且在连接到共享内存段之前需要先使用shmget函数创建共享内存段(与shmctl的注意事项一样)。连接后,可以使用shm_ptr指针访问共享内存中的数据。在不再需要访问共享内存时,需要使用shmdt函数将进程与共享内存分离。

shmdt系统函数

在Linux中,shmdt 是一个用于解除进程与共享内存段之间连接的系统调用。具体来说,shmdt 函数用于将一个指定的共享内存段从当前进程的地址空间中分离。

函数原型如下:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

其中,shmaddr 参数是要分离的共享内存段的起始地址(也就是shmat函数的返回值)。

调用 shmdt 的效果是将进程与共享内存段的关联解除,但这并不影响共享内存段本身的存在,其他仍然连接到相同共享内存段的进程不受影响。在解除连接后,进程就不能再访问该共享内存段的数据了。

下面是一个简单的例子,演示如何使用 shmdt:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    key_t key = ftok("/tmp", 'A');  // 创建一个key
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);  // 创建共享内存段

    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    void *shmaddr = shmat(shmid, NULL, 0);  // 连接到共享内存段

    if (shmaddr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 执行一些操作,然后解除连接
    printf("Detaching shared memory...\n");
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

这个例子首先创建一个共享内存段,然后将进程连接到该共享内存段,最后使用 shmdt 解除连接。注意,这只是一个简单的演示,实际应用中可能需要更多的错误处理和逻辑。

代码:

//Makefile
.PHONT:all
all:server client

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

.PHONY:clean
clean:
	rm -rf server client
//comm.hpp
#ifndef __COMM_HPP_
#define __COMM_HPP_

#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>
#include <assert.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

#define PATHNAME "." //路径
#define PROJID 0x6666

const int gsize = 4096;

using namespace std;
key_t gerKey()
{
    key_t k = ftok(PATHNAME, PROJID);
    if(k == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(1);
    }

    return k;
}

string toHex(int x)//转16进制
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", x);

    return buffer;
}

static int createShmHelper(key_t k, int size, int flag)
{
    int shmid = shmget(k, gsize, flag);
    if(shmid == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;//cout对象类似的功能。
        exit(2);
    }

    return shmid;
}

//创建共享内存
int createShm(key_t k, int size)
{
    return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);//0666:添加权限,普通用户则无法获取共享内存的状态信息
}

//获取共享内存
int getShm(key_t k, int size)
{
    return createShmHelper(k, size, IPC_CREAT);
}

//把共享内存和进程进行关联
char* attachShm(int shmid)
{
    char* start = (char*)shmat(shmid, nullptr, 0);
    return start;
}

//将进程和共享内存去关联
void detachShm(char* start)
{
    int n = shmdt(start);
    assert(n != -1);
    (void)n;
}

//删除共享内存
void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
}



#endif
//client.cc
#include "comm.hpp"

int main()
{
    //1.创建key
    key_t k = gerKey();
    cout << "client key: " << toHex(k) << endl;

    //2.获取共享内存
    int shmid = getShm(k, gsize);
    cout << "client shmid: " << toHex(shmid) << endl;

    //3.把共享内存和自己进行关联(连接)
    char* start = attachShm(shmid);

    //sleep(15);

    //4.开始通信
    //直接把start当成一个数组写入数据即可,因为start是共享内存的起始地址。
    //........

    //5.将自己和共享内存去关联
    detachShm(start);

    return 0;
}
//server.cc
#include "comm.hpp"

int main()
{
    //1.创建key
    key_t k = gerKey();
    cout << "server key: " << toHex(k) << endl;

    //2.创建共享内存
    int shmid = createShm(k, gsize);
    cout << "server shmid: " << toHex(shmid) << endl;

    //现象:第二次运行程序时,创建共享内存的进程已经早就退出了,但是我们发现共享内存一定还存在
    //说明共享内存不会随着进程的退出而删除

    // //获取共享内存的状态信息
    // struct shmid_ds ds;
    // int n = shmctl(shmid, IPC_STAT, &ds);
    // if(n != -1)
    // {
    //     cout << "perm: " << toHex(ds.shm_perm.__key) << endl;
    //     cout << "creater pid: " << ds.shm_cpid << " : " << getpid() << endl;
    // }

    //3.把共享内存和自己进行关联(连接)
    char* start = attachShm(shmid);

    //sleep(15);

    //4.开始通信
    //直接把start当成一个数组写入数据即可,因为start是共享内存的起始地址。
    while(true)
    {
        //........
    }

    //5.将自己和共享内存去关联
    detachShm(start);


    //删除共享内存
    delShm(shmid);

    return 0;
}

共享内存的空间分配

在许多操作系统中,包括常见的类Unix系统(例如Linux和Unix),共享内存的大小通常以页面(page)大小为单位来管理和分配。页面大小是操作系统内存管理的基本单位,通常为4KB(4096kb),尽管在某些系统上也可以是其他大小,比如2KB、8KB或更大。

因此,在这些系统中,共享内存段的大小通常以页面大小的整数倍来分配和管理。当请求创建共享内存时,操作系统会根据页面大小向上舍入,以确保共享内存的大小是页面大小的整数倍。

这种基于页面大小的分配有几个原因和优势:

1.内存管理效率: 使用页面大小为单位可以简化内存管理,减少碎片化,提高系统效率。

2.硬件支持: 大多数硬件和处理器架构的内存管理也是基于页面大小的,因此使用页面大小的内存管理更为自然。

3.简化处理: 页面大小是内存分页和虚拟内存管理的基础,能够更有效地利用操作系统提供的内存管理功能。

因此,共享内存的大小一般以页面大小的倍数来分配和管理,这有助于确保内存管理的效率和系统的稳定性。

在典型的共享内存实现中,用户请求分配共享内存时,操作系统通常按照以下步骤进行大小分配:

1.用户请求: 用户进程通过系统调用(例如,在Linux中的 shmget)向操作系统请求创建一个新的共享内存段或获取已存在的共享内存段。

2.页面大小对齐: 操作系统通常以页面大小为基本单位进行内存分配。共享内存的大小通常是以页面大小的整数倍来对齐的。这是因为操作系统以页面为单位进行内存映射和管理,确保内存的物理和虚拟映射是整数页的倍数,以提高内存访问效率。

3.内存映射: 操作系统将请求的共享内存大小映射到物理内存上。这涉及到将虚拟地址空间分配给共享内存,同时将这些虚拟地址映射到实际的物理内存页。

4.权限和控制: 操作系统会根据用户请求和系统的安全策略来设置共享内存的访问权限和控制策略。这可能包括读写权限、进程访问控制等。

5.返回共享内存标识符: 操作系统返回一个共享内存标识符给用户进程,用户可以使用这个标识符来访问共享内存段。这个标识符是一个唯一的标志,用于标识特定的共享内存段。

6.用户访问: 用户进程通过系统调用(例如,在Linux中的 shmat)将共享内存连接到自己的地址空间中,获得共享内存的起始地址。此后,用户可以通过这个地址在进程间共享数据。

总的来说,共享内存的大小分配由操作系统负责,并且通常以页面大小的倍数为单位。这有助于操作系统更有效地管理内存,提高系统性能。

共享内存通信和管道通信进行对比

共享内存在通信的时候和管道通信相比,在于没有使用任何接口(如:write,read....,等系统接口),一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了!因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的。

在共享内存机制中,多个进程可以直接访问同一块物理内存,而无需通过中间的接口或中介。这与其他进程间通信(IPC)的方式相比,如消息队列或管道,避免了数据的多次拷贝,从而提高了通信的效率。

一旦共享内存被映射到进程的地址空间,所有拥有访问权限的进程都可以直接在它上面读写数据,而不需要复制数据到各个进程的私有地址空间。这使得共享内存成为一种非常高效的进程间通信方式,特别适用于需要频繁交换大量数据的场景。

然而,共享内存的使用也需要小心谨慎,因为直接访问共享内存可能引发一些同步和互斥的问题。在多进程环境中,需要使用诸如信号量、互斥锁等同步机制来确保数据的一致性和避免竞态条件。此外,由于共享内存不提供像消息队列那样的消息传递机制,因此在设计时需要确保进程间的通信方式符合应用程序的需求。

共享内存和管道是两种不同的进程间通信(IPC)机制,它们有各自的特点和适用场景。下面是它们之间的一些比较:

共享内存 vs. 管道
  1. 数据传输方式:
    • 共享内存: 直接在进程之间共享同一块物理内存,进程可以直接读写这块内存。数据不需要在进程之间复制。
    • 管道: 通过管道的读取和写入端之间进行数据传输。数据在管道中传递,但不在进程间共享内存。
  1. 效率:
    • 共享内存: 由于数据直接在共享内存中传递,没有中间缓冲和拷贝,因此通常更高效。
    • 管道: 数据需要在管道中复制,可能涉及到两次拷贝(一次写入管道,一次从管道读取),因此相对较慢。
  1. 同步与互斥:
    • 共享内存: 需要使用同步机制(如信号量、互斥锁)来防止多个进程同时访问共享内存引发的竞态条件。
    • 管道: 由于管道是一个单向的通信机制,自然有一些防止竞态条件的机制,但需要谨慎设计以避免死锁等问题。
  1. 通信方式:
    • 共享内存: 主要适用于大量数据的频繁交换,适合对数据进行随机访问的场景。
    • 管道: 适用于流式数据的有序传递,通常用于父子进程之间的通信。
  1. 实时性:
    • 共享内存: 由于直接读写共享内存,通常具有更低的延迟,更适合实时性要求高的场景。
    • 管道: 由于数据需要经过管道传递,可能引入一些延迟,适用于非实时性要求严格的场景。
  1. 复杂性:
    • 共享内存: 使用共享内存需要更加小心谨慎,因为直接访问共享内存可能引发同步和互斥问题。
    • 管道: 管道相对简单,但对于复杂的通信场景可能需要使用多个管道或其他IPC机制。
使用场景:
  • 共享内存: 适用于需要高效传递大量数据、实时性要求高、进程之间需要随机访问数据的场景。
  • 管道: 适用于流式数据传递、父子进程通信、进程之间简单的单向通信的场景。

在实际应用中,选择使用共享内存还是管道取决于具体的需求和场景,以及对实时性、数据量、复杂性等方面的要求。

system V信号量

互斥概念

我们把大家都能看到的资源:公共资源

a.互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问这种行为,称为加锁

b.我们把任何一个时刻,都只允许一个执行流在进行访问的共享资源,叫做临界资源

c.临界资源是要通过代码访问的,凡是访问临界资源的代码,叫做临界区

d.原子性:要么不做,要么做完,只有两种确定状态的属性,原子性。

工作原理:

  • 当一个进程或线程希望访问共享资源时,它首先尝试获取互斥锁。
  • 如果该锁已经被其他进程或线程持有,则当前进程或线程会被阻塞,直到锁被释放。
  • 当持有锁的进程或线程完成对共享资源的操作后,释放锁,使得其他进程或线程可以获取锁并访问资源。

信号量

信号量是一种用于进程间同步和互斥的同步原语,用于控制对共享资源的访问。信号量可以被看作是一个计数器,用于跟踪可用的资源数量,同时提供了一组原子操作,允许进程在竞争访问共享资源时进行协调。

基本概念:
  1. 计数器: 信号量内部包含一个整数计数器,通常表示可用资源的数量。当资源被占用时,计数器减小;当资源被释放时,计数器增加。
  2. 原子操作: 信号量的操作是原子的,不会被中断。这确保了在多线程或多进程环境中对信号量的操作是可靠的。
  3. 等待和通知: 进程可以通过等待信号量来请求资源,而释放资源的进程则通过通知信号量来增加计数器,使得等待资源的进程能够继续执行。
类型:
  1. 二进制信号量: 取值为0或1,常被用于实现互斥锁。通常用于控制对单一资源的访问,如文件、共享内存等。
  2. 计数信号量: 可以取多个非负整数值,用于表示多个相同类型的资源的数量。常用于资源池管理等场景。
基本操作:
  1. 初始化: 初始化信号量,设置其初始值。
  2. 等待(Wait): 如果计数器大于零,则将计数器减一,否则阻塞进程。等待资源的进程会阻塞,直到计数器变为正。
  3. 通知(Signal): 将计数器加一,通知等待资源的进程可以继续执行。通知通常在释放了一个资源后发生。
  4. P 操作和 V 操作:
    • P(sem): 等同于 wait(sem),原子地检查信号量并减少其值。
    • V(sem): 等同于 signal(sem),原子地增加信号量的值。
  1. 销毁:
    • destroy(sem): 销毁信号量,释放相关的资源。

注:进程通过执行代码来申请信号量资源(所有的进程都得先看到信号量,才能访问共享资源),只要申请信号量成功,就一定能拿到一个子资源(P操作)。释放信号量资源,只要将计数器增加,就表示我们对应的资源进行了归还(V操作)。

信号量是多进程或多线程(多执行流)环境下常用的同步工具之一,用于协调对共享资源的访问,确保程序的正确性和一致性。

semget函数

在Linux中,semget 函数用于创建一个新的信号量集,或者获取一个已存在的信号量集的标识符。这个函数通常是在使用 System V 信号量时调用的。下面是 semget 函数的基本信息和用法:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

参数说明:

  • key:用于唯一标识信号量集的键值。可以使用 ftok 函数生成,也可以手动指定。不同的进程可以使用相同的键值来引用同一个信号量集。
  • nsems:表示信号量集中包含的信号量个数,即信号量集的大小。
  • semflg:用于指定一些标志,例如权限标志。可以通过按位或操作设置多个标志,比如 IPC_CREAT 表示如果信号量不存在则创建,IPC_EXCL 表示如果信号量已存在则返回错误。

返回值:

  • 如果成功,返回一个非负整数,表示信号量集的标识符(用于后续的信号量操作)。
  • 如果失败,返回 -1,并设置 errno 表示错误类型。

使用示例:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

int main() {
    key_t key = ftok("/tmp/semfile", 'a');
    int nsems = 1; // 单个信号量
    int semflg = IPC_CREAT | 0666; // 创建并设置权限为0666

    int semid = semget(key, nsems, semflg);
    if (semid == -1) {
        perror("semget");
        return 1;
    }

    printf("Semaphore ID: %d\n", semid);

    return 0;
}

这个示例演示了如何使用 semget 函数创建一个包含一个信号量的信号量集,并获取其标识符。注意,这里使用了 ftok 函数生成一个键值,该键值将文件路径和一个字符 'a' 结合起来。这确保了不同进程使用相同的键值来引用同一个信号量集。在实际应用中,需要根据需要设置不同的参数。

semctl函数

在Linux中,semctl 函数用于对信号量集进行控制操作,如获取信号量的值、设置信号量的值、删除信号量等。semctl 函数通常与 semget 和 semop 函数一起使用,用于完成对 System V 信号量的管理。以下是 semctl 函数的基本信息和用法:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

参数说明:

  • semid:是 semget 返回的信号量集标识符。
  • semnum:指定要操作的信号量在信号量集中的索引(信号量编号)。对于整个信号量集的操作,通常将其设置为 0。
  • cmd:是控制命令,用于指定要执行的操作。以下是一些可能的命令:
    • IPC_RMID: 从系统中删除信号量集。
    • IPC_SET: 设置信号量集的参数。需要提供 sembuf 结构作为可变参数,用于设置新的值。
    • IPC_STAT: 获取信号量集的信息,将结果存储在一个 semid_ds 结构中。
    • GETALL: 获取信号量集中所有信号量的值,结果存储在一个数组中。
    • GETPID: 获取最后一次执行 semop 操作的进程的 PID。
    • GETVAL: 获取指定信号量的值。
    • GETNCNT: 获取等待在该信号量上进行 V(signal)操作的进程数。
    • GETZCNT: 获取等待在该信号量上进行 P(wait)操作的进程数。
    • SETALL: 设置信号量集中所有信号量的值,需要提供一个数组作为可变参数。

返回值:

  • 成功时,返回执行的操作的结果。对于获取值的操作(如 GETVAL),返回相应的值。
  • 失败时,返回 -1,并设置 errno 表示错误类型。

使用示例:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

int main() {
    key_t key = ftok("/tmp/semfile", 'a');
    int nsems = 1;
    int semflg = IPC_CREAT | 0666;

    int semid = semget(key, nsems, semflg);
    if (semid == -1) {
        perror("semget");
        return 1;
    }

    // 设置信号量的值
    int init_val = 5;
    if (semctl(semid, 0, SETVAL, init_val) == -1) {
        perror("semctl SETVAL");
        return 1;
    }

    // 获取信号量的值
    int sem_val = semctl(semid, 0, GETVAL);
    if (sem_val == -1) {
        perror("semctl GETVAL");
        return 1;
    }

    printf("Semaphore Value: %d\n", sem_val);

    // 删除信号量
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        return 1;
    }

    return 0;
}

这个示例演示了如何使用 semctl 函数设置、获取信号量的值,并最终删除信号量。在实际应用中,可以根据需要选择适当的命令和参数。请注意,semctl 函数的可变参数部分在不同的系统上可能有所不同。上述示例使用了 ... 表示可变参数,实际上,semctl 函数在参数上使用了 union semun 来表示可变参数。这是因为不同的命令可能需要不同类型的参数。在上面的示例中,使用了 SETVAL 命令,所以设置了 union semun 结构体。

以下是一个典型的 semun 结构体定义:

union semun {
    int val;                // for SETVAL
    struct semid_ds *buf;   // for IPC_STAT and IPC_SET
    unsigned short *array;  // for GETALL and SETALL
    struct seminfo *__buf;  // buffer for IPC_INFO
};

这个结构体允许根据需要选择不同的字段。例如,使用 val 字段来设置或获取信号量的值。在使用 semctl 函数时,应根据实际情况选择正确的参数类型。

semop函数

semop 函数用于执行对信号量集进行操作,如等待(P操作)和释放(V操作)信号量。这个函数与 semctl 和 semget 一起,用于管理 System V 信号量。它允许进程对一个或多个信号量进行操作。以下是 semop 函数的基本信息和用法:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明:

  • semid:是 semget 返回的信号量集标识符。
  • sops:是一个指向结构体数组的指针,结构体类型为 sembuf,用于定义要执行的操作。
  • nsops:是 sops 数组的大小,即要执行的操作数目。

sembuf 结构体:

struct sembuf {
    unsigned short sem_num;  // 信号量在信号量集中的索引
    short sem_op;            // 操作,通常是 -1(等待)或 1(释放)
    short sem_flg;           // 操作标志,通常为 SEM_UNDO(允许撤销操作)
};

返回值:

  • 成功时,返回 0。
  • 失败时,返回 -1,并设置 errno 表示错误类型。

使用示例:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

int main() {
    key_t key = ftok("/tmp/semfile", 'a');
    int nsems = 1;
    int semflg = IPC_CREAT | 0666;

    int semid = semget(key, nsems, semflg);
    if (semid == -1) {
        perror("semget");
        return 1;
    }

    // 设置信号量的值
    int init_val = 1;
    if (semctl(semid, 0, SETVAL, init_val) == -1) {
        perror("semctl SETVAL");
        return 1;
    }

    struct sembuf sem_op;  // 定义操作结构体

    // P(等待)操作
    sem_op.sem_num = 0;    // 信号量集中的索引
    sem_op.sem_op = -1;    // 等待操作
    sem_op.sem_flg = 0;    // 标志,可以设置为 SEM_UNDO

    // 执行 P 操作
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semop P operation");
        return 1;
    }

    printf("Semaphore acquired!\n");

    // V(释放)操作
    sem_op.sem_op = 1;     // 释放操作

    // 执行 V 操作
    if (semop(semid, &sem_op, 1) == -1) {
        perror("semop V operation");
        return 1;
    }

    printf("Semaphore released!\n");

    // 删除信号量
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        return 1;
    }

    return 0;
}

这个示例演示了如何使用 semop 函数执行 P(等待)和 V(释放)操作来获取和释放信号量。在实际应用中,可以根据需要执行不同的操作,通过设置 sembuf 数组中的不同值来操作多个信号量。

注意:在实际的应用中,应确保对信号量的操作在适当的时候进行释放,以避免死锁等问题。因此,确保在适当的情况下执行 V(释放)操作是非常重要的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值