进程间通信
- 什么是进程?
进程就是运行着的程序 - 进程之间通信的方式
进程之间的通信机制包括:管道,信号量,共享内存,消息队列,套接字网络编程
(1)管道
管道分为有名管道和无名管道,保存在管道中的数据均在内存中,而不是磁盘上,断电之后,管道文件数据将丢失,管道文件可以自动做到同步,当写端进程关闭,则读端进程也会结束(读端的读函数返回值为0,则结束该进程)。当读端彻底关闭,则写端会产生一个信号SIGPIPE
,使写端也结束,而并未按我们预想的那样来结束,所以,我们可以调用signal()
函数,来改变该信号的响应方式,就可以解决该问题。
对于无名管道来说,在程序中直接调用一个系统调用,这个系统调用会保存一个读写的文件描述符,但读和写端的文件描述符不能混用,那么利用无名管道实现进程间通信的方法就是fork()
一个子进程,让子进程与父进程进行通信。这个系统调用就是pipe
函数,该函数的参数为一个长度为2的数组,用来存放文件描述符,0下标代表为读端文件描述符,1代表为写端文件描述符,当父子进程中的写端彻底都关闭,则管道文件才关闭。
有名管道和无名管道的异同点:
不同:有名管道可以在任意两个进程之间进行通信,而无名管道只能实现父进程与子进程之间的进程通信
相同:
- 数据均保存在内存中,打开管道文件之后,在内存中分配一块空间,读写数据就是对内存进行操作。
- 管道均为半双工(可以接收或发送,但在某一时刻就只能从一端到另一端)。
创建管道文件的命令:$ mkfifo name
代码示例:
loopa.c
#include<stdio.h>
#include<stdlib.h>
#include<unitsd.h>
#include<string.h>
#include<assert.h>
#include<fcntl.h>
int main()
{
int fd=open(“./fifo”,O_WRONLY);
assert(fd!=-1);
printf(“fd=%d”,fd);
while(1)
{
char buff[128]={0};
printf(“input:\n”;)
fgets(buff,128,stdin);
if(strncmp(buff,”end”,3)==0)
break;
write(fd,buff,strlen(buff));
}
close(fd);
return 0;
}
loopb.c:
int main()
{
int fd=open(“./fifo”,O_RDONLY);
assert(fd!=-1);
printf(“fd=%d\n”,fd);
char buff[128]={0};
int n=0;
while((n=read(fd,buff,127))>0)
{
printf(“read:%s\n”,buff);
memset(buff,0,128);
}
close(fd);
return 0;
}
(2)信号量
临界资源是同一时刻只允许一个进程或线程或有限个进程或线程访问的资源。临界区是指访问临界资源的代码段。原子操作是不可被分割的,不允许被打断的操作。信号量是一个比较特殊的变量,它只能取正整数值,并且加1和减1操作为原子操作,如果信号量为0,再进行减一操作时,会阻塞。用来控制对临界资源的访问,使得同一时刻只有一个进程或线程来访问资源,以实现同步进程,依靠的方法是控制程序的推进或执行速度。
信号量初始值代表可访问资源数,0代表当前可用资源数目为0.
方法:
semget():全新创建一个信号量,或者获得一个已有的信号量命令:
semop():对信号量进行修改,执行加一和减一操作
semctl():对信号量做控制,为信号量赋初值
ipcrm -s uid:移除已创建的信号量
ipcs:查看内存和共享信号量
(3) 共享内存
将一块内存空间作为共享内存,使其两个进程都可以进行访问修改,两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
方法:
shmget():创建或获取共享内存进程同步:
shmat():将共享内存映射到当前进程的虚拟内存空间中
shmdt():断开映射
shmctl():移除共享内存空间
进程同步也即生产者消费者模式,实现进程同步的方案就是定义两个临界资源信号量,s1=0;s2=1;若进程1是生产者,那么在进程1中执行p(s2),也就是减1,请求操作,此时进程1就可以执行,而进程2则阻塞等待释放资源,在进程1完成一系列操作后执行v(s1)操作,此时s2=0,s1=1,此时进程2进行p(s1)操作,拿到资源,而此时进程1因为s2资源仍未被释放而阻塞,在进程2完成一系列操作之后,再执行v(s2)操作,此时s1=0,s2=1,于是进程1又拿到了资源,又可以继续“生产”,而进程2则扮演着“消费者”的角色。
(4) socket套接字网络编程
当我们进行socket套接字编程时,首先需要设置静态ip地址以及子网掩码,使自己与通信的对方处于同一网段之内。
1.关防火墙的命令
- iptables -F
- setenforce 0
2. 设置ip地址
- vi /etc/sysconfig/network-scripts/ifcfg-eth0
之后将该文件的BOOTPROTO改为static,ONBOOT改为yes,再添加IPADDR=192.168.31.44,添加NETMASK=255.255.255.0
-也可以使用如下命令进入图形化界面修改静态ip以及子网掩码
system-config-network
3. 重启网络服务
- service network restart
4. 查看ip地址
- ifconfig
5. 查看网络是否可以连通
- ping ip地址
6. 查看TCP底层如何进行三次握手,确认-应答机制,以及四次挥手机制
- 法1:tcpdump ‘port 端口号’ -i lo -S
- 法2:tcpdump -i eth0 -nt ’ (src 本地即客户端ip地址 and dst 目的ip地址)’
7. socket套接字编程
- listen(sockfd,5)
这个函数是将sockfd转为被动监听套接字,创建监听队列
第二个参数的意思是内核为此套接口排队的最大连接数:首先有一个未完成三次握手队列以及一个已完成三次握手队列,当连接完成时,该信息就被放入到了已完成三次握手队列中,当连接未完成时,该条连接的信息就被放入未完成三次握手队列中,等到连接完成时,再将该队列中的该条连接信息放入到已完成三次握手队列中。现在一般第二个参数是指已完成三次握手的队列的最大长度,但早期是未完成三次握手队列以及已完成三次握手队列的长度之和。
- connect(…)
这个函数是用来发起连接,进行三次握手的。
TCP三次握手过程:
客户端给服务器端发送一个包含同步报文标志SYN以及一个序列号i的TCP报文段,服务器收到后会给客户端发送一个包含SYN同步报文标志以及一个序列号j,并发送一个确认报文ACK,确认序列号为i+1的tcp报文段,当客户端收到来自服务器端的确认信息之后,会发送给服务器端一个包含ACK确认报文,序列号为j+1的tcp报文段,在这些过程完成之后,连接就建立了,此时connect()函数才返回。(从发送数据到接收到对方发来的确认报文的时间间隔称为RTT)
- accept(…)
这个函数是用来处理已建立的连接。
确认-应答机制:
当客户端发送数据给服务器端时,tcp机制会将客户数据以及序列号,还有对方要确认的序列号以及本次确认的ACK报文序列号发送给服务器,服务器在收到该数据后,底层需要给客户端一个确认,发送以个序列号(为上一条中的”对方要确认的序列号”),以及一个确认ACK报文号,当用户层服务器端需要给客户端发送相应回复的数据时,底层就会将服务器端的数据发送给客户端,并且包含数据,序列号,以及对方要确认的序列号以及本次确认ACK报文号,当客户端收到数据后会再次给服务器端确认数据已经收到的一条确认信息,会给服务器发送一个ACK确认报文号。这样一个简单的从客户端应用层发送一条数据,服务器收到该数据后从用户层回复数据给客户端应用层的过程就完成了。这个过程称为”确认-应答”机制。
- close(sockfd)
此函数是用来触发四次挥手,关闭连接的。
四次挥手机制:
当客户端执行该函数时,客户端会给服务器发送一个包含通知连接关闭的FIN结束报文段标志,一个报文序列号i(这个i的值是在三次握手建立连接的第一条的序列号加1)以及一个确认关闭的标志ack,并且该ack的序列号为在三次握手建立连接时中的第三条中的客户端给服务器端发送的确认序列号的值。(关闭连接第一条);当服务器端收到通知之后,服务器会给客户端发送一个包含确认报文标志ack,以及确认序列号为i+1的tcp报文段,状态变为close wait(关闭连接第二条);之后服务器关闭连接,给客户端发送一个包含一个通知关闭的报文标志FIN,一个序列号j(j的值为关闭连接第一条中的确认序列号的值),以及一个确认关闭标志ack,并且确认序列号的值为关闭连接第二条中的确认序列号的tcp报文段(关闭连接第三条);当客户端收到该通知之后,发送给服务器端一个确认收到关闭通知的报文ACK,序列号为j+1(关闭连接第四条)。
当服务器端要确认关闭时刚好服务器端也执行了close()函数时,那么第二条与第三条合并,变为“三次挥手”。
状态转换:
1).观察TCP状态(哪个监听套接字在监听):netstat -natcp
2).状态分析
当建立连接,执行connnect函数时:
没发起连接时为listen状态,当调用connect函数完成连接之后,已完成三次握手队列中的连接状态就变成ESTABLISHED状态,在未连接队列里的套接字状态为SYN_RECV.
当客户端执行close函数时:
发送第一条:服务器状态变为CLOSE_WAIT,客户端状态变为FIN_WAIT_1;
发送第二条:当服务器发送了确认关闭时,客户端状态变为FIN_WAIT_2;
发送第三条:服务器状态变为LAST_ACK,客户端状态变为TIME_WAIT;
发送第四条:服务器状态变为CLOSE.
TIME_WAIT的意义:
TIME_WAIT状态系统大约会持续两分钟,其意义在于:
a) 可靠地终止TCP的连接。
b) 让迟来的报文能够有足够时间被识别,并且被丢弃。
可靠地终止TCP的连接意思是说当最后确认服务器关闭连接的最后一个tcp报文段丢失,那么服务器会重新发送结束报文,所以客户端需要停留在某个阶段来处理这些重复收到的结束报文段,即向服务器端发送确认结束报文段。
让迟来的报文能够有足够的时间被识别并丢弃是说如果没有TIME_WAIT状态,那么该端口会立即被其他连接所使用,重新建立一个与本连接具有相同的ip地址以及端口号的新连接,本连接一些迟来的数据报文便可能会被该新连接所接收,但这是不正确的,所以这就是对于迟到的报文为什么要被丢弃的原因。
8. 套接字编程中的问题以及解决方案
问题描述:当我们在服务器端循环接收消息,但每次只recv一个字符时,这时客户端就可能出现多个重复回复的信息。
客户端有发送缓冲区和接收缓冲区,服务器端同样也有发送缓冲区以及接收缓冲区,客户端的发送缓冲区中的数据会被发送到服务器端的接收缓冲区中,而服务器端给客户端回复的数据将会从服务器端的发送缓冲区发送到客户端的接收缓冲区中,由此也可以看出客户端与服务器端的通信机制是全双工的,在每次发送数据时,流式服务的机制是将发送缓冲区中的所有数据一次性全部送至服务器端的接收缓冲区中,由于它的处理时间并不由用户来控制,也就是说什么时候会发送这些发送缓冲区中的数据我们无从知晓,所以当用户进行多次输入数据后,并不一定按照用户所期望的那样按用户输入的次数来将数据分开存放或发送,因为可能上次用户输入的数据还没被发送到服务器端的接收缓冲区中,而是仍然存在于客户端的发送缓冲区中,所以多次输入在底层看来客户端的发送缓冲区并不区分到底是一次还是多次,它只会将所有数据全部发送到服务器端的接收缓冲区中,即使按用户所希望的那样依据用户每次输入的数据来分批发送至服务器的接收缓冲区中,当服务器端在接收时也不一定按照用户希望的那样将数据分批存放并显示出来,这种现象称为” 粘包现象“。
解决的方案是我们将每一条数据都添加报头和报尾,这样就会避免这样的情况,而tcp机制就是这样的。由此,以tcp为代表的面向连接的可靠的协议特点如下:
- 面向连接:三次握手过程
- 可靠的:应答确认,超时重传
- 流式服务
- 流量控制,滑动窗口协议
关于tcp协议的超时重传以及拥塞控制(滑动窗口,慢启动,拥塞避免,快速重传以及快速恢复)在另一篇中会专门整理。