【Linux】进程间通信

news2025/4/7 4:36:05

文章目录

      • 1.进程间通信基础
      • 2.管道
        • 2.1匿名管道
          • 2.1.1匿名管道的原理
        • 2.2匿名管道的特点
        • 2.3匿名管道函数
          • 2.3.1用例
          • 2.3.2实现ps -ajx | grep bash指令
        • 2.4匿名管道的特点
        • 2.5管道的大小
        • 2.6管道的生命周期
        • 2.7进程池
      • 3.命名管道FIFO
        • 3.1命名管道的接口
        • 3.2命名管道和匿名管道的区别
        • 3.3用FIFO实现server&cilent间通信
      • 4.System V进程间通信
      • 5.System V共享内存
        • 5.1共享内存的原理
        • 5.2共享内存接口
          • 5.2.1创建共享内存
          • 5.2.2共享内存数据结构
          • 5.2.3key的获取
          • 5.2.4获取IPC资源
          • 5.2.5操作共享内存
          • 5.2.6挂载共享内存
          • 5.2.7共享内存的使用
      • 6.System V消息队列
        • 6.1消息队列原理
        • 6.2消息队列接口
          • 6.2.1创建消息队列
          • 6.2.2控制消息队列
          • 6.2.3添加到消息队列
          • 6.2.4消息队列接收函数
        • 6.3示例
      • 7.System V信号量
        • 7.1信号量的数据结构
        • 7.2信息量的接口
        • 7.3信号量的原理
      • 8.IPC资源管理
      • 9.mmap共享映射区
        • 9.1mmap使用实例
        • 9.2mmap常见的问题
        • 9.3mmap实现父子进程通信
        • 9.4匿名映射
        • 9.5mmap实现无血缘关系的进程通信

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缓冲区中。

2.2匿名管道的特点

  • 半双工:数据只能从管道的一段写入,从另一端读出。数据在同一时刻只能有一个流向。
  • 匿名管道不是普通文件,不属于某个文件系统,其只存在于内存中。

2.3匿名管道函数

#include <unistd.h>
#define _GNU_SOURCE             
#include <fcntl.h>  
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]数组中。

管道的创建过程

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;
}

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;
}

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;
}

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;
}

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); 

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;
}

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;
}

client.cpp中需要注意两点:

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

comm.h

#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define IPC_PATH "./myfifo"

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 <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数说明:

  • 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 */
};

上面的结构体内部有一个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 */
};

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

5.2.3key的获取

为什么key由用户提供?

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

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

该函数可以将文件路径和项目标识符转换为一个特异的数字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;
}

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;
}

Log.hpp

#include<iostream>
#include <ctime>
std::ostream& Log(){
    std::cout<<"Fot Debug |"<<"timestamp"<<(uint64_t)time(NULL)<<"| ";
    return std::cout;
}

执行结果:

image-20221105172104856

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

5.2.4获取IPC资源
ipcs 查找ipc资源
	-m	查找共享内存资源
	-s	查看信号量
	-q	查看共享队列资源

ipcrm  -m/-s/-q		删除ipc资源
ipcrm	-shmid删除共享内存资源

image-20221105173004223

ipcs命令各参数意义:

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

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

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

参数说明:

  • 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;
}

监视命令

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

执行结果

5.2.6挂载共享内存

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

关联进程地址空间

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

参数说明:

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

shmlg参数:

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

返回值:

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

去关联

int shmdt(const void *shmaddr);

参数说明:

  • 待去关联共享内存的起始地址,即调用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;
}

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());
}

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;
}

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;
}

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

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

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

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

6.2消息队列接口

6.2.1创建消息队列

msgget函数

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

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

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

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

说明:

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

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;
}

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;
}

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;
}

结果展示

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

和消息队列和共享内存一样,权限信息存放在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;
};

7.2信息量的接口

semget函数

功能:⽤用来创建和访问⼀一个信号量集
    
原型
int semget(key_t key, int nsems, int semflg);

参数
	key: 信号集的名字
	nsems:信号集中信号量的个数
	semflg: 由九个权限标志构成,它们的⽤用法和创建⽂文件时使⽤用的mode模式标志是⼀一样的
        
返回值:成功返回⼀一个⾮非负整数,即该信号集的标识码;失败返回-1

shmctl函数

功能:⽤用于控制信号量集
    
原型
int semctl(int semid, int semnum, int cmd, ...);

参数
	semid:由semget返回的信号集标识码
	semnum:信号集中信号量的序号
cmd:将要采取的动作(有三个可取值)
最后⼀一个参数根据命令不同⽽而不同
	返回值:成功返回0;失败返回-1

image-20221106012831917

semop函数

功能:⽤用来创建和访问⼀一个信号量集
原型
int semop(int semid, struct sembuf *sops, unsigned nsops);

参数
	semid:是该信号量的标识码,也就是semget函数的返回值
	sops:是个指向⼀一个结构数值的指针
	nsops:信号量的个数
返回值:成功返回0;失败返回-1

说明

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

7.3信号量的原理

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

信号量本质上是⼀一个计数器
struct semaphore
{
	int value;
	pointer_PCB queue;
}	

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

信号量的PV原语

P原语 :用于申请信号量

P(s)
{
	s.value = s.value--;
	if (s.value < 0)
	{
	该进程状态置为等待状状态
	将该进程的PCB插⼊入相应的等待队列s.queue末尾
	}
}

V原语 :用于释放信号量

V(s)
{
	s.value = s.value++;
	if (s.value < =0)
	{
	唤醒相应等待队列s.queue中等待的⼀一个进程
	改变其状态为就绪态
	并将其插⼊入就绪队列
	}
}

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 <sys/mman.h>
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
*/

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;
}

编译文件

$ gcc mmaptest.c -o mmaptest
$ ./mmaptest
$ cat book.txt

输出: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;
}

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); 

需注意的是,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)

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

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

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;
}

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;
}

编译文件

$ 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

image-20220728025702026

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据面试重点之kafka(七)

大数据面试重点之kafka(七) Kafka的分区器、拦截器、序列化器&#xff1f; 问过的一些公司&#xff1a;ebay 参考答案&#xff1a; Kafka中&#xff0c;先执行拦截器对消息进行相应的定制化操作&#xff0c;然后执行序列化器将消息序列化&#xff0c;最后执行分 区器选择对应分…

python:基础知识

环境&#xff1a; window11python 3.10.6vscodejavascript、c/c/java/c#基础&#xff08;与这些语言对比&#xff09; 注释 一、数据类型 基础六大数据类型&#xff0c;可以使用 type()查看&#xff0c;如下图&#xff1a; 1.1 数字&#xff08;Number&#xff09; 支持 整…

联邦学习--记录

简介 联邦学习&#xff08;Federated Learning&#xff09;是一种新兴的人工智能基础技术&#xff0c;其设计目标是在保障大数据交换时的信息安全、保护终端数据和个人数据隐私、保证合法合规的前提下&#xff0c;在多参与方或多计算结点之间开展高效率的机器学习。其中&#…

【机器学习大杀器】Stacking堆叠模型-English

1. Introduction The stacking model is very common in Kaglle competitions. Why? 【机器学习大杀器】Stacking堆叠模型&#xff08;English&#xff09; 1. Introduction 2. Model 3: Stacking model 2.1 description of the algorithms: 2.2 interpretation of the es…

浅谈Vue中 ref、reactive、toRef、toRefs、$refs 的用法

&#x1f4ad;&#x1f4ad; ✨&#xff1a; 浅谈ref、reactive、toRef、toRefs、$refs   &#x1f49f;&#xff1a;东非不开森的主页   &#x1f49c;: 技术需沉淀&#xff0c;不要浮躁&#x1f49c;&#x1f49c;   &#x1f338;: 如有错误或不足之处&#xff0c;希望可…

Redhat(3)-Bash-Shell-正则表达式

1.bash脚本 2.bash变量、别名、算术扩展 3.控制语句 4.正则表达式 1.bash脚本 #!/bin/bash#this is basic bash script<< BLOCK This is the basic bash script BLOKC: This is the basic bash script echo "hello world!" 双引号、单引号只有在变量时才有区…

健身房信息管理系统/健身房管理系统

21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存储达到…

VCS 工具学习笔记(1)

目录 引言 平台说明 关于VCS 能力 Verilog 仿真事件队列 准备 VCS工作介绍 工作步骤 支持 工作机理 编译命令格式 编译选项 示例 仿真命令格式 仿真选项 示例 库调用 -y 总结 实践 设计文件 仿真文件 编译 仿真 关于增量编译 日志文件记录 编译仿真接续进…

链接脚本和可执行文件

几个重要的概念 摘取自知乎内容&#xff1a; 链接器与链接脚本 - 知乎 linker 链接器 链接器(linker) 是一个程序&#xff0c;这个程序主要的作用就是将目标文件(包括用到的标准库函数目标文件)的代码段、数据段以及符号表等内容搜集起来并按照 ELF或者EXE 等格式组合成一个…

【C++学习】string的使用

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《C学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; string的使用&#x1f640;模板&#x1f639;函数模板&#x1f639;类模板&#x1f640;string模板简…

【菜鸡读论文】Former-DFER: Dynamic Facial Expression Recognition Transformer

Former-DFER: Dynamic Facial Expression Recognition Transformer 哈喽&#xff0c;大家好呀&#xff01;本菜鸡又来读论文啦&#xff01;先来个酷炫小叮当作为我们的开场&#xff01; 粉红爱心泡泡有没有击中你的少女心&#xff01;看到这么可爱的小叮当陪我们一起读论文&am…

有了PySnooper,不用print、不用debug轻松查找问题所在!

PySnooper是一个非常方便的调试器&#xff0c;它是通过python注解的方式来对函数的执行过程进行监督的。 应用起来比较简单&#xff0c;不用一步一步的去走debug来查找问题所在&#xff0c;并且将运行过程中函数的变量值打印出来结果一目了然&#xff0c;相当于替代了print函数…

Boundary Loss 原理与代码解析

paper&#xff1a;Boundary loss for highly unbalanced segmentation Introduction 在医学图像分割中任务中通常存在严重的类别不平衡问题&#xff0c;目标前景区域的大小常常比背景区域小几个数量级&#xff0c;比如下图中前景区域比背景区域小500倍以上。 分割通常采用的交…

SpringBoot实践(三十三):Maven使用及POM详解

文章目录maven是什么maven怎么装settings.xml本地仓库地址&#xff1a;localRepository远程镜像&#xff1a;mirrorsJDK 版本&#xff1a;profile私服配置POM.xml中的常用标签projectmodelVersiongroupIdartifactIdversionpropertiesdependenciesbuild和pluginsresourcesdepend…

【学生管理系统】用户登录三种验证方式—图片验证、短信验证、邮件验证

目录 一、页面需求展示 二、验证方式—按钮组件 三、手机短信验证 四、邮件验证 五、图片验证邮件验证 &#x1f49f; 创作不易&#xff0c;不妨点赞&#x1f49a;评论❤️收藏&#x1f499;一下 一、页面需求展示 二、验证方式—按钮组件 2.1前端 <el-form-item labe…

【Linux】第十章 进程间通信(管道+system V共享内存)

&#x1f3c6;个人主页&#xff1a;企鹅不叫的博客 ​ &#x1f308;专栏 C语言初阶和进阶C项目Leetcode刷题初阶数据结构与算法C初阶和进阶《深入理解计算机操作系统》《高质量C/C编程》Linux ⭐️ 博主码云gitee链接&#xff1a;代码仓库地址 ⚡若有帮助可以【关注点赞收藏】…

工作流的例子

工作流的例子目录概述需求&#xff1a;设计思路实现思路分析1.配置bean2.examples3.no bean4.activiti-api-basic-process-example5.taskspringweb参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c…

C++ 多态类型

多态 C在面向对象中&#xff0c;多态就是不同对象收到相同消息&#xff0c;执行不同的操作。在程序设计中&#xff0c;多态性是名字相同的函数&#xff0c;这些函数执行不同或相似的操作&#xff0c;这样就可以用同一个函数名调用不同内容的函数。简而言之“一个接口&#xff…

2022 国赛postgresql

安装postgresql配置postgresql [root@linux3 ~]# postgresql-setup --initdb //初始化数据库Initializing database in ‘/var/lib/pgsql/data’Initialized, logs are in /var/lib/pgsql/initdb_postgresql.log[root@linux3 ~]# systemctl enable postgresql.service Created …

澳洲最热门职业,护士排第一,医生竟然不如程序员?

2022澳洲最新的职业紧缺名单出炉了&#xff0c;令人惊讶的是护士竟然排行第一名&#xff0c;可见澳洲的医疗人力资源紧缺的问题。 既然人力资源紧缺&#xff0c;那么首当其冲的医生作为高学历且同属医疗行业的代表理应收到重视&#xff0c;然而令人意外的是&#xff0c;通过榜单…