• Linux-8-进程通信


    前言

    Vue框架:Vue驾校-从项目学Vue-1
    算法系列博客友链:神机百炼

    通信背景:

    目的:

    1. 数据传输:

    • 含义:一个进程将其数据发送给另一进程
    • 举例:下棋落一子

    2. 资源共享:

    • 含义:两个进程共享相同的数据
    • 举例:下棋时当前的棋局

    3. 事件通知:

    • 含义:一个进程向另一进程发送消息,通知某种事件发生
    • 举例:子进程通知父进程其到达终止阶段

    4. 进程控制

    • 含义:一个进程想要控制另一进程执行
    • 举例:拦截进程的异常和陷阱,及时知道进程状态改变

    分类:

    1. 管道通信

    1. 匿名管道:适用于含有亲属关系的进程对之间
    2. 命名管道:适用于任意两进程之间,但常用于无亲属关系的进程

    2. System V IPC:

    1. 消息队列
    2. 共享内存
    3. 信号量

    3. Posix PC:

    1. 消息队列
    2. 共享内存
    3. 信号量
    4. 互斥量
    5. 条件变量
    6. 读写锁

    匿名管道:

    原理:

    • 父子进程通过将内容相同的file_struct,可以“看见同一块内存”
      父子进程看到同一块内存
    • 写时拷贝:
    1. 发生条件:父子进程对自己的内存地址空间操作时发生
    2. 文件操作:只有操作系统才有权力对file进行操作,进程不可直接操作
    3. 匿名管道:
      1. 写入:进程将数据传递给OS,由OS将数据传递给文件。
      2. 读出:文件将数据传递给OS,由OS将数据传递给进程。
      3. 不涉及写时拷贝,不涉及和硬盘的IO
    • 管道文件:
    1. 含义:一个进程负责向文件写入内容,一个进程负责向文件读取内容,就像单向水流在水管中一样,所以叫“管道”

    2. 举例:bash进程下的两个命令行子进程之间使用匿名管道通信
      bash进程匿名管道

    • 为什么管道必须是文件,而不能是全局变量?

      全局变量属于进程地址空间中的初始化/未初始化数据区,进程可以脱离OS直接对全局变量操作

      也就是父子进程对全局变量操作时必然发生写时拷贝,导致父子进程看到的不是同一块空间,只能读取自己的空间

    核心函数:

    pipe():

    • 作用:在父子进程之间打开一个匿名管道文件
    • 头文件:#include
    • pipe():
    int pipefd[2];		
    if(pipe(pipefd) < 0){
    	perror(pipe error);
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 输出型参数:int[2]
      1. 读取管道文件存储在pipefd[0]
      2. 写入管道文件存储在pipefd[1]
    • 返回值:获取管道文件失败返回-1

    系统调用接口:

    • write():向管道文件pipefd[0]中写
    • read():在管道文件pipefd[1]中读
    • close():关闭文件

    实例演示:

    • 父进程写,子进程读:
    #include 
    #include 
    #include 
    int main(){
    	int pipefd[2];
    	if(pipe(pipefd) < 0){
    		perror("pipe error");
    		return -1;
    	}
    	pid_t pid = fork();
    	if(pid < 0){
    		perror("fork error");
    		return -2;
    	}else if(pid == 0){
    		close(pipefd[1]);
    		char buffer[100];
    		while(read(pipefd[0], buffer, sizeof(buffer))){
    			printf("子进程读取到:%s\n",buffer);
    		}
    		close(pipefd[0]);
    		exit(0);
    	}else {
    		close(pipefd[0]);
    		char* msg = "pipe()匿名管道\n";
    		write(pipefd[1], msg, sizeof(msg));
    		printf("父进程写入完成\n");
    		close(pipefd[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

    linux命令:

    • |:将前命令的输出,作为后命令的输入
    • 举例:
    who | wc -l
    #who输出的是该主机上的用户
    #wc -l统计输入内容条数
    
    • 1
    • 2
    • 3

    命名管道:

    原理:

    • 两个进程通过OS提供的系统调用接口open()打开同一文件及其缓冲区
      命名管道示意图
    • 特殊点:
      1. 命名管道虽然也是文件,但是和匿名管道一样,不占硬盘大小,只用内存映射
      2. open()命名管道文件后,之后的操作完全和文件操作一样,一个进程write,一个进程read()

    核心函数:

    mkfifo():

    • 作用:在指定路径下创建一个命名管道文件
    • 头文件:
      1. #include
      2. #include
    • mkfifo():
    #define FILE_NAME = "name_pipe"
    #define mode 0644
    if(mkfifo(FILE_NAME, mode) < 0){
    	perror("mkfifo error");
    	return -1;
    }
    int fd = open(FILE_NAME, O_RDWR);
    if(fd < 0){
    	perror("open error");
    	return -2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 参数:

      1. char* filename:文件路径和名称,默认为当前路径下
      2. mode_t mode: 所创文件的权限码,一般为0644
    • 返回值:

      创建命名管道文件失败/已创建过:-1

    access():

    • 作用:由于mkfifo不能重复创建同一路径下同名的命名管道文件,所以在mkfifo()前,需要我们确定该文件是否已存在
    • 头文件:#include
    • access():
    if(access(FILE_NAME, F_OK)){
    	if(mkfifo(FILE_NAME, 0644) < 0){
    		perror("mkfifo error");
    		return -1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 参数:
      1. FILE_NAME:文件路径&文件名
      2. mode:四种查找模式,写权限W_OK,读权限R_OK,执行权限X_OK,存在F_OK
    • 返回值:
      1. 对应权限或文件存在,返回0
      2. 对应权限或文件不存在,返回1

    系统调用接口:

    • open():mkfile()返回值并不是直接打开命名管道的fd,所以需要我们根据FILE_NAME来手动打开文件
    • close()
    • write()
    • read()
    • 对命名管道的操作还是这四大文件调用接口,体现了linux下一切皆文件的思想

    实例演示:

    • 父进程读,子进程写:
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define FILE_NAME "name_pipe"
    int main(){
    	if(access(FILE_NAME, F_OK)){
    		if(mkfifo(FILE_NAME, 0644) < 0){
    			perror("mkfifo error");
    			return -1;
    		}
    	}
    	pid_t pid = fork();
    	if(pid < 0){
    		perror("fork error");
    		return -2;
    	}else if(pid == 0){
    		int fd = open(FILE_NAME, O_RDONLY);
    		char buffer[100];
    		while(read(fd, buffer, sizeof(buffer))){
    			printf("子进程读取到:%s\n",buffer);
    		}
    		close(fd);
    		exit(0);
    	}else{
    		char* msg = "mkfifo命名管道\n";
    		int fd = open(FILE_NAME, O_WRONLY);
    		write(fd, msg, sizeof(msg));
    		printf("父进程写入完毕\n");
    		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

    linux命令:

    • mkfifo:

      mkfifo fifo
      #在本目录下创建管道文件fifo
      
      • 1
      • 2
    • > >> < <<:

      cmd > file					#将cmd的结果输出到file中,直接覆盖file
      cmd >> file					#将cmd的结果输出到file中,追加输出
      cmd < file					#将file中内容输出到cmd命令中
      
      • 1
      • 2
      • 3
    • 利用命名管道mkfifo实现内容写入:

    共享内存:

    原理:

    • 基本原理:进程地址空间的共享内存部分指向内存中同一区域

    • 操作步骤:

      1. 建立共享内存:
        1. 申请共享内存:向物理内存申请一块地址空间作为共享内存
        2. 页表地址挂接:向页表中建立进程地址空间共享区和共享内存之间的地址映射关系
      2. 释放共享内存:
        1. 页表地址去关联:删除页表中进程地址空间共享区和共享内存之间的地址映射关系
        2. 释放共享内存:将共享内存这一块地址空间归还给系统
    • 共享内存管理:

      由于每个进程都可以创建共享内存,所以OS需要对所有共享内存进行管理

      1. shmid:shared memory id,每块共享内存用户层面上的独立唯一标识
      2. key:每块共享内存系统层面上的独立唯一标识

    核心函数:

    ftok():

    • 作用:返回系统层面对于共享内存的唯一标识key_t
    • ftok():
    key_t key = ftok(FILE_NAME, PROJ_ID);
    if(key < 0){
    	perror("ftok error");
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 参数:

      1. FILE_NAME:文件路径+文件名,默认文件路径为当前文件夹下
      2. PROJ_ID:工程id,可以随便取,运气不好可能和其他共享内存的PROJ_ID发生冲突
    • 返回值:

      1. 调用失败:-1
      2. 调用成功:系统层面共享内存的唯一标识

    shmget():

    • 作用:利用系统层面唯一标识key,生成共享内存,并返回共享内存在用户层上的唯一id,供用户使用
    • shmget():
    int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0644);
    if(shmid < 0){
    	perror("shmget error");
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 参数:
      1. key:系统层面对于共享内存的唯一标识
      2. SIZE:预计开辟共享内存的大小,由于要与磁盘中分协同,所以只能是1024的整数倍
      3. 打开模式:IPC_CREAT为不存在则创建,IPC_CREAT | IPC_EXCL为不存在则报错,0644为创建的共享内存权限
    • 返回值:
      1. 获取共享内存成功:共享内存在用户层面的唯一标识shmid
      2. 获取共享内存失败:-1

    shmctl:

    • 作用:删除shmid所指定的共享内存
    • shmctl:
    shmctl(shmid, IPC_RMID, NULL);
    
    • 1
    • 参数:
      1. shmid:用户层面对共享内存的唯一标识
      2. IPC_RMID:宏定义的整数,代表删除命令
      3. struct shmid_ds *buf

    shmat():

    • 作用:将物理内存中开辟好的共享内存挂载到某个进程的进程地址空间中,供其使用
    • shmat():
    char* shm = shmat(shmid, NULL, 0);
    
    • 1
    • 参数:
      1. shmid:用户层对共享内存的唯一标识
      2. NULL
      3. 0
    • 返回值:
      1. 共享内存的起始地址(页表映射加工过)
      2. shmget()时规定了共享内存的空间大小

    shmdt():

    • 作用:将已经挂载到某个进程的进程地址空间中的共享内存卸下
    • shmdt():
    shmdt(shm);
    
    • 1
    • 参数:已经挂载的共享内存地址

    实例演示:

    • 采用客户端服务器模式,客户端给服务器发送消息,两份代码体现效果更直观:
    • 公共头文件comm.h:
    #ifndef _COMMON_H_
    #define _COMMIN_H_
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define PATH_NAME "/home/whb/testipc"
    #define PROJ_ID 0x8668	//此处可能冲突,报错就换个数字
    #define SIZE 4096		//必须为1024倍数
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • server.c:
    #include "comm.h"
    int main(){
    	key_t key = ftok(FILE_NAME, PROJ_ID);
    	if(key < 0){
    		perror("ftok error");
    		return -1;
    	}
    	int shmid = shmget(key, SIZE, IPC_CREAT);
    	if(shmid < 0){
    		perror("shmget error");
    		return -2;
    	}
    
    	char* shm = shmat(shmid, NULL, 0);
    	while(1){
    		printf("%s\n", shm);
    	}
    	shmdt(shmid);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • client.c:
    #include "comm.h"
    int main(){
    	key_t key = ftok(FILE_NAME, PROJ_ID);
    	if(key < 0){
    		perror("ftok error");
    		return -1;
    	}
    	int shmid = shmget(key, SIZE, O_CREAT|O_EXCL|0644);
    	if(shmid < 0){
    		perror("shmger error");
    		return -2;
    	}
    
    	char* shm = shmat(shmid, NULL, 0);
    	int i=0;
    	while(1){
    		shm[i++] = 'A' + i;
    		shm[i] = 0;
    		sleep(1);
    		if(i == 27) break;
    	}
    	shmdt(shm);
    	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

    linux命令:

    查看所有共享内存信息:

    • 指令:
    ipcs
    
    ###
    ------ Message Queues --------
    key        msqid      owner      perms      used-bytes   messages    
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00005feb 0          root       666        12000      1                       
    0x650101d0 1          whb        0          4096       0                       
    
    ------ Semaphore Arrays --------
    key        semid      owner      perms      nsems
    ###
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 字段解释:
      1. key:ftok()的运行结果,共享内存在系统层唯一特殊标识
      2. shmid:shmget()的运行结果,共享内存在用户层唯一特殊标识
      3. owner:创建者
      4. perms:权限
      5. bytes:共享内存大小
      6. nattch:共享内存关联进程数
      7. status:共享内存当前状态

    删除指定共享内存命令:

    • 命令:
    ipcrm -m shmid
    
    • 1
    • 共享内存由内核创建,内核维护,生命周期由OS内核决定而非具体进程
    • 即使创建共享内存的进程终止,共享内存也不会释放,除非ipcrm或者关机
  • 相关阅读:
    PrestoSQL, PrestoDB 和 Trino
    spring三级缓存
    不止于“初见成效”,阿斯利康要让数据流转,以 AI 带动决策智能
    5-2 Pytorch中的模型层layers
    Qt的信号与槽的使用
    【Vagrant】使用 Vagrant 快速创建多台 centos7 虚拟机
    Spring Boot 内置工具类 ObjectUtils
    TPD4E05U06DQAR功能和参数及如何正确安装使用
    手撕 视觉slam14讲 ch7 / pose_estimation_3d2d.cpp (2)
    2059authentication plugin
  • 原文地址:https://blog.csdn.net/buptsd/article/details/126511171