linux下進程間通訊的幾種主要手段:node
本文講述進程間通訊方法——共享內存linux
原文:http://www.ibm.com/developerworks/cn/linux/l-ipc/part6/編程
一個套接口能夠看做是進程間通訊的端點(endpoint),每一個套接口的名字都是惟一的(惟一的含義是不言而喻的),其餘進程能夠發現、鏈接而且與之通訊。通訊域用來講明套接口通訊的協議,不一樣的通訊域有不一樣的通訊協議以及套接口的地址結構等等,所以,建立一個套接口時,要指明它的通訊域。比較常見的是unix域套接口(採用套接口機制實現單機內的進程間通訊)及網際通訊域。服務器
linux目前的網絡內核代碼主要基於伯克利的BSD的unix實現,整個結構採用的是一種面向對象的分層機制。層與層之間有嚴格的接口定義。這裏咱們引用[1]中的一個圖表來描述linux支持的一些通訊協議:數據結構
咱們這裏只關心IPS,即因特網協議族,也就是一般所說的TCP/IP網絡。咱們這裏假設讀者具備網絡方面的一些背景知識,如瞭解網絡的分層結構,一般所說的7層結構;瞭解IP地址以及路由的一些基本知識。dom
目前linux網絡API是基於BSD套接口的(系統V提供基於流I/O子系統的用戶接口,可是linux內核目前不支持流I/O子系統)。套接口能夠說是網絡編程中一個很是重要的概念,linux以文件的形式實現套接口,與套接口相應的文件屬於sockfs特殊文件系統,建立一個套接口就是在sockfs中建立一個特殊文件,並創建起爲實現套接口功能的相關數據結構。換句話說,對每個新建立的BSD套接口,linux內核都將在sockfs特殊文件系統中建立一個新的inode。描述套接口的數據結構是socket,將在後面給出。socket
下面是在網絡編程中比較重要的幾個數據結構,讀者能夠在後面介紹編程API部分再回過頭來了解它們。async
套接口是由socket數據結構表明的,形式以下:
struct socket { socket_state state; /* 指明套接口的鏈接狀態,一個套接口的鏈接狀態能夠有如下幾種 套接口是空閒的,尚未進行相應的端口及地址的綁定;尚未鏈接;正在鏈接中;已經鏈接;正在解除鏈接。 */ unsigned long flags; struct proto_ops ops; /* 指明可對套接口進行的各類操做 */ struct inode inode; /* 指向sockfs文件系統中的相應inode */ struct fasync_struct *fasync_list; /* Asynchronous wake up list */ struct file *file; /* 指向sockfs文件系統中的相應文件 */ struct sock sk; /* 任何協議族都有其特定的套接口特性,該域就指向特定協議族的套接口對 象。 */ wait_queue_head_t wait; short type; unsigned char passcred; }; |
(2)描述套接口通用地址的數據結構struct sockaddr
因爲歷史的緣故,在bind、connect等系統調用中,特定於協議的套接口地址結構指針都要強制轉換成該通用的套接口地址結構指針。結構形式以下:
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ }; |
(3)描述因特網地址結構的數據結構struct sockaddr_in(這裏侷限於IP4):
struct sockaddr_in { __SOCKADDR_COMMON (sin_); /* 描述協議族 */ in_port_t sin_port; /* 端口號 */ struct in_addr sin_addr; /* 因特網地址 */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; |
通常來講,讀者最關心的是前三個域,即通訊協議、端口號及地址。
int socket( int domain, int type, int ptotocol); |
參數domain指明通訊域,如PF_UNIX(unix域),PF_INET(IPv4),PF_INET6(IPv6)等;type指明通訊類型,如SOCK_STREAM(面向鏈接方式)、SOCK_DGRAM(非面向鏈接方式)等。通常來講,參數protocol可設置爲0,除非用在原始套接口上(原始套接口有一些特殊功能,後面還將介紹)。
注:socket()系統調用爲套接口在sockfs文件系統中分配一個新的文件和dentry對象,並經過文件描述符把它們與調用進程聯繫起來。進程能夠像訪問一個已經打開的文件同樣訪問套接口在sockfs中的對應文件。但進程毫不能調用open()來訪問該文件(sockfs文件系統沒有可視安裝點,其中的文件永遠不會出如今系統目錄樹上),當套接口被關閉時,內核會自動刪除sockfs中的inodes。
根據傳輸層協議(TCP、UDP)的不一樣,客戶機及服務器的處理方式也有很大不一樣。可是,無論通訊雙方使用何種傳輸協議,都須要一種標識本身的機制。
通訊雙方通常由兩個方面標識:地址和端口號(一般,一個IP地址和一個端口號經常被稱爲一個套接口)。根據地址能夠尋址到主機,根據端口號則能夠尋址到主機提供特定服務的進程,實際上,一個特定的端口號表明了一個提供特定服務的進程。
對於使用TCP傳輸協議通訊方式來講,通訊雙方須要給本身綁定一個惟一標識本身的套接口,以便創建鏈接;對於使用UDP傳輸協議,只須要服務器綁定一個標識本身的套接口就能夠了,用戶則不須要綁定(在須要時,如調用connect時[注1],內核會自動分配一個本地地址和本地端口號)。綁定操做由系統調用bind()完成:
int bind( int sockfd, const struct sockaddr * my_addr, socklen_t my_addr_len) |
第二個參數對於Ipv4來講,實際上須要填充的結構是struct sockaddr_in,前面已經介紹了該結構。這裏只想強調該結構的第一個域,它代表該套接口使用的通訊協議,如AF_INET。聯繫socket系統調用的第一個參數,讀者可能會想到PF_INET與AF_INET究竟有什麼不一樣?實際上,原來的想法是每一個通訊域(如PF_INET)可能對應多個協議(如AF_INET),而事實上支持多個協議的通訊域一直沒有實現。所以,在linux內核中,AF_***與PF_***被定義爲同一個常數,所以,在編程時能夠不加區分地使用他們。
注1:在採用非面向鏈接通訊方式時,也會用到connect()調用,不過與在面向鏈接中的connect()調用有本質的區別:在非面向鏈接通訊中,connect調用只是先設置一下對方的地址,內核爲本地套接口記下對方的地址,而後採用send()來發送數據,這樣避免每次發送時都要提供相同的目的地址。其中的connect()調用不涉及握手過程;而在面向鏈接的通訊方式中,connect()要完成一個嚴格的握手過程。
對於採用面向鏈接的傳輸協議TCP實現通訊來講,一個比較重要的步驟就是通訊雙方創建鏈接(若是採用udp傳輸協議則不須要),由系統調用connect()完成:
int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen) |
第一個參數爲本地調用socket後返回的描述符,第二個參數爲服務器的地址結構指針。connect()向指定的套接口請求創建鏈接。
注:與connect()相對應,在服務器端,經過系統調用listen(),指定服務器端的套接口爲監聽套接口,監聽每個向服務器套接口發出的鏈接請求,並經過握手機制創建鏈接。內核爲listen()維護兩個隊列:已完成鏈接隊列和未完成鏈接隊列。
服務器端經過監聽套接口,爲全部鏈接請求創建了兩個隊列:已完成鏈接隊列和未完成鏈接隊列(每一個監聽套接口都對應這樣兩個隊列,固然,通常服務器只有一個監聽套接口)。經過accept()調用,服務器將在監聽套接口的已鏈接隊列頭中,返回用於表明當前鏈接的套接口描述字。
int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen) |
第一個參數指明哪一個監聽套接口,通常是由listen()系統調用指定的(因爲每一個監聽套接口都對應已鏈接和未鏈接兩個隊列,所以它的內部機制實質是經過sockfd指定在哪一個已鏈接隊列頭中返回一個用於當前客戶的鏈接,若是相應的已鏈接隊列爲空,accept進入睡眠)。第二個參數指明客戶的地址結構,若是對客戶的身份不感興趣,可指定其爲空。
注:對於採用TCP傳輸協議進行通訊的服務器和客戶機來講,必定要通過客戶請求創建鏈接,服務器接受鏈接請求這一過程;而對採用UDP傳輸協議的通訊雙方則不須要這一步驟。
客戶機能夠經過套接口接收服務器傳過來的數據,也能夠經過套接口向服務器發送數據。前面全部的準備工做(建立套接口、綁定等操做)都是爲這一步驟準備的。
經常使用的從套接口中接收數據的調用有:recv、recvfrom、recvmsg等,經常使用的向套接口中發送數據的調用有send、sendto、sendmsg等。
int recv(int s, void * buf, size_t len, int flags) int recvfrom(int s, void * buf, size_t len, int flags, struct sockaddr * from, socklen_t * fromlen) int recvmsg(int s, struct msghdr * msg, int flags) int send(int s,const void * msg, size_t len, int flags) int sendto(int s, const void * msg, size_t len, int flags const struct sockaddr * to, socklen_t tolen) int sendmsg(int s, const struct msghdr * msg, int flags) |
這裏再也不對這些調用做具體的說明,只想強調一下,recvfrom()以及recvmsg()可用於面向鏈接的套接口,也可用於面向非鏈接的套接口;而recv()通常用於面向鏈接的套接口。另外,在調用了connect()以後,就應給調用send()而不是sendto()了,由於調用了connect以後,目標就已經肯定了。
前面講到,socket()系統調用返回套接口描述字,實際上它是一個文件描述符。因此,能夠對套接口進行一般的讀寫操做,即便用read()及write()方法。在實際應用中,因爲面向鏈接的通訊(採用TCP傳輸協議)是可靠的,同時又保證字節流原有的順序,因此更適合用read及write方法。而非面向鏈接的通訊(採用UDP傳輸協議)是不可靠的,字節流也不必定保持原有的順序,因此通常不宜用read及write方法。
由close()來完成此項功能,它惟一的參數是套接口描述字,再也不贅述。
處處能夠發現基於套接口的客戶機及服務器程序,這裏再也不給出完整的範例代碼,只是給出它們的典型調用代碼,並給出簡要說明。
... ... int listen_fd, connect_fd; struct sockaddr_in serv_addr, client_addr; ... ... listen_fd = socket ( PF_INET, SOCK_STREAM, 0 ); /* 建立網際Ipv4域的(由PF_INET指定)面向鏈接的(由SOCK_STREAM指定, 若是建立非面向鏈接的套接口則指定爲SOCK_DGRAM) 的套接口。第三個參數0表示由內核肯定缺省的傳輸協議, 對於本例,因爲建立的是可靠的面向鏈接的基於流的套接口, 內核將選擇TCP做爲本套接口的傳輸協議) */ bzero( &serv_addr, sizeof(serv_addr) ); serv_addr.sin_family = AF_INET ; /* 指明通訊協議族 */ serv_addr.sin_port = htons( 49152 ) ; /* 分配端口號 */ inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ; /* 分配地址,把點分十進制IPv4地址轉化爲32位二進制Ipv4地址。 */ bind( listen_fd, (struct sockaddr*) serv_addr, sizeof ( struct sockaddr_in )) ; /* 實現綁定操做 */ listen( listen_fd, max_num) ; /* 套接口進入偵聽狀態,max_num規定了內核爲此套接口排隊的最大鏈接個數 */ for( ; ; ) { ... ... connect_fd = accept( listen_fd, (struct sockaddr*)client_addr, &len ) ; /* 得到鏈接fd. */ ... ... /* 發送和接收數據 */ } |
注:端口號的分配是有一些慣例的,不一樣的端口號對應不一樣的服務或進程。好比通常都把端口號21分配給FTP服務器的TCP/IP實現。端口號通常分爲3段,0-1023(受限的衆所周知的端口,由分配數值的權威機構IANA管理),1024-49151(能夠從IANA那裏申請註冊的端口),49152-65535(臨時端口,這就是爲何代碼中的端口號爲49152)。
對於多字節整數在內存中有兩種存儲方式:一種是低字節在前,高字節在後,這樣的存儲順序被稱爲低端字節序(little-endian);高字節在前,低字節在後的存儲順序則被稱爲高端字節序(big-endian)。網絡協議在處理多字節整數時,採用的是高端字節序,而不一樣的主機可能採用不一樣的字節序。所以在編程時必定要考慮主機字節序與網絡字節序間的相互轉換。這就是程序中使用htons函數的緣由,它返回網絡字節序的整數。
... ... int socket_fd; struct sockaddr_in serv_addr ; ... ... socket_fd = socket ( PF_INET, SOCK_STREAM, 0 ); bzero( &serv_addr, sizeof(serv_addr) ); serv_addr.sin_family = AF_INET ; /* 指明通訊協議族 */ serv_addr.sin_port = htons( 49152 ) ; /* 分配端口號 */ inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ; /* 分配地址,把點分十進制IPv4地址轉化爲32位二進制Ipv4地址。 */ connect( socket_fd, (struct sockaddr*)serv_addr, sizeof( serv_addr ) ) ; /* 向服務器發起鏈接請求 */ ... ... /* 發送和接收數據 */ ... ... |
對比兩段代碼能夠看出,許多調用是服務器或客戶機所特有的。另外,對於非面向鏈接的傳輸協議,代碼還有簡單些,沒有鏈接的發起請求和接收請求部分。
下面列出了網絡編程中的其餘重要概念,基本上都是給出這些概念可以實現的功能,讀者在編程過程當中若是須要這些功能,可查閱相關概念。
I/O複用提供一種能力,這種能力使得當一個I/O條件知足時,進程可以及時獲得這個信息。I/O複用通常應用在進程須要處理多個描述字的場合。它的一個優點在於,進程不是阻塞在真正的I/O調用上,而是阻塞在select()調用上,select()能夠同時處理多個描述字,若是它所處理的全部描述字的I/O都沒有處於準備好的狀態,那麼將阻塞;若是有一個或多個描述字I/O處於準備好狀態,則select()不阻塞,同時會根據準備好的特定描述字採起相應的I/O操做。
前面主要介紹的是PF_INET通訊域,實現網際間的進程間通訊。基於Unix通訊域(調用socket時指定通訊域爲PF_LOCAL便可)的套接口能夠實現單機之間的進程間通訊。採用Unix通訊域套接口有幾個好處:Unix通訊域套接口一般是TCP套接口速度的兩倍;另外一個好處是,經過Unix通訊域套接口能夠實如今進程間傳遞描述字。全部可用描述字描述的對象,如文件、管道、有名管道及套接口等,在咱們以某種方式獲得該對象的描述字後,均可以經過基於Unix域的套接口來實現對描述字的傳遞。接收進程收到的描述字值不必定與發送進程傳遞的值一致(描述字是特定於進程的),可是特們指向內核文件表中相同的項。
原始套接口提供通常套接口所不提供的功能:
建立原始套接口須要root權限。
對數據鏈路層的訪問,使得用戶能夠偵聽本地電纜上的全部分組,而不須要使用任何特殊的硬件設備,在linux下讀取數據鏈路層分組須要建立SOCK_PACKET類型的套接口,並須要有root權限。
若是有一些重要信息要馬上經過套接口發送(不通過排隊),請查閱與帶外數據相關的文獻。
linux內核支持多播,可是在默認狀態下,多數linux系統都關閉了對多播的支持。所以,爲了實現多播,可能須要從新配置並編譯內核。具體請參考[4]及[2]。
結論:linux套接口編程的內容能夠說是極大豐富,同時它涉及到許多的網絡背景知識,有興趣的讀者可在[2]中找到比較系統而全面的介紹。
至此,本專題系列(linux環境進程間通訊)所有結束了。實際上,進程間通訊的通常意義一般指的是消息隊列、信號燈和共享內存,能夠是posix的,也能夠是SYS v的。本系列同時介紹了管道、有名管道、信號以及套接口等,是更爲通常意義上的進程間通訊機制。
參考資料