5 进程间通信

1、进程间通信的种类

(1) 命令行参数

  • 在通过exec函数创建新进程时,可以为其指定命令行参数,借助命令行参数可以将创建者进程的某些数据传入新进程
  • execl (“login”,“login”,“username”,“password”,NULL);

(2) 环境变量

  • 类似地,也可以在调用exec函数时为新进程提供环境变量
- sprintf (envp[0],"USERNAME=%s",username);
- sprintf (envp[1],"PASSWORD=%s",password);
- execle ("login","login",NULL,envp);

(3)内存映射文件

  • 通信双方分别将自己的一段虚拟内存映射到同一个文件中

(4)信号

  • sigaction/sigqueue

(5)管道

  • 管道是Unⅸ系统中最古老的进程间通信方式,并目所有的Unix系统和包括Linux系统在内的各种类Unⅸ系统也都提供这种进程间通信机制。管道有两种限制:
    ① 管道都是半双工的,数据只能沿着一个方向流动
    ② 管道只能在具有公共祖先的进程之间使用,通常一个管道由一个进程创建,然后该进程通过fork函数创建子进程,父子进程之间通过管道交换数据
  • 大多数Unⅸ/Linux系统除了提供传统意义上的无名管道以外,还提供有名管道,对后者而言第二种限制已不复存在

(6)共享内存

  • 共享内存允许两个或两个以上的进程共享同一块给定的内存区域。因为数据不需要在通信诸方之间来回复制,所以这是速度最快的一种进程间通信方式

(7)消息队列

  • 消息队列是由系统内核负责维护并可在多个进程之间共享存取的消息链表。它的优点是:传输可靠、流量受控、面向有结构的记录.支持按类型过滤

(8)信号量

  • 与共享内存和消息队列不同,信号量并不是为了解决进程间的数据交换问题。它所关注的是有限的资源如何在无限的用户间合理分配,即资源竞争问题

(9)本地套接字

  • BSD版本的有名管道。编程模型和网络通信统一。

2、管道

2.1 有名管道

  • 有名管道亦称FIFO,是一种特殊的文件,它的路径名存在于文件系统中。通过mkfifo命令可以创建管道文件
    mkfifo myfifo
  • 即使是毫无亲缘关系的进程,也可以通过管道文件通信
    echo ‘Hello,FIFO !’ > myfifo
    cat myfifo
    Hello,FIFO
  • 管道文件在磁盘上只有i节点没有数据块,也不保存数据
    (1)基于有名管道实现进程间通信的逻辑模型
    在这里插入图片描述

(2)有名管道不仅可以用于shell命令,也可以在代码中使用基于有名管道实现进程间通信的编程模型
在这里插入图片描述

mkfifo

// 头文件 sys/stat.h
int mkfifo(char const* pathname,mode_t mode);
- 功能:创建有名管道
- 参数:
	- pathname:有名管道名,即管道文件的路径。
	- mode:权限模式。
- 返回值:成功返回0,失败返回-1

案例

// 向管道中写入数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(){
	// 创建管道文件
	if(mkfifo("./fifo",0664)==-1){
		perror("mkfifo");
		return -1;
	}
	// 打开管道文件
	int fd = open("./fifo",O_WRONLY);
	if(fd == -1){
		perror("open");
		return -1;
	}
	// 写入管道文件
	printf("%d进程写入数据:\n",getpid());
	for(;;){
		// 通过键盘获取数据
		char buf[64] = {};
		fgets(buf,sizeof(buf),stdin);
		// 输入!则结束
		if(strcmp(buf,"!\n")==0){
			break;
		}
		// 写入数据
		if(write(fd,buf,strlen(buf))==-1){
			perror("write");
			return -1;
		}
	}
	// 关闭管道文件
	close(fd);
	// 删除管道文件
	unlink("./fifo");
	printf("%d进程:删除管道文件\n",getpid());
	return 0;
}
// 向管道中获取数据
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
	// 打开管道文件
	int fd = open("./fifo",O_RDONLY);
	if(fd==-1){
		perror("open");
		return -1;
	}
	// 读取管道文件
	printf("%d进程:开始读取数据\n",getpid());
	for(;;){
		// 读取数据
		char buf[64]={};
		int size = read(fd,buf,sizeof(buf)-1);
		if(size == -1){
			perror("read");
			return -1;
		}
		if(size == 0){
			printf("%d进程:对方关闭了管道文件\n",getpid());
			break;
		}
		// 显示数据
		printf("%s",buf);
	}
	// 关掉管道文件
	close(fd);
	return 0;
}

2.2 无名管道

(1)无名管道是一个与文件系统无关的内核对象,主要用于父子进程之间的通信,
由专门的系统调用函数创建
pipe

//头文件 unistd.h
int pipe(int pipefd[2]);
- 功能:创建无名管道
- 参数:
	- pipefd输出两个文件描述符
		 pipefd\[O\] - 用于从无名管道中读取数据;
		 pipefd\[1\] - 用于向无名管道中写入数据。
- 返回值:成功返回0,失败返回-1

(2)基于无名管道实现进程间通信的编程模型

  • 1 父进程调用pipe函数在系统内核中创建无名管道对象,并通过该函数的输出参数pipefd,获得分别用于读写该管道的两个文件描述符pipefd[O]和pipefd[1]
  • 2 父进程调用fork函数,创建子进程。子进程复制父进程的文件描述符表,因此子进程同样持有分别用于读写该管道的两个文件描述符pipefd[O]和pipefd[1]
  • 3 负责写数据的进程关闭无名管道对象的读端文件描述符pipefd[O],而负责读数据的进程则关闭该管道的写端文件描述符pipefd[1]
  • 4 父子进程通过无名管道对象以半双工的方式传输数据,如果需要在父子进程间实现双向通信,较一般化的做法是创建两个管道,一个从父流向子,一个从子流向父
  • 5 父子进程分别关闭自己所持有的写端或读端文件描述符。在与一个无名管道对象相关联的所有文件描述符部被关闭以后,该无名管道对象即从系统内核中被销毁
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(){
	// 父进程创建无名管道
	printf("%d进程:创建无名管道\n",getpid());
	int fd[2]={};// 用于保存文件描述符 fd[0] 存放读端 fd[1]存放写端
	if(pipe(fd)==-1){
		perror("pipe");
		return -1;
	}
	// 父进程创建子进程
	pid_t pid = fork();
	if(pid == -1){
		perror("fork");
		return -1;
	}
	// 子进程代码接收数据
	if(pid==0){
		printf("%d进程:关闭写端,并读取数据\n",getpid());
		close(fd[1]);
		for(;;){
			char buf[64]={};
			// 读取数据
			ssize_t size = read(fd[0],buf,sizeof(buf)-1);
			if(size==0){ // 当没有任何一个进程使用写端时,这个条件才会成立
					break;// 说明写入端关闭
			}
			printf("%d进程读取到:%s",getpid(),buf);
		}
		printf("%d进程:读取端关闭\n",getpid());
		close(fd[0]);
		return 0;
	}
	// 父进程代码发送数据
	printf("%d进程:关闭读端,并向写段写入数据\n",getpid());
	close(fd[0]);// 父进程关闭读端
	for(;;){
		char buf[64] ={};
		fgets(buf,sizeof(buf),stdin);
		// 输入!则结束
		if(strcmp(buf,"!\n")==0){
			break;
		}
		// 发送数据
		if(write(fd[1],buf,strlen(buf))==-1){
			perror("write");
			return -1;
		}
	}
	printf("%d进程:关闭写端\n",getpid());
	close(fd[1]);
	// 父进程收尸
	if(wait(NULL) == -1){
		perror("wait");
		return -1;
	}
	return 0;
}

2.3 特殊情况

  • 1 从写端已被关闭的管道读取,只要管道中还有数据,依然可以被正常读取,一直到管道中没有数据了,这时read函数会返回0(既不是返回-1,也不是阻塞),指示读到文件尾。
  • 2 向读端已被关闭的管道写入会直接触发SIGPIPE(13)信号。该信号的默认操作是终止执行写入动作的进程。但如果执行写入动作的进程事先已将对SIGPIPE(13)信号的处理设置为忽略或捕获,则write函数会返回-1,并置errno为EPIPE。
  • 3 系统内核通常为每个管道维护一个大小为4096字节的内存缓冲区。如果写管道时发现缓冲区的空闲空间不足以容纳此次write所要写入的字节,则write会阻塞,直到缓冲区的空闲空间变得足够大为止。
  • 4 读取一个写端处于开放状态的空管道,将直接导致read函数阻塞。

2.4 管道符号

  • Unix/Linux系统中的多数Shell环境都支持通过管道符号"|"将前一个命令的输出作为后一个命令的输入的用法
    ls -l /etc | more
    ifconfig | grep inet
  • 系统管理员经常使用这种方法,把多个简单的命令连接成一条工具链,去解决一些通常看来可能很复杂的问题
    命令1|命令2|命令3
  • 假设用户输入如下命令:a|b
    ① Shell进程调用fork函数创建子进程A
    ② 子进程A调用pipe函数创建无名管道,而后执行
    dup2 (pipefd[1],STDOUT_FILENO);// 此时子进程的文件描述符1不在指向显示器,而指向管道写端
    ③ 子进程A调用fork函数创建子进程B,子进程B执行
    dup2 (pipefd[O],STDIN_FILENO);// 此时子进程的文件描述符0不在指向键盘,而指向管道读端
    ④ 子进程A和子进程B分别调用exec函数创建a、b进程
    ⑤ a进程所有的输出都通过写端进入管道,而b进程所有的输入则统统来自该管道的读端。
    ⑥ 这就是管道符号的工作原理

2.5 总结

有名管道在任意进程之间都可以实现数据传递,而无名管道只能用在父子进行或者兄弟进程之间实现数据传递

3、IPC对象

共享内存、消息队列和信号量集这三种统称为IPC对象,都是实现进程之间的数据交换的。
相同点:
- 这三种IPC对象都有一个键(外部名称),进程通过这个键就能找到对应的IPC对象,就能得到IPC对象的ID(内部名称),后续就可以利用这个ID来使用IPC对象。
- IPC对象标识符不是最小整数。当一个IPC对象被创建,以后又被销毁时,与该类型对象相关的标识符会持续加1,直至达到一个整型数的最大正值,然后又回转到0
- 创建或者获取一个IPC对象都必须指定一个键。键的数据类型为key_t,在<sys/types.h>头文件中被定义为int。系统内核负责维护键与标识符的对应关系
ftok

// 头文件 sys/ipc.h
// 同一个文件、同一个整数,得到的键是相同的
key_t ftok(const char* pathname,int proj_id);
- 功能:用于合成一个键
- 参数:
	- pathname一个真实存在的路径名(这里只是为了获取文件的元数据中的st_dev和st_ino)
	- proj_id:项目ID,仅低8位有效,0255之间的数
- 返回值:成功返回可用于创建或获取IPC对象的键,失败返回-1

3.1 共享内存

  • 两个或者更多进程,共享同一块由系统内核负责维护的内存区域,其地址空间通常被映射到堆和栈之间
  • 共享内存是所有进程间通信中速度最快的
    在这里插入图片描述

相关函数
1:shmget 创建共享内存

// 头文件 sys/shm.h
int shmget(key_t key,size_t size,int shmflg);
- 功能:创建新的或获取已有的共享内存
- 参数:
	- key:键。
	- size:字节数,自动按页取整,如果是获取的时候,设置为0
	- shmflg:创建标志,可取以下值
			0     - 获取,不存在即失败
		IPC_CREAT  - 创建,不存在即创建,已存在即获取
		IPC_CREAT|IPC_EXCL  - 排它,不存在即创建,已存在即失败,通过位或组合读写权限
- 返回值:成功返回共享内存的ID,失败返回-1

2:shmat 加载共享内存

// 头文件 sys/shm.h
void* shmat(int shmid,void const* shmaddr,int shmflg);
- 功能:加载共享内存,将物理内存中的共享区域映射到进程用户空间的虚拟内存中。
- 参数:
	- shmid:共享内存的ID。
	- shmaddr:映射到共享内存的虚拟内存起始地址,取NULL,由系统自动选择
	- shmflg:加载标志,可取以下值:
			0 - 以读写方式使用共享
			SHM_RDONLY - 以只读方式使用共享内存
	- 返回值:成功返回共享内存的起始地址,失败返回-1/* shmat函数负责将给定共享内存映射到调用进程的虚拟内存空间,返回映射区的起始地址,同时将系统内核中共享内存对象的加载计数加一
调用进程在获得shmat函数返回的共享内存起始地址以后,就可以象访问普通内存一样访问共享内存中的数据。*/

3:shmdt 卸载共享内存

// 头文件 sys/shm.h
int shmdt(void const* shmaddr)
- 功能:卸载共享内存
- 参数:
	- shmaddr:共享内存的起始地址
- 返回值:成功返回0失败返回-1
/*shmdti函数负责从调用进程的虚拟内存中结束shmaddr所指向的映射区到共享内存的映射,同时将系统内核中共享内存的加载计数减1。*/

4:shmctl 销毁共享内存

// 头文件 sys/shm.h
int shmctl(int shmid,IPC_RMID,NULL);
- 功能:销毁共享内存
- 参数:
	- shmid:共享内存对象ID
- 返回值:成功返回0,失败返回-1/*销毁共享内存。其实并非真的销毁,而只是做一个销毁标记,禁止任何进程对该共享内存形成新的加载,但已有的加载依然保留。只有当其使用者们纷纷卸载,直至其加载计数降为0时,共享内存才会真的被销毁*/

注意:共享内存只能自己手动销毁,程序异常退出或者结束都不会自动释放共享内存

  • 基于共享内存实现进程间通信的编程模型
    在这里插入图片描述
// 创建共享内存
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
int main(){
	// 合成键
	printf("%d进程:合成键\n",getpid());
	key_t key = ftok(".",100);
	if(key==-1){
		perror("ftok");
		return -1;
	}
	// 创建共享内存
	int shmid = shmget(key,4096,IPC_CREAT|IPC_EXCL|0664);
	if(shmid == -1){
		perror("shmid");
		return -1;
	}
	// 加载共享内存
	char* shmaddr = shmat(shmid,NULL,0);
	if(shmaddr == (void*)-1){
		perror("shmad");
		return -1;
	}
	// 写入共享内存
	printf("%d进程:载入数据\n",getpid());
	strcpy(shmaddr,"zpyl");
	// 卸载共享内存
	if(shmdt(shmaddr) == -1){
		perror("shmdt");
		return -1;
	}
	printf("销毁\n");
	// 销毁共享内存
	if(shmctl(shmid,IPC_RMID,NULL)==-1){
		perror("shmctl");
		return -1;
	}
	return 0;
}

// 获取共享内存
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h>
int main(){
	// 合成键
	printf("%d进程:合成键\n",getpid());
	key_t key = ftok(".",100);
	if(key==-1){
		perror("ftok");
		return -1;
	}
	// 获取共享内存
	int shmid = shmget(key,0,0);
	if(shmid == -1){
		perror("shmid");
		return -1;
	}
	// 加载共享内存
	char* shmaddr = shmat(shmid,NULL,0);
	if(shmaddr == (void*)-1){
		perror("shmad");
		return -1;
	}
	// 读取共享内存
	printf("%d进程:读取数据\n",getpid());
	printf("%s\n",shmaddr);
	// 卸载共享内存
	if(shmdt(shmaddr) == -1){
		perror("shmdt");
		return -1;
	}
	return 0;
}

查看ipc对象,可以在命令行输入ipcs进行查看,如果有不使用的共享内存,使用ipcrm -m xxxxx进行删除操作

3.2 消息队列

·消息队列是一个由系统内核负责存储和管理,并通过消息队列标识符引用的消息链表队列
在这里插入图片描述

  • 可以通过msggeti函数创建一个新的消息队列或获取一个已有的消息队列。
  • 可以通过msgsnd函数向消息队列的尾端追加消息,追加的消息除了包含消息数据以外,还包含消息类型和数据长度(以字节为单位)。
  • 可以通过msgrcv函数从消息队列中提取消息,但不一定非按先进先出的顺序提取,也可以按消息的类型提取
    系统限制
    可发送消息字节数上限:8192 (一个消息最多8192个字节,也就是2页)
    单条队列消息总字节数上限:16384(16K)
    全系统总消息队列数上限:16
    全系统消息总字节数上限:262144(255K=16*16K)
    由于存在系统限制,相较于其它几种IPC机制,消息队列具有明显的优势
    • 流量控制:如果系统资源(内存)短缺或者接收消息的进程来不及处理更多的消息,则发送消息的进程会在系统内核的控制下进入睡眠状态,待条件满足后再被内核唤醒,继续之前的发送过程
    • 面向记录:每个消息都是完整的信息单元.发送端是一个消息一个消息地发,接收端也是一个消息一个消息地收,而不象管道那样收发两端所面对的都是字节流,彼此间没有结构上的一致性
    • 类型过滤:先进先出是队列的固有特征,但消息队列还支持按类型提取消息的做法,这就比严格先进先出的管道具有更大的灵活性
    • 天然同步:消息队列本身就具备同步机制,空队列不可读,满队列不可写,不发则不收,无需象共享内存那样编写额外的同步代码
      相关函数
      1:msgget 创建新的或获取已有的消息队列
// 头文件 sys/msg.h
int msgget(key_t key,int msgflg);
- 功能:创建新的或获取已有的消息队列
- 参数:
	- key:键。
	- msgf1g:创建标志,可取以下值
			0 - 获取,不存在即失败。
		IPC_CREAT - 创建,不存在即创建,已存在即获取。
		IPC_CREAT|IPC_EXCL  - 排它,不存在即创建,已存在即失败,通过位或组合读写权限
- 返回值:成功返回消息队列的ID,失败返回-1.

2:msgsnd 发送消息

// 头文件 sys/msg.h
int msgsnd(int msgid,void const* msgp,size_t msgsz,int msgflg);
- 功能:发送消息
- 参数:
	- msgid:消息队列的ID
	- msgp:指向一个包含消息类型和消息数据的内存块。该内存块的前4个字节必须是一个大于0的整数,代表消息类型,其后紧跟消息数据
	- msgsz:期望发送消息数据(不含消息类型)的字节数
	- msgflg:发送标志,一般取0即可
- 返回值:成功返回0,失败返回-1/*注意
1:msgsnd函数的msgp参数所指向的内存块中包含消息类型,其值必须大于0,但该函数的msgsz参数所表示的期望发送字节数中却不包含消息类型所占的4个字节消息
2:消息队列缺省为阻塞模式,如果调用msgsnd函数发送消息时,超过了系统内核有关消息的上限,该函数会阻塞,直到系统内核允许加入新消息为止。比如有消息因被接收而离开消息队列
3:如果msgflg参数中包含lPC_NOWAIT,则msgsnd函数在系统内核中的消息已达上限的情况下不会阻塞,而是返回-1,同时置errno为EAGAIN。
*/

3:msgrcv 接收消息

// 头文件 sys/msg.h
int msgrcv(int msgid,void* msgp,size_t msgsz,long msgtyp,int msgflg);
- 功能:接收消息
- 参数:
	- msgid:消息队列的ID。
	- msgp:指向一块包含消息类型(4字节)和消息数据的内存
	- msgsz:期望接收消息数据(不含消息类型)的字节数
	- msgflg:接收标志,一般取0即可
	- msgtyp:消息类型,可取以下值
			0 - 提取消息队列的第一条消息
			\>0 - 若msgflg参数不包含MSG_EXCEPT位,则提取消息队列的第一条类型为msgtyp的消息;若msgflg参数包含MSG_EXCEPT位,则提取消息队列的第一条类型不为msgtyp的消息
			<0 - 提取消息队列中类型小于等于msgtyp的绝对值的消息,类型越小的消息越被优先提取
- 返回值:成功返回实际接收到的消息数据字节数,失败返回-1/* 注意:
1:msgrcv函数的msgp参数所指向的内存块中包含消息类型,其值由该函数输出,但该函数的msgsz参数所表示的期望接收字节数以及该函数所返回的实际接收字节数中都不包含消息类型所占的4个字节
2:当数据长度大于msgsz参数时,如果msgflg参数包含MSG_NOERROR位,则只截取该消息数据的前msgsz字节返回,剩余部分直接丢弃;但如果msgflg参数不包含MSG_NOERROR位,则不处理该消息,直接返回-1,并置errno为E2BIG
3:若消息队列中有可接收消息,则msgrcv函数会将消息移出消息队列,并立即返回所接收到的消息数据的字节数,表示接收成功,否则此函数会阻塞,直到消息队列中有可接收消息为止。若msgflg参数中包含IPC_NOWAIT位,则msgrcv函数在消息队列中没有可接收消息的情况下不会阻塞,而是返回-1,同时置errno为ENOMSG。
*/

4:msgctl 销毁消息队列

// 头文件 sys/msg.h
int msgctl(int msgid,IPC_RMID,NULL);
- 功能:销毁消息队列
- 参数:
	- msgid:消息队列的ID
- 返回值:成功返回0,失败返回-1

基于消息队列实现进程间通信的编程模型
在这里插入图片描述

// 写入消息队列
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/msg.h>
int main(){
	// 合成键
	key_t key = ftok(".",100);
	if(key==-1){
		perror("ftok");
		return -1;
	}
	// 创建消息队列
	int msgid = msgget(key,IPC_CREAT|IPC_EXCL|0664);
	if(msgid==-1){
		perror("msgget");
		return -1;
	}
	// 发送数据
	for(;;){
		// 通过键盘获取数据
		struct{
			long type;// 消息类型
			char data[64];// 消息内容
		}buf = {1234,""};
		fgets(buf.data,sizeof(buf.data),stdin);
		if(strcmp(buf.data,"!\n")==0){
			break;
		}
		// msgsnd
		if(msgsnd(msgid,&buf,strlen(buf.data),0) == -1){
			perror("msgsnd");
			return -1;
		}
	}
	// 销毁消息队列
	if(msgctl(msgid,IPC_RMID,NULL)==-1){
		perror("msgctl");
		return -1;
	}
	printf("销毁完成\n");
	return 0;
}
// 读取消息队列
#include <stdio.h>
#include <unistd.h>
#include <sys/msg.h>
#include <errno.h>
int main(){
	// 合成键
	key_t key = ftok(".",100);
	if(key==-1){
		perror("ftok");
		return -1;
	}
	// 获取消息队列
	int msgid = msgget(key,0);
	if(msgid==-1){
		perror("msgget");
		return -1;
	}
	//接收数据
	for(;;){
		// msgrcv
		struct{
			long type;// 消息类型
			char data[64];// 消息内容
		}buf ={};
		if(msgrcv(msgid,&buf,sizeof(buf.data),1234,0) == -1){
			if(errno == EIDRM){
				printf("消息队列被销毁了");
				break;
			}
			perror("msgrcv");
			return -1;
		}
		printf("%ld-->%s\n",buf.type,buf.data);
	}
	printf("进程结束\n");
	return 0;
}

3.3 IPC命令

  • 查看系统中的IPC对象
    • ipcs -m (memory,共享内存)
    • ipcs -q (message queue,消息队列)
    • ipcs -s (semphore,信号量集)
    • ipcs -a (all,所有的)
  • 删除系统中的IPC对象
    • ipcrm -m 删除共享内存
    • ipcrm -q 删除消息对列
    • ipcrm -s 删除信号量集
  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

启航zpyl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值