目录
进程间通信介绍
一、为什么要进行进程间通信?
二、进程间通信目的
三、进程间通信的宏观理解
四、如何让不同的进程看到公共的资源?
进程间通信发展
进程间通信分类
管道
System V IPC
POSIX IPC
五、管道
父子进程间的通信
为什么struct file_struct要拷贝一份,而struct file不需要拷贝一份?
进程调用文件操作的原理
父子进程间的公共资源struct file
站在文件描述符角度-深度理解管道
为什么这里会有父进程会有两个文件描述符?
如果我们利用即能读又能写方式打开呢?
pipe
管道通信实例
管道的4种情况和5个特点
六、命名管道
那么这两个进程是如何看到同一份资源的呢?也就是说这两个进程是如何看到并打开同一个文件的呢?
利用命名管道进行进程间通信
为什么我们之前的pipe叫做匿名管道,fifo叫做命名管道呢?
七、System V
共享内存
如何利用共享内存的方式进行进程间通信呢?
不使用共享内存了,我们该怎么办呢?
背景知识补充
1.OS内可不可能存在多个进程,同时使用不同的共享内存来进行进程间通信呢?
2.如何保证多个进程看到的是同一个共享内存?
共享内存相关接口
创建共享内存
删除共享内存
进程间利用共享内存通信实例
这里我有没有调用类似管道或者命名管道中类似read这样的接口呢?
当client没有写入,甚至启动的时候,server端有没有直接读取shm呢?有没有等待client写入呢?
共享内存三个特点
共享内存的size大小问题
为什么是4096的整数倍呢?
消息队列
信号量
什么是信号量?
什么是临界资源?
什么是临界区?
深入理解信号量
可是信号量是用来保护临界资源的,可信号量本身也是临界资源,信号量如何保护自己?
什么是原子性?
什么是互斥?
编辑什么是同步?
进程之间可能会存在特定的协同工作场景!也就意味着一个进程要把自己的数据交付给另外一个进程让其进行处理,这就叫做进程间通信。
eg:统计我的源代码有多少行
这里的cat pipe_process就是一个进程,它的核心功能是打印数据,wc是用来统计输出的文本行有多少个的进程,它的数据源就从上一个进程 cat pipe_process通过管道获得的,实际上这就是一种进程协同。
进程是具有独立性的!交互数据成本一定会很高。这个成本就是:一个进程是看不到另外一个进程的资源(空间或者数据)的。就拿父子进程来说他俩都是具有独立的地址空间的,父进程只允许子进程读取,如果子进程要写入,就要进行写时拷贝,才能让子进程写入。更别说数据了,就连保存数据的空间都看不到,因为进程间都是独立的地址空间,用页表映射到了不同的物理内存,相互之间是看不到的,所以要完成进程间通信,就不能在应用层解决,就需要OS参与进来。进程与进程之间是不能互相影响的,现在想让一个进程把资源传给另一个进程,其中就必须让OS设计通信间的方案才能让进程看到对应的数据。
两个进程要相互通信,又因为进程具有独立性,所以就必须得先看到一份公共资源。这里的资源就是一段内存!这个公共资源属于OS,它肯定不属于两个进程其中之一,因为如果属于进程A,进程之间具有独立性,他就一定不能让其他进程看到,要不然就破坏了独立性。eg:你上网课,你与老师之间的公共资源就是你和老师共同使用的直播软件。
进程间通信的本质:其实是由OS参与,提供一份所有通信进程能看到的公共资源。OS提供的这段内存,可能以文件方式提供,也可能以队列的方式提供,也可能提供的就是原始的内存块。因为OS提供公共资源的组织方式不同,所以就有了多种的通信方式。
eg:有的提供的公共资源是队列,俩进程通信就叫消息队列,如果是以公共文件的方式就叫做管道,如果看到的是一个原始的内存块就叫做共享内存。
进程间通信方式有一种就叫做管道。
我们从父子进程开始,父子进程是两个独立的进程,所以父子通信也属于进程间通信,匿名管道就是基于父子进程。
1.struct file_struct是属于进程PCB的,而且也包含了大量的数据。
2.如果共用一个struct file_struct,父进程进行读写文件,会被子进程看到,不能做到独立性
struct file 则不需要重新拷贝一份,因为文件是被进程打开了,但是进程与文件之间只有关联关系,不具备拥有关系,这个 struct file属于文件部分的内容和创建进程没有关系,但是子进程继承的struct file_struct是以父进程为模板的,所以父子进程指向同一个文件。
当调用系统的读写方法的时候,不是直接写入到磁盘中,OS会开辟一段文件的内核缓冲区,eg:调用write方法时,除了调用底层对应的写方法还会把内容写到了文件的内核缓冲区中,同时这个文件的内核缓冲区也必须被struct file 找到,然后OS定期把数据更新到磁盘中。实际上这个write只做了两件事情。1.拷贝数据从用户到内核 2.触发底层的写入函数。
现在父子进程看到了一份公共资源,这个公共资源就是文件struct file.如果父进程将自己的数据写入到了对应缓冲区中不刷新磁盘,那么子进程就可以通过fd找到同一个struct file读取到这个缓冲区的数据,此时就可以做到将一个进程的数据交给下一个进程,这就叫做让不同进程看到同一份资源,这种基于文件的通信方式就叫做管道。说白了就是两个进程可以看到同一个文件,文件里面有缓冲区,一个进程向文件缓冲区中写数据,另一个进程向文件缓冲区中读数据,就完成了通信。
文件不属于进程,而是和进程同样属于内核的数据结构,和进程相关,OS帮助不同进程看到同一分资源。
将刚刚的图片简化一下
首先分别以读方式和写方式打开同一个文件,同一个文件在内核中被打开两次是可以的那么他就既可以读又可以写。但是我们是不会这样做的,因为这样没意义。
eg: 同一个文件在内核中被打开两次
然后fork创建子进程,子进程继承父进程,所以两者都可看到读端和写端,又因为管道是一个只能单向通信的的通信信道(如果想要双向通信就建立两个管道),所以接下来就需要父子进程关闭对应的读写端,究竟关闭谁,取决于你想让父进程读还是父进程写,还是你想让子进程读还是子进程写
因为如果只打开一个,那么继承下去的子进程也只能读了,就不能实现通信。
比如C语言的rw。这样虽然只有一个文件描述符,子进程继承下来也是可以读写了,但因为管道是一个只能单向通信的的通信信道,这时候你是让他读呢还是让他写呢?虽然可以规定一种方式解决,但是容易误操作。所以OS单独针对管道设计了一个接口叫做pipe
创建成功返回0,失败返回-1。pipefd[2]:是一个输出型参数,我们通过这个参数读取到打开的两个fd。
pidfd[0] pidfd[1] 哪一个是读,哪一个是写呢?
0下标:读取端 1下标:写入端.我们现在的代码让父进程进行读取,子进程进行写入,所以父进程关闭写端(pidfd[1]),子进程关闭读端(pidfd[0]),我们让子进程向管道中写入hello Linux。ps:因为这是向管道写入管道也是文件,又因为\0只是C语言的标准,文件里面我们只关心内容,\0并不是文件内容。所以我们不用写入\0.
read的返回值:返回值代表你读到的字节数,0表示对应文件结束,如果read的返回值是0,意味着子进程关闭文件描述符了,子进程关闭了就没人写了相当于读到文件结尾。
父进程的buffer是自己定义的,默认是清空的,意味着这个缓冲区将来读到了数据,读到的一定是子进程发来的消息,如果进程间能通信的话。
include
#include
#include
#include
#include
#include
#include
int main()
{
int pipefd[2]={0};
if(pipe(pipefd) !=0 )
{
perror("pipe error!");
return 1;
}
//pidfd[0] pidfd[1] 哪一个是读,哪一个是写呢?
//0下标:读取端 1下标:写入端
printf("pipefd[0]: %d\n",pipefd[0]); //3
printf("pipefd[1]: %d\n",pipefd[1]); //4
//我们让父进程读取,子进程写入
if(fork()==0)
{
//子进程:
close(pipefd[0]);
const char *msg = "hello Linux";
while(1)
{
write(pipefd[1], msg, strlen(msg));//这里strlen(msg)+1 是不需要的,因为这是向管道写入管道也是文件,因为\0是C语言的标准,
//文件里面我们只关心内容,\0并不是文件内容
sleep(1);
}
exit(0);
}
//父进程
close(pipefd[1]);
while(1)
{
char buffer[64]={0};
//返回值代表你读到的字节数,0表示文件结束啦,如果read的返回值是0,意味着子进程关闭文件描述符了,子进程关闭了就没人写了相当于读到文件结尾
ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
if(s==0)
{
break;
}
else if(s>0)
{
buffer[s]=0;
printf("chid say to father# %s\n", buffer);
}
else
{
break;
}
}
return 0;
}
运行结果:一直在持续的打印这条hello Linux消息。也就是子进程一直写入hello Linux ,父进程一直读取,这就是所谓的管道通信,我们做到了让子进程把数据通过管道交给了我们的父进程。
eg2:我们让子进程写完后不进行sleep ,让父进程先sleep(1)后再进行读,其余均不变。
执行结果:这次我们发现父进程读一次就拿了这个么多的hello Linux。这是为什么?
因为对于当前子进程来说,只要pipe里面有缓冲区,就一直写入。对于父进程来说,只要有数据就一直读取。父进程根本不管你里面有几个hello Linux 只要里面有数据统统读出来。这就特性就叫做字节流。所以如果父子进程间通信是需要我们定义协议的。
eg3:在子进程中定义一个计数器count,每次写一个字符,写完后count++,然后输出我的count,相当于子进程周而复始一次往管道里写一个字符,父进程除了关闭写端,每隔一秒休眠一下以外啥也不干。
执行结果:我们发现子进程写到65536就不再写了,65536字节就是64kb。当写满64kb的时候,write就不再写入了,这是因为管道有大小!
当write写满了,为什么不写了?
我可以进行对之前内容的覆盖,还能进行写入啊。因为我要让读端来读,如果把之前的内容覆盖,那么我们之间的通信工作相当于白干,本质就是等读端来读。所以当我们写满了对方没时间读我们就需要等一等。
eg4:让父进程先等上10秒,在这段时间内子进程已经写满了管道,然后让父进程一次读一个字节
执行结果:父进程拿走了,但是子进程没啥反应,按理来说应该是父进程一拿走,子进程就立刻写
这次我们让父进程一次读的多一些 ,一次读63个字节
执行结果:发现还是没啥反应
我们再次改大点,改成1024*4+1,也就是一次读4kb,这里我们只打印一个字节,因为如果都打印数据太多了会刷屏。
执行结果:
这次我们发现每隔10秒,子进程就会进行写入了!!!
这究竟是为什么呢?
eg5:子进程每个10秒写一个字符串,父进程不再进行休眠,一直进行读取,一次读取64字节
执行结果:我们发现父进程就是以写入的节奏为主了,也就是读端在等写端
eg6:子进程写一条消息就break,并且关闭写端文件描述符,父进程通过read的返回值进行判断
执行结果:
子进程的消息被父进程读到了,10秒后子进程退出,并关闭了写端,接着父进程发现read的返回值变成0了,同样也退出。所以如果写端关闭了,读端就读到文件结尾。
eg7: 写端一直在写,父进程sleep(10)开始读,读端只要读一条消息就break,接着关闭读端
执行结果:
我们这里用一个脚本来看 :制作脚本命令如下:
while :; do ps axj | grep pipe_process | grep -v grep; sleep 1; echo "##################################################"; done
结果:
父进程读了一条消息就直接break退出了,符合我们的预期,但是最后我们发现俩进程一个都没有了,子进程也退出了。
当我们的读端关闭,写端还在写入,此时站在OS的层面,这严重不合理,已经没有人读了,你还在写入,本质就是在浪费OS的资源,OS会直接终止写入进程!OS给目标进程发送信号SIGPIPE eg:老师给学生上课,但是学生都走了,如果学生走后,老师继续上课,就会变的没有任何意义。
如何证明OS发送了信号呢?
子进程写入,出现OS杀死子进程,子进程就属于异常退出,异常退出后,父进程就能读取子进程的退出信息。
以上就是管道的4种情况和5个特点
4种情况
匿名管道的5个特点
命名管道的引入:为了解决匿名管道只能父子通信,引入了命名管道。
mkfifo 表示制作命名管道
while :; do echo "你好,Linux"; sleep 1; done > myfifo
进程是具有独立性的->进程通信的成本是比较高的->必须先解决一个问题->让不同的进程看到同一份资源(内存文件,内存,队列 )[一定需要OS提供] -> pipe的本质:是通过子进程继承父进程资源的特性,达到一个让不同的进程看到同一份资源!
我们通常标识一个磁盘文件,我们采用路径+文件名的方案,因为这样的方案是具有唯一性的。ps:inode是系统用来进行标识文件的。
一个进程先把数据写到磁盘中的文件里,然后用另外一个进程从磁盘中的文件中进行读取就可以完成进程间通信。
eg:echo和cat进行了进程间通信,借助的文件是tmp.txt
只不过这种方式是有些慢的,所以我们可以将磁盘中的文件通过某种方式,比如open这个文件,把这个文件放到内存在中,此时要OS采用它对应的内存数据结构和缓冲区,我们让两个进程同时都open这个文件,所以我们可以让一个进程写,另一个进程读,这个时候不要把数据刷新到磁盘上(因为没有意义而且还会让通信效率变低)这个数据只是基于内存通信就可以了。所以这个两个进程就可以通过这个文件进行通信。
采用路径+文件名的方案。
所以现在我们有两个需求:
命名管道:首先它是文件,命名就是给他起了名字。
我们首先创建两个可执行程序,运行起来就是俩进程,而且毫不相干
mkfifo
第一个参数代表创建的文件名,第二个参数代表创建的管道的权限(因为管道也是文件)成功返回0,失败返回-1。
在server.c中建立管道
运行后出现了一个fifo
PS:但是我们发现形成的fifo的权限是664。根本原因是因为你在创建时,是要受到OS的umask影响的。
但是我们不想受到umask的影响,我们只要将umask设为0就可以了。umask也是一个系统调用。
此时在运行./server就可以创建666的fifo了(再次执行之前要把fifo给删除掉,否则就会创建失败)
至此管道文件就创建好了。一旦我们有了一个命名管道,此时我们只需要让通信双方按照文件操作即可!
comm.h
#pragma once
#include
#include
#include
#include
#include
#define MY_FIFO "./fifo" // 我的管道就在当前路径下放着
server.c
#include"comm.h"
int main()
{
umask(0); //不想让系统影响文件的权限
if(mkfifo(MY_FIFO, 0666) < 0) //完成管道文件的建立
{
perror("mkfifo");
return 1;
}
//只需要文件操作即可
int fd = open(MY_FIFO, O_RDONLY);
if(fd < 0)
{
perror("open");
return 2;
}
//业务逻辑, 可以进行对应的读写
while(1)
{
char buffer[64]={0};
ssize_t s=read(fd, buffer, sizeof(buffer)-1); // 减1的目的就是不想让他把缓冲区打满,因为要把它当做字符串来看
if(s > 0)
{
//success
buffer[s]=0; // 字符串结束
printf("client# %s\n", buffer);
}
else if(s == 0)
{
//peer close
printf("client quit ...\n");
break;
}
else
{
//error
perror("read");
break;
}
}
close(fd);
return 0;
}
client.c
#include"comm.h" //此时两个程序具有了能看到同一个资源的能力 都能看到该头文件包含的管道
#include
int main()
{
//不需要创建一个fifo,只需要获取即可
int fd=open(MY_FIFO, O_WRONLY); //不需要O_CREAT,因为本来就存在
if(fd < 0)
{
perror("open");
return 1;
}
//业务逻辑
while(1)
{
printf("请输入# ");
fflush(stdout);
char buffer[64]={0};
//先把数据从标准输入拿到我们的client进程内部
ssize_t s=read(0, buffer, sizeof(buffer)-1); //键盘输入的时候,\n也是输入字符的一部分
if(s > 0)
{
buffer[s-1]=0; //去掉键盘输入的\n
printf("%s\n", buffer);
//拿到了数据
write(fd, buffer, strlen(buffer)); //不要-1,管道也是文件
//读一条消息,把这条消息回显出来,并且发送给对方
}
}
close(fd);
return 0;
}
执行结果:
我们必须得先执行./server生成fifo才能通信,接着运行./client .我们发现我们在左侧写入,右侧即时接受信息被显示,client 退出 server接着退出。
因为命名管道也是基于字节流的,所以实际上,信息传递的时候,是需要通信双方定值“协议的”
举个栗子
比如我们想要直接让client控制server ,如果输入的字符是show 就执行ls -l 命令,如果是run就执行sl命令。除此之外就输出对应的字符
eg2:这次让server隔50秒再读
此时我们发了很多消息,但是server并没有读取,一般来讲数据只能在管道文件里,可是我们发现管道文件的大小是0。这说明命名管道的数据为了效率,不会刷新到磁盘
因为fifo它有名字,为了保证不同的进程看到同一个文件,所以它必须有名字。而pipe的文件没有名字,因为它是通过父子继承的方式看到同一份资源,所以它不需要名字来标识同一个资源。
我们之前介绍的都是基于文件的通信方式,现在我们介绍system v标准的进程间通信方式,它是很多NB的计算机科学家和程序员在OS层面专门为进程间通信设计的一个方案。它是被设计在OS层面的,所以OS不相信任何用户,给用户提供功能的时候,采用系统调用!所以System V进程间通信一定会存在专门用来通信的接口(system call)
进程间通信的本质:先让不同的进程看到同一份资源。
所以system v提供的主流方式有三个:
1.共享内存 2.消息队列(有些落伍)3.信号量
前两个以传送数据为目的,第三个以实现进程间同步后者互斥为目的。
2.通过某种调用,让参与通信的多个进程“挂接”到这份新开辟的内存空间上!解释:把物理空间上新开辟的内存通过页表映射到进程的地址空间中,地址空间上就可以拿到映射这部分内存的起始地址。
此时我们就让不同的进程看到了同一份资源,这种通信方案称之为共享内存
1.去关联(挂接)
2.释放共享内存
肯定可以,共享内存在系统中可能有多份。共享内存可能有多份,那么OS就必须要管理这些不同的共享内存,如何管理?先描述后组织!所以共享内存一定有对应的内核数据结构,里面包含了共享内存的相关属性。
共享内存一定要有一个标识唯一性的ID,方便让不同的进程能够师表同一个共享内存资源。这个ID一定在描述共享内存的内核数据结构里面。这个唯一的标识符,本质使用来进程间通信的,让不同的进程能看到同一份资源。前提就是先让不同的进程看到同一个ID。这个ID是由用户自己设定的。但是我们自己设置的往往不太好,我们就可以借助ftok接口
自定义路径名,自定义项目ID。只要我们形成key的算法+原始互数据是一样的,形成同一个ID。这里的key值就是会设置进内核的关于共享内存在内核中的数据结构中。
shemget:创建共享内存
size大小建议4kb的整数倍
shmflag
- 如果单独使用IPC_CREATE或者shmflg为0:如果不存在共享内存,则创建一个共享内存,如果创建的共享内存已经存在,直接返回当前已经存在的共享内存。(基本不会空手而归)
- IPC_EXCL:单独使用没有意义。必须按位或上IPC_CREATE使用。
- IPC_CREATE | IPC_EXCL:如果不存在共享内存,则创建;如果已经有了共享内存,则返回出错。意义在于如果我调用成功,得到的一定是一个最新的,没有被别人使用过的共享内存!
./server执行完毕后,进程运行结束,第二次执行./server后显示文件已存在,说明共享内存创建失败。也就是说该进程曾经创建的共享内存并没有被释放。
ipcs 查看ipc资源
我们发现这个共享内存的nattch是0,并没有其他进程和它关联。这就与文件不一样了,只要和文件相关的所有进程都退出,这个文件就被OS关掉了。system V 的IPC资源,生命周期是随内核的!这个IPC只能通过程序员显示的释放(利用系统调用命令)或者是OS重启 !
小问题:C/C++在堆上申请空间后没有进行主动释放,当进程退出以后,内存泄露的问题还在吗?
答案是已经不在了!!!就是因为进程退出后,这个进程曾经申请的堆空间或者各种资源就会被OS回收。不管是堆空间还是文件描述符,当进程结束以后OS是会进行回收的
(1) 利用ipcrm -m +shmid
我们发现利用key是删不了的。
ps:命令行是就属于用户层的,它绝对使用的是shmid。
(2)利用shmctl
shmctl:控制共享内存
shmid: 由 shmget 返回的共享内存标识码- cmd:是一个选项,将要采取的动作(有三个可取值)。
- shmid_ds:就是描述共享内存的数据结构,只不过它是用户层的数据结构,它是内核上描述共享内存的子集。
返回值:成功返回0;失败返回-1
这里我们进行删除,cmd使用IPC_RMID,shmid_ds设置NULL就可.
eg:
while :; do ipcs -m; sleep 1; echo "##########################################################"; done
执行结果:
除此之外,还发现每次执行server,共享内存的是shmid是递增的,我们自然会联想到数组下标,内核再组织IPC资源的时候是通过数组组织的。
目前我们创建的共享内存的perms是0,代表权限为0,意思就是谁都不能读谁都不能写 ,如果我们想要共享内存被人读取,我们就可以在创建的时候加一个0666,这样perms就变成了666。说明我们可以给共享内存设置权限,并且共享内存的权限管理依赖于文件系统,也就是一切皆文件。
shmat: 关联(让进程和共享内存产生关系)
功能:将共享内存段连接到进程地址空间参数
- shmid: 共享内存标识
- shmaddr:指定连接的地址
- shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
- 返回值:成功返回一个指针,指向共享内存的起始地址(这个地址是虚拟地址。ps:只要用户用的地址都是虚拟地址);失败返回-1。这个返回值如同函数malloc的返回值。
说明
- shmaddr为NULL,OS自动选择一个地址。
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)。
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。
shmdt :去关联(让进程和共享内存去除关系)
功能:将共享内存段与当前进程脱离参数shmaddr: 由 shmat 所返回的指针返回值:成功返回 0 ;失败返回 -1注意:将共享内存段与当前进程脱离不等于删除共享内存段。也就是说它并不是释放共享内存,而是取消当前进程和共享内存的关系。
eg:
配合刚才的监视脚本来看
现在的模型是客服端向共享内存里写ABCDE,server进行读取。
comm.h
#pragma once
#include
#include
#include
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4097
client.c
#include"comm.h"
#include
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if( key < 0 )
{
perror("ftok");
return 1;
}
printf("%u\n", key); //%u是输入输出格式说明符,表示按unsigned int格式输入或输出数据。
//client 只需要获取即可
int shmid = shmget(key, SIZE, IPC_CREAT); //key形成的规则和server的相等,就代表OS在内核中能找到同一个共享内存
if(shmid < 0)
{
perror("shmget");
return 1;
}
char *mem =(char*) shmat(shmid, NULL, 0);
printf("client process attaches success!\n");
//这个地方就是通信的区域
char c = 'A';
while(c <= 'Z')
{
mem[c-'A']= c;
c++;
mem[c-'A']= 0;
sleep(2);
} //向0下标处写一个A字符,再把1下标写成0,下次循环在把1下标写成B....
shmdt(mem);
printf("client process detaches success \n");
//client是不需要删除共享内存的,因为共享内存是server创建的
return 0;
}
sever.c
#include"comm.h"
#include
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if( key < 0 )
{
perror("ftok");
return 1;
}
//printf("%u\n", key); //%u是输入输出格式说明符,表示按unsigned int格式输入或输出数据。
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL |0666); //创建全新的共享内存,如果和系统已经存在的ID冲突,我们出错返回
if(shmid < 0)
{
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
char *mem = (char *)shmat(shmid, NULL, 0 ); //把共享内存挂接到了我的进程当中
printf("attaches shm success \n");
//这里就是我后面进行的通信逻辑
while(1)
{
sleep(1);
//这里我有没有调用类似管道或者命名管道中类似read的接口呢?
printf("%s\n",mem); //server 认为共享内存里面放的是一个长字符串
}
shmdt(mem);
printf("detaches shm success\n");
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
printf("key: 0x%x, shmid: %d-> shm delete success\n", key, shmid);
return 0;
}
我们只运行server,发现它已经开始读共享内存了,它并没有等client。所以它才会一直向下刷屏。
现在运行client
此时我们看到client所写的消息就都被server读到了,server一秒钟读一次,之所以读到的字符串是成对的是因为client每写完一次后停留两秒。至此就完成了client向server的通信过程。
根本没有,所以,共享内存一旦建立好并映射进自己进程的地址空间,该进程就可以直接看到该共享内存,就如同malloc的空间一般,不需要任何系统调用接口。
我们使用系统接口的本质是因为曾经我们的管道是把数据从进程先拷贝到内核文件里,然后再由内核文件拷贝到另外一个进程的空间里。所以read或者write的本质是将数据从内核拷贝到用户,或者从用户拷贝到内核。
根本不会等待client写入,会直接读取shm。
因此共享内存是所有进程间通信中速度最快的 !省略了若干次数据拷贝的问题,用户到内核1次,内核到用户2次,如果只考虑进程有数据之后,共享内存最多拷1次就是你把数据放到共享内存的时候。当然考虑上数据没有放到管道之前,从标准输入里读数据,读到用户层缓冲区,然后在把他read到管道里,因此管道至少需要4次。
共享内存不提供任何同步或者互斥机制,需要程序员自行保证数据的安全!
我向管道里写入的时候,管道写满了,我就写不进去了,我在管道里读是时候,如果没数据了,我也就不能读了...诸多概念就是为了保证数据安全。
eg:我向共享内存中写了一个hello world两个单词必须同时写,另一边必须完整的读到hello world才有意义。如果像我们的共享内存,你刚写了个hello,另一边就读到了,此时在数据读取的时候就可能发生一些相关的数据问题,造成了数据不一致。我写个hello world你读到hello,我并不希望这样。所以共享内存在多进程通信的时候是不太安全的。
size大小建议4kb的整数倍 ,也就是4096的整数倍,但是我们这里申请的是4097。
共享内存在内核中申请的基本单位是页,这个页叫做内存页,这个内存页叫做4KB。
如果我申请4097个字节,内核会向上取整,给你4096byte*2(多要了一个就得多申请一页,因为没有4097)。但是我们实际看到的并不是4096*2,而是4097,这又是怎么回事呢?
如果我向OS要了10个字节,OS只给了9个,我就认为是错的,因为很容易发生越界,OS就有问题。但如果我要了10个字节,OS给了20个字节,有时候也是会出错的,比如你设置了超过这10个字节就抛异常,但是OS给了你20个,就导致程序正常运行,不符合预期,这同样是OS的黑锅。所以你实际要多少,OS就实际给你多少用,你要4097,就给你4097,但是OS底层申请的时候是按照2页去申请的。
信号量的申请
信号量的删除
信号量的PV操作
管道(匿名或者命名),共享内存,消息队列:都是以传输数据为目的的。信号量不适宜传输数据为目的的!通过共享“资源”的方式,来达到多个进程的同步或者互斥的目的!
信号量:本质是一个计数器,类似 int count;衡量临界资源中资源数目的。更重要的是OS提供给我们对临界资源的预订机制!
凡是被多个执行流同时能够访问的资源就是临界资源!比如多进程启动后,同时向显示器上打印,这个显示器就叫做临界资源。进程间通信的时候,管道,共享内存,消息队列等都是临界资源。管道内部提供了写保护机制将临界资源保护起来了,而共享内存就是最典型的临界资源。
凡是要进程间通信,必定要引入被多个进程看到的资源(通信需要),同时也早就了引入一个新的问题,临界资源的问题。
父子进程对应的全局变量不是临界资源,因为发生了写时拷贝,父子访问的并不是同一个。
日常生活中,比如公共厕所就是临界资源 。
进程的代码是有很多的,用来访问临界资源的代码,叫做临界区。
eg:server和client的临界区(红框部分)
再比如:多个进程向显示器上打印消息,显示器就是临界资源,进程中只有printf访问临界区,printf就叫做临界区。
问题:电影院的某一个放映厅是不是一个临界资源呢?是不是我坐在放映厅的座位上,这个座位才属于我?
电影院的某一个放映厅是一个临界资源。当我买到票的时候,这个座位就属于我。买票的本质:对临界资源的预订机制!
一个放映厅最怕什么?
一共有100个座位,卖了110张票。最多只能卖100张票,所以此时使用信号量进行约束。
如果来人买票,count--,直到减为0。直到人们看完电影后,count++。每个人想进入电影院,必须先对count--,前提是每个人都得先看到count!count本身也是临界资源。所以信号量本身就是临界资源。
进入信号量,要对信号量值--,退出进行++,既然信号量本身也是临界资源,就要求我们对信号量--或者++必须是安全的,所以信号量内部对临界资源的--或者++是原子的。这就叫做信号量的PV操作。P操作就是对计数器进行--,V操作就是对计数器进行++。PV操作的共同特点就是他们是原子的。因为信号量本身就是临界资源,他要保护别人,首先保证自己的安全。所以信号量的PV操作就被设置成原子的。
一件事情要么不做,要么就做完,没有中间态,就叫做原子性!
eg:你妈嫌弃你天天在家玩,你妈就和你说,要么就你别学,要么你就往第一名学,我不管你了。之后,你可能就各种学习,你妈不关心也不知道,最后,要么就是一个什么都不学的你,要么就是一个考第一名的你,这就叫做两派。站在你妈的角度,就是你实现了一个原子性。
非原子性:做某一件事情有中间过程就叫做非原子性。
父进程把count读到CPU中,变成99,当他正准备写回的时候,父进程被切走了,此时这个99在父进程的上下文保存着,所以最终这个99被挂起了,然后子进程读取count,同样遵守这个规则,可是子进程在进行count--的时候没人干涉,一直进行循环,可能把count减成了5,count成了5之后,还没向内存中读的时候,子进程被切走了,这个5在子进程的上下文中保存了起来,恢复到父进程的时候,父进程继续执行它还没执行完的代码,也就是继续向内存中读取进行,可是父进程中count是99,直接将子进程好不容易计算的5改成了99。此时导致了多进程对全局数据出现错乱的问题。所以count--本身不是原子的。
在任意一个时刻,只能允许一个执行流进入临界资源,执行他自己的临界区。
eg:这里有个VIP自习室,一次只能允许一个人进去,如果有人进去,门上就显示有人自习,请勿打扰。如果没人,门上就显示空闲。这就是一个互斥。类比成代码
是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。