Linux處理序間通訊—— 套介面

來源:互聯網
上載者:User

原文自:https://www.ibm.com/developerworks/cn/linux/l-ipc/part6/

 

簡介: 在本專題的前面幾個部分,如訊息佇列、號誌、共用記憶體等,都是基於Sys V的IPC機制進行討論的,它們的應用局限在單一電腦內的處理序間通訊;基於BSD套介面不僅可以實現單機內的處理序間通訊,還可以實現不同電腦進程之間的通訊。本文將主要介紹BSD套介面(sockets),以及基於套介面的重要而基本的API。

本文的標籤:  linux, linux進程, 程式碼程式庫, 套介面, 環境處理序間通訊

 

 

一個套介面可以看作是處理序間通訊的端點(endpoint),每個套介面的名字都是唯一的(唯一的含義是不言而喻的),其他進程可以發現、串連並且與之通訊。通訊域用來說明套介面通訊的協議,不同的通訊域有不同的通訊協定以及套介面的地址結構等等,因此,建立一個套介面時,要指明它的通訊域。比較常見的是unix域套介面(採用套介面機制實現單機內的處理序間通訊)及網際通訊域。

1、背景知識

linux目前的網路核心代碼主要基於伯克利的BSD的unix實現,整個結構採用的是一種物件導向的分層機制。層與層之間有嚴格的介面定義。這裡我們引用[1]中的一個圖表來描述linux支援的一些通訊協定:

 

我們這裡只關心IPS,即網際網路協議族,也就是通常所說的TCP/IP網路。我們這裡假設讀者具有網路方面的一些背景知識,如瞭解網路的分層結構,通常所說的7層結構;瞭解IP地址以及路由的一些基本知識。

目前linux網路API是基於BSD套介面的(系統V提供基於流I/O子系統的使用者介面,但是linux核心目前不支援流I/O子系統)。套介面可以說是網路編程中一個非常重要的概念,linux以檔案的形式實現套介面,與套介面相應的檔案屬於sockfs特殊檔案系統,建立一個套介面就是在sockfs中建立一個特殊檔案,並建立起為實現套介面功能的相關資料結構。換句話說,對每一個新建立的BSD套介面,linux核心都將在sockfs特殊檔案系統中建立一個新的inode。描述套介面的資料結構是socket,將在後面給出。

 

2、重要資料結構

下面是在網路編程中比較重要的幾個資料結構,讀者可以在後面介紹編程API部分再回過頭來瞭解它們。

(1)表示套介面的資料結構struct socket

套介面是由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_tsa_family;/* address family, AF_xxx*/charsa_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)];  };

一般來說,讀者最關心的是前三個域,即通訊協定、連接埠號碼及地址。

 

3、套介面編程的幾個重要步驟:

(1)建立套介面,由系統調用socket實現:

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。

(2)綁定地址

根據傳輸層協議(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()要完成一個嚴格的握手過程。

(3)請求建立串連(由TCP客戶發起)

對於採用連線導向的傳輸協議TCP實現通訊來說,一個比較重要的步驟就是通訊雙方建立串連(如果採用udp傳輸協議則不需要),由系統調用connect()完成:

int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)

第一個參數為本地調用socket後返回的描述符,第二個參數為伺服器的地址結構指標。connect()向指定的套介面請求建立串連。

註:與connect()相對應,在伺服器端,通過系統調用listen(),指定伺服器端的套介面為監聽套介面,監聽每一個向伺服器套介面發出的串連請求,並通過握手機制建立串連。核心為listen()維護兩個隊列:已完成串連隊列和未完成串連隊列。

(4)接受串連請求(由TCP伺服器端發起)

伺服器端通過監聽套介面,為所有串連請求建立了兩個隊列:已完成串連隊列和未完成串連隊列(每個監聽套介面都對應這樣兩個隊列,當然,一般伺服器只有一個監聽套介面)。通過accept()調用,伺服器將在監聽套介面的已串連隊列頭中,返回用於代表當前串連的套介面描述字。

int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen)

第一個參數指明哪個監聽套介面,一般是由listen()系統調用指定的(由於每個監聽套介面都對應已串連和未串連兩個隊列,因此它的內部機制實質是通過sockfd指定在哪個已串連隊列頭中返回一個用於當前客戶的串連,如果相應的已串連隊列為空白,accept進入睡眠)。第二個參數指明客戶的地址結構,如果對客戶的身份不感興趣,可指定其為空白。

註:對於採用TCP傳輸協議進行通訊的伺服器和客戶機來說,一定要經過客戶請求建立串連,伺服器接受串連請求這一過程;而對採用UDP傳輸協議的通訊雙方則不需要這一步驟。

(5)通訊

客戶機可以通過套介面接收伺服器傳過來的資料,也可以通過套介面向伺服器發送資料。前面所有的準備工作(建立套介面、綁定等操作)都是為這一步驟準備的。

常用的從套介面中接收資料的調用有: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方法。

(6)通訊的最後一步是關閉套介面

由close()來完成此項功能,它唯一的參數是套介面描述字,不再贅述。

 

4、典型調用代碼:

到處可以發現基於套介面的客戶機及伺服器程式,這裡不再給出完整的範例代碼,只是給出它們的典型調用代碼,並給出簡要說明。

(1)典型的TCP伺服器代碼:

... ...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函數的原因,它返回網路位元組序的整數。

(2)典型的TCP客戶代碼:

... ...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 ) ) ; /* 向伺服器發起串連請求 */... .../* 發送和接收資料 */... ...

對比兩段代碼可以看出,許多調用是伺服器或客戶機所特有的。另外,對於非連線導向的傳輸協議,代碼還有簡單些,沒有串連的發起請求和接收請求部分。

 

5、網路編程中的其他重要概念

下面列出了網路編程中的其他重要概念,基本上都是給出這些概念能夠實現的功能,讀者在編程過程中如果需要這些功能,可查閱相關概念。

(1)、I/O複用的概念

I/O複用提供一種能力,這種能力使得當一個I/O條件滿足時,進程能夠及時得到這個資訊。I/O複用一般應用在進程需要處理多個描述字的場合。它的一個優勢在於,進程不是阻塞在真正的I/O調用上,而是阻塞在select()調用上,select()可以同時處理多個描述字,如果它所處理的所有描述字的I/O都沒有處於準備好的狀態,那麼將阻塞;如果有一個或多個描述字I/O處於準備好狀態,則select()不阻塞,同時會根據準備好的特定描述字採取相應的I/O操作。

(2)、Unix通訊域

前面主要介紹的是PF_INET通訊域,實現網際間的處理序間通訊。基於Unix通訊域(調用socket時指定通訊域為PF_LOCAL即可)的套介面可以實現單機之間的處理序間通訊。採用Unix通訊域套介面有幾個好處:Unix通訊域套介面通常是TCP套介面速度的兩倍;另一個好處是,通過Unix通訊域套介面可以實現在進程間傳遞描述字。所有可用描述字描述的對象,如檔案、管道、有名管道及套介面等,在我們以某種方式得到該對象的描述字後,都可以通過基於Unix域的套介面來實現對描述字的傳遞。接收進程收到的描述字值不一定與發送進程傳遞的值一致(描述字是特定於進程的),但是特們指向核心檔案表中相同的項。

(3)、原始套介面

原始套介面提供一般套介面所不提供的功能: 

  • 原始套介面可以讀寫一些用於控制的控制協議分組,如ICMPv4等,進而可實現一些特殊功能。
  • 原始套介面可以讀寫特殊的IPv4資料包。核心一般只處理幾個特定協議欄位的資料包,那麼一些需要不同協議欄位的資料包就需要通過原始套介面對其進行讀寫;
  • 通過原始套介面可以構造自己的Ipv4頭部,也是比較有意思的一點。

建立原始套介面需要root許可權。

(4)、對資料連結層的訪問

對資料連結層的訪問,使得使用者可以偵聽本地電纜上的所有分組,而不需要使用任何特殊的硬體裝置,在linux下讀取資料連結層分組需要建立SOCK_PACKET類型的套介面,並需要有root許可權。

(5)、帶外資料(out-of-band data)

如果有一些重要訊息要立刻通過套介面發送(不經過排隊),請查閱與帶外資料相關的文獻。

(6)、多播

linux核心支援多播,但是在預設狀態下,多數linux系統都關閉了對多播的支援。因此,為了實現多播,可能需要重新設定並編譯核心。具體請參考[4]及[2]。

結論:linux套介面編程的內容可以說是極大豐富,同時它涉及到許多的網路背景知識,有興趣的讀者可在[2]中找到比較系統而全面的介紹。

至此,本專題系列(linux環境處理序間通訊)全部結束了。實際上,處理序間通訊的一般意義通常指的是訊息佇列、號誌和共用記憶體,可以是posix的,也可以是SYS v的。本系列同時介紹了管道、有名管道、訊號以及套介面等,是更為一般意義上的處理序間通訊機制。

參考資料

  • Understanding the Linux Kernel, 2nd Edition, By Daniel P. Bovet, Marco Cesati , 對各主題闡述得重點突出,脈絡清晰。網路部分分析集中在TCP/IP協議棧的資料連路層、網路層以及傳輸層。

  • UNIX網路編程第一卷:套介面API和X/Open傳輸介面API,作者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。不僅對套介面網路編程有極好的描述,而且極為詳盡的闡述了相關的網路背景知識。不論是入門還是深入研究,都是不可多得的好資料。
  • Linux核心原始碼情景分析(下),毛德操、胡希明著,浙江大學出版社,給出了unix域套介面部分的核心程式碼分析。
  • GNU/Linux編程指南,入門、應用、精通,第二版,Kurt Wall等著,張輝譯

關於作者

鄭彥興,男,現攻讀國防科大電腦學院網路方向博士學位。您可以通過電子郵件 mlinux@163.com和他聯絡。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.