1. 程式人生 > >用戶空間和內核空間通訊之【Netlink 上】

用戶空間和內核空間通訊之【Netlink 上】

提取 意思 數據報 multi 主動 聯合 興趣 ora 指示

原文地址:用戶空間和內核空間通訊之【Netlink 上】 作者:wjlkoorey258

引言

Alan Cox在內核1.3版本的開發階段最先引入了Netlink,剛開始時Netlink是以字符驅動接口的方式提供內核與用戶空間的雙向數據通信;隨後,在2.1內核開發過程中,Alexey Kuznetsov將Netlink改寫成一個更加靈活、且易於擴展的基於消息通信接口,並將其應用到高級路由子系統的基礎框架裏。自那時起,Netlink就成了Linux內核子系統和用戶態的應用程序通信的主要手段之一。

2001年,ForCES IETF委員會正式對Netlink進行了標準化的工作。Jamal Hadi Salim提議將Netlink定義成一種用於網絡設備的路由引擎組件和其控制管理組件之間通信的協議。不過他的建議最終沒有被采納,取而代之的是我們今天所看到的格局:Netlink被設計成一個新的協議域,domain。

Linux之父托瓦斯曾說過“Linux is evolution, not intelligent design”。什麽意思?就是說,Netlink也同樣遵循了Linux的某些設計理念,即沒有完整的規範文檔,亦沒有設計文檔。只有什麽?你懂得---“Read the f**king source code”。

當然,本文不是分析Netlink在Linux上的實現機制,而是就“什麽是Netlink”以及“如何用好Netlink”的話題和大家做個分享,只有在遇到問題時才需要去閱讀內核源碼弄清個所以然。

什麽是Netlink

關於Netlink的理解,需要把握幾個關鍵點:

1、面向數據報的無連接消息子系統

2、基於通用的BSD Socket架構而實現

關於第一點使我們很容易聯想到UDP協議,能想到這一點就非常棒了。按著UDP協議來理解Netlink不是不無道理,只要你能觸類旁通,做到“活學”,善於總結歸納、聯想,最後實現知識遷移這就是學習的本質。Netlink可以實現內核->用戶以及用戶->內核的雙向、異步的數據通信,同時它還支持兩個用戶進程之間、甚至兩個內核子系統之間的數據通信。本文中,對後兩者我們不予考慮,焦點集中在如何實現用戶<->內核之間的數據通信。

看到第二點腦海中是不是瞬間閃現了下面這張圖片呢?如果是,則說明你確實有慧根;當然,不是也沒關系,慧根可以慢慢長嘛,呵呵。

在後面實戰Netlink套接字編程時我們主要會用到socket(),bind(),sendmsg() recvmsg()等系統調用,當然還有socket提供的輪訓(polling)機制。

Netlink通信類型

Netlink支持兩種類型的通信方式:單播多播

單播:經常用於一個用戶進程和一個內核子系統之間1:1的數據通信。用戶空間發送命令到內核,然後從內核接受命令的返回結果。

多播:經常用於一個內核進程和多個用戶進程之間的1:N的數據通信。內核作為會話的發起者,用戶空間的應用程序是接收者。為了實現這個功能,內核空間的程序會創建一個多播組,然後所有用戶空間的對該內核進程發送的消息感興趣的進程都加入到該組即可接收來自內核發送的消息了。如下: 其中進程A和子系統1之間是單播通信,進程B、C和子系統2是多播通信。上圖還向我們說明了一個信息。從用戶空間傳遞到內核的數據是不需要排隊的,即其操作是同步完成;而從內核空間向用戶空間傳遞數據時需要排隊,是異步的。了解了這一點在開發基於Netlink的應用模塊時可以使我們少走很多彎路。假如,你向內核發送了一個消息需要獲取內核中某些信息,比如路由表,或其他信息,如果路由表過於龐大,那麽內核在通過Netlink向你返回數據時,你可以好生琢磨一下如何接收這些數據的問題,畢竟你已經看到了那個輸出隊列了,不能視而不見啊。

Netlink的消息格式

Netlink消息由兩部分組成:消息頭和有效數據載荷,且整個Netlink消息是4字節對齊,一般按主機字節序進行傳遞。消息頭為固定的16字節,消息體長度可變:

Netlink的消息頭

消息頭定義在<include/linux/netlink.h>文件裏,由結構體nlmsghdr表示:

點擊(此處)折疊或打開

  1. struct nlmsghdr
  2. {
  3. __u32 nlmsg_len; /* Length of message including header */
  4. __u16 nlmsg_type; /* Message content */
  5. __u16 nlmsg_flags; /* Additional flags */
  6. __u32 nlmsg_seq; /* Sequence number */
  7. __u32 nlmsg_pid; /* Sending process PID */
  8. };

消息頭中各成員屬性的解釋及說明:

nlmsg_len:整個消息的長度,按字節計算。包括了Netlink消息頭本身。

nlmsg_type:消息的類型,即是數據還是控制消息。目前(內核版本2.6.21)Netlink僅支持四種類型的控制消息,如下:

NLMSG_NOOP-空消息,什麽也不做;

NLMSG_ERROR-指明該消息中包含一個錯誤;

NLMSG_DONE-如果內核通過Netlink隊列返回了多個消息,那麽隊列的最後一條消息的類型為NLMSG_DONE,其余所有消息的nlmsg_flags屬性都被設置NLM_F_MULTI位有效。

NLMSG_OVERRUN-暫時沒用到。

nlmsg_flags:附加在消息上的額外說明信息,如上面提到的NLM_F_MULTI。摘錄如下:

標記

作用及說明

NLM_F_REQUEST

如果消息中有該標記位,說明這是一個請求消息。所有從用戶空間到內核空間的消息都要設置該位,否則內核將向用戶返回一個EINVAL無效參數的錯誤

NLM_F_MULTI

消息從用戶->內核是同步的立刻完成,而從內核->用戶則需要排隊。如果內核之前收到過來自用戶的消息中有NLM_F_DUMP位為1的消息,那麽內核就會向用戶空間發送一個由多個Netlink消息組成的鏈表。除了最後個消息外,其余每條消息中都設置了該位有效。

NLM_F_ACK

該消息是內核對來自用戶空間的NLM_F_REQUEST消息的響應

NLM_F_ECHO

如果從用戶空間發給內核的消息中該標記為1,則說明用戶的應用進程要求內核將用戶發給它的每條消息通過單播的形式再發送給用戶進程。和我們通常說的“回顯”功能類似。

大家只要知道nlmsg_flags有多種取值就可以,至於每種值的作用和意義,通過谷歌和源代碼一定可以找到答案,這裏就不展開了。上一張2.6.21內核中所有的取值情況:

nlmsg_seq:消息序列號。因為Netlink是面向數據報的,所以存在丟失數據的風險,但是Netlink提供了如何確保消息不丟失的機制,讓程序開發人員根據其實際需求而實現。消息序列號一般和NLM_F_ACK類型的消息聯合使用,如果用戶的應用程序需要保證其發送的每條消息都成功被內核收到的話,那麽它發送消息時需要用戶程序自己設置序號,內核收到該消息後對提取其中的序列號,然後在發送給用戶程序回應消息裏設置同樣的序列號。有點類似於TCP的響應和確認機制。

註意:當內核主動向用戶空間發送廣播消息時,消息中的該字段總是為0。

nlmsg_pid:當用戶空間的進程和內核空間的某個子系統之間通過Netlink建立了數據交換的通道後,Netlink會為每個這樣的通道分配一個唯一的數字標識。其主要作用就是將來自用戶空間的請求消息和響應消息進行關聯。說得直白一點,假如用戶空間存在多個用戶進程,內核空間同樣存在多個進程,Netlink必須提供一種機制用於確保每一對“用戶-內核”空間通信的進程之間的數據交互不會發生紊亂。 即,進程A、B通過Netlink向子系統1獲取信息時,子系統1必須確保回送給進程A的響應數據不會發到進程B那裏。主要適用於用戶空間的進程從內核空間獲取數據的場景。通常情況下,用戶空間的進程在向內核發送消息時一般通過系統調用getpid()將當前進程的進程號賦給該變量,即用戶空間的進程希望得到內核的響應時才會這麽做。從內核主動發送到用戶空間的消息該字段都被設置為0。

Netlink的消息體

Netlink的消息體采用TLV(Type-Length-Value)格式: Netlink每個屬性都由<include/linux/netlink.h>文件裏的struct nlattr{}來表示:

Netlink提供的錯誤指示消息

當用戶空間的應用程序和內核空間的進程之間通過Netlink通信時發生了錯誤,Netlink必須向用戶空間通報這種錯誤。Netlink對錯誤消息進行了單獨封裝,<include/linux/netlink.h>:

點擊(此處)折疊或打開

  1. struct nlmsgerr
  2. {
  3. int error; //標準的錯誤碼,定義在errno.h頭文件中。可以用perror()來解釋
  4. struct nlmsghdr msg; //指明了哪條消息觸發了結構體中error這個錯誤值
  5. };

Netlink編程需要註意的問題

基於Netlink的用戶-內核通信,有兩種情況可能會導致丟包:

1、內存耗盡;

2、用戶空間接收進程的緩沖區溢出。導致緩沖區溢出的主要原因有可能是:用戶空間的進程運行太慢;或者接收隊列太短。

如果Netlink不能將消息正確傳遞到用戶空間的接收進程,那麽用戶空間的接收進程在調用recvmsg()系統調用時就會返回一個內存不足(ENOBUFS)的錯誤,這一點需要註意。換句話說,緩沖區溢出的情況是不會發送在從用戶->內核的sendmsg()系統調用裏,原因前面我們也說過了,請大家自己思考一下。

當然,如果使用的是阻塞型socket通信,也就不存在內存耗盡的隱患了,這又是為什麽呢?趕緊去谷歌一下,查查什麽是阻塞型socket吧。學而不思則罔,思而不學則殆嘛。

Netlink的地址結構體

在TCP博文中我們提到過在Internet編程過程中所用到的地址結構體和標準地址結構體,它們和Netlink地址結構體的關系如下:

struct sockaddr_nl{}的詳細定義和描述如下:

點擊(此處)折疊或打開

  1. struct sockaddr_nl
  2. {
  3. sa_family_t nl_family; /*該字段總是為AF_NETLINK */
  4. unsigned short nl_pad; /* 目前未用到,填充為0*/
  5. __u32 nl_pid; /* process pid */
  6. __u32 nl_groups; /* multicast groups mask */
  7. };

nl_pid:該屬性為發送或接收消息的進程ID,前面我們也說過,Netlink不僅可以實現用戶-內核空間的通信還可使現實用戶空間兩個進程之間,或內核空間兩個進程之間的通信。該屬性為0時一般適用於如下兩種情況:

第一,我們要發送的目的地是內核,即從用戶空間發往內核空間時,我們構造的Netlink地址結構體中nl_pid通常情況下都置為0。這裏有一點需要跟大家交代一下,在Netlink規範裏,PID全稱是Port-ID(32bits),其主要作用是用於唯一的標識一個基於netlink的socket通道。通常情況下nl_pid都設置為當前進程的進程號。然而,對於一個進程的多個線程同時使用netlink socket的情況,nl_pid的設置一般采用如下這個樣子來實現:

點擊(此處)折疊或打開

  1. pthread_self() << 16 | getpid();

第二,從內核發出的多播報文到用戶空間時,如果用戶空間的進程處在該多播組中,那麽其地址結構體中nl_pid也設置為0,同時還要結合下面介紹到的另一個屬性。

nl_groups:如果用戶空間的進程希望加入某個多播組,則必須執行bind()系統調用。該字段指明了調用者希望加入的多播組號的掩碼(註意不是組號,後面我們會詳細講解這個字段)。如果該字段為0則表示調用者不希望加入任何多播組。對於每個隸屬於Netlink協議域的協議,最多可支持32個多播組(因為nl_groups的長度為32比特),每個多播組用一個比特來表示。

關於Netlink剩下的知識點,我們在後面的實戰環節有用到時再討論。

未完,待續…

用戶空間和內核空間通訊之【Netlink 上】