進程間通信 (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通訊