• 【Linux】进程间通信


    1.进程间通信基础

    为什么需要进程间通信?

    进程是一个独立的资源分配单元,不同的进程之间资源是独立的。没有关联,不能在一个进程中直接访问另一个进程的资源。

    但是进程不是孤立的,不同的进程需要进行信息交互和状态的传递,因此需要进程间通信。

    进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

    进程间通信的目的:

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

    • 资源共享:多个进程之间共享同样的资源

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

    • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程

      希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

    进程间通信的本质:

    进程间通信的本质是:让不同的进程看到同一份资源

    各个进程之间若想实现通信,需要借助第三方资源。

    这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

    image-20221104155754701

    进程间通信的分类

    image-20220726214919579

    2.管道

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

    image-20221104155831359

    2.1匿名管道

    匿名管道用于有血缘关系的进程间通信。

    2.1.1匿名管道的原理
    • 子进程在创建时,会拷贝父进程的task_struct,如果父进程打开了文件,那么子进程也能够访问父进程打开的文件【子进程拷贝父进程的files_struct】
    • struct file对应的是被打开的文件,struct inode对应的是磁盘文件。因此每一个struct file内部都有一个struct inode的指针用于找到对应的磁盘文件。

    image-20221104163021541

    • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
    • 管道采用的是文件做为第三方资源;但是对于匿名管道,操作系统不会将进程间通信的数据刷新到磁盘中。因为子进程通过拷贝父进程的task_struct,可以访问父进程的文件;刷新到磁盘中,这样IO的效率会降低。也说明了,磁盘文件和内存文件不一定是一一对应的,有些文件只存在于内存中,而不会存在磁盘中。(比如这里的匿名管道)

    操作系统如何判断一个文件是什么类型?

    在struct inode内部有一个联合体,操作系统通过联合体判断文件的类型

    union{
        struct pipe_inode_info* i_pipe;	//表示为管道文件
        struct block_device* ibdev;
        struct cdev* idev;
    }
    而在struct pipe_inode_info结构体中,有一个pipe_buffer的缓冲区
    通过匿名管道进行进程间通信的数据,就存放在pipe_buffer缓冲区中。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    2.2匿名管道的特点
    • 半双工:数据只能从管道的一段写入,从另一端读出。数据在同一时刻只能有一个流向。
    • 匿名管道不是普通文件,不属于某个文件系统,其只存在于内存中。
    2.3匿名管道函数
    #include 
    #define _GNU_SOURCE             
    #include   
    int pipe(int pipefd[2]);            
    int pipe2(int pipefd[2], int flags);
    
    参数:
    	pipefd[2],两个文件描述符 pipefd[0]代表读的文件描述符,pipefd[1]代表写的文件描述符
    返回值:成功返回0,失败-1
    
    底层:
        pipe的底层调用了两次open函数,并将open返回的文件描述符写入到pipefd[2]数组中。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    管道的创建过程

    image-20221104163158562

    为什么父进程在创建子进程之前需要打开文件的读写端?

    子进程是拷贝父进程发files_struc,父进程找到对应的文件描述符,子进程才能继承父进程的文件描述符。

    为什么父子进程要关闭对应的读写?

    操作系统的底层决定了,管道必须是单向通信。

    2.3.1用例

    实现子进程读取管道,父进程写入管道。

    int main(){
        int pipefd[2]={0};
        if(pipe(pipefd)!=0){
            cerr<<"pipe error"<<endl;
        }
        int pid=fork();
        if(pid<0){
            cerr<<"for error"<<endl;
        }
        else if(pid==0){//child
            close(pipefd[1]);//关闭子进程的写文件
            #define NUM 1024
            char buf[NUM];
            while (1)
            {
                memset(buf,0,sizeof(buf));
                sleep(5);
                ssize_t s=read(pipefd[0],buf,sizeof(buf)-1);
                if(s>0){
                    buf[s]='\0';
                    cout<<"子进程接收到父进程的消息,内容是:"<<buf<<endl;
                }
                else if(s==0){
                    cout<<"父进程没有再写入,读取完成"<<endl;
                    break;
                }
                else{
                    //什么事都不做,等待有数据输入
                }
            }
            close(pipefd[0]);
            exit(0);
        }
        else{//parent
            close(pipefd[0]);   //关闭父进程的读文件描述符
            const char* msg="父进程发送消息,信息编号为:";
            int cnt=1;
            while(1){
                char sendbuff[1024];
                sprintf(sendbuff,"%s %d",msg,cnt);
                cnt++;
                sleep(2);
                write(pipefd[1],sendbuff,strlen(sendbuff));
            }
            close(pipefd[1]);
            cout<<"父进程写入完毕:"<<endl;
            //回收子进程
            pid_t res=waitpid(pid,nullptr,0);
            if(res>0){
                cout<<"等待子进程成功"<<endl;
            }
        }
        cout<<"fd[0]:"<<pipefd[0]<<endl;
        cout<<"fd[1]:"<<pipefd[1]<<endl;
        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

    image-20221104164649101

    可以看到,子进程永远也不会退出。为什么?

    read和write都是阻塞等待。

    • 管道内部,没有数据,read就必须阻塞等待,等待有数据写入管道。
    • 管道内部,如果数据写满了,write就必须阻塞等待,等待有数据被读走。

    image-20220706155119212

    2.3.2实现ps -ajx | grep bash指令

    子进程实现ps -ajx指令,父进程实现grep bash指令

    int main(){
        int pipefd[2]={0};
        if(pipe(pipefd)!=0){
            cerr<<"pipe error"<<endl;
        }
        //创建子进程
        int pid=fork();
        if(pid<0){
            cerr<<"for error"<<endl;
        }
        if(pid==0){
            //关闭读端,子进程执行ps -ajx
            close(pipefd[0]);
            dup2(pipefd[1],STDOUT_FILENO);
            execlp("ps","ps","ajx",NULL);
        }
        else{
            //关闭写端
            close(pipefd[1]);
            dup2(pipefd[0],STDIN_FILENO);
            execlp("grep","grep","bash",NULL);
        }
        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

    image-20221104165139087

    2.4匿名管道的特点

    1.匿名管道只能用于进行具有血缘关系的进程间通信,常用于父子进程通信。

    2.管道只能单向通信(半双工通信)

    3.管道自带同步和互斥机制

    • **同步:**两个或两个以上的进程在运行过程中协同步调,按预定先后次序运行。比如A进程运行依赖B进程产生的数据
    • **互斥:**一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源

    4.管道是面向字节流

    对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

    • 流式服务:先写的字符,一定是先被读取的。没有格式边界,需要用户来区分边界和数据格式。
    • 数据报服务:数据有明确的分割,拿数据按报文段拿
    2.5管道的大小

    可以使用ulimit -a命令查看系统中创建管道文件所对应的内核缓冲区大小。

    image-20221104165247109

    Linux下创建的管道大小可以通过下面的程序进行测试:

    读端一直不读,写端一直写;当管道满了,写端就被挂起。打印写端写入的数据量。

    int main(){
        int fd[2]={0};
        pipe(fd);
        pid_t pid=fork();
        if(pid==0){
            close(fd[0]);//关闭读端
            int cnt=0;
            char a='a';
            while(1){
                write(fd[1],&a,1);
                cnt++;
                printf("cnt:%d\n",cnt);
            }
        }
        waitpid(pid,NULL,0);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20221104165347549

    2.6管道的生命周期

    管道是文件。进程推出,被该进程打开的文件对应的计数器-1,如果计数器为0,那么文件关闭。对于管道而言,通信的两个进程都退出,那么管道被关闭。

    2.7进程池

    主进程向每个子进程发送任务,主进程关闭每个pipe的读端,记录每个pipe的写端。

    子进程收到主进程发送的指令后,执行发送的任务。

    image-20221104170244947

    #define PROCESSNUM 10
    typedef void(*functor)();
    typedef pair<int32_t,int32_t>elem;
    unordered_map<int32_t,string> info;
    vector<functor>functors;    //保存函数指针
    
    
    void f1()
    {
        cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
             << "执行时间是[" << time(nullptr) << "]\n" << endl;
    }
    void f2()
    {
        cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
             << "执行时间是[" << time(nullptr) << "]\n" << endl;
    }
    void f3()
    {
        cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
             << "执行时间是[" << time(nullptr) << "]\n" << endl;
    }
    void londfunctor(){
        info[functors.size()]="处理日志的任务";
        functors.push_back(f1);
        info[functors.size()]="备份数据任务";
        functors.push_back(f2);
        info[functors.size()]="处理网络连接的任务";
        functors.push_back(f3);
    }
    
    void work(int fd){
        cout<<"进程:["<<getpid()<<"]开始工作"<<endl;
        while(1){
            int32_t task=0;
            ssize_t s=read(fd,&task,sizeof(int32_t));
            if(s==0){   //如果读取完成
                break;
            }
            assert(s==sizeof(int32_t));
            //执行任务
            if(task<functors.size())
            {
                functors[task]();
                sleep(1);
            }
        }
        cout<<"进程:["<<getpid()<<"]开始结束"<<endl;
    }
    void blancesendtask(const vector<elem>&assmap){   //派发方式是负载均衡的
        srand((long long)time(nullptr));    //生成随机数种子
        while(1){   //派发什么任务,向谁派发。
            int32_t pick=rand()%assmap.size();
            int32_t task=rand()%functors.size();
            //指派任务,写入一个地址
            write(assmap[pick].second,&task,sizeof(int32_t));
            cout<<"父进程给子进程:"<<"["<<assmap[pick].first<<"]"<<"派发任务:"<<task<<endl;
        }
    }
    int main(){
        vector<elem> assmmap;
        londfunctor();//加载任务
        //创建进程
        for(int i=0;i<PROCESSNUM;i++){
            int pipefd[2]={0};
            if(pipe(pipefd)!=0){
                cerr<<"pipe error"<<endl;
            }
            //创建子进程
            int pid=fork();
            if(pid<0){
                cerr<<"fork error"<<endl;
            }
            //子进程的工作是,完成父进程派发的任务。
            if(pid==0){
                close(pipefd[1]);   //关闭子进程的写端
                work(pipefd[0]);//子进程读取管道,从而执行自己的任务。
                close(pipefd[0]);
                exit(0);
            }
            else{//父进程,负责收集子进程的信息和派发任务
                close(pipefd[0]);   //关闭父进程的读端
                elem e(pid,pipefd[1]);
                assmmap.push_back(e);
            }
        }
        cout<<"all process create successfully"<<endl;
        //收集好信息后,父进程给子进程派发任务
        blancesendtask(assmmap);
    
        //回收子进程
        for(int i=0;i<PROCESSNUM;i++){
            int res=waitpid(assmmap[i].first,NULL,0);
            if(res>0){
                cout<<"等待子进程"<<assmmap[i].first<<"成功!"<<endl;
            }
            close(assmmap[i].second);
        }
        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
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    image-20221104170418248

    3.命名管道FIFO

    • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
    • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
    • 命名管道是一种特殊类型的文件

    注意:

    • 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
    • FIFO是linux基础文件类型中的一种,但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道。属于管道伪文件。
    3.1命名管道的接口

    创建方式

    创建方式有两种:

    ​ 第一种是bash命令 mkfifo 管道名字

    ​ 第二种是使用库函数。:int mkfifo(const char *pathname, mode_t mode);

    ​ pathname表示管道名,mode是管道权限

    $ mkfifo myfifo
    $ ls -lrt
    
    int mkfifo(const char *pathname,  mode_t mode); 
    
    • 1
    • 2
    • 3
    • 4

    image-20220727102614701

    3.2命名管道和匿名管道的区别
    • 匿名管道由pipe函数创建并打开。
    • 命名管道由mkfifo函数创建,打开用open
    • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式相同,一但这些工作完成之后,它们具有相同的语义。
    3.3用FIFO实现server&cilent间通信

    实现服务端和客户端之间的通信。让服务端先运行,创建服务端和客户端通信之间通信的管道。

    客户端以写的方式打开管道向管道中写入数据;服务端以读的方式打开管道,读取客户端发送的信息。

    server.cpp

    #include "comm.h"
    #define NUM 1024
    using namespace std;
    
    int main()
    {
        umask(0);
        if(mkfifo(IPC_PATH, 0600) != 0){
            cerr << "mkfifo error" << endl;
            return 1;
        }
        int pipefd = open(IPC_PATH, O_RDONLY);
        if(pipefd < 0){
            cerr << "open fifo error" << endl;
            return 2;
        }
        //正常的通信过程
        char buffer[NUM];
        while(true){
            ssize_t s = read(pipefd, buffer, sizeof(buffer)-1);
            if(s > 0){
                buffer[s] = '\0';
                cout << "客户端->服务器# " << buffer << endl;
            }
            else if(s == 0){
                cout << "客户退出啦,我也退出把";
                break;
            }
            else{
                //do nothing
                cout << "read: " << strerror(errno) << endl;
                break;
            }
        }
        close(pipefd);
        cout << "服务端退出啦" << endl;
        unlink(IPC_PATH);
        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

    client.cpp

    #include"comm.h"
    using namespace std;
    
    int main(){
        int pipefd=open(IPC_PATH,O_WRONLY);
        if(pipefd<0){
            cerr<<"open:"<<strerror(errno)<<endl;
            return 1;
        }
    #define NUM 1024
        char line[NUM];
        while(1){
            cout<<"请输入需要发送的消息:";
            fflush(stdout);
            memset(line,0,sizeof(line));
            if(fgets(line,sizeof(line),stdin)!=nullptr){    //从键盘中获取数据,在输入时,我们会输入一个\n
                line[strlen(line)-1]='\0';
                write(pipefd,line,strlen(line));
            }
            else{
                break;
            }
        }
        close(pipefd);
        cout<<"客户端退出:"<<endl;
        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

    client.cpp中需要注意两点:

    • fgets在结尾会自动添加上\0
    • 在从键盘输入时,回车键也被读取到fgets中

    comm.h

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define IPC_PATH "./myfifo"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20221104171110016

    4.System V进程间通信

    管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。

    System V IPC提供的通信方式有以下三种

    • System V共享内存
    • System V消息队列
    • System V信号量

    System V中的共享内存和消息队列的作用是传输数据,而信号量是为了保证进程间通信的同步和互斥而设计。

    5.System V共享内存

    5.1共享内存的原理

    进程间通信的本质是:让不同的进程看到同一份资源。

    而共享内存的方式:和动态库加载到不同进程一样。**共享内存在物理内存中申请一块空间,通过不同进程的页表,映射到不同进程的共享区,这些进程就可以通过共享区的进程地址空间,访问到同一块物理内存。**达到进程间通信的目的。

    image-20221105153529076

    • 共享内存的创建和删除-------> OS完成
    • 共享内存的关联---------------> 进程完成
    5.2共享内存接口
    5.2.1创建共享内存
    #include 
    #include 
    int shmget(key_t key, size_t size, int shmflg);
    
    • 1
    • 2
    • 3

    参数说明:

    • key:待创建共享内存在内核中的唯一标识,该值由用户提供
    • size:共享内存大小,为页(4096)的整数倍
    • shmlg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
    shmlg含义
    IPC_CREAT共享内存不存在时,创建共享内存
    IPC_EXCL共享内存存在时,报错。不单独使用,必须和IPC_CREAT配合使用。IPC_CREAT | IPC_EXCL:可以保证如果shmget调用成功,一定会得到一个全新的共享内存,否则就会出错返回。

    返回值:

    • 成功:返回一个非负整数shmid,为该共享内存段的用户层面的唯一标识码;
    • 失败:返回-1

    shmid和key都是标识共享内存的标识符,但是属于不同的层面。内核在管理共享内存时,使用的是key,而用户在管理共享内存时使用的是shmid。

    5.2.2共享内存数据结构

    在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

    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 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面的结构体内部有一个struct ipc_perm结构体,该结构体存放了与权限相关的内容。每个共享内存的key都存放在该结构体中。

    struct ipc_perm {
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and
                                               SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    用户对共享内存的管理都使用key作为标识符。

    5.2.3key的获取

    为什么key由用户提供?

    共享内存由key作为标识符;不同的进程要看到同一块共享内存资源,则需要同一个key。如果key由进程提供,其他进程如何获得key?无法获取key,如何找到共享内存。

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

    该函数可以将文件路径和项目标识符转换为一个特异的数字key。在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中

    参数说明:

    • pathname:一般传递当前路径
    • proj_id:任意传递。

    创建一个共享内存

    ipcshmser.cpp

    #include"comm.hpp"
    #include"Log.hpp"
    using namespace std;
    #define SIZE 4096
    int main(){
        key_t key=Creatkey();
        Log()<<"key: "<<key<<std::endl;
        //创建共享内存
        int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);  //创建一个全新的共享内存
        if(shmid<0){
            Log()<<"shmget error"<<strerror(errno)<<std::endl;
            return 2;
        }
        Log()<<"create shmid sucess:"<<shmid<<std::endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    comm.hpp

    #define PATH_NAME "/home/west/linuxtest/bit1/shared_buff"
    #define PROJ_ID 0x666
    key_t Creatkey(){
        key_t key=ftok(PATH_NAME,PROJ_ID);
        if(key<0){
            std::cerr<<"ftok error"<<std::endl;
            exit(1);
        }
        return key;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Log.hpp

    #include
    #include 
    std::ostream& Log(){
        std::cout<<"Fot Debug |"<<"timestamp"<<(uint64_t)time(NULL)<<"| ";
        return std::cout;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    执行结果:

    image-20221105172104856

    System V下的共享内存的生命周期是随内核的,不会随进程的退出而关闭。共享内存只能被显示删除或者重启系统。

    5.2.4获取IPC资源
    ipcs 查找ipc资源
    	-m	查找共享内存资源
    	-s	查看信号量
    	-q	查看共享队列资源
    
    ipcrm  -m/-s/-q		删除ipc资源
    ipcrm	-shmid删除共享内存资源
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image-20221105173004223

    ipcs命令各参数意义:

    标题含义
    key系统区别各个共享内存的唯一标识
    shmid共享内存的用户层id(句柄)
    owner共享内存的拥有者
    perms共享内存的权限
    bytes共享内存的大小
    nattch关联共享内存的进程数
    status共享内存的状态
    5.2.5操作共享内存

    如果每次都要使用命令行去删除共享内存,那就太麻烦了。下面的接口可以实现对共享内存的操作。

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

    参数说明:

    • shmid:需要控制的共享内存用户标识
    • cmd:具体的控制动作
    • buf:用户获取或者设置共享内存的数据结构。
    选项具体作用
    IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
    IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值。
    IPC_RMID删除共享内存段

    返回值:

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

    下面,我们在创建一个共享内存后,等待几秒后删除它,检测共享内存的状态。

    #include"comm.hpp"
    #include"Log.hpp"
    using namespace std;
    #define SIZE 4096
    int main(){
        key_t key=Creatkey();
        Log()<<"key: "<<key<<std::endl;
        //创建共享内存
        int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);  //创建一个全新的共享内存
        if(shmid<0){
            Log()<<"shmget error"<<strerror(errno)<<std::endl;
            return 2;
        }
        Log()<<"create shmid sucess:"<<shmid<<std::endl;
        Log()<<"del shmid begin"<<"\n";
        sleep(4);
        int ret=shmctl(shmid,IPC_RMID,nullptr);
        if(ret<0){
            Log()<<"shmctl error"<<strerror(errno)<<"\n";
            return 1;
        }
        Log()<<"del shmid end"<<"\n";
        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

    监视命令

    while :; do ipcs -m;echo "##########";sleep(1);done;
    
    • 1

    执行结果

    5.2.6挂载共享内存

    将共享内存连接到进程地址空间我们需要用shmat函数,将共享内存从进程空间剥离需要使用shndt函数:

    关联进程地址空间

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

    参数说明:

    • 第一个参数shmid,表示待关联共享内存的用户级标识符。
    • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
    • 第三个参数shmflg,表示关联共享内存时设置的某些属性

    shmlg参数:

    选项作用
    SHM_RDONLY关联共享内存后只进行读取操作
    SHM_RND若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
    0默认为读写权限

    返回值:

    • 成功:返回共享内存起始地址
    • 失败:返回(void*)-1

    去关联

    int shmdt(const void *shmaddr);
    
    • 1

    参数说明:

    • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

    返回值:

    • 成功:返回0
    • 失败:返回-1

    示例

    我们在创建共享内存后,先与该进程关联,5秒钟后,再去关联。

    #include"comm.hpp"
    #include"Log.hpp"
    using namespace std;
    #define SIZE 4096
    int main(){
        key_t key=Creatkey();
        Log()<<"key: "<<key<<std::endl;
        //创建共享内存
        int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);  //创建一个全新的共享内存
        if(shmid<0){
            Log()<<"shmget error"<<strerror(errno)<<std::endl;
            return 2;
        }
        Log()<<"create shmid sucess:"<<shmid<<std::endl;
        char* str=(char*)shmat(shmid,nullptr,0);
        sleep(5);
        int res=shmdt((const void*)str);
        sleep(5);
        int ret=shmctl(shmid,IPC_RMID,nullptr);
        if(ret<0){
            Log()<<"shmctl error"<<strerror(errno)<<"\n";
            return 1;
        }
        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

    image-20221105182335423

    五秒后,挂载数变为0;

    image-20221105182422802

    最后共享内存被删除:

    image-20221105182446878

    5.2.7共享内存的使用

    共享内存的使用:可以不使用任何系统接口,因为共享内存是映射到了我们进程地址空间的用户空间(堆栈之间的共享区),对于每一个进程,挂接到自己进程的共享内存,属于自己的堆栈空间,可以向内存一样使用。

    实例

    使用共享内存实现进程间通信,服务器接收到客户端发送的信号,就打印内容。

    comm.hpp

    #define PATH_NAME "/home/west/linuxtest/bit1/shared_buff"
    #define PROJ_ID 0x666
    #define FIFO_FIEL "myfifo"
    key_t Creatkey(){
        key_t key=ftok(PATH_NAME,PROJ_ID);
        if(key<0){
            std::cerr<<"ftok error"<<std::endl;
            exit(1);
        }
        return key;
    }
    //创建命名管道
    void CreateFifo(){
        umask(0);
        if(mkfifo(FIFO_FIEL,0666)<0){
            Log()<<"mkfifo error"<<"\n";
            exit(2);
        }
    }
    #define READER O_RDONLY
    #define WRITER O_WRONLY
    
    //打开文件
    int Open(const std::string&filename,int flags){
        return open(filename.c_str(),flags);
    }
    //等待信号
    int Wait(int fd){
        uint32_t values=0;
        ssize_t s=read(fd,&values,sizeof(values));
        return s;
    }
    //发送信号
    int Signal(int fd){ //发送信号,告诉接收消息
        uint32_t cmd=1;
        write(fd,&cmd,sizeof(cmd));
    }
    //关闭文件
    int Close(const std::string filename,int fd){
        close(fd);
        unlink(filename.c_str());
    }
    
    • 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

    ipcshmser.cpp

    负责接收客户端发送的信号,并打印共享内存中的内容。

    #include"comm.hpp"
    #include"Log.hpp"
    using namespace std;
    #define SIZE 4096
    int main(){
        //打开命名管道
        CreateFifo();
        int fd=Open(FIFO_FIEL,READER);
        assert(fd>0);
    
        key_t key=Creatkey();
        Log()<<"key: "<<key<<std::endl;
        //创建共享内存
        int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);  //创建一个全新的共享内存
        if(shmid<0){
            Log()<<"shmget error"<<strerror(errno)<<std::endl;
            return 2;
        }
        Log()<<"create shmid sucess:"<<shmid<<std::endl;
        char* str=(char*)shmat(shmid,nullptr,0);
        while (true){
            //读端一直等待
            if(Wait(fd)<=0) break;
            printf("%s\n",str);
            sleep(1);
        }    
        int res=shmdt((const void*)str);
        int ret=shmctl(shmid,IPC_RMID,nullptr);
        if(ret<0){
            Log()<<"shmctl error"<<strerror(errno)<<"\n";
            return 1;
        }
        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

    ipcshmcil.cpp

    客户端负责发送信号和向共享内存中写入数据

    #define SIZE 4096
    int main(){
        int fd=Open(FIFO_FIEL,WRITER);
        //创建一个相同的key值
        key_t key=Creatkey();
        Log()<<"key:"<<key<<"\n";
        //创建共享内存
        int shmid=shmget(key,SIZE,IPC_CREAT);
        if(shmid<0){
            Log()<<"shmget:"<<strerror(errno)<<"\n";
            return 2;
        }
        //挂接
        char* str=(char*)shmat(shmid,nullptr,0);
        while(true){
            printf("Please Enter# ");
            fflush(stdout);
            ssize_t s=read(0,str,SIZE);
            if(s>0){
                str[s]='\0';
            }
            Signal(fd);
        }
        //去关联
        shmdt(str);
        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

    Makefile

    .PHTONY:all
    all:ipcshmser ipcshmcli
    
    ipcshmser:ipcshmser.cpp
    	g++ -o $@ $^ -std=c++11
    ipcshmcli:ipcshmcli.cpp
    	g++ -o $@ $^ -std=c++11
    
    .PHTONY:clean
    clean:
    	rm -f ipcshmser ipcshmcli
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    6.System V消息队列

    • 消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成;
    • 两个互相通信的进程通过某种方式看到同一个消息队列
    • 这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。
    6.1消息队列原理

    image-20221106002816076

    其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型。

    消息队列的数据结构

    struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first; /* first message on queue,unused */
    struct msg *msg_last; /* last message in queue,unused */
    __kernel_time_t msg_stime; /* last msgsnd time */
    __kernel_time_t msg_rtime; /* last msgrcv time */
    __kernel_time_t msg_ctime; /* last change time */
    unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long msg_lqbytes; /* ditto */
    unsigned short msg_cbytes; /* current number of bytes on queue */
    unsigned short msg_qnum; /* number of messages in queue */
    unsigned short msg_qbytes; /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid; /* last receive pid */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    和共享内存一样,消息队列有一个存放权限的结构体

    struct ipc_perm {
    key_t __key; /* Key supplied to xxxget(2) */
    uid_t uid; /* Effective UID of owner */
    gid_t gid; /* Effective GID of owner */
    uid_t cuid; /* Effective UID of creator */
    gid_t cgid; /* Effective GID of creator */
    unsigned short mode; /* Permissions */
    unsigned short __seq; /* Sequence number */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    6.2消息队列接口
    6.2.1创建消息队列

    msgget函数

    功能:⽤用来创建和访问⼀一个消息队列
    原型
    int msgget(key_t key, int msgflg);
    参数
    key: 某个消息队列的名字
    msgflg:由九个权限标志构成,它们的⽤用法和创建⽂文件时使⽤用的mode模式标志是⼀一样的
    返回值:成功返回⼀一个⾮非负整数,即该消息队列的标识码;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    和共享内存一样,key是内核级标识符,使用ftok()函数创建

    6.2.2控制消息队列
    功能:消息队列的控制函数
    原型
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    参数
    msqid: 由msgget函数返回的消息队列标识码
    cmd:是将要采取的动作,(有三个可取值)
    返回值:成功返回0,失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    cmd选项

    image-20221106003422711

    6.2.3添加到消息队列

    表明是谁发的消息

    功能:把⼀一条消息添加到消息队列中
    原型
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    参数
    msgid: 由msgget函数返回的消息队列标识码
    msgp:是⼀一个指针,指针指向准备发送的消息,
    msgsz:是msgp指向的消息⻓长度,这个⻓长度不含保存消息类型的那个long int⻓长整型
    msgflg:控制着当前消息队列满或到达系统上限时将要发⽣生的事情
    msgflg=IPC_NOWAIT表⽰示队列满不等待,返回EAGAIN错误。
        
    返回值:成功返回0;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:

    1.消息结构在两⽅方⾯面受到制约:
    ⾸首先,它必须⼩小于系统规定的上限值;
    其次,它必须以⼀一个long int⻓长整数开始,接收者函数将利⽤用这个⻓长整数确定消息的类型
        
    2.消息结构参考形式如下:
    struct msgbuf {
    	long mtype;
    	char mtext[1];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    6.2.4消息队列接收函数

    表明接收谁的消息

    功能:是从⼀一个消息队列接收消息
    原型
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    参数
    msgid: 由msgget函数返回的消息队列标识码
    msgp:是⼀一个指针,指针指向准备接收的消息,
    msgsz:是msgp指向的消息⻓长度,这个⻓长度不含保存消息类型的那个long int⻓长整型
    msgtype:它可以实现接收优先级的简单形式
    msgflg:控制着队列中没有相应类型的消息可供接收时将要发⽣生的事
    返回值:成功返回实际放到接收缓冲区⾥里去的字符个数,失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    说明:

    msgtype=0返回队列第⼀一条信息
    msgtype>0返回队列第⼀一条类型等于msgtype的消息 
    msgtype<0返回队列第⼀一条类型⼩小于等于msgtype绝对值的消息,并且是满⾜足条件的消息类型最⼩小的消息
    msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
    msgflg=MSG_NOERROR,消息⼤大⼩小超过msgsz时被截断
    msgtype>0且msgflg=MSG_EXCEPT,接收类型不等于msgtype的第⼀一条消息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    6.3示例

    消息队列实现进程间通信

    comm.h

    using namespace std;
    #define PATHNAME "."
    #define PROJ_ID 0x6666
    #define SERVER_TYPE 1
    #define CLIENT_TYPE 2
    struct msgbbuf{
        long mtype;
        char mtext[1024];
    };
    static int commMsgQueue(int flags)
    {
        key_t _key = ftok(PATHNAME, PROJ_ID);
        if(_key < 0)
        {
            cerr<<"ftok"<<endl;
            return -1;
        }
        int msgid = msgget(_key, flags);
        if(msgid < 0)
        {
            cerr<<"msgget"<<endl;
        }
        return msgid;
    }
    int createMsgQueue()
    {
        return commMsgQueue(IPC_CREAT|IPC_EXCL|0666);
    }
    int getMsgQueue()
    {
        return commMsgQueue(IPC_CREAT);
    }
    int destroyMsgQueue(int msgid)
    {
        if(msgctl(msgid, IPC_RMID, NULL)<0)
        {
            cerr<<"msgctl"<<endl;
            return -1;
        }
        return 0;
    }
    int sendMsg(int msgid, int who, char *msg)
    {
        struct msgbbuf buf;
        buf.mtype = who;
        strcpy(buf.mtext, msg);
        if(msgsnd(msgid, (void*)&buf, sizeof(buf.mtext),0)<0)
        {
            cerr<<"msgsnd"<<endl;
            return -1;
        }
        return 0;
    }
    int recvMsg(int msgid, int recvType, char out[])
    {
        struct msgbbuf buf;
        if(msgrcv(msgid, (void*)&buf, sizeof(buf.mtext), recvType,0) < 0)
        {
            cerr<<"msgrcv"<<endl;
            return -1;
        }
        strcpy(out, buf.mtext);
        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

    server.cpp

    #include "comm.h"
    int main()
    {
        int msgid = createMsgQueue();
        char buf[1024];
        while(1)
        {
            buf[0] = 0;
            recvMsg(msgid, CLIENT_TYPE, buf);
            printf("client# %s\n", buf);
            printf("Please Enter# ");
            fflush(stdout);
            ssize_t s = read(0, buf, sizeof(buf));
            if(s>0)
            {
                buf[s-1] = 0;
                sendMsg(msgid, SERVER_TYPE, buf);
                printf("send done, wait recv...\n");
            }
        }
        destroyMsgQueue(msgid);
        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

    client.hpp

    #include "comm.h"
    int main()
    {
        int msgid = getMsgQueue();
        char buf[1024];
        while(1)
        {   
            buf[0] = 0;
            printf("Please Enter# ");
            fflush(stdout);
            ssize_t s = read(0, buf, sizeof(buf));
            if(s>0)
            {
                buf[s-1] = 0;
                sendMsg(msgid, CLIENT_TYPE, buf);
                printf("send done, wait recv...\n");
            }
                recvMsg(msgid, SERVER_TYPE, buf);
                printf("server# %s\n", buf);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    结果展示

    image-20221106011125256

    7.System V信号量

    信号量的作用是保证进程间通信的同步和互斥。

    下面是一些重要概念

    临界资源

    临界资源:被多个进程或者线程,能够同时看到的资源。

    解释:如果没有对临界资源进行保护,多个进程多临界资源进行访问时,就会出现乱序。比较经典的例子是:父子进程向stdout打印是乱序的。

    临界区

    临界区:访问临界资源的代码段

    原子性

    原子性:我们把一件事,要么做完,要么不做,没有中间状态,叫做该动作是原子的。

    互斥

    互斥:任何时刻,都只有一个进程或者线程在访问临界资源

    7.1信号量的数据结构
    struct semid_ds {
    	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
    	__kernel_time_t sem_otime;      /* last semop time */
    	__kernel_time_t sem_ctime;      /* last change time */
    	struct sem  *sem_base;      /* ptr to first semaphore in array */
    	struct sem_queue *sem_pending;      /* pending operations to be processed */
    	struct sem_queue **sem_pending_last;    /* last pending operation */
    	struct sem_undo *undo;          /* undo requests on this array */
    	unsigned short  sem_nsems;      /* no. of semaphores in array */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    和消息队列和共享内存一样,权限信息存放在ipc_perm结构体中

    struct ipc_perm{
    	__kernel_key_t  key;
    	__kernel_uid_t  uid;
    	__kernel_gid_t  gid;
    	__kernel_uid_t  cuid;
    	__kernel_gid_t  cgid;
    	__kernel_mode_t mode;
    	unsigned short  seq;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    7.2信息量的接口

    semget函数

    功能:⽤用来创建和访问⼀一个信号量集
        
    原型
    int semget(key_t key, int nsems, int semflg);
    
    参数
    	key: 信号集的名字
    	nsems:信号集中信号量的个数
    	semflg: 由九个权限标志构成,它们的⽤用法和创建⽂文件时使⽤用的mode模式标志是⼀一样的
            
    返回值:成功返回⼀一个⾮非负整数,即该信号集的标识码;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    shmctl函数

    功能:⽤用于控制信号量集
        
    原型
    int semctl(int semid, int semnum, int cmd, ...);
    
    参数
    	semid:由semget返回的信号集标识码
    	semnum:信号集中信号量的序号
    cmd:将要采取的动作(有三个可取值)
    最后⼀一个参数根据命令不同⽽而不同
    	返回值:成功返回0;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20221106012831917

    semop函数

    功能:⽤用来创建和访问⼀一个信号量集
    原型
    int semop(int semid, struct sembuf *sops, unsigned nsops);
    
    参数
    	semid:是该信号量的标识码,也就是semget函数的返回值
    	sops:是个指向⼀一个结构数值的指针
    	nsops:信号量的个数
    返回值:成功返回0;失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    说明

    sembuf结构体:
    struct sembuf {
    	short sem_num;
    	short sem_op;
    	short sem_flg;
    };
    
    sem_num是信号量的编号。
    sem_op是信号量⼀一次PV操作时加减的数值,⼀一般只会⽤用到两个值:
    ⼀一个是“-1”,也就是P操作,等待信号量变得可⽤用;
    另⼀一个是“+1”,也就是我们的V操作,发出信号量已经变得可⽤用
        
    sem_flag的两个取值是IPC_NOWAIT或SEM_UNDO
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    7.3信号量的原理

    信号量的本质是一个计数器

    信号量本质上是⼀一个计数器
    struct semaphore
    {
    	int value;
    	pointer_PCB queue;
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    信号量的操作是原子的。当计数器为0,表示资源被使用完,大于0表示有资源可用的个数,小于0表示等待进程的个数。

    信号量的PV原语

    P原语 :用于申请信号量

    P(s)
    {
    	s.value = s.value--;
    	if (s.value < 0)
    	{
    	该进程状态置为等待状状态
    	将该进程的PCB插⼊入相应的等待队列s.queue末尾
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    V原语 :用于释放信号量

    V(s)
    {
    	s.value = s.value++;
    	if (s.value < =0)
    	{
    	唤醒相应等待队列s.queue中等待的⼀一个进程
    	改变其状态为就绪态
    	并将其插⼊入就绪队列
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    8.IPC资源管理

    从上面IPC资源的数据结构可以看出,所有的资源都有一个ipc_prem的结构体,其他资源属性不同,而ipc_prem存放的是权限相关的数据,key唯一识别一个资源

    在内核中有一个ipc_ids的结构体管理着IPC相关资源

    image-20221106015155888

    结构体指针和结构体第一个成员的指针在数值上是相等的,因此在需要具体类型时,可以强制类型转化为相应的类型即可。比如创建一个共享内存,可以强制类型转为struct shmid_ds*类型

    因为所有的IPC资源第一个成员都是struct ipc_prem;所以内核只需要管理struc ipc_prem数值,就可以管理所有的IPC资源。

    这也是最早的多态技术。

    9.mmap共享映射区

    原理图片

    image-20220727131950765

    mmap是直接操作内存,是进程间通信速度最快的方式。

    #include 
    void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
    /*
    参数说明
    	addr:以前需要传递一个映射到的内存地址,现在只需传递NULL即可
    	length  mmap映射的部分的长度。
    	prot:端口的权限描述
    		PROT_READ 可读
    		PROT_WRITE 可写
    	flags:(对内存和文件之间关系的描述)
    		MAP_SHARED 共享的,对内存的修改会影原文件
    		MAP_PRIVATE 私有的,对内存的修改不会影响原文件
    	fd:
    		文件描述符,需要用open函数打开一个文件
    	offset:
    		偏移量
    	返回值:
    		成功:返回可用的内存首地址
    		失败:返回信号MAP_FAILED
    */
    
    //释放内存
    int munmap(void *addr, size_t length);
    /*
    参数说明:
    	addr:需要释放内存的首地址,一般为mmap的返回值
    	length:释放内存的长度
    返回值:
    	成功:0
    	失败:-1
    */
    
    • 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
    9.1mmap使用实例

    创建book.txt文件,并在book.txt中输入xxxxxxxxxxxxx

    mmaptest.c文件

    int main()
    {
        //具有读写的的权利
        int fd=open("book.txt",O_RDWR);
        //映射长度为8,端口权限为可读可写,内存权限为共享,偏移量为0
        char* mem=mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
        //如果没有映射成功
        if(mem==MAP_FAILED)
        {
            perror("mem err");
            return -1;
        }
        strcpy(mem,"hello");
        munmap(mem,8);
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    编译文件

    $ gcc mmaptest.c -o mmaptest
    $ ./mmaptest
    $ cat book.txt
    
    • 1
    • 2
    • 3

    输出:helloxxxxxxxxxxxxxxxxx

    9.2mmap常见的问题
    • 如果更改mem变量的地址,释放mummap时,mem传入失败
    • 文件的偏移量,应该是是4k(4096)的整数倍。
    • open文件选择O_WRONLY可以吗?不可以,内存映射的过程有读取文件的操作
    • 选择MAP_SHARED的时候,,prot选择PROT_READ|PROT_WRITE,open文件应该选择可读可写O_RDWR。否则权限会发生冲突。
    9.3mmap实现父子进程通信
    int main()
    {
        //创建映射区
        int fd=open("book.txt",O_RDWR);
        int* mem=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
        if(mem==MAP_FALIED)
        {
            perror("mem err");
            return -1;
        }
        //创建子进程
        pid_t pid=fork();
        //修改内存映射区的值
        if(pid==0)
        {
            *mem=100;
            printf("child mem: %d\n",*mem);
            sleep(3);
            printf("child mem: %d\n",*mem);
        }
        else if(pid>0)
        {
            sleep(1);
            printf("parent mem: %d\n",*mem);
            *mem=101;
            printf("parent mem: %d\n",*mem);
            //阻塞等待杀死子进程
            wait(NULL);
        }
        close(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

    image-20220728012017941

    在上述父子进程通信中,需要打开一个文件作为通信的中介。对于空间是一种占用,所以linux文件系统有匿名映射的方式

    9.4匿名映射

    使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易。但是缺点是,每次创建映射区都依赖一个文件才能完成,通常建立映射区要open一个临时文件,创建好了再unlink,close。

    linux系统提供了创建匿名映射区的方法,无需依赖一个文化即可创建映射区。同样需要借助标志位参数flags来指定。使用宏MAP_ANONYMOUS (或者MAP_ANON)

    //例子
    int*p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS, -1, offset); 
    
    • 1
    • 2

    需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。

    int fd=open("/dev/zero",O_RDWR);  
    //  /dev/zero可以随意的映射    /dev/null一般错误信息重定向到改文件中
    void*p=mmap(NULL,size,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0)
    
    • 1
    • 2
    • 3

    对于上述父子进程通信的程序,只需要修改创建映射区的部分

    int* mem=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0)
    
    • 1

    image-20220728014431012

    9.5mmap实现无血缘关系的进程通信

    ​ 实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。**由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。**只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

    mmapnorelt_w.c用于修改共享内存区域的数据

    typedef struct student
    {
        int number;
        char name[20];
    }student;
    
    int main(int argc,char* argv[])
    {
        if(argc!=2)
        {
            printf("./a.out filename\n");
            return -1;
        }
        //打开文件
        int fd=open(argv[1],O_RDWR);
        ftruncate(fd,sizeof(student));
        int length=sizeof(student);
        //创建映射区
        student* stu=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
        if(stu==MAP_FAILED)
        {
            perror("stu err");
            return -1;
        }
        //改变内存数据
        int num=1;
        while(1)
        {
            stu->number=num;
            sprintf(stu->name,"stu_name%4d",num++);
            sleep(1);
        }
        //关闭内存映射区
        munmap(stu,length);
        close(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

    mmapnorelt_r.c,用于读取内存映射区域的数据

    typedef struct student
    {
        int number;
        char name[20];
    }student;
    
    int main(int argc,char* argv[])
    {
        if(argc!=2)
        {
            printf("./a.out filename\n");
            return -1;
        }
        //打开文件
        int fd=open(argv[1],O_RDWR);
        int length=sizeof(student);
        student* stu=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
        if(stu==MAP_FAILED)
        {
            perror("stu err");
            return -1;
        }
        while(1)
        {
            printf("stu->number: %d     stu->name: %s\n",stu->number,stu->name);
            sleep(1);
        }
        //关闭内存区域和文件
        munmap(stu,length);
        close(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

    编译文件

    $ touch norelt.txt
    $ gcc mmapnorelt_r.c -o mmapnorelt_r
    $ gcc mmapnorelt_w.c -o mmapnorelt_w
    $ ./mmaonorelt_w norelt.txt
    $ ./mmaonorelt_r norelt.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220728025702026

  • 相关阅读:
    React——react 的基本使用
    基于C#实现协同推荐 SlopeOne 算法
    Android Studio 多渠道打包
    npm i 的时候--save 与 --save-dev
    BGP&BGP4原理
    LeetCode - Medium - 63. Unique Paths II
    Find My技术|防止你的宠物跑丢,苹果Find My技术可以帮到你
    注解案例:山寨Junit与山寨JPA
    Shell借助curl调用接口结合Python解析Python案例
    通过循环查找完数
  • 原文地址:https://blog.csdn.net/qq_53893431/article/details/127711812