微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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 <unistd.h>
  • pipe():
int pipefd[2];		
if(pipe(pipefd) < 0){
	perror(pipe error);
	return -1;
}
  • 输出型参数:int[2]
    1. 读取管道文件存储在pipefd[0]
    2. 写入管道文件存储在pipefd[1]
  • 返回值:获取管道文件失败返回-1

系统调用接口:

实例演示:

  • 父进程写,子进程读:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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;
}

linux命令:

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

命名管道:

原理:

  • 两个进程通过OS提供的系统调用接口open()打开同一文件及其缓冲区

    命名管道示意图

  • 特殊点:
    1. 命名管道虽然也是文件,但是和匿名管道一样,不占硬盘大小,只用内存映射
    2. open()命名管道文件后,之后的操作完全和文件操作一样,一个进程write,一个进程read()

核心函数

mkfifo():

  • 作用:在指定路径下创建一个命名管道文件
  • 文件
    1. #include <sys/types.h>
    2. #include <sys/stat.h>
  • 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. char* filename:文件路径和名称认为当前路径下
    2. mode_t mode: 所创文件的权限码,一般为0644
  • 返回值:

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

access():

  • 作用:由于mkfifo不能重复创建同一路径下同名的命名管道文件,所以在mkfifo()前,需要我们确定该文件是否已存在
  • 文件:#include <unistd.h>
  • access():
if(access(FILE_NAME, F_OK)){
	if(mkfifo(FILE_NAME, 0644) < 0){
		perror("mkfifo error");
		return -1;
	}
}
  • 参数:
    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <error.h>
#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;
}

linux命令:

  • mkfifo:

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

    cmd > file					#将cmd的结果输出到file中,直接覆盖file
    cmd >> file					#将cmd的结果输出到file中,追加输出
    cmd < file					#将file中内容输出到cmd命令中
    
  • 利用命名管道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. 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. 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. shmid:用户层面对共享内存的唯一标识
    2. IPC_RMID:宏定义的整数,代表删除命令
    3. struct shmid_ds *buf

shmat():

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

shmdt():

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

实例演示:

  • 采用客户端服务器模式,客户端给服务器发送消息,两份代码体现效果更直观:
  • 公共头文件comm.h:
#ifndef _COMMON_H_
#define _COMMIN_H_
#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PATH_NAME "/home/whb/testipc"
#define PROJ_ID 0x8668	//此处可能冲突,报错就换个数字
#define SIZE 4096		//必须为1024倍数
#endif
  • 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;
}
  • 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;
}

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. key:ftok()的运行结果,共享内存在系统层唯一特殊标识
    2. shmid:shmget()的运行结果,共享内存在用户层唯一特殊标识
    3. owner:创建者
    4. perms:权限
    5. bytes:共享内存大小
    6. nattch:共享内存关联进程数
    7. status:共享内存当前状态

删除指定共享内存命令:

  • 命令:
ipcrm -m shmid
  • 共享内存由内核创建,内核维护,生命周期由OS内核决定而非具体进程
  • 即使创建共享内存的进程终止,共享内存也不会释放,除非ipcrm或者关机

原文地址:https://www.jb51.cc/wenti/3280492.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐