文章目录
-
目录
文章目录
一、进程间有哪几种的通信方式
1.为什么需要进程间通信?
2.进程通信的几种方式
3.进程间通信的目的
二、管道
1.匿名管道
2.命名管道
3.管道总结
三、消息队列
四、共享内存
1.共享内存的原理
2.共享内存的接口
1.创建共享内存
2.将共享内存附加到进程
3.将共享内存和进程分离
4.操作共享内存
5.删除共享内存
3.共享内存总结
五、信号量(难点)
六、信号
七、socket通信
1. UDP-socket编程
2.tcp-socket编程
八、总结
前言
我们要了解 进程间是如何通信的,那么就需要了解进程是什么。
进程:其实进程就是运行起来的程序,程序运行起来需要被加载到内存中。进程和可执行文件很像(文件名.exe)->这就是可执行文件.但是他们又有所不同.可执行文件就像是静态的,躺在我们的硬盘中,但是,我们在任务管理器中可以明显的看到我们的进程是动态的,是在内存中不断被加载的.
我们可以清楚的看到,进程他是一个运行的程序,是在内存中不断被加载的。那么这些进程是如何通信的?
一、进程间有哪几种的通信方式
1.为什么需要进程间通信?
每一个进程都拥有自己的独立的进程虚拟地址空间,造成了进程独立性,从而进程间通信技术就是为了各个进程之间可以很好的交换数据或者进程控制等应运而生的
·目前,我们所见到的最大的进程间通信技术是 : 网络
2.进程通信的几种方式
进程间的通信有 管道 消息队列 共享内存 信号 套接字。
我们常见的面经都会说到进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、套接字socket。
-
管道:包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。
-
系统IPC
消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。
信号:用于通知接收进程某个事件的发生。
内存共享:使多个进程访问同一块内存空间。
-
套接字socket:用于不同主机直接的通信。
3.进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
但是我们该如何理解。
二、管道
1.匿名管道
$ ps auxf | grep mysql
了解linux的朋友肯定熟悉 【|】这个符号 其实竖线就是一个管道,管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行,这种管道是没有名字,所以【|】表示的管道称为匿名管道,用完了就销毁。
匿名管道的创建:
int pipe(int fd[2])
这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0]
,另一个是管道的写入端描述符 fd[1]
。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。
我们可以看到,管道就是一端写入数据,另一端读取。 所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取。
看到这,你可能会有疑问,两个进程都在一个进程里,怎么实现通信的。
我们可以使用 fork
创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0]
与 fd[1]
」
这样就实现了不同进程之间的通信,但是问题又来了,因为管道是只能一端写一段读,如果两个进程同时写的话,那岂不是会混乱。
所以我们一般会这样:
- 父进程关闭读取的 fd[0],只保留写入的 fd[1];
- 子进程关闭写入的 fd[1],只保留读取的 fd[0];
通过以上我们知道:匿名管道是一个半双工的通信方式,就是一个发 另一个读。并且只能在具有亲缘关系的进程之间通信。很不方便。于是我们有了命名管道
2.命名管道
命名管道:需要通过 mkfifo
命令来创建并指定好名字。相当提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
$ mkfifo Pipe
Pipe就是这个管道的名字。
接下来,我们往 Pipe 这个管道写入数据:
$ echo "hello" > Pipe // 将数据写进管道
// 停住了 ...
你操作了后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。
于是,我们执行另外一个命令来读取这个管道里的数据:
$ cat < Pipe // 读取管道里的数据
hello
可以看到,管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo 那个命令也正常退出了。
3.管道总结
我们发现,不管是匿名管道还是命名管道,都是半双工的通信方式,并且只能一方写,另一方读。通信效率非常低下,所以我们又引出了消息队列。
三、消息队列
我们说到,管道的通信效率低下,不适合进程间频繁的交流,于是消息队列很好的解决了这个问题。
消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。当一个进程需要通信的时候,只需要将数据写入这个消息列表当中,就可以正常退出干其他事情了,另一个进程需要数据的时候只需去读取数据就行了。
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
1.队列的特性是先进先出,消息队列也是满足先进先出的特性的,内核当中实现消息队列的时候,是采用链表这个结构体
2.消息队列当中的元素是有类型的,每一种类型是有优先级概念的
同一类型保证先进先出的特性;
int msgget(key_t key, int msgflg):
int msgsnd(int msqid, const void *msgp, size t msgsz, int msgflg);
//msgflg :
// IPC NOWAIT : 非阻塞模式
// 0: 阻塞模式
ssize_t msgrcv(int msqid, void *msgp, size t msgsz, long msgtyp,int msgflg);
// long msgtype : 数据类型
// ==0:取队列当中的第一个
// > 0:取和msgtypc相等的元素
// < 0: 先取绝对值,然后在范围内去最小的优先级的元素
int msgctl(int msqid, int cmd, struct msqid ds *buf);
结论:消息队列的生命周期也是跟随内核.
消息队列的常用函数如下表:
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。声明周期跟随内核,如果不去释放或者关闭操作系统,它会一直存在。
综上说所,消息队列很好的解决了管道不能频繁交流的问题,但是也存在了不足,就是交流不及时,还有大小受限制。并且在通信的过程中,会存在大量频繁的用户态和内核态的不断转换,进程写入消息时,会发生用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
为了更好的解决这一问题,于是我们就有了 共享内存.........
四、共享内存
1.共享内存的原理
1. 首先在物理内存当中创建了一块内存
1. 不同的进程通过页表映射, 将同一块物理内存映射到自己的虚拟地址空间
1.3不同的进程, 操作进程虚拟地址, 通过页表的映射, 就相当于操作同一块内存,从而完成了数据交换
2.共享内存的接口
1.创建共享内存
int shmget(key_t key, size t size, int shmflg);
/*
key :共享内存标识符, 这个标识符相当于共享内存的身份证 程序员在第一次创建的时候, 可以随意给 值, 只要和当前操作系统当中的其他的共享内存标识符不重复
// eg : 0x99999999 0x88888888 0x12345678
// size : 共享内存的大小,单位字节
// shmrlg :
// IPC CREAT : 如果共享内存不存在, 则创建共享内存
// IPC_EXCL: 需要搭配IPC CREAT一起使用, 这样的宏在搭配使用的时候, 还是采用按位或的方式
//其实核心的思想就是位图
// eg :IPC CREAT IPC EXCL:
// 如果想要获取的共享内存,已经存在, 则报错如果想要过去的共享内存, 是刚刚新创建出来的
//共享内存,则返回舞作句柄
总结:使用shmget这个函数的时候一定更自己刚刚创建出来的共享内存
返回值:返回值是返回共享内存的操作句柄
共享内存的标识符和共享内存的操作句柄区别是什么?
标识符:是用来标识共享内存的, 相当于共享内存的身份证,意味者不同的进程可以通过标识符找到这个共享内存
操作句柄:进程可以通过操作句柄来对共享内存进行操作(附加, 分离, 删除)
*/
2.将共享内存附加到进程
void *shmat (int shmid, const void *shmaddr, int shmflg);
/*
shmid : 共享内存操作句柄
shmaddr : 附加到共享内存的什么虚拟地址, 允许传递NULL值, 让操作系统帮我们选择附加到共享区当中的那个地址, 这个地址通过该函数的返回值返回给我们
shmflgt:
SHM RDONLY 规定当前进程只能对共享内存进行读操作
0 : 可读可写
返回值: 返回共享内存附加到共享区的地址
*/
3.将共享内存和进程分离
int shmdt (const void *shmaddr) ;
//shmaddr : shmat的返回值
4.操作共享内存
int shmctl (int shmid, int emd, struct shmid ds *buf) ;
/*
shmid : 共享操作句柄
cmd : 告诉shmct1函数需要做什么操作
IPC STAT :获取当前共享内存的属性信息,放在buf当中, buf是出参
IPC SET :设置共享内存的属性信息, 是用buf来进行设置的, bur是入参
IPC RMID :删除共享内存, buf可以直接传递为NULL
buf : 共享内存的结构体
*/
5.删除共享内存
1. 当使用shmct1或者使用ipcrm,删除共享内存之后, 共享内存就实际被释
放掉了2. 当共享内存被群放掉之后, 共享内存的标识符会被设置成为0x00000000.表示其他进程不能通过之前的标识符找到该共享内存, 并且共享内存的状态会被设置成为dest (destroy)
3. 当共享内存被群放掉之后了, 但是还是有进程在附加着共享内尺,当前描述共享内存的结构体并没有被释放, 直到当前共享内存的附加进程数量为0的时候才会被释放掉
3.共享内存总结
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。但是也引入了一个问题,就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。为了解决这一问题我们又引入了信号量的概念。
五、信号量(难点)
为了防止多个进程同时访问公共资源,造成数据混乱,必须想一个保护机制,使得共享的资源,在任意时刻只能被一个进程访问。于是便提出了信号量。
信号量(semaphore)它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
控制信号量的方式有两种原子操作:
一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
进程互斥访问资源 初始化信号量为 1
假如有A B两个进程 信号量初始化为 1, 现在A 要访问共享内存 进行了p操作,信号量变为 0,说明还有可用资源,A就可以顺利访问共享资源了。
结果这会B进程又要来访问资源,进行了p操作,信号量变成了-1,说明资源已经有进程占用了,那么B进程就会阻塞等待。
A进程这会访问完了,出来了,进行了V操作,信号量变成了 0 ,看见了B在哪等待,于是唤醒了B,说你可以进去了,于是B就可以访问了,访问完之后,进行V操作,信号量又变回了1。
可以发现,信号初始化为 1
,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。
进程同步访问资源 初始化信号量为 0
我们都知道,进程是抢占式占用资源,但是我们有时想让多个进程相互合作,实现同一个任务,比如先让A进程生产数据,B进程才能读取数据。但是我们不知道到底那个进程先抢占了资源。假如A还没有生产数据呢,但是B进程又要读取,我们该如何做?
于是我们边有了进程同步:
我们可以初始化信号量为
0:
如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
可以发现,信号初始化为 0
,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。
总结:信号量不是用来通信的,信号量是和共享内存结合,来限制多进程同时访问共享资源的。防止冲突的一种保护机制
六、信号
我们上述说到的都是正常情况下的进程通信,那么如果进程出现异常了呢,这个时候我们就需要用信号通知的方式,实现通信。
使用 kill -l 命令可以罗列信号
总共定义了62个信号。
非实时信号: 非可靠信号
特点: 信号可能会丢失
1~31信号
实时信号:可靠信号
特点: 信号不会丢失
33~64
说到这里,我们发现上面的所有进程间通信方式都只是局限在本机,那我我们自然而然的就引出了网络Socket 通信,实现不同电脑中的进程通信。
七、socket通信
这里如果网络不太了解的话可以看看这篇博客https://blog.csdn.net/qq_45615577/article/details/117260949?spm=1001.2014.3001.5501
1. UDP-socket编程
编程流程:
服务端:创建套接字,绑定地址信息
客户端:创建套接字,不推荐绑定地址信息 (可以绑定 )
创建套接字的含义:
将进程和网卡进行绑定,进程可以从网卡当中接收数据,也可以通过网卡发送数据。
绑定地址信息的含义:
绑定ip,绑定端口,是为了在网络当中可以标识出来一台主机和一个进程。
对于接收方而言:发送数据的人就知道接收方在那台机器那个进程了对于发送方而言:能标识网络数据从那台机器那个进程发送出去的
2.tcp-socket编程
服务端: 创建套接字,绑定地址信息,监听,获取新连接,收发数据,关闭连接。
客户端: 创建套接字, 不推荐绑定地址信息 (可以绑定),发起连接,收发数据,关闭连接。
监听的含义:监听tcp客户端新的链接, 同客户端建立tcp连接。 注意: 这个时候,TCP连接的建立在内核当中就完成了。
获取新连接的含义:获取新连接的套接字描述符, 每一个TCP连接会产生一个套接字描述符。
目前,我们所见到的最大的进程间通信技术是 : 网络
八、总结
如果面试官问你说说,进程间是如何通信的,你该做一下回答。
首先 进程间的通信有 管道 消息队列 共享内存 信号 套接字。
管道分为 匿名管道和命名管道。
匿名管道 :他是一个半双工的通信方式,就是一个发 另一个读。并且只能在具有亲缘关系的进程之间通信。很不方便,于是便有了命名管道: 同理命名管道也是一个半双工的通信方式,一个发 另一个读。但是可以实现不同进程之间的通信了。
这又产生了问题,就是这个通信不迅速,效率低下又成了问题,于是又产生出了消息队列。
消息队列:消息队列是保存在内核中的消息链表,比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。这样就是像写信一样,一来一封,我回一封,而且消息队列在内核当中,我们去读取的时候,还会有用户态和内核态之间的转换,效率虽然说有所改变,但是还是不够及时。所以共享内存又出来了。
共享内存:就是两个进程,各拿出一块虚拟内存空间,映射到相同的物理空间。这样一个进程在进行读写操作的时候,另外一个进程立马就可以看到。通信的效率大大提高,但是又带来了新的问题。就是如果遇到两个或者多个进程同时给这个空间写东西,就会产生冲突。
那么为了防止多个进程同时共享资源,就提出了信号量,使得在任意时刻资源只能被一个进程访问。信号量说白了就相当于一个计数器,有两个操作:(他不能通信只能配合共享内存)
P操作:相当于每来一个进程要访问的时候,先给信号量减1,减1之后 如果信号量还>>=0 说明这会这个资源还没有占用可以访问,如果减一之后<0,说明有其他进程正占用着资源,需要等待。
V操作:每个进程要走的时候,先给信号量+1 如果说+1了之后,还<<=0,说明前面还有排队的进程,这个时候就会把前面排队的进程唤醒,说我走了,你可以去访问资源了。如果+1>0 说明前面没有排队的进程,也就是没有阻塞的进程,如果后面有进程要访问的话,直接就可以访问。
说了这些都是在一台主机上的进程间的通信,那么也产生出了Socket,也就是套接字。这实现了不同电脑上的进程通信。
最后还有个 信号: 信号就是可以给进程发送一个命令,进程就会做相应的工作。
说到这里就结束了,有问题欢迎大家指正。