我们知道,进程之间是具有独立性的,两进程互不影响,但是如果所有的进程都完全独立,那也会有诸多问题,会造成进程与进程间相互协作的难点,所以我们引入了进程间通信
进程间通信的核心是将两进程指向同一资源
进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信发展
管道System V 进程间通信POSIX 进程间通信
管道
什么是管道
管道是 Unix 中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个 “ 管道”符号:|
我们的管道只支持单向通信,一个进程读,一个进程写,事实上,我们的管道实际上是一个文件,而我们进程间通信就是借助这个文件完成的
管道的本质
匿名管道
#include <unistd.h>功能 : 创建一无名管道原型int pipe(int fd[2]);参数fd :文件描述符数组 , 其中 fd[0] 表示读端 , fd[1] 表示写端返回值 : 成功返回 0 ,失败返回错误代码
匿名管道,顾名思义,这个管道是没有名字的,常使用在具有亲缘关系的进程之间,比如父子进程等
在具有亲缘关系的进程中,他们会继承祖先的部分内容,其中files_struct是继承祖先进程的,这可以使两进程指向同一文件,这里我们也就可以理解之前的stdin,stdout,stderr,这三个文件为什么是子进程默认打开,原因就是他们继承了祖先的open,只要祖先打开了,子进程就会拷贝这部分内容
匿名管道原理
用fork来共享管道原理
站在文件描述符角度-深度理解管道
因为我们的管道也是文件,我们就可以站在文件的角度去理解原理
其实本质上就是,父进程以读写的方式打开一个管道文件,而后进行fork()创建子进程,子进程就会以父进程为模板进行拷贝此时file_struct里的数组(表示文件描述符与文件映射关系)会是父进程的拷贝,此时父子就指向了同一个管道,此时父子进程都对这个管道具有读写功能,如果此时将父进程的读,子进程的写,关闭,那么就形成了一个单向通信的管道,完成了对文件的通信
站在内核角度-管道本质
本质就是,父子进程指向同一个文件,该文件在内存中占用空间,于是两进程就指向了同一块空间,此时我们就出现了一个问题需要澄清
文件加载带内存需要开辟空间,一个进程如何找到文件的内存进行读写的?
task_struct有一个files_struct指针,可以找到files_struct,files_struct里有一个数组,数组下标对应文件描述符,可以找到对应文件struct_file。这样就找到文件了。
struct_file中有一个struct_path,可以找到对应目录,目录中保存文件名和inode的对应关系,就可以找到文件的inode。文件inode中有一个struct address_space,进入里面有struct radix_tree_root page_tree,就可以找到对应内存空间。
再调用struct_file里的const struct file_operations *f_op指针,调用对应的读写函数。就实现了进程间的通信
创建管道
我们创建管道的方式为系统调用
//头文件
#include <unistd.h>
//pipe系统调用是以读写的方式打开一个管道文件
int pipe(int pipefd[2]);
/*返回值:打开成功0,失败返回异常信号
参数:pipefd[2]是两个输出型参数,保存的是文件描述符
fd[0]读打开的管道文件描述符,fd[1]写打开管道的文件描述符*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
int fd[2]={0};
//以读写方式打开管道
int res=pipe(fd);
if(res==0){
printf("fd[0]:%d,fd[1]:%d\n",fd[0],fd[1]);
}
else{
perror("pipe error");
exit(1);
}
return 0;
}
管道读写规则
当没有数据可读时O_NONBLOCK disable : read 调用阻塞,即进程暂停执行,一直等到有数据来到为止。O_NONBLOCK enable : read 调用返回 -1 , errno 值为 EAGAIN 。当管道满的时候O_NONBLOCK disable : write 调用阻塞,直到有进程读走数据O_NONBLOCK enable :调用返回 -1 , errno 值为 EAGAIN如果所有管道写端对应的文件描述符被关闭,则 read 返回 0如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE, 进而可能导致 write 进程退出当要写入的数据量不大于 PIPE_BUF 时, linux 将保证写入的原子性。当要写入的数据量大于 PIPE_BUF 时, linux 将不再保证写入的原子性。
其实管道通信通俗来讲就是当管道中无数据,读端关闭,进入阻塞队列,当管道中满数据,写端关闭,进入阻塞队列,当管道中数据非空非满时则正常开启读写端读写
而在我们实际读写操作时,通常读写速度是不一样的,当读进程速度快时,管道大部分时间都为空,读进程会进入阻塞,等待有数据,当写进程快时,则管道大部分时间都是满的,写端进入阻塞,这样其实我们整体进程间通信的效率其实变低了,一个慢导致整体都慢
进程间同步:一个进程快导致另一个进程也快,一个慢导致另一个也慢,一个进程受控制导致另一个进程也受控制,这就叫做进程同步
补充:读进程也并不是一行一行读的,而是读到缓冲区中,当缓冲区满了再进行刷新输出;当读进程关闭读文件描述符时,系统为了优化效率,可能会将写进程直接杀死,传入13号信号SIGPIPE杀死进程,进程异常退出
所以我们关闭文件时,应该先关闭写进程的写文件描述符,再关闭读文件描述符
管道大小64k
-
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
-
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
管道特点
![](https://img-blog.csdnimg.cn/b97c1a97113849689ddfc524d6d33083.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bmzICDnlJ8=,size_20,color_FFFFFF,t_70,g_se,x_16)
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork ,此后父、子进程之间就可应用该管道。管道提供流式服务一般而言,进程退出,管道释放,所以管道的生命周期随进程一般而言,内核会对管道操作进行同步与互斥管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
命名管道
我们上面介绍过匿名管道,匿名管道是针对具有亲缘关系的两组进程而服务的,而这里的命名管道是服务于不具有亲缘性的两个进程间的通信
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用 FIFO 文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件
创建一个命名管道
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
命名管道创建原理其实与匿名管道是类似的,一个进程先创建一个命名管道,再以读或者写的方式来打开管道文件,另一个进程直接以读或写的方式打开管道文件,再调用读写系统调用来往文件中读或写进行通信
匿名管道与命名管道的区别
匿名管道由 pipe 函数创建并打开。命名管道由 mkfififo 函数创建,打开用 openFIFO (命名管道)与 pipe (匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
如果当前打开操作是为读而打开 FIFO 时O_NONBLOCK disable :阻塞直到有相应进程为写而打开该 FIFOO_NONBLOCK enable :立刻返回成功如果当前打开操作是为写而打开 FIFO 时O_NONBLOCK disable :阻塞直到有相应进程为读而打开该 FIFOO_NONBLOCK enable :立刻返回失败,错误码为 ENXIO
命名管道文件的本质
实际上我们的命名管道fifo只是一个标志,大小恒为0,系统会在内存开辟一段空间给管道,往管道中写入就是往这块内存写,从管道里读就是从这块内存中读
如果真的在硬盘中创建了一个文件,再往硬盘文件读或写,那么就是文件I/O,效率很低
其实我们的进程间通信,就是通过两个线程访问同一份资源,通过一个进程读,一个进程写来实现进程间通信
管道实现通信是单向,同步,互斥的
system V共享内存
共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存示意图
我们还有一种进程间通信方式,共享内存,共享内存实际上是操作系统在物理内存中开辟的一块共享内存,具体操作是两个进程分别在自己的页表中将一块物理内存与自己进程地址空间中共享区的一块地址形成映射关系
那么这两个进程就同时在自己的共享区中指向了一块内存,一个进程写值另一个进程也就可以接收到了,这便实现了进程间通信
注意:共享内存实现进程间通信是进程间最快的
共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
因为我们的共享内存是借各个进程地址空间中共享区而指向同一段物理内存的,所以我们的共享内存不仅限于两个进程之间,可以是多个进程之间,那么既然这样,众多进程需要通信,就一定需要将他们管理起来,所以有了我们的共享内存数据结构
共享内存函数
ftop函数
我们的ftop函数得作用就是标识资源的唯一性,会返回一个key值,来标记这块资源,所以我们其他的进程才可以看到
功能:算出唯一key值返回
参数:第一个是地址,第二个是项目id(至少8位)
ftop中的参数可以随便写,不过要符合格式,其只是利用任意两个参数,进行一套算法来计算key值而已,这个key值可以传给共享内存参数,作为struct ipc_perm中唯一标识共享内存的key
shmget函数
功能:用来创建共享内存原型int shmget(key_t key, size_t size, int shmflg);参数key: 这个共享内存段名字size: 共享内存大小shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的 mode 模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回 -1
这里有一个点值得注意,我们之前说过共享内存的生命周期是跟随内核的,而并不是跟随进程的,所以共享内存在被创建了之后,若没有被释放,就会一直占用,shmget创建的共享内存在使用完毕后一定要释放,不然会内存泄漏
可以用命令行来释放共享内存:ipcrm -m shmid(shmget返回值)
shmat函数
功能:将共享内存段连接到进程地址空间原型void *shmat(int shmid, const void *shmaddr, int shmflg);参数shmid: 共享内存标识shmaddr: 指定连接的地址shmflg: 它的两个可能取值是 SHM_RND 和 SHM_RDONLY返回值:成功返回一个指针,指向共享内存第一个节;失败返回 -1shmaddr 为 NULL ,核心自动选择一个地址shmaddr 不为 NULL 且 shmflg 无 SHM_RND 标记,则以 shmaddr 为连接地址。shmaddr 不为 NULL 且 shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式: shmaddr -(shmaddr % SHMLBA)shmflg=SHM_RDONLY ,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离原型int shmdt(const void *shmaddr);参数shmaddr: 由 shmat 所返回的指针返回值:成功返回 0 ;失败返回 -1注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:用于控制共享内存原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数shmid: 由 shmget 返回的共享内存标识码cmd: 将要采取的动作(有三个可取值)buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构返回值:成功返回 0 ;失败返回 -1
实现进程间通信
server.c
1 #include<stdio.h>
2 #include"head.h"
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/ipc.h>
6 #include<sys/shm.h>
7 int main()
8 {
9 key_t k=ftok(PATHNAME,PROJ_ID);
10 printf("key=%d\n",k);
11 int shmid=shmget(k,SIZE,IPC_CREAT|IPC_EXCL|0666);//创建共享内存
12
13 if(shmid==-1)
14 {
15 printf("创建共享内存失败!\n");
16 return 1;
17 }
18 printf("shmid=%d\n",shmid);
19 sleep(10);
20
21 char* str=(char*)shmat(shmid,NULL,0);//关联共享内存
22 // sleep(5);
23 while(1)
24 {
25 sleep(1);
26 printf("%s\n",str);
27 }
28
29 shmdt(str);//去除关联的共享内存
30 // sleep(5);
31
32 shmctl(shmid,IPC_RMID,NULL);//删除共享内存
33 sleep(5);
34 return 0;
35 }
client.c
1 #include<stdio.h>
2 #include"head.h"
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/ipc.h>
6 #include<sys/shm.h>
7 int main()
8 {
9 key_t k=ftok(PATHNAME,PROJ_ID);
10 printf("key=%p\n",k);
11 int shmid=shmget(k,SIZE,0);//创建共享内存
12
13 if(shmid==-1)
14 {
15 printf("创建共享内存失败!\n");
16 return 1;
17 }
18 printf("shmid=%d\n",shmid);
19 // sleep(10);
20
21 char* str=(char*)shmat(shmid,NULL,0);//关联共享内存
22 // sleep(5);
23
24
25 char c='a';
26 for(c;c<='z';c++)
27 {
28 str[c-'a']=c;
29 sleep(3);
30 }
31 shmdt(str);//去除关联的共享内存
32 sleep(5);
33
34 // shmctl(shmid,IPC_RMID,NULL);//删除共享内存
35 // sleep(10);
36 return 0;
37 }
head.h
1 #pragma once
2 #define PATHNAME "/tmp"
3 #define PROJ_ID 0x6688
4
5 #define SIZE 4096
我们发现了一个问题,这里的写端比较慢,按理来说读端应该会进入阻塞状态,但实际上并没有,而是一直在读,这里其实我们就可以得出共享内存与管道之间的差别,共享内存在通信底层不提供互斥与同步机制,如果想让他们配合起来,需要IPC信号量来支持
补充:共享内存最好是以4096字节的整数倍开辟,size是以页为单位的,但是如果我们用了比4096多一点的内存,系统会显示给你自己使用量的内存,但其实在底部给你的还是4096的倍数也就是2*4096
system V消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值特性方面IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核
system V信号量
进程互斥由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。在进程中涉及到互斥资源的程序段叫临界区特性方面IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核