![](https://img-blog.csdnimg.cn/63c7bb63bc404b4a98a7e05607227eea.png)
关于进程,已经讲过进程概念、进程控制,还差进程间通信,关于进程间通信,今天主要讲管道(分为匿名管道、命名管道)、共享内存。我们知道进程是具有独立性的,那它们又如何做到相互联系,达到通信的目的呢?今天就让我们来探索吧~
进程间通信介绍
进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信方式不同的原因
由上面的关系,进程之间要想进行通信,首先让不同的进程看到一份公共的资源,这个资源只能由OS提供。
通信方式
以文件的方式提供:管道通信
以队列的方式提供:消息队列
以原始内存块方式提供:共享内存
进程间通信分类
管道
匿名管道pipe
命名管道
System V
System V 消息队列
System V 共享内存
System V 信号量
管道
匿名管道
原理
上面的过程是通过子进程是以父进程为模板创建,这时候子进程可以通过父进程继承的文件描述符表,找到相同的struct file,然后使用系统调用接口(read、write)进行读写,这样可以把数据直接写到文件的内核缓冲区里面,只要我们不刷新数据到磁盘上,这时候,父子进程就都可以看到一块公共的资源了。
注意
1、这个公共的资源既不属于父进程,也不属于子进程。如果要属于它们其中一个的话,那样就和进程之间具有独立性的话冲突了。
2、struct files_struct属于进程部分,子进程被创建后,是会拷贝父进程一份的。因为进程之间具有独立性,所以它是不会共享父进程的文件描述符表的。
匿名管道实现模型
调用接口
pipe
pipefd[2]:是输出型参数,当创建管道成功之后,返回两个文件描述符,其中pipefd[0]是读端,pipefd[1]是写端。
代码实现
情况一
写端不写或写得慢,读端要等写端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {0};
if(pipe(pipefd) != 0) //匿名管道,返回0就创建成功,-1表示创建失败
{
perror("pipe error!");
return 1;
}
//我们规定父进程进行读取,子进程进行写入
if(fork() == 0)
{
//子进程
close(pipefd[0]);
const char* str = "hello wmm";
while(1)
{
sleep(1);
//pipe里面只要有缓冲区,就一直写入
write(pipefd[1], str, strlen(str)); //这里strlen不用+1,\0不用写入到管道里面
}
return 0;
}
//父进程
close(pipefd[1]);
while(1)
{
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff));
if(s == 0)
{
//此时意味着子进程关闭文件描述符了
printf("child quit...\n");
break;
}
else if(s > 0)
{
buff[s] = 0;
printf("child say to parent: %s\n", buff);
}
else
{
printf("read error...\n");
break;
}
}
return 0;
}
上面的代码是父进程读得快,子进程写得慢,父进程是一直在等子进程的。
演示结果:
情况二
读端不读或读得慢,写端要等待读端。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {0};
if(pipe(pipefd) != 0) //匿名管道,返回0就创建成功,-1表示创建失败
{
perror("pipe error!");
return 1;
}
//我们规定父进程进行读取,子进程进行写入
if(fork() == 0)
{
//子进程
close(pipefd[0]);
const char* str = "hello wmm";
while(1)
{
//pipe里面只要有缓冲区,就一直写入
write(pipefd[1], str, strlen(str)); //这里strlen不用+1,\0不用写入到管道里面
}
return 0;
}
//父进程
close(pipefd[1]);
while(1)
{
sleep(1);
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1); //-1是为了防止越界
if(s == 0)
{
//此时意味着子进程关闭文件描述符了
printf("child quit...\n");
break;
}
else if(s > 0)
{
buff[s] = 0;
printf("child say to parent: %s\n", buff);
}
else
{
printf("read error...\n");
break;
}
}
return 0;
}
这时候我们让写端不休眠,读端每次读之前休眠1s。
运行代码:
这时候我们发现读到的数据并不是我们期望的字符串("hello wmm"),而是每次读到的是写端1s内往管道里写的全部数据。
这说明了什么?
管道是面向字节流的! 一次读到的数据多少,是读端控制的。ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
另外,管道是有大小的,在Linux下是64KB。如果写端写满了,写端就不再写了,这时候要让读端来读。一般linux下读端读走4KB后写端将接着写,针对情况二有兴趣的老铁们可以下去测试哦~
情况三
读端关闭,写端收到SIGPIPE信号直接终止
还是之前的代码,我们在父进程最后改一下:
让父进程关闭读端,退出前查看子进程的退出信号。
运行结果:
父进程关闭读端,子进程会立马收到13号信号,然后被终止掉。
至于13号信号:
实际上这是一种特殊情况,因为读端已经关闭的话,写端一直在写,那么OS认为写端的进程就是在浪费OS的资源,于是OS就发给写端13号信号终止掉它!
情况四
写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾
我们对子进程代码略微修改:
运行结果:
这时候我们发现,读端把写端写入到pipe里面的内容读完之后,然后就会读到0,也就是ssize_t s的返回值为0,此时表明读到文件结尾。
匿名管道总结
4种情况
a、读端不读或者读得慢,写端要等待读端
b、写端不写或者写得慢,读端要等待写端
c、读端关闭,写端收到SIGPIPE信号直接终止
d、写端关闭,读端读完pipe内部的所有数据然后再读,会读到0,表示读到文件结束
5个特点
a、管道是一个只能单向通信的通信管道
b、管道是面向字节流的
c、仅限于具有血缘关系的进程之间进行通信(因为匿名管道没有名字)
d、管道自带同步机制,原子性写入
补充
管道是文件吗?是的!
如果一个文件只被当前进程打开,相关进程退出了(会自动递减struct file的ref引用计数),那么被打开的文件呢?会被OS自动关闭!
总结:管道的生命周期是随进程的!
命名管道
为什么要有命名管道?
如果想让两个不相关的进程之间进行通信,这时候匿名管道就做不到了,这时候就要用命名管道!
命名管道原理
我们通常标识一个磁盘文件,我们用什么方案呢?
答案是:路径+文件名(具有唯一性!)
A,B两个进程是如何看到同一份资源的呢?
上面的问题意思是:A,B两个进程是如何看到并打开同一个文件呢?
通过命名管道,因为为通过搜索路径,就可以找到一个同一个文件。
一旦我们有了命名管道,此时我们只需要让通信双方按照文件操作即可。
因为通信双方也是基于字节流的,所以实际上,信息传递的时候,是需要通信双方定制"协议"的,在这里先不考虑。
命令行创建命名管道
mkfifo
我们就在这里简单演示一下:
创建一个叫fifo的命名管道:
[cyq@VM-0-7-centos fifo]$ mkfifo fifo
我们然后让一个终端往fifo里面写数据,另一个终端从fifo中读数据:
在这里就不多介绍这个了,我们的重点在于代码创建。
调用接口
mkfifo
const char pathname:*路径名/文件名,在指定的目录下创建指定文件的命名管道。
mode:管道权限
创建成功返回0,创建失败返回-1。
代码练习
comm.h
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<string.h>
#define MY_PATH "./fifo" //文件路径
client.c
#include"comm.h"
int main()
{
//这里不用再创建fifo了,只需要获取即可
int fd = open(MY_PATH, O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
//业务逻辑s
while(1)
{
printf("请输入: ");
fflush(stdout); //printf会有缓冲区,这里要提前刷新一下
//先把数据从标准输入拿到我们的client进程内部
char buff[64] = {0};
ssize_t s = read(0 ,buff, sizeof(buff)-1);
if(s > 0)
{
buff[s-1] = 0; //\n不进行写入
write(fd, buff, strlen(buff));
}
}
return 0;
}
server.c
#include "comm.h"
int main()
{
if(mkfifo(MY_PATH, 0666) < 0) //创建失败
{
perror("mkfifo");
return 1;
}
//只需要读文件操作即可
int fd = open(MY_PATH, O_RDONLY);
if(fd < 0)
{
perror("open");
return 2;
}
//业务逻辑,可以进行对应的读写及控制了
while(1)
{
char buff[64] = {0};
ssize_t s = read(fd, buff, sizeof(buff)-1);
if(s > 0)
{
//success
buff[s] = 0;//相当于设置\0,保证读到有效字符串
if(strcmp(buff, "show") == 0)
{
if(fork() == 0)
{
execl("/usr/bin/ls", "ls", "-l", NULL);
break;
}
waitpid(-1, NULL, 0);
}
else if(strcmp(buff, "run") == 0)
{
if(fork() == 0)
{
execl("/usr/bin/sl", "sl", NULL);
break;
}
waitpid(-1, NULL, 0);
}
else
{
printf("client say: %s\n", buff);
}
}
else if(s == 0) //没有client方的数据了
{
printf("client quit...\n");
break;
}
else
{
perror("read");
break;
}
}
close(fd);
return 0;
}
Makefile
.PHONY:all
all:client server
client:client.c
gcc -o $@ $^ -std=c99
server:server.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f client server fifo
代码结果演示:
现象
我们让client写一部分数据到管道中,但是我们让server端休眠30s,这时候写的数据一直在管道文件里面,这时候我们看看管道的大小有什么变化。
我们发现管道文件大小前后一直是0!
**因为fifo文件里面的数据不会刷新到磁盘上,是为了保证效率。 **
思考
为什么我们之前的pipe叫做匿名管道?
因为文件没有名字,它是通过父子继承的方式,看到同一份资源,不需要名字来标识同一个资源。
为什么现在的fifo叫做命名管道呢?
一定要有名字,为了保证不同的进程看到同一个文件,必须有名字!
共享内存
共享内存原理
上面的意思就是在物理内存中开辟好一块空间,让不同的进程通过页表映射可以找到这块空间,这样就达到了不同的进程能够看到同一块空间资源的目的。
进一步理解
既然OS要管理很多个共享内存,那么为了保证不同的进程找到对应的内存块,就需要用一个标示性的"ID",方便让不同的进程识别对应的共享内存的资源!
共享内存调用接口
shmget
key_t key:这里的key值就是会被设置进内核的,关于shm在内核中的数据结构。 关于key值,我们就有专门的接口帮助我们生成keykey值。
size_t size:创建共享内存的大小,建议是4KB的整数。
int shmflag:和open的第二个参数场景类似。参数选择如下:
IPC_CREAT:如果单独使用IPC_CREAT,或者shmflg为0,创建一个共享内存,如果创建的共享内存已经存在,则直接返回当前已经存在的共享内存。(基本不会空手而归)
IPC_EXCL(单独使用没有意义):这些大写的变量实际上就是定义的宏,和IO一样,只有一个位为1,且比特位不冲突。
IPC_CREAT | IPC_EXCL:如果不存在共享内存,则创建;如果有了共享内存,则返回出错!意义:如果调用成功,得到的一定是最新的,没有被别人使用的共享内存!
返回值:调用成功就返回对应的shmid编号(提供shmctl使用); 返回-1就表示调用失败。
ftok
const char pathname:*自定义路径
int proj_id:自定义ID
这个函数会根据传的路径名和id值,通过算法形成一个key值,然后作为返回值返回。如果返回-1就表示出错。
shmctl
int shmid:指定哪个共享内存。
int cmd:在这里我们只了解删除删除的接口,IPC_RMID
** struct sgmid_ds* buf:**OS提供给用户的"简易版"的内核数据结构。在这里我们不需要关心,只需要传NULL就可以了。
shmat
** int shmid:**共享内存的"id"编号,在shmget创建成功的返回值就可以获取。
const void shmaddr:*关联共享内存挂接到指定的位置。我们不用关心,传NULL,交给OS处理就可以了。
int shmflg:这里我们给他传0就行。
返回值:返回共享内存的起始地址。(这个和malloc开辟空间返回开辟好空间的起始地址类似。)这里的地址,指的是虚拟地址!
这个接口就是让该进程与创建的共享内存块关联起来,也就是可以通过页表映射,该进程能找到对应内存块。
shmdt
*const void shmaddr: **传共享内存的起始地址,这样就可以让该进程和共享内存取消关系。注意:和共享内存去关联,并不是释放共享内存,而是取消进程和共享内存的关系。
代码练习
client.c
#include "comm.h"
#include<unistd.h>
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
printf("%u\n",key);
//client只需要进行获取即可 SIZE表示共享内存大小
int shmid = shmget(key,SIZE,IPC_CREAT);//第三个参数是获得方式,存在即获取,在这里只能获取
if(shmid < 0)
{
perror("shmget");
return 1;
}
char* mem = (char*)shmat(shmid,NULL,0);
//sleep(5); --> 测试nattch用
printf("client process assaches success!\n");
//这个地方就是我们要通信的区域
char c = 'A';
while(c <= 'Z')
{
mem[c-'A'] = c;
c++;
mem[c-'A'] = 0;//保证字符串最后有\0
sleep(2);
}
shmdt(mem);
//sleep(5); --> 测试nattch用
printf("client process detaches success\n");
//client要不要删除呢?不需要!
return 0;
}
server.c
#include "comm.h"
#include<unistd.h>
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
printf("%u\n",key);
//client这里只需要进行获取即可
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//设置全新的shm,如果和系统已经存在的ID冲突,我们出错返回
if(shmid < 0)
{
perror("shmget");
return 2;
}
printf("key: %u, shmid, %d\n",key,shmid);
//sleep(3); --> 测试nattch用
//加关联,shmid是共享内存的起始地址
const char* mem =(char*) shmat(shmid,NULL,0);
printf("attach shm success\n");
//sleep(15);//server多休息几秒,横穿整个client。 ->测试nattch用
//这里就是我后面要进行通信逻辑
while(1)
{
sleep(1);
//这里我有没有调用类似pipe或fifo的read这样类似的接口呢?
printf("%s\n",mem);//server 认为共享内存里面放的是一个常字符串
}
shmdt(mem);//去关联
printf("detaches shm success\n");
//sleep(5); // --> 测试nattch用
shmctl(shmid,IPC_RMID,NULL);
printf("key:0x%x, shmid:%d -> shm delete sucess\n",key, shmid);
sleep(10); //->测试nattch用
return 0;
}
Makefile
.PHONY:all
all:client server
#all是一个伪目标,在自顶向下扫描时,all后面的两个文件没有,
#那么就会自动向下找对应的生成方法生成文件
client:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server
运行效果:
查看共享内存
我们创建共享内存后,如果要查看其中情况使用ipcs -m就可以。
[cyq@VM-0-7-centos test_fifo2]$ ipcs
** ipcs:**用来查看所有内存块的情况。
Message Queues:表示消息队列
Shared Memory Segments:共享内存
Semaphore Arrays:信号量
ipcs选项
ipcs -m 查看共享内存
shmid:表示共享内存块的"ID",用户层使用
key:key值,在内核层使用的
owner:用户名
bytes:内存块的空间大小
nattch:与该内存块关联的进程数目
status:内存块的状态
ipcs -q 查看消息队列
ipcs -s 查看信号量
命令行删除共享内存
我们运行上面的./serer程序,然后终止掉,看看共享内存的情况:
我们通过ipcs -m发现,我们把程序终止了,但是共享内存块还在!这可是还在占用内存空间!浪费OS资源!所以我们要释放它!
释放共享内存有两种方式:
1、OS重启
2、程序员手动释放
命令行方式释放共享内存
[cyq@VM-0-7-centos test_fifo2]$ ipcrm -m shmid编号
ipcs -m 后面只能跟对应的shmid编号,删除对应共享内存块,不能使用key。为什么呢?
key:只是用来在OS层面上唯一标识的,不能用来管理shm
shmid:是OS给用户的id,用来在用户层进行shm管理
代码接口释放共享内存
实际上我们在上面的代码已经展现出来了,shmctl就可以删除共享内存。在这里我们就截图看一下:
总结
说明System V的PC资源,生命周期是随内核的!(就算创建System V资源的进程退出了,但是它申请的资源还存在!)只能通过OS重启,或者程序员显示释放来清理资源。
总结
1、共享内存一旦建立好并映射到自己的进程地址空间后,该进程就可以直接看到共享内存,就如同malloc的空间一样,不需要任何系统调用!
2、共享内存是所有进程间通信中最快的,因为他中间拷贝次数极少。
3、当client没有写入,甚至没有启动的时候,server端不会等待client端写入!因为共享内存不提供任何同步或互斥机制,需要程序员自行保证数据的安全!
看到这里,支持博主一下吧~
![](https://img-blog.csdnimg.cn/bada5e553c174f158835f7897a638e20.png)
版权归原作者 暴走的橙子~ 所有, 如有侵权,请联系我们删除。