全文约 7319 字,预计阅读时长: 22分钟
进程间通信介绍
- 进程间要通过中间媒介的方式来进行传递数据;进程间通信,让不同的进程先看到同一份在内存空间中的公共资源。
- 进程间通信目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
- 数据传输过去了,信号也就过去了,就可以实现进程控制。
- 进程间通信发展:
- 管道:OS提供
- 匿名管道pipe
- 命名管道
- System V进程间通信:本地主机内通信
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX进程间通信:主机间跨网络通信
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
- 管道:OS提供
管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
匿名管道
- 父子进程间关闭不需要的文件描述符,来达到构建单向通信的信道的目的。或许是文件诞生的那一刻起,读写端口只有一对儿,所以单向。
- 匿名管道是一种特殊的文件;父进程需要读的方式打开一次,写的方式打开一次;子进程fd表的内容是父进程的一份拷贝,所以能看到同一份文件。
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
*A pipe is created using pipe(2), which creates a new pipe and returns
two file descriptors, one referring to the read end of the pipe, the
other referring to the write end. Pipes can be used to create a
communication channel between related processes; see pipe(2) for an example.*/
- 输入输出错误占了三个 fd,故 03读端,14写入端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipe_fd[2]={0};//输出型参数 fork之前数据共享
if(pipe(pipe_fd)<0)
{
perror("pipe");
return 1;
}
pid_t id =fork();
if(id<0)
{
perror("fork");
return 2;
}
else if(id == 0)//写
{
close(pipe_fd[0]);
const char* msg = "test for pipe!";
int i=3;
while(i--)
{
write(pipe_fd[1],msg,strlen(msg));//从msg 向1号文件描述符里写msg长度个字节
sleep(1);
}
close(pipe_fd[1]);
exit(0);//关闭并退出
}
else{//father
close(pipe_fd[1]);
char ret[66];
while(1)
{
ret[0]='\0';
ssize_t ss = read(pipe_fd[0],ret,sizeof(ret)-1);
if(ss>0)
{
ret[ss]='\0';
printf("childe:%s\n",ret);
}
else if (ss==0)
{
printf("read done,childe quit\n");
break;
}
else{
perror("read");
break;
}
}
int ss=0;
auto it = waitpid(-1,&ss,0);
if(it >0)
{
printf("wait done father quit\n");
}
else{
perror("waitpid");
}
}
return 0;
}
- 匿名管道特性:
- 进程间同步:
- 若管道里没有消息,父进程渎端则会等待,等管道内部有数据;
- 若写端已经写满了,不能继续写入;进入等待,等待内部有空闲空间。
- 防止没写入时读取到垃圾数据,或者写满时继续写入覆盖了原有数据,是一种对临界资源的保护机制。
- 管道自带同步机制、单向通信、是面向字节流的;只能保证具有血缘关系的进程通信,常用于父子;管道可以保证一定程度的数据读取原子性;进程退出,曾经打开的文件也会被关闭掉。
- 原子性:管道文件缓冲区的数据4KB以内时,可以保证读写数据时,具有原子性。
- 退出后会关闭:进程退出后,就没人再打开这个文件,不需要保存文件状态相关的结构数据了,OS会把它关闭,清理闲置资源;同时也证明了为什么进程退出时,会把数据刷新到文件缓冲区里;
- 管道文件的生命周期伴随着进程。
管道读写规则
read端 | write端 | 结果 |
---|---|---|
不读 | 写 | write阻塞 |
读 | 不写 | read阻塞 |
不读且关闭 | 写 | 无意义的写入,浪费系统资源,写进程会被OS终止掉 |
读 | 不写且关闭 | read读取到0,文件结束 |
- OS通过发型号的方式终止掉进程;waitpid() 中的 status 包含了子进程退出的原因。
- 当要写入的数据量大于 PIPE_BUF 时,linux将不再保证写入的原子性。
命名管道
- 先保证同一份资源:通过打开磁盘上的同一个文件进行通信;一个进程对其写入,另一个读取。
- 不需要将数据刷新到磁盘。
- 命名管道是一种特殊类型的文件。
- 命名管道可以从命令行上创建,命名管道也可以从程序里创建。
---//命令行
$ mkfifo filename
---//程序里创建
int mkfifo(const char *filename,mode_t mode);
---//查看帮助
man 7 fifo //先进先出
man mkfifo
- 命令行实现:
[saul@VM-12-7-centos tt801]$ while :; do echo "test for mkfifo"; sleep 1; done >fio
[saul@VM-12-7-centos ~]$ cat < fio
test for mkfifo
test for mkfifo
test for mkfifo
....
- 程序内实现:
- 读取端阻塞直到写入端 写入,反之亦然…
---//srever.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO "./fifo" //定义文件及路径
int main()
{
int ret = mkfifo(FIFO, 0644);
if(ret < 0){
perror("mkfifo");
return 1;
}
int fd = open(FIFO, O_RDONLY);//读方式打开
if(fd<0){
perror("open");
return 2;
}
char buffer[128];
while(1){
buffer[0] = 0;
ssize_t s = read(fd, buffer, sizeof(buffer)-1);
if(s > 0){
buffer[s] = 0;
printf("client# %s\n", buffer);
}
else if(s == 0){
printf("client quit...\n");
break;
}
else{
break;
}
}
close(fd);
return 0;
}
---//cilent.c
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO "./fifo"
int main()
{
int fd = open(FIFO, O_WRONLY);
if(fd<0){
perror("open");
return 2;
}
char buffer[128];
while(1){
printf("Please Enter# ");
fflush(stdout);
buffer[0] = 0;
ssize_t s = read(0, buffer, sizeof(buffer)-1);
if(s > 0){
buffer[s] = 0;
write(fd, buffer, strlen(buffer));
}
else if(s == 0){
break;
}
else{
break;
}
}
close(fd);
return 0;
}
共享内存
- OS申请一块儿物理内存空间,将该内存映射金对应进程的共享区中(堆栈之间),OS可以将映射之后的虚拟地址返回给用户。
- 申请共享内存
- 进程a,b分别挂接对应的共享内存到自己的共享区地址空间
- 双方就看到了同一份资源,就可以正常通信了。
- OS内部提供了通信机制的模块(IPC),管理系统中的共享内存。有对应的系统调通接口,给我们提供类似的服务,进程调用。
key_t ftok(const char *pathname, int proj_id);
:提供shmget()
中的K值。- 参数根据自己的情况任意填写。
int shmget(key_t key, size_t size, int shmflg);
:用来创建共享内存- 参数:key:这个共享内存段名字, size:共享内存大小, shmflg:标志位
shmflg:
如果IPC_CREAT and IPC_EXCL
都设置,则获取一块儿新的共享内存;如果已经存在,返回-1.IPC_CREAT
单独设置,没有共贡献内存,创建之。有就获取之。- 后续在 | 一个类似文件掩码的数值 设置IPC权限
- 返回值:成功返回一个整数,失败-1。
- 参数:key:这个共享内存段名字, size:共享内存大小, shmflg:标志位
void *shmat(int shmid, const void *shmaddr, int shmflg);
:将共享内存段关联到进程地址空间。- 参数:shmid: 共享内存标识, shmaddr:指定连接的地址, shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
shmaddr
为NULL,核心自动选择一个地址shmflg
:设置为0.
- 返回值:成功返回一个指针,指向共享内存的起始地址;失败返回-1。
- 参数:shmid: 共享内存标识, shmaddr:指定连接的地址, shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
int shmdt(const void *shmaddr);
:将共享内存段与当前进程脱离,去关联。- 参数:
shmaddr
: 由 shmat 所返回的指针 - 返回值:成功返回0;失败返回-1。
- 将共享内存段与当前进程脱离不等于删除共享内存段。
- 参数:
- 查看共享内存的命令:
ipcs -m / ipcs
- 删除共享内存:
- 手动:
ipcrm -m shmid
- 进程内部:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
:cmd
:宏,有多个;设置成其中一个:IPC_RMID
。标记要销毁的段。只有在最后一个进程将其分离后(即当关联结构 shmid 的nattch 成员为零时),该段才会被实际销毁。buf
:NULL
- 手动:
- 关于
IPC_CREAT and IPC_EXCL
这样的标志位:- 是大写的宏,平时如果自己传状态,int只能传一种状态。
- OS用位图来传参,32个比特位代表32个标志位,各标志位对应的比特位只有一个是1,且位置不同,用这两个位按位或,去设置参数
shmflg
,这样参数就可以表示多种状态。 - OS内部再通过按位与来检测哪种状态的标志位被设置,进而进行对应状态的操作。
- 所有共享内存ipc资源的生命周期是随内核的,不跟随进程;删除方法:
- 进程退出的时候,调用接口释放
- 指令释放
- OS重启
- 共享内存的大小,OS在分配shm的时候,按照4KB的倍数分的;共享内存没有进行同步与互斥!
- 综上:一个进程创建申请内存等…,另一个进程获取共享内存、关联、进行通信、去关联;申请IPC资源的一方需要进行释放删除。
----//server.c
#include "comm.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main()
{
//创建key
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k < 0){
perror("ftok");
return 1;
}
printf("key: %x\n", k);
//创建共享内存
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0644); //共享内存如果不存在,创建之,如果存在,出错返回!
if(shmid < 0){
perror("shmget");
return 2;
}
printf("shmid: %d\n", shmid);
//将当前进程和共享内存进行关联!
char *start = (char*)shmat(shmid, NULL, 0);
printf("server already attach on shared memory!\n");
//开始使用共享内存通信了
//TODO
for( ; ; ){
printf("%s\n", start);
//sleep(1);
}
//将当前进程和共享内存去关联
shmdt(start);
printf("server already dattch off shared memory!\n");
//释放共享内存
shmctl(shmid, IPC_RMID, NULL);
printf("delete shm!\n");
return 0;
}
----//client.c
#include "comm.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main()
{
//获取同一个key
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k < 0){
perror("ftok");
return 1;
}
printf("%x\n", k);
//不需要自己创建shm,获取共享内存
int shmid = shmget(k, SIZE, IPC_CREAT);
if(shmid < 0){
perror("shmget");
return 2;
}
//client挂接自己到shm
char * start = (char*)shmat(shmid, NULL, 0);
//TODO
char c = 'A';
while(c <= 'Z'){
start[c - 'A'] = c;
c++;
sleep(2);
}
//去关联
shmdt(start);
return 0;
}
----//makefile
CC=gcc //编译器
.PHONY:all
all:client server
client:client.c
$(CC) -o $@ $^
server:server.c
$(CC) -o $@ $^
.PHONY:clean
clean:
rm -f client server
shmid
vskey
- key:是一个用户层生成的唯一键值,核心作用是为了区分“唯一性”,不能用来进行IPC资源的操作。
- shmid:是一个系统给我们返回的IPC资源标识符,用来进行操作ipc资源。
- 内核角度是一个柔性数组,共享内存、消息队列、信号量的结构体的第一个成员都是ipc_perm都是一样的。事实上,这个数组就是按照
ipc_perm*
类型存储的,把各种类型的结构体切片放进去,是通过强转做到的,要访问结构体中其它成员,再强转回来就行了~
- 内核角度是一个柔性数组,共享内存、消息队列、信号量的结构体的第一个成员都是ipc_perm都是一样的。事实上,这个数组就是按照
- 拓展:彻底理解mmap()
- 一、是什么:
- mmap()系统调用并不是完全为了用于共享内存而设计的。
- 它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。
- 而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
- 二、原理简介:
- Linux通过内存映像机制来提供用户程序对内存直接访问的能力。
- 内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。
- 也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。
- 举个例子理解一下,使用mmap方式获取磁盘上的文件信息,只需要将磁盘上的数据拷贝至那块共享内存中去,用户进程可以直接获取到信息,
- 而相对于传统的write/read IO系统调用, 必须先把数据从磁盘拷贝至到内核缓冲区中(页缓冲),然后再把数据拷贝至用户进程中。两者相比,mmap会少一次拷贝数据,这样带来的性能提升是巨大的。
- Linux通过内存映像机制来提供用户程序对内存直接访问的能力。
- 一、是什么:
消息队列
- 获取,发送,接受,删除:
msgget
、msgsnd
、msgrcv
、msgctl
… - wc这就是消息队列吗?
system V信号量
- 概念补充:
- 临界资源:多个进程看到的同一份内存空间。
- 临界区:进程内的所有代码,不是全部的代码都在访问临界资源,而是只有一部分在访问;可能造成读写数据不一致问题的,是这部分的少量代码。
- 为了避免数据不一致,保护临界资源,需要对临界区代码进行某种保护。
- 互斥:一部分空间任何时候,有且只能有一个进程在进行访问,串行化执行方式:锁,二元信号量
- 信号量用来描述临界资源中,资源数目的计数器。而信号量本身要让多个进程看见,首先得确保得自身的安全。
- 信号量获取:
int semget(key_t key, int nsems, int semflg);
nsems
:系统可以允许你一次创建多个信号量。- semflg:O_CREAT | O_EXCL
- 返回信号量标识符
- 信号量删除:
int semctl(int semid, int semnum, int cmd, ...);
- 拓展:linux进程间通信之System V 信号量(semaphore)用法详解
结语
- OS通过一众数据结构:结构体,指针,指针数组,双向链表等管理软硬件资源,向上提供接口…