进程间通信可以分为两种类型,一种是通过操作系统本身提供的通信机制,另一种是使用socket进行网络通信。
1. 操作系统内部通信
1.1. 多线程
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。 然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
当pid==0时,表示的进入生成的子进程的代码段中,否则,进程处于父进程的代码段中。当fork返回值小于0,则表示创建进程失败。
1.2. 管道
创建无名管道
pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe. For further details, see pipe(7).
创建有名管道
mkfifo
mkfifo ( name, mode)
Create named pipes (FIFOs) with the given NAMEs.
1.3. 信号量
一般用来同步进程顺序。
signal(signum, sigaction), 将信号量与相应的动作绑定,一旦该信号量产生后,相应的动作将会被触发执行。
kill( pid, signum), 向进程pid发送信号量signum。
采用信号量进行进程间同步时,出现不确定的情况(如书中的例子),目前未知是什么原因。
1.4. 共享内存
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
ftok(pathname, id),利用id和pathname产生一个IPC key,即共享内存的标识
shmget(key, size, mode),利用key产生唯一一块大小为size的共享内存区,访问模式为mode,返回内存标识
shmat(shmid, NULL, 0),将进程私有内存映射到共享内存中,当第二个参数为NULL时,系统将自动找一个未用的内存区域。
shmdt(const void *shmaddr) 解除映射关系
1.5. 消息队列
消息队列实现包括穿件、添加、读取和控制消息队列这四种操作。
其中msgget(),创建和打开消息队列;msgsnd()是将消息添加到消息队列的末尾; msgrcv()是从消息队列末尾获取消息,也支持取走某条指定的消息;
1.6. 总结
学习IPC的要点就是熟知各个API接口的含义及其参数的意义,灵活运用。
编程流程可总结为:
1) 创建连接部件(如共享内存,消息队列,信号量,管道)
2) 向连接部件中写
3) 从连接部件中读
4) 依次往复
其中,信号量机制主要用于同步进程执行,很少作为进程间通信的方式。
2. 网络通信
要想理解socket首先得熟悉一下TCP/IP协议族, TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准,从字面意思来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于ISO模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中
![96934ffa821baa60465a2efab714c252.png](https://i-blog.csdnimg.cn/blog_migrate/d6b3ce967667c07f0bbdf0337067dc14.jpeg)
Figure 1 Internet的通信协议层次划分
在Internet层,解析IP地址,寻找通往目标IP的目的地的下一个路由地址。在网络接口层,则是寻找响应的硬件(MAC)地址。数据流以及网络拓扑结构如下图所示。
![a89b0b69cfd8e35c76a04a89172f7be0.png](https://i-blog.csdnimg.cn/blog_migrate/d6625974abeeec262592d4d76391d659.jpeg)
Figure 2 网络拓扑以及数据流
我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
![9df5937fbeb9c23c3c176be5af6cae4d.png](https://i-blog.csdnimg.cn/blog_migrate/c8cc5ca5979c3068a970080c720052a9.jpeg)
Figure 3 Socket通信与网络协议之间的层次关系
socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的
![dae89f286c13f500bb648d5a6c8cd9c4.png](https://i-blog.csdnimg.cn/blog_migrate/a4fd64d0af704b4bf01ef89f7e4086df.jpeg)
Figure 4 socket 客户端和服务端建立连接过程
l 服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
l 服务器为socket绑定ip地址和端口号
l 服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开
l 客户端创建socket
l 客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket
l 服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
l 客户端连接成功,向服务器发送连接状态信息
l 服务器accept方法返回,连接成功
l 客户端向socket写入信息
l 服务器读取信息
l 客户端关闭
l 服务器端关闭
2.1. TCP
TCP/IP协议中的三次握手,TCP协议通过三次握手建立一个可靠的连接。
![eea827814270377d1ff9c68d7a3053e6.png](https://i-blog.csdnimg.cn/blog_migrate/915e8cc5eba89622f5a2c24d7444c189.jpeg)
Figure 5 TCP通过三次握手建立安全连接
第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认
第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态
第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手
定睛一看,服务器socket与客户端socket建立连接的部分其实就是大名鼎鼎的三次握手
![6c259af40894ee98f0ec1a7c3ddaecc1.png](https://i-blog.csdnimg.cn/blog_migrate/0dab6e324a340c4aab7f703a9d288f87.jpeg)
Figure 6 TCP在SOCKET中的三次握手过程
2.2. UDP
UDP和TCP编程步骤有些不同,如下:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
与之对应的UDP编程步骤要简单许多,分别如下:
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接;
其中udp通信过程中不需要建立连接,也不要进行三次握手的过程。UDP客户端直接向服务体端发送数据,服务端循环读取相应地址的数据信息。
recvfrom和sendto的参数中包含了通信的地址信息,在传输数据的同时,能够找寻通信目标地址。
2.3. Select
select函数的作用:
select()在SOCKET编程中还是比较重要的,可是对于初学SOCKET的人来说都不太爱用select()写程序,他们只是习惯写诸如 conncet()、accept()、recv()或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用select()就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况。如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
select函数格式:
select()函数的格式(所说的是Unix系统下的Berkeley Socket编程,和Windows下的有区别,一会儿说明):
Unix系统下解释:
int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
先说明两个结构体:
第一:struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以,毫无疑问,一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合:FD_ZERO(fd_set*),将一个给定的文件描述符加入集合之中FD_SET(int, fd_set*),将一个给定的文件描述符从集合中删除FD_CLR(int, fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int, fd_set*)。一会儿举例说明。
第二:struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个毫秒数。
具体解释select的参数:
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数值无所谓,可以设置不正确。
fd_set* readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set* writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
fe_set* errorfds同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态。
第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
select函数返回值:
负值:select错误
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件