当进程在内存中执行这一步步代码时,并不是立即把文件内容写到硬盘上的文件,而是先写到文件缓存区,进程结束由操作系统刷新至硬盘上对应的文件。
进程间通信的本质就是让两个进程看到同一份资源,即这个文件内存缓冲区。
当fork创建子进程,子进程会继承父进程的文件描述符等。通过struct file就能找到文件缓冲区
先写一个makefile文件
现在我们对于这个命令就要有这深入的理解,bash创建子进程,子进程execl(ls)
进程替换,使用dup2,将显示的文件重定向到makfile里。
创建。
int pipefd[2],是一个大小为2的形参数组,他是一个输出型参数,也就是说在这个函数内会改变这个数组,我们在外面会拿到他。
pipe这个系统调用创建匿名管道,但为什么要打开两次文件,分配两个文件描述符。而且一个以读方式打开文件,一个以写方式打开文件。
上面讲到父进程必须以读写方式打开文件,pipe这个函数调用封装了这种操作方式,而返回的数组里,pipefd[0]表示读端,pipefd[1]表示写端。
下面代码表示,子进程写,父进程读,而且子进程关闭了读,父进程关闭了写,保证管道的单向数据传输。
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include<unistd.h>
6 #include<string.h>
7 int main()
8 {
9 int pipefd[2]={
0};
10 pipe(pipefd);
11 pid_t id= fork();
12 if(id==0)
13 {
14 const char* str="i am child ,child writing";
15 close(pipefd[0]);
16 while(1)
17 {
18 //子进程写
19 write(pipefd[1],str,strlen(str));
20 sleep(1);
21 }
22 }
23 else if(id>0){
24 close(pipefd[1]);
25 char buf[64];
26 while(1)
27 {
28 //第三个参数为你期望读多少,返回值为实际读了多少字节
29 ssize_t s=read(pipefd[0],buf,sizeof(buf)-1);
30 if(s>0)
31 {
32 buf[s]=0;
33 }
34 printf("father get message:%s\n",buf);
35
36 }
37 }
38 return 0;
39 }
这里有四个特征,需要进一步深入挖掘。
让子进程(写端)sleep5s,父进程(读端)不变。那么就会发现子进程写一次,停顿的时候,父进程也在停,也就是说父进程以子进程节奏为主,子进程不动,父进程也就在阻塞式等待,就是将自己pcb由R状态设置为S状态,从运行队列挪到等待队列,那么假如写段不关闭文件描述符,而且一直不往里面写数据,那么读端就可能会一直堵塞(等待)。
让父进程(读端)sleep5s,子进程(写端),就会发现写端一次性写满了管道,读端读一次,可能过一会写端才会往管道里在写数据。
假如当子进程(写端)写了5次,关闭掉写段文件描述符,最后read会返回0,即读到文件结尾
假如当子进程(写端)一次写满,父进程(读端)读3次关掉文件描述符。写端进程(子进程)会被操作系统通过信号直接杀掉,又由于我们代码中父进程死循环,没有回收,所以子进程僵尸了。
管道也是一种文件,他也有自己的inode,在struct file中存在path,path中有一个dentry,里面存着目录的inode,inode里面有和block的映射,就找到了文件名和inodeid,在找到文件的inode。
当我们敲下这个命令的时候就可以理解他的原理
who | wc - l
首先bash命令行,调用pipe()创建匿名管道,在创建两个子进程,execl进程替换为who和wc-l。两个进程都拷贝了bash的文件描述符,bash关闭两个文件描述符,who为写端,wc-l为读端,关掉who的读端文件描述符,关掉wc-l的写端文件描述符。who本来要往显示器上打印,结果要写到管道文件里。dup2(pipefd[1],1。严谨一点关掉pipefd[1]。wc-l原本读的是键盘,结果要读管道文件,所以dup2(pipefd[0],0)。然后关掉pipefd[0]。
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 int main()
5 {
6 mkfifo("./fifo",0666);
7 return 0;
8 }
p为管道文件
匿名管道由pipe系统调用创建,我们并不关心他叫什么名字,只用他的文件内存缓冲区。常用于有亲缘关系的进程,子进程继承了文件信息。但两个没有关系的进程怎么通信呢?
mkfifo是一个库函数,作用为创建一个命名管道文件。
下面来写一个客户端与服务端,客户端发送,服务端接收并打印出来。
首先服务端创建管道,只读的方式open,返回fd,然后调用read方法读取到一个数组buf,最后打印出来
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 int main()
7 {
8 if(-1==mkfifo("./fifo",0644))
9 {
10 perror("err");
11 }
12 int fd=open("./fifo",O_RDONLY);
13 if(fd>=0)
14 {
15 char buf[64];
16 while(1)
17 {
18 ssize_t s=read(fd,buf,sizeof(buf)-1);
19 if(s>0)
20 {
21 buf[s]=0;
22 printf("client#: %s",buf);
23 }
24 else if(s==0)
25 {
26 //当实际读到字节为0时
printf("client quit \n");
break;
27 }
28 else{
29 perror("read err");
30 break;
31 }
32 return 0;
33 }
34 }
35
客户端负责写,读端已经创建管道,所以写端不必在创建。把什么写进管道呢,我们重定向,从键盘输入进管道。这样服务端就可以接受到了。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 #include<fcntl.h>
6 int main()
7 {
8 int fd=open("./fifo",O_WRONLY);
9 if(fd>=0)
10 {
11 char buf[64];
12 while(1)
13 {
14 //从键盘输入的字符到buf里
15
16 printf("please write memsage# \n");
17
18 ssize_t s=read(0,buf,sizeof(buf)-1);
19 if(s>0)
20 {
21 buf[s]=0;
22 //往fd里写buf,想发s个字节
23 write(fd,buf,s);
24 }
25 }
26 }
27 return 0;
28 }
命名管道通过文件名+文件描述符来实现通信。
管道文件始终大小为0,这就是最开始提到的,操作系统直接创建文件,加载到内存中,但是只需要用到它的文件内存缓冲区,所以不需要把信息写入到硬盘中的fifo中。
而对于管道的4条规则,命名管道也同样适用。
匿名管道,pipe(int pipefd[2])系统调用,在内存中创建了一个文件,形参输出型参数,会返回两个文件描述符,用于操作,fork之后,子进程会继承这两个文件描述符,一个进程有两个文件描述符,不要那个就关闭那个,它在内存中创建了一个匿名管道文件,亲缘进程继承了文件信息可以看见他,从而使用它的文件内存缓冲区。
命名管道通过mkfifo库函数在硬盘上创建文件,加载进内存,任意进程通过文件名和文件描述符来看见他,也是只使用了它的文件内存缓冲区,实际上信息并未刷新至硬盘。
管道的4个规则。
system V标准共享内存
进程间通信本质是让两个进程看到同一块资源,对于管道我们借助了pipe文件的文件内存缓冲区,那么共享内存就是绕过文件,直接在物理内存开辟一段空间,通过某种映射,让两个进程与这段空间关联起来,这样一个进程修改,另一个进程就能直接看见他,因为这段空间是两者共享的。
写端写到管道的文件内存缓冲区,读端从管道的文件内存缓冲区读。用户到内核,内核到用户。共享内存只需要一次写入。所以它几乎是最快的IPC方式。
系统中存在这大量进程无时无刻不在通信,所以我们对进程间通信也要先描述在组织,通信的本质在于看到同一块资源,而管道,共享内存都是资源,只是取决于操作系统通过文件系统还是内存管理来分配资源。
所以我们需要创建共享内存,关联共享内存,取消关联,删除共享内存。
用户标识信息的结构体
struct kern_ipc_perm {
spinlock_t lock;
bool deleted;
int id;
key_t key;
kuid_t uid;
kgid_t gid;
kuid_t cuid;
kgid_t cgid;
umode_t mode;
unsigned long seq;
void *security;
struct rhash_head khtnode;
struct rcu_head rcu;
refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;
其中key值代表这块共享内存的唯一值,操作系统把它分配给新创建的共享内存。
先认识一个库函数
他用来创建一个唯一的key值,之后我们就可以把它分配给创建共享内存的函数,填到共享内存的ipc_perm里,来让共享内存有个唯一的标识。
key_t key = ftok(PATHNAME,PROJ_ID);
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
形参IPC_CREAT|IPC_EXECL,代表共享内存不存在则创建,存在则出错返回。
第一次创建成功,第二次失败,说明共享内存的生命周期和管道不一样,是随内核的。
这段代码是创建了共享内存,5s之后使用系统调用删掉。而实际上我们要知道操作系统分配资源,使用一个struct shmid_ds的结构体来描述他。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 sleep(5);
18 shmctl(shmid,IPC_RMID,NULL);
19 return 0;
20 }
while :; do ipcs -m; sleep 1;echo "####";done
所以可以写一个完整的共享内存的生命周期,先创建(同时操作系统分配资源,struct shmid_ds结构体描述),5s后此进程与内存相关联,nattch变成1,5s后又变成0,5s后被删除
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 char* str= (char*)shmat(shmid,NULL,0);
18 sleep(5);
19 shmdt(str);
20 sleep(5);
21 shmctl(shmid,IPC_RMID,NULL);
22 return 0;
23 }
|权限
这里显示0和管道一样写一个服务器端,客户端是最好的验证。
服务器端,创建共享内存,直接可以看到数据
srever.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 char* str= (char*)shmat(shmid,NULL,0);
18 while(1)
19 {
20
21 //str为首元素地址,直接可以用它打印字符串,就是打印共享区里的内容
22 printf("%s\n",str);
23 sleep(1);
24 }
25 shmdt(str);
26 shmctl(shmid,IPC_RMID,NULL);
27 return 0;
28 }
client.c
客户端,ftok函数的路径与服务器端保持一致,这样才能拿到key,key是给操作系统用的。shmget不需要任何权限了,因为服务器端已经创建了共享内存,通过key值他们看到了同一块资源。返回值shmid供用户进行操作(挂接,删除等),注意!!客户端不能在删除共享内存因为在服务器端已经删除过了
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 //形参一样得到的key值一样
10 key_t key = ftok(PATHNAME,PROJ_ID);
11 //去掉权限,因为在server端已经创建共享内存。
12 int shmid=shmget(key,SIZE,0);
13 sleep(5);
14 if(shmid<0)
15 {
16 perror("err");
17 return 1;
18 }
19 //挂接,拿到地址
20 char* str= (char*)shmat(shmid,NULL,0);
21 char s='a';
22 for(;s<='z';s++)
23 {
24 str[s-'a']=s;
25 //5s写一次
26 sleep(5);
27 }
28 shmdt(str);
29 //shmctl(shmid,IPC_RMID,NULL);
30 return 0;
31 }
在客户端,5s往共享内存写一次,但是服务器端在客户端不写的那4s,还是把东西重复打印出来了,而在管道中假如写端不写,读端是会阻塞等待,直到你在写。得出结论,共享内存不提供任何同步与互斥机制。
消息队列和管道基本上都是4次拷贝,而共享内存(mmap, shmget)只有两次。
4次:1,由用户空间的buf中将数据拷贝到内核中。2,内核将数据拷贝到内存中。3,内存到内核。4,内核到用户空间的buf.
2次: 1,用户空间到内存。 2,内存到用户空间。
消息队列和管道都是内核对象,所执行的操作也都是系统调用,而这些数据最终是要存储在内存中执行的。因此不可避免的要经过4次数据的拷贝。但是共享内存不同,当执行mmap或者shmget时,会在内存中开辟空间,然后再将这块空间映射到用户进程的虚拟地址空间中,即返回值为一个指向一个内存地址的指针。当用户使用这个指针时,例如赋值操作,会引起一个从虚拟地址到物理地址的转化,会将数据直接写入对应的物理内存中,省去了拷贝到内核中的过程。当读取数据时,也是类似的过程,因此总共有两次数据拷贝。