进程间通信 (IPC) 方法总结(三)
信号量(SEMAPHORE)
信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
为了获得共享资源,进程需要执行下列操作:
- 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
- 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
- 挂出一个信号量:该操作将信号量的值加1,也称为V操作。
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。
Posix有名信号量
Posix基于内存的信号量
System V信号量
信号量与普通整型变量的区别
- 信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
操作也被成为PV原语(P来源于荷兰语proberen"测试",V来源于荷兰语verhogen"增加",P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;
信号量与互斥量之间的区别
互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
套接字(SOCKET)
套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
套接字特性
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。
套接字的域
它指定套接字通信中使用的网络介质,最常见的套接字域有两种:
- AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
AF_UNIX,表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。
套接字的端口号
每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。
套接字协议类型
- 流套接字
流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。 - 数据报套接字
它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。 原始套接字
原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。
原始套接字与标准套接字的区别
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
套接字通信的建立
- 服务端
- 首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。(socket)
- 服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。(bind)
- 系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。(listen)
- 服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。(accept--read/write)
- 客户端
- 客户应用程序首先调用socket来创建一个未命名的套接字。(socket)
- 将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。(connect)
- 一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)(read/write)
eg.
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //socket listen bind
#include <sys/socket.h>//socket listen bind
#include <unistd.h>//unlink
#include <sys/un.h>//struct sockaddr_unint main()
{ /* delete the socket file */ unlink("server_socket"); /* create a socket */ int server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un server_addr; server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path, "server_socket"); /* bind with the local file */ bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); /* listen */ listen(server_sockfd, 5); char ch; int client_sockfd; struct sockaddr_un client_addr; socklen_t len = sizeof(client_addr); while(1) { printf("server waiting:\n"); /* accept a connection */ client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len); /* exchange data */ read(client_sockfd, &ch, 1); printf("get char from client: %c\n", ch); ++ch; write(client_sockfd, &ch, 1); /* close the socket */ close(client_sockfd); } return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //socket listen bind
#include <sys/socket.h>//socket listen bind
#include <unistd.h>//unlink
#include <sys/un.h>//struct sockaddr_un int main()
{ /* create a socket */ int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un address; address.sun_family = AF_UNIX; strcpy(address.sun_path, "server_socket"); /* connect to the server */ int result = connect(sockfd, (struct sockaddr *)&address, sizeof(address)); if(result == -1) { perror("connect failed: "); exit(1); } /* exchange data */ char ch = 'A'; write(sockfd, &ch, 1); read(sockfd, &ch, 1); printf("get char from server: %c\n", ch); /* close the socket */ close(sockfd); return 0;
}
如果我们首先运行tcp_client,会提示没有这个文件:
因为我们是以AF_UNIX方式进行通信的,这种方式是通过文件来将服务器和客户端连接起来的,因此我们应该先运行tcp_server,创建这个文件,默认情况下,这个文件会创建在当前目录下,并且第一个s表示它是一个socket文件:
程序运行的结果如下图:
参考文章:
进程间通信IPC (InterProcess Communication)
进程间通信--管道
UNIX/Linux进程间通信IPC系列(四)消息队列
Linux进程间通信(四) - 共享内存
本地socket通讯