switch_set_state/netlink/kmod



許多代碼調用接口 switch_set_state 向用戶態傳遞消息,在 switch_set_state 中:
switch_set_state 調用接口 kobject_uevent_env
在 kobject_uevent_env 中,如果定義了 CONFIG_NET ,則會通過netlink向用戶態發送消息;
在kernel3.4上,不判斷,直接調用call_usermodehelper通過kmod向用戶態發送消息。
在kernel4.4上,如果定義了 CONFIG_UEVENT_HELPER ,則會調用call_usermodehelper_exec通過kmod向用戶態發送消息;如果沒定義 CONFIG_UEVENT_HELPER ,則不會通過kmod向用戶態發送消息。
在kernel3.4上,switch_set_state 通過kmod向用戶態發送消息時,會調用 call_usermodehelper  來進行發送,該函數 call_usermodehelper 的第一個參數 uevent_helper 的路徑來自 CONFIG_UEVENT_HELPER_PATH 。我們可在此配置uevent_helper的名字(含路徑)
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
...
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
char *argv [3];


argv [0] = uevent_helper;
argv [1] = (char *)subsystem;
argv [2] = NULL;
retval = add_uevent_var(env, "HOME=/");
if (retval)
goto exit;
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
if (retval)
goto exit;


retval = call_usermodehelper(argv[0], argv,
    env->envp, UMH_WAIT_EXEC);
}




內核中也有多處代碼直接調用 call_usermodehelper ,第一個參數在模塊代碼中指定,例如在 drivers\net\hamradio\baycom_epp.c
static char eppconfig_path[256] = "/usr/sbin/eppfpga";
...
return call_usermodehelper(eppconfig_path, argv, envp, UMH_WAIT_PROC);


drivers\macintosh\therm_pm72.c 中:
static char * critical_overtemp_path = "/sbin/critical_overtemp";
...
static int call_critical_overtemp(void)
{
char *argv[] = { critical_overtemp_path, NULL };
static char *envp[] = { "HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL };


return call_usermodehelper(critical_overtemp_path,
  argv, envp, UMH_WAIT_EXEC);
}


轉幾篇文章:

http://blog.csdn.net/sunweizhong1024/article/details/7928530

Uevent 上報event事件給上層的詳細講解

本文章講解插入headphone的時候,向上層上報event函數的整個過程

headphone_event(wm8903->hp_state);

當有headphone 插入的時候,那麼就將hp_state的狀態設置爲1

 

#ifdef CONFIG_I_LOVE_PBJ30

void headphone_event(int state)

{

       switch_set_state(&wired_switch_dev, state);

}

EXPORT_SYMBOL_GPL(headphone_event);

#endif

headphone_event 函數會調用switch_set_state函數進行上報事件

 

staticstruct switch_dev wired_switch_dev = {

       .name = "h2w",

};

 

void switch_set_state(struct switch_dev *sdev, int state)

{

       char name_buf[120];

       char state_buf[120];

       char *prop_buf;

       char *envp[3];

       int env_offset = 0;

       int length;

 

       if (sdev->state != state) {

              sdev->state = state;

 

              prop_buf = (char *)get_zeroed_page(GFP_KERNEL);

              if (prop_buf) {

                     length = name_show(sdev->dev, NULL, prop_buf);

                     if (length > 0) {

                           if (prop_buf[length - 1] == '\n')

                                  prop_buf[length - 1] = 0;

                           snprintf(name_buf, sizeof(name_buf),

                                  "SWITCH_NAME=%s", prop_buf);

                           envp[env_offset++] = name_buf;

                     }

                     length = state_show(sdev->dev, NULL, prop_buf);

                     if (length > 0) {

                           if (prop_buf[length - 1] == '\n')

                                  prop_buf[length - 1] = 0;

                           snprintf(state_buf, sizeof(state_buf),

                                  "SWITCH_STATE=%s", prop_buf);

                           envp[env_offset++] = state_buf;

                     }

                     envp[env_offset] = NULL;

                     kobject_uevent_env(&sdev->dev->kobjKOBJ_CHANGE, envp);

                     free_page((unsignedlong)prop_buf);

              else {

                     printk(KERN_ERR "out of memory in switch_set_state\n");

                     kobject_uevent(&sdev->dev->kobjKOBJ_CHANGE);

              }

       }

}

EXPORT_SYMBOL_GPL(switch_set_state);

講解下上的switch_set_state函數

我們首先會判斷sdev裏面的狀態和我們需要設置的狀態是否一樣,如果一樣的話,那麼就不需要去管它,如果不一樣的話,那麼就將state的值修改

 

 

上面主要會調用以下幾個函數:

name_show:

state_show:

kobject_uevent_env:

 

switch_set_state函數裏面,會申請一個buffer,然後調用name_show函數

static ssize_t name_show(struct device *dev, struct device_attribute *attr,

              char *buf)

{

       struct switch_dev *sdev = (struct switch_dev *)

              dev_get_drvdata(dev);

 

       if (sdev->print_name) {

              int ret = sdev->print_name(sdev, buf);

              if (ret >= 0)

                     return ret;

       }

       return sprintf(buf, "%s\n", sdev->name);

}

 

name_show函數會判斷sdev裏面的print_name函數有沒有被實現,如果沒有實現的話,那麼就將sdev->name的值輸入到buf中去

我們的sdev裏面實現的如下:

staticstruct switch_dev wired_switch_dev = {

       .name = "h2w",

};

所以就直接將name 輸入到buf中去

然後將輸入到buf裏面的值賦值到envp數組裏面去:envp[env_offset++] = name_buf;

 

接下來的state_show函數的執行和上面的name_show函數是一樣的,只是一個輸出的是name,另外一個是state

也是將state的值賦值到envp 的數組裏面去

 

接下來會調用kobject_uevent_env函數進行上報事件

kobject_uevent_env(&sdev->dev->kobjKOBJ_CHANGE, envp);

kobject_uevent_env函數會首先判斷 我這個kobject是屬於哪個kset裏面的,然後找出這個kset裏面的uevent_ops

 

kobject_uevent_env函數的實現全部過程如下:

 

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,

                     char *envp_ext[])

{

       struct kobj_uevent_env *env;

       constchar *action_string = kobject_actions[action];

       constchar *devpath = NULL;

       constchar *subsystem;

       struct kobject *top_kobj;

       struct kset *kset;

       conststruct kset_uevent_ops *uevent_ops;

       u64 seq;

       int i = 0;

       int retval = 0;

#ifdef CONFIG_NET

       struct uevent_sock *ue_sk;

#endif

 

       pr_debug("kobject: '%s' (%p): %s\n",

               kobject_name(kobj), kobj, __func__);

 

       /* search the kset we belong to */

       top_kobj = kobj;

       while (!top_kobj->kset && top_kobj->parent)//取出這個kobject屬於哪一個kset

              top_kobj = top_kobj->parent;

 

       if (!top_kobj->kset) {

              pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "

                      "without kset!\n", kobject_name(kobj), kobj,

                      __func__);

              return -EINVAL;

       }

 

       kset = top_kobj->kset;//找到這個kobject的kset成員

       uevent_ops = kset->uevent_ops; //找到這個kset創建的時候,kset裏面的uevent_ops成員

 

       /* skip the event, if uevent_suppress is set*/

       if (kobj->uevent_suppress) {//判斷kobject裏面有沒有uvent_supress,如果有那麼就代表這個

event是忽略的

              pr_debug("kobject: '%s' (%p): %s: uevent_suppress "

                            "caused the event to drop!\n",

                            kobject_name(kobj), kobj, __func__);

              return 0;

       }

       /* skip the event, if the filter returns zero. */

       if (uevent_ops && uevent_ops->filter)

              if (!uevent_ops->filter(kset, kobj)) {//kset裏面的uevent_ops裏面的filter成員決定是否將事件傳遞到用戶空間去,如果返回0的話,那麼就直接忽略,通過代碼看的出來

                     pr_debug("kobject: '%s' (%p): %s: filter function "

                            "caused the event to drop!\n",

                            kobject_name(kobj), kobj, __func__);

                     return 0;

              }

 

       /* originating subsystem */

       if (uevent_ops && uevent_ops->name)

              subsystem = uevent_ops->name(kset, kobj);//調用uevent_ops裏面的name成員將字符串傳遞給用戶空間的熱插拔程序

       else

              subsystem = kobject_name(&kset->kobj);

       if (!subsystem) {

              pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "

                      "event to drop!\n", kobject_name(kobj), kobj,

                      __func__);

              return 0;

       }

 

       /* environment buffer */

       env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);

       if (!env)

              return -ENOMEM;

 

       /* complete object path */

       devpath = kobject_get_path(kobj, GFP_KERNEL);

       if (!devpath) {

              retval = -ENOENT;

              goto exit;

       }

 

       /* default keys */

       retval = add_uevent_var(env, "ACTION=%s", action_string);

       if (retval)

              goto exit;

       retval = add_uevent_var(env, "DEVPATH=%s", devpath);

       if (retval)

              goto exit;

       retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);

       if (retval)

              goto exit;

 

       /* keys passed in from the caller */

       if (envp_ext) {

              for (i = 0; envp_ext[i]; i++) {

                     retval = add_uevent_var(env, "%s", envp_ext[i]);//將我們設置的參數,添加到環境變量中去

                     if (retval)

                           goto exit;

              }

       }

 

       /* let the kset specific function add its stuff */

       if (uevent_ops && uevent_ops->uevent) {

              retval = uevent_ops->uevent(kset, kobj, env);

              if (retval) {

                     pr_debug("kobject: '%s' (%p): %s: uevent() returned "

                            "%d\n", kobject_name(kobj), kobj,

                            __func__, retval);

                     goto exit;

              }

       }

 

       /*

        * Mark "add" and "remove" events in the object to ensure proper

        * events to userspace during automatic cleanup. If the object did

        * send an "add" event, "remove" will automatically generated by

        * the core, if not already done by the caller.

        */

       if (action == KOBJ_ADD)

              kobj->state_add_uevent_sent = 1;

       elseif (action == KOBJ_REMOVE)

              kobj->state_remove_uevent_sent = 1;

 

       /* we will send an event, so request a new sequence number */

       spin_lock(&sequence_lock);

       seq = ++uevent_seqnum;

       spin_unlock(&sequence_lock);

       retval = add_uevent_var(env, "SEQNUM=%llu", (unsignedlonglong)seq);

       if (retval)

              goto exit;

 

#if defined(CONFIG_NET)

       /* send netlink message */

       mutex_lock(&uevent_sock_mutex);

       list_for_each_entry(ue_sk, &uevent_sock_list, list) {

              struct sock *uevent_sock = ue_sk->sk;

              struct sk_buff *skb;

              size_t len;

 

              /* allocate message with the maximum possible size */

              len = strlen(action_string) + strlen(devpath) + 2;

              skb = alloc_skb(len + env->buflen, GFP_KERNEL);

              if (skb) {

                     char *scratch;

 

                     /* add header */

                     scratch = skb_put(skb, len);

                     sprintf(scratch, "%s@%s", action_string, devpath);

 

                     /* copy keys to our continuous event payload buffer */

                     for (i = 0; i < env->envp_idx; i++) {

                           len = strlen(env->envp[i]) + 1;

                           scratch = skb_put(skb, len);

                           strcpy(scratch, env->envp[i]);

                     }

 

                     NETLINK_CB(skb).dst_group = 1;

                     retval = netlink_broadcast_filtered(uevent_sock, skb,

                                                    0, 1, GFP_KERNEL,

                                                    kobj_bcast_filter,

                                                    kobj);

                     /* ENOBUFS should be handled in userspace */

                     if (retval == -ENOBUFS)

                           retval = 0;

              else

                     retval = -ENOMEM;

       }

       mutex_unlock(&uevent_sock_mutex);

#endif

 

       /* call uevent_helper, usually only enabled during early boot */

       if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {

              char *argv [3];

 

              argv [0] = uevent_helper;

              argv [1] = (char *)subsystem;

              argv [2] = NULL;

              retval = add_uevent_var(env, "HOME=/");

              if (retval)

                     goto exit;

              retval = add_uevent_var(env,

                                  "PATH=/sbin:/bin:/usr/sbin:/usr/bin");

              if (retval)

                     goto exit;

 

              retval = call_usermodehelper(argv[0], argv,

                                       env->envpUMH_WAIT_EXEC);

       }

 

exit:

       kfree(devpath);

       kfree(env);

       return retval;

}

EXPORT_SYMBOL_GPL(kobject_uevent_env);

 

 

看下如何將用戶空間需要的參數添加到環境變量中去的??

傳進去需要的參數

add_uevent_var(env, "%s", envp_ext[i]);

**

 * add_uevent_var - add key value string to the environment buffer

 * @env: environment buffer structure

 * @format: printf format for the key=value pair

 *

 * Returns 0 if environment variable was added successfully or -ENOMEM

 * if no space was available.

 */

int add_uevent_var(struct kobj_uevent_env *env, constchar *format, ...)

{

       va_list args;

       int len;

 

       if (env->envp_idx >= ARRAY_SIZE(env->envp)) {

              WARN(1, KERN_ERR "add_uevent_var: too many keys\n");

              return -ENOMEM;

       }

 

       va_start(args, format);

       len = vsnprintf(&env->buf[env->buflen],

                     sizeof(env->buf) - env->buflen,

                     format, args);

       va_end(args);

 

       if (len >= (sizeof(env->buf) - env->buflen)) {

              WARN(1, KERN_ERR "add_uevent_var: buffer size too small\n");

              return -ENOMEM;

       }

 

       env->envp[env->envp_idx++] = &env->buf[env->buflen];

       env->buflen += len + 1;

       return 0;

}

EXPORT_SYMBOL_GPL(add_uevent_var);

上面的函數在將參數添加到環境變量中去,如果添加添加成功的話,那麼就返回0




用戶空間和內核空間通訊之Netlink

http://www.360doc.com/content/13/0912/17/496343_314005658.shtml



引言

         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的用法,看看它到底是如何實現用戶-內核空間的數據通信的。我們依舊是在2.6.21的內核環境下進行開發。

      在</usr/include/linux/netlink.h>文件裏包含了Netlink協議簇已經定義好的一些預定義協議:
  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST    20 /* 用戶添加的自定義協議 */

      如果我們在Netlink協議簇裏開發一個新的協議,只要在該文件中定義協議號即可,例如我們定義一種基於Netlink協議簇的、協議號是20的自定義協議,如上所示。同時記得,將內核頭文件目錄中的netlink.h也做對應的修改,在我的系統中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h

      接下來我們在用戶空間以及內核空間模塊的開發過程中就可以使用這種協議了,一共分爲三個階段。


Stage 1:

      我們首先實現的功能是用戶->內核單向數據通信,即用戶空間發送一個消息給內核,然後內核將其打印輸出,就這麼簡單。用戶空間的示例代碼如下【mynlusr.c】

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;
  19.         
  1.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //創建套接字
  2.             perror("can't create netlink socket!");
  3.             return 1;
  4.     }
  5.     memset(&dest_addr, 0, sizeof(dest_addr));
  6.     dest_addr.nl_family = AF_NETLINK;
  7.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  8.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
  9.         
  10.     //將套接字和Netlink地址結構體進行綁定
  1.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  2.           perror("can't bind sockfd with sockaddr_nl!");
  3.           return 1;
  4.     }

  5.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  6.           perror("alloc mem failed!");
  7.           return 1;
  8.     }

  9.     memset(nlh,0,MAX_PAYLOAD);
  10.     /* 填充Netlink消息頭部 */
  11.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  12.     nlh->nlmsg_pid = 0;
  13.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  14.     nlh->nlmsg_flags = 0;

  15.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  16.     strcpy(NLMSG_DATA(nlh), argv[1]);

  17.     /*這個是模板,暫時不用糾結爲什麼要這樣用。有時間詳細講解socket時再說*/
  18.     memset(&iov, 0, sizeof(iov));
  19.     iov.iov_base = (void *)nlh;
  20.     iov.iov_len = nlh->nlmsg_len;
  21.     memset(&msg, 0, sizeof(msg));
  22.     msg.msg_iov = &iov;
  23.     msg.msg_iovlen = 1;

  24.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  25.     /* 關閉netlink套接字 */
  26.     close(sock_fd);
  27.     free(nlh);
  28.     return 0;
  29. }

      上面的代碼邏輯已經非常清晰了,都是socket編程的API,唯一不同的是我們這次編程是針對Netlink協議簇的。這裏我們提前引入了BSD層的消息結構體struct msghdr{},定義在<include/linux/socket.h>文件裏,以及其數據塊struct iovec{}定義在<include/linux/uio.h>頭文件裏。這裏就不展開了,大家先記住這個用法就行。以後有時間再深入到socket的骨子裏去轉悠一番。

      另外,需要格外注意的就是Netlink的地址結構體和其消息頭結構中pid字段爲0的情況,很容易讓人產生混淆,再總結一下:

 

0

netlink地址結構體.nl_pid

1、內核發出的多播報文

2、消息的接收方是內核,即從用戶空間發往內核的消息

netlink消息頭體. nlmsg_pid

來自內核主動發出的消息


     這個例子僅是從用戶空間到內核空間的單向數據通信,所以Netlink地址結構體中我們設置了dest_addr.nl_pid = 0,說明我們的報文的目的地是內核空間;在填充Netlink消息頭部時,我們做了nlh->nlmsg_pid = 0這樣的設置。

     需要注意幾個宏的使用:

     NLMSG_SPACE(MAX_PAYLOAD),該宏用於返回不小於MAX_PAYLOAD且4字節對齊的最小長度值,一般用於向內存系統申請空間是指定所申請的內存字節數,和NLMSG_LENGTH(len)所不同的是,前者所申請的空間裏不包含Netlink消息頭部所佔的字節數,後者是消息負載和消息頭加起來的總長度。

     NLMSG_DATA(nlh),該宏用於返回Netlink消息中數據部分的首地址,在寫入和讀取消息數據部分時會用到它。

     它們之間的關係如下:

     內核空間的示例代碼如下【mynlkern.c】:

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <linux/netlink.h>

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static void nl_data_ready (struct sock *sk, int len)
  14. {
  15.     struct sk_buff *skb;
  16.     struct nlmsghdr *nlh = NULL;

  17.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  18.     {
  19.           nlh = (struct nlmsghdr *)skb->data;
  20.           printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
  21.           kfree_skb(skb);
  22.     }
  23.     printk("recvied finished!\n");
  24. }

  25. static int __init myinit_module()
  26. {
  27.     printk("my netlink in\n");
  28.     nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  29.     return 0;
  30. }

  31. static void __exit mycleanup_module()
  32. {
  33.     printk("my netlink out!\n");
  34.     sock_release(nl_sk->sk_socket);
  35. }

  36. module_init(myinit_module);
  37. module_exit(mycleanup_module);

     在內核模塊的初始化函數裏我們用

nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);

創建了一個內核態的socket,第一個參數我們擴展的協議號;第二個參數爲多播組號,目前我們用不上,將其置爲0;第三個參數是個回調函數,即當內核的Netlink socket套接字收到數據時的處理函數;第四個參數就不多說了。

 

      在回調函數nl_data_ready()中,我們不斷的從socket的接收隊列去取數據,一旦拿到數據就將其打印輸出。在協議棧的INET層,用於存儲數據的是大名鼎鼎的sk_buff結構,所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然後通過NLMSG_DATA(nlh)定位到netlink的消息負載。

 

      將上述代碼編譯後測試結果如下:


Stage 2:

      我們將上面的代碼稍加改造就可以實現用戶<->內核雙向數據通信。

      首先是改造用戶空間的代碼:
  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  28.           perror("can't bind sockfd with sockaddr_nl!");
  29.           return 1;
  30.     }
  31.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  32.           perror("alloc mem failed!");
  33.           return 1;
  34.     }
  35.     
  36.     memset(nlh,0,MAX_PAYLOAD);
  37.     /* 填充Netlink消息頭部 */
  38.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.     nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.     nlh->nlmsg_flags = 0;

  42.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.     strcpy(NLMSG_DATA(nlh), argv[1]);

  44.     /*這個是模板,暫時不用糾結爲什麼要這樣用。*/
  45.     memset(&iov, 0, sizeof(iov));
  46.     iov.iov_base = (void *)nlh;
  47.     iov.iov_len = nlh->nlmsg_len;
  48.     memset(&msg, 0, sizeof(msg));
  49.     msg.msg_iov = &iov;
  50.     msg.msg_iovlen = 1;

  51.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  52.     //接收內核消息的消息
  53.     printf("waiting message from kernel!\n");
  54.     memset((char*)NLMSG_DATA(nlh),0,1024);
  55.     recvmsg(sock_fd,&msg,0);
  56.     printf("Got response: %s\n",NLMSG_DATA(nlh));

  57.     /* 關閉netlink套接字 */
  58.     close(sock_fd);
  59.     free(nlh);
  60.     return 0;
  61. }

      內核空間的修改如下:

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> /*該文頭文件裏包含了linux/netlink.h,因爲我們要用到net/netlink.h中的某些API函數,nlmsg_pug()*/

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. //向用戶空間發送消息的接口
  14. void sendnlmsg(char *message,int dstPID)
  15. {
  16.     struct sk_buff *skb;
  17.     struct nlmsghdr *nlh;
  18.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  19.     int slen = 0;

  20.     if(!message || !nl_sk){
  21.         return;
  22.     }

  23.     // 爲新的 sk_buffer申請空間
  24.     skb = alloc_skb(len, GFP_KERNEL);
  25.     if(!skb){
  26.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  27.         return;
  28.     }

  29.     slen = strlen(message)+1;

  30.     //用nlmsg_put()來設置netlink消息頭部
  31.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  32.     // 設置Netlink的控制塊
  33.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  34.     NETLINK_CB(skb).dst_group = 0; //如果目的組爲內核或某一進程,該字段也置0

  35.     message[slen] = '\0';
  36.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  37.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  38.     netlink_unicast(nl_sk,skb,dstPID,0);
  39.     printk("send OK!\n");
  40.     return;
  41. }

  42. static void nl_data_ready (struct sock *sk, int len)
  43. {
  44.     struct sk_buff *skb;
  45.     struct nlmsghdr *nlh = NULL;

  46.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  47.     {
  48.         nlh = (struct nlmsghdr *)skb->data;
  49.         printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
  50.         kfree_skb(skb);
  51.         sendnlmsg("I see you",nlh->nlmsg_pid); //發送者的進程ID我們已經將其存儲在了netlink消息頭部裏的nlmsg_pid字段裏,所以這裏可以拿來用。
  52.     }
  53.     printk("recvied finished!\n");
  54. }

  55. static int __init myinit_module()
  56. {
  57.     printk("my netlink in\n");
  58.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  59.     return 0;
  60. }

  61. static void __exit mycleanup_module()
  62. {
  63.     printk("my netlink out!\n");
  64.     sock_release(nl_sk->sk_socket);
  65. }

  66. module_init(myinit_module);
  67. module_exit(mycleanup_module);

      重新編譯後,測試結果如下:


Stage 3:

      前面我們提到過,如果用戶進程希望加入某個多播組時才需要調用bind()函數。前面的示例中我們沒有這個需求,可還是調了bind(),心頭有些不爽。在前幾篇博文裏有關於socket編程時幾個常見API的詳細解釋和說明,不明白的童鞋可以回頭去複習一下。

      因爲Netlink是面向無連接的數據報的套接字,所以我們還可以用sendto()和recvfrom()來實現數據的收發,這次我們不再調用bind()。將Stage 2的例子稍加改造一下,用戶空間的修改如下:

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     //struct iovec iov;
  17.     int sock_fd=-1;
  18.     //struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.    /*不再調用bind()函數了
  28.    if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  29.           perror("can't bind sockfd with sockaddr_nl!");
  30.           return 1;
  31.    }*/

  32.    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  33.           perror("alloc mem failed!");
  34.           return 1;
  35.    }
  36.    memset(nlh,0,MAX_PAYLOAD);
  37.    /* 填充Netlink消息頭部 */
  38.    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.    nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.    nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.    nlh->nlmsg_flags = 0;

  42.    /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.    strcpy(NLMSG_DATA(nlh), argv[1]);

  44.    /*這個模板就用不上了。*/
  45.    /*
  46.    memset(&iov, 0, sizeof(iov));
  47.    iov.iov_base = (void *)nlh;
  48.    iov.iov_len = nlh->nlmsg_len;
  49.    memset(&msg, 0, sizeof(msg));
  50.    msg.msg_iov = &iov;
  51.    msg.msg_iovlen = 1;
  52.    */

  53.    //sendmsg(sock_fd, &msg, 0); //不再用這種方式發消息到內核
  54.    sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr)); 
  55.         
  56.    //接收內核消息的消息
  57.    printf("waiting message from kernel!\n");
  58.    //memset((char*)NLMSG_DATA(nlh),0,1024);
  59.    memset(nlh,0,MAX_PAYLOAD); //清空整個Netlink消息頭包括消息頭和負載
  60.    //recvmsg(sock_fd,&msg,0);
  61.    recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
  62.    printf("Got response: %s\n",NLMSG_DATA(nlh)); 

  63.    /* 關閉netlink套接字 */
  64.    close(sock_fd);
  65.    free(nlh);
  66.    return 0;
  67. }

      內核空間的代碼完全不用修改,我們仍然用netlink_unicast()從內核空間發送消息到用戶空間。

      重新編譯後,測試結果如下:

      和Stage 2中代碼運行效果完全一樣。也就是說,在開發Netlink程序過程中,如果沒牽扯到多播機制,那麼用戶空間的socket代碼其實是不用執行bind()系統調用的,但此時就需要用sendto()和recvfrom()完成數據的發送和接收的任務;如果執行了bind()系統調用,當然也可以繼續用sendto()和recvfrom(),但給它們傳遞的參數就有所區別。這時候一般使用sendmsg()和recvmsg()來完成數據的發送和接收。大家根據自己的實際情況靈活選擇。


關於Netlink多播機制的用法

        在上一篇博文中我們所遇到的情況都是用戶空間作爲消息進程的發起者,Netlink還支持內核作爲消息的發送方的情況。這一般用於內核主動向用戶空間報告一些內核狀態,例如我們在用戶空間看到的USB的熱插拔事件的通告就是這樣的應用。

       先說一下我們的目標,內核線程每個一秒鐘往一個多播組裏發送一條消息,然後用戶空間所以加入了該組的進程都會收到這樣的消息,並將消息內容打印出來。

        Netlink地址結構體中的nl_groups是32位,也就是說每種Netlink協議最多支持32個多播組。如何理解這裏所說的每種Netlink協議?在</usr/include/linux/netlink.h>裏預定義的如下協議都是Netlink協議簇的具體協議,還有我們添加的NETLINK_TEST也是一種Netlink協議。

  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST 20 /* 用戶添加的自定義協議 */

       在我們自己添加的NETLINK_TEST協議裏,同樣地,最多允許我們設置32個多播組,每個多播組用1個比特表示,所以不同的多播組不可能出現重複。你可以根據自己的實際需求,決定哪個多播組是用來做什麼的。用戶空間的進程如果對某個多播組感興趣,那麼它就加入到該組中,當內核空間的進程往該組發送多播消息時,所有已經加入到該多播組的用戶進程都會收到該消息。

       再回到我們Netlink地址結構體裏的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號。如何根據多播組號取得多播組號的掩碼呢?在af_netlink.c中有個函數:
  1. static u32 netlink_group_mask(u32 group)
  2. {
  3.     return group ? 1 << (group - 1) : 0;
  4. }

       也就是說,在用戶空間的代碼裏,如果我們要加入到多播組1,需要設置nl_groups設置爲1;多播組2的掩碼爲2;多播組3的掩碼爲4,依次類推。爲0表示我們不希望加入任何多播組。理解這一點很重要。所以我們可以在用戶空間也定義一個類似於netlink_group_mask()的功能函數,完成從多播組號到多播組掩碼的轉換。最終用戶空間的代碼如下:

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>
  11. #include <errno.h>

  12. #define MAX_PAYLOAD 1024 // Netlink消息的最大載荷的長度

  13. unsigned int netlink_group_mask(unsigned int group)
  14. {
  15.     return group ? 1 << (group - 1) : 0;
  16. }

  17. int main(int argc, char* argv[])
  18. {
  19.     struct sockaddr_nl src_addr;
  20.     struct nlmsghdr *nlh = NULL;
  21.     struct iovec iov;
  22.     struct msghdr msg;
  23.     int sock_fd, retval;

  24.     // 創建Socket
  25.     sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
  26.     if(sock_fd == -1){
  27.         printf("error getting socket: %s", strerror(errno));
  28.         return -1;
  29.     }

  30.     memset(&src_addr, 0, sizeof(src_addr));
  31.     src_addr.nl_family = PF_NETLINK;
  32.     src_addr.nl_pid = 0; // 表示我們要從內核接收多播消息。注意:該字段爲0有雙重意義,另一個意義是表示我們發送的數據的目的地址是內核。
  33.     src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號來自我們執行程序時輸入的第一個參數

  34.     // 因爲我們要加入到一個多播組,所以必須調用bind()
  35.     retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
  36.     if(retval < 0){
  37.         printf("bind failed: %s", strerror(errno));
  38.         close(sock_fd);
  39.         return -1;
  40.     }

  41.     // 爲接收Netlink消息申請存儲空間
  42.     nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
  43.     if(!nlh){
  44.         printf("malloc nlmsghdr error!\n");
  45.         close(sock_fd);
  46.         return -1;
  47.     }

  48.     memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  49.     iov.iov_base = (void *)nlh;
  50.     iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);

  51.     memset(&msg, 0, sizeof(msg));
  52.     msg.msg_iov = &iov;
  53.     msg.msg_iovlen = 1;

  54.     // 從內核接收消息
  55.     printf("waitinf for...\n");
  56.     recvmsg(sock_fd, &msg, 0);
  57.     printf("Received message: %s \n", NLMSG_DATA(nlh));
  58.     
  59.     close(sock_fd);

  60.     return 0;
  61. }

       可以看到,用戶空間的程序基本沒什麼變化,唯一需要格外注意的就是Netlink地址結構體中的nl_groups的設置。由於對它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網上搜集資料。有分析不當之處,還請大家幫我指出。

       內核空間我們添加了內核線程和內核線程同步方法completion的使用。內核空間修改後的代碼如下:
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> 

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static struct task_struct *mythread = NULL; //內核線程對象

  14. //向用戶空間發送消息的接口
  15. void sendnlmsg(char *message/*,int dstPID*/)
  16. {
  17.     struct sk_buff *skb;
  18.     struct nlmsghdr *nlh;
  19.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  20.     int slen = 0;

  21.     if(!message || !nl_sk){
  22.         return;
  23.     }

  24.     // 爲新的 sk_buffer申請空間
  25.     skb = alloc_skb(len, GFP_KERNEL);
  26.     if(!skb){
  27.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  28.         return;
  29.     }

  30.     slen = strlen(message)+1;

  31.     //用nlmsg_put()來設置netlink消息頭部
  32.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  33.     // 設置Netlink的控制塊裏的相關信息
  34.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  35.     NETLINK_CB(skb).dst_group = 5; //多播組號爲5,但置成0好像也可以。

  36.     message[slen] = '\0';
  37.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  38.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  39.     //netlink_unicast(nl_sk,skb,dstPID,0);
  40.     netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //發送多播消息到多播組5,這裏我故意沒有用1之類的“常見”值,目的就是爲了證明我們上面提到的多播組號和多播組號掩碼之間的對應關係
  41.     printk("send OK!\n");
  42.     return;
  43. }

  44. //每隔1秒鐘發送一條“I am from kernel!”消息,共發10個報文
  45. static int sending_thread(void *data)
  46. {
  47.      int i = 10;
  48.      struct completion cmpl;
  49.      while(i--){
  50.             init_completion(&cmpl);
  51.             wait_for_completion_timeout(&cmpl, 1 * HZ);
  52.             sendnlmsg("I am from kernel!");
  53.      }
  54.      printk("sending thread exited!");
  55.      return 0;
  56. }

  57. static int __init myinit_module()
  58. {
  59.     printk("my netlink in\n");
  60.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);

  61.     if(!nl_sk){
  62.         printk(KERN_ERR "my_net_link: create netlink socket error.\n");
  63.         return 1;
  64.     }

  65.     printk("my netlink: create netlink socket ok.\n");
  66.     mythread = kthread_run(sending_thread,NULL,"thread_sender");
  67.     return 0;
  68. }

  69. static void __exit mycleanup_module()
  70. {
  71.     if(nl_sk != NULL){
  72.         sock_release(nl_sk->sk_socket);
  73. }
  74. printk("my netlink out!\n");
  75. }

  76. module_init(myinit_module);
  77. module_exit(mycleanup_module);

       關於內核中netlink_kernel_create(int unit, unsigned int groups,…)函數裏的第二個參數指的是我們內核進程最多能處理的多播組的個數,如果該值小於32,則默認按32處理,所以在調用netlink_kernel_create()函數時可以不用糾結第二個參數,一般將其置爲0就可以了。

 

       在skbuff{}結構體中,有個成員叫做"控制塊",源碼對它的解釋如下:

  1. struct sk_buff {
  2.     /* These two members must be first. */
  3.     struct sk_buff        *next;
  4.     struct sk_buff        *prev;
  5.     … …
  6.     /*
  7.      * This is the control buffer. It is free to use for every
  8.      * layer. Please put your private variables there. If you
  9.      * want to keep them across layers you have to do a skb_clone()
  10.      * first. This is owned by whoever has the skb queued ATM.
  11.      */
  12.     char            cb[48];

  13.     … …
  14. }
       當內核態的Netlink發送數據到用戶空間時一般需要填充skbuff的控制塊,填充的方式是通過強制類型轉換,將其轉換成struct netlink_skb_parms{}之後進行填充賦值的:
  1. struct netlink_skb_parms
  2. {
  3.     struct ucred        creds;        /* Skb credentials    */
  4.     __u32            pid;
  5.     __u32            dst_group;
  6.     kernel_cap_t        eff_cap;
  7.     __u32            loginuid;    /* Login (audit) uid */
  8.     __u32            sid;        /* SELinux security id */
  9. };

       填充時的模板代碼如下:

  1. NETLINK_CB(skb).pid=xx;
  2. NETLINK_CB(skb).dst_group=xx;

       這裏要注意的是在Netlink協議簇裏提到的skbuff的cb控制塊裏保存的是屬於Netlink的私有信息。怎麼講,就是Netlink會用該控制塊裏的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數據。打個比方,以開車爲例,開車的時候我們要做的就是打火、控制方向盤、適當地控制油門和剎車,車就開動了,這就是汽車提供給我們的“功能”。汽車的發動機,輪胎,傳動軸,以及所用到的螺絲螺栓等都屬於它的“私有”數據cb。汽車要運行起來這些東西是不可或缺的,但它們之間的協作和交互對用戶來說又是透明的。就好比我們Netlink的私有控制結構struct netlink_skb_parms{}一樣。

       目前我們的例子中,將NETLINK_CB(skb).dst_group設置爲相應的多播組號和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請Netlink的大蝦們幫我點撥點撥。

       編譯後重新運行,最後的測試結果如下:

       注意,這裏一定要先執行insmod加載內核模塊,然後再運行用戶空間的程序。如果沒有加載mynlkern.ko而直接執行./test 5在bind()系統調用時會報如下的錯誤:

       bind failed: No such file or directory

       因爲網上有寫文章在講老版本Netlink的多播時用法時先執行了用戶空間的程序,然後才加載內核模塊,現在(2.6.21)已經行不通了,這一點請大家注意。

       小結:通過這三篇博文我們對Netlink有了初步的認識,並且也可以開發基於Netlink的基本應用程序。但這只是冰山一角,要想寫出高質量、高效率的軟件模塊還有些差距,特別是對Netlink本質的理解還需要提高一個層次,當然這其中牽扯到內核編程的很多基本功,如臨界資源的互斥、線程安全性保護、用Netlink傳遞大數據時的處理等等都是開發人員需要考慮的問題。


Linux中與內核通信的Netlink機制(實例)

      Netlink在2.6版本的內核中變化也是很大的,在最新的2.6.37內核中,其定義已經改成下面這種形式,傳遞的參數已經達到6個。其中第一個參數 和mutex參數都是最新添加的。Mutex也可以爲空。這裏主要是關於內核空間中的netlink函數的使用。

extern struct sock *netlink_kernel_create(struct net *net,
                     int unit,unsigned int groups,
                     void (*input)(struct sk_buff *skb),
                     struct mutex *cb_mutex,
                     struct module *module);

  struct net是一個網絡名字空間namespace,在不同的名字空間裏面可以有自己的轉發信息庫,有自己的一套net_device等等。默認情況下都是使用 init_net這個全局變量,下面是內核中調用netlink_kernel_create()函數的一個示例。
在內核中,

audit_sock = netlink_kernel_create(&init_net, NETLINK_AUDIT, 0,
                                      audit_receive, NULL, THIS_MODULE);

模塊調用函數 netlink_unicast 來發送單播消息:

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)


    參數ssk爲函數 netlink_kernel_create()返回的socket,參數skb存放消息,它的data字段指向要發送的netlink消息結構,而 skb的控制塊保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用於方便設置該控制塊, 參數pid爲接收消息進程的pid,參數nonblock表示該函數是否爲非阻塞,如果爲1,該函數將在沒有接收緩存可利用時立即返回,而如果爲0,該函 數在沒有接收緩存可利用 定時睡眠。
    netlink的內核實現在.c文件 net/core/af_netlink.c中,內核模塊要想使用netlink,也必須包含頭文件linux/netlink.h。內核使用 netlink需要專門的API,這完全不同於用戶態應用對netlink的使用。如果用戶需要增加新的netlink協議類型,必須通過修改 linux/netlink.h來實現,當然,目前的netlink實現已經包含了一個通用的協議類型NETLINK_GENERIC以方便用戶使用,用 戶可以直接使用它而不必增加新的協議類型。前面講到,爲了增加新的netlink協議類型,用戶僅需增加如下定義到linux/netlink.h就可 以:
只要增加這個定義之後,用戶就可以在內核的任何地方引用該協議。
在內核中,爲了創建一個netlink socket用戶需要調用如下函數:

extern struct sock *netlink_kernel_create(struct net *net,
                                     int unit,unsigned int groups,
                                     void (*input)(struct sk_buff *skb),
                                     struct mutex *cb_mutex,
                                     struct module *module);

struct net是一個網絡名字空間namespace,在不同的名字空間裏面可以有自己的轉發信息庫,有自己的一套net_device等等。默認情況下都是使用init_net這個全局變量

    參數unit表示netlink協議類型,如 NETLINK_MYTEST,參數input則爲內核模塊定義的netlink消息處理函數,當有消息到達這個netlink socket時,該input函數指針就會被引用。函數指針input的參數skb實際上就是函數netlink_kernel_create返回的 struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的socket在內核中也會有一個struct sock結構來表示。

     函數input()會在發送進程執行sendmsg()時 被調用,這樣處理消息比較及時,但是,如果消息特別長時,這樣處理將增加系統調用sendmsg()的執行時間,也就是說當用戶的程序調用sendmsg ()函數時,如果input()函數處理時間過長,也就是說input()函數不執行不完,用戶程序調用的sendmsg()函數就不會返回。只有當內核 空間中的input()函數返回時,用戶調用的sendmsg()函數纔會返回。對於這種情況,可以定義一個內核線程專門負責消息接收,而函數input 的工作只是喚醒該內核線程,這樣sendmsg將很快返回。(這裏網上的的說明)不過在查看Linux2.6.37版本的內核時並沒有發現這種處理過程, 一般都是按下面的方法進行處理。

這裏註冊的netlink協議爲NETLINK_XFRM。

nlsk = netlink_kernel_create(net, NETLINK_XFRM, XFRMNLGRP_MAX,
                                 xfrm_netlink_rcv, NULL, THIS_MODULE);
 

static void xfrm_netlink_rcv(struct sk_buff *skb)
{
       mutex_lock(&xfrm_cfg_mutex);

       netlink_rcv_skb(skb, &xfrm_user_rcv_msg);
       mutex_unlock(&xfrm_cfg_mutex);
}
在netlink_rcv_skb()函數中進行接收處理。
 

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
                    u32 group, gfp_t allocation)


    前面的三個參數與 netlink_unicast相同,參數group爲接收消息的多播組,該參數的每一個位代表一個多播組,因此如果發送給多個多播組,就把該參數設置爲 多個多播組組ID的位或。參數allocation爲內核內存分配類型,一般地爲GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用於 原子的上下文(即不可以睡眠),而GFP_KERNEL用於非原子上下文。

NETLINK_CB(skb).pid = 0;

NETLINK_CB(skb).dst_pid = 0;

NETLINK_CB(skb).dst_group = 1;

   字段pid表示消息發送者進程 ID,也即源地址,對於內核,它爲 0, dst_pid 表示消息接收者進程 ID,也即目標地址,如果目標爲組或內核,它設置爲 0,否則 dst_group 表示目標組地址,如果它目標爲某一進程或內核,dst_group 應當設置爲 0。
   下面是參考網上使用netlink寫的和內核通信的兩個程序,一個是用戶空間,一個是內核空間。
內核空間:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/time.h>
#include <linux/types.h>
#include <net/sock.h>
#include <net/netlink.h> 

#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024
int stringlength(char *s);
void sendnlmsg(char * message);
int pid;
int err;
struct sock *nl_sk = NULL;
int flag = 0;

void sendnlmsg(char *message)
{
    struct sk_buff *skb_1;
    struct nlmsghdr *nlh;
    int len = NLMSG_SPACE(MAX_MSGSIZE);
    int slen = 0;
    if(!message || !nl_sk)
    {
        return ;
    }
    skb_1 = alloc_skb(len,GFP_KERNEL);
    if(!skb_1)
    {
        printk(KERN_ERR "my_net_link:alloc_skb_1 error\n");
    }
    slen = stringlength(message);
    nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);

    NETLINK_CB(skb_1).pid = 0;
    NETLINK_CB(skb_1).dst_group = 0;

     message[slen]= '\0';
    memcpy(NLMSG_DATA(nlh),message,slen+1);
    printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh));

    netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);

}

int stringlength(char *s)
{
    int slen = 0;


    for(; *s; s++){
        slen++;
    }

    return slen;
}

void nl_data_ready(struct sk_buff *__skb)
 {
     struct sk_buff *skb;
     struct nlmsghdr *nlh;
     char str[100];
    struct completion cmpl;
    int i=10;
     skb = skb_get (__skb);
     if(skb->len >= NLMSG_SPACE(0))
     {
         nlh = nlmsg_hdr(skb);

         memcpy(str, NLMSG_DATA(nlh), sizeof(str));
           printk("Message received:%s\n",str) ;
             pid = nlh->nlmsg_pid;
    while(i--)
    {
        init_completion(&cmpl);
     wait_for_completion_timeout(&cmpl,* HZ);
        sendnlmsg("I am from kernel!");
    }
        flag = 1;
         kfree_skb(skb);
    }

 }

// Initialize netlink

int netlink_init(void)
{


    nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1,
                                 nl_data_ready, NULL, THIS_MODULE);

    if(!nl_sk){
        printk(KERN_ERR "my_net_link: create netlink socket error.\n");
        return 1;
    }

    printk("my_net_link_3: create netlink socket ok.\n");


    return 0;
}

static void netlink_exit(void)
{
    if(nl_sk != NULL){
        sock_release(nl_sk->sk_socket);
    }

    printk("my_net_link: self module exited\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

MODULE_AUTHOR("frankzfz");
MODULE_LICENSE("GPL");

下面是用戶空間的程序:

#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>

#define NETLINK_TEST 25
#define MAX_PAYLOAD 1024 // maximum payload size

int main(int argc, char* argv[])
{
    int state;
    struct sockaddr_nl src_addr, dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    struct msghdr msg;
    int sock_fd, retval;
    int state_smg = 0;
    // Create a socket

    sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(sock_fd == -1){
        printf("error getting socket: %s", strerror(errno));
        return -1;
    }

    // To prepare binding

    memset(&msg,0,sizeof(msg));
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); // self pid

    src_addr.nl_groups = 0; // multi cast


    retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
    if(retval < 0){
        printf("bind failed: %s", strerror(errno));
        close(sock_fd);
        return -1;
    }

    // To prepare recvmsg

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    if(!nlh){
        printf("malloc nlmsghdr error!\n");
        close(sock_fd);
        return -1;
    }

    memset(&dest_addr,0,sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;
    dest_addr.nl_groups = 0;

    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;
    strcpy(NLMSG_DATA(nlh),"Hello you!");

    iov.iov_base = (void *)nlh;
   iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
    // iov.iov_len = nlh->nlmsg_len;

    memset(&msg, 0, sizeof(msg));
   
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    printf("state_smg\n");
    state_smg = sendmsg(sock_fd,&msg,0);

    if(state_smg == -1)
    {
        printf("get error sendmsg = %s\n",strerror(errno));
    }

    memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
    printf("waiting received!\n");
    // Read message from kernel

    while(1){
        printf("In while recvmsg\n");
        state = recvmsg(sock_fd, &msg, 0);
        if(state<0)
        {
            printf("state<1");
        }
        printf("In while\n");
        printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
    }

    close(sock_fd);

    return 0;
}

下面是Makefile文件:

obj-:= netlink_k.
KERNELBUILD := /lib/modules/`uname -r`/build 
default: 
    @echo "BUILE Kmod" 
    @make -C $(KERNELBUILD) M=$(shell pwd) modules 
    gcc -o netlink_2 netlink_2.
clean: 
    @echo " CLEAN kmod" 
    @rm -rf *.
    @rm -rf .depend .*.cmd *.ko *.mod..tmp_versions *.symvers .*.

其中,netlink_k.c爲內核的空間的程序。
    先運行內核代碼netlink_k.ko,也就是在執行完makefile文件後,會生成一個netlink_k.ko文件,可以使用下面的命令進行安 裝,insmod netlink_k.ko,使用lsmod查看,當安裝成功後,然後,執行./netlink用戶空間程序,可以在另一個終端下執行dmesg命令,查看 內核通信的情況。這裏netlink程序向內核空間發送一個hello you!內核返回給一個I am from kernel!在這裏使用了一個定時器,也就是每3秒中發送一次I am from kernel!只有內核把10個字符串全部發送完畢後,用戶空間的sendmsg()纔會返回,也就是在用戶空間的netlink纔會輸出內核空間發送過 來的數據,這裏只有一個簡單的程序,並沒有什麼實際的意義,因爲,正如前面所說的一般情況下不會在回調函數中處理太多的東西,以免sendmsg()函數 返回不及時。下面是使用dmesg命令輸出的信息。

[873791.498039] my_net_link_3: create netlink socket ok.
[873810.263676] Message received:Hello 
[873813.260848] my_net_link_4:send message 'I am from kernel!'.
[873816.260821] my_net_link_4:send message 'I am from kernel!'.
[873819.260860] my_net_link_4:send message 'I am from kernel!'.
[873822.260762] my_net_link_4:send message 'I am from kernel!'.
[873825.260883] my_net_link_4:send message 'I am from kernel!'.
[873828.260669] my_net_link_4:send message 'I am from kernel!'.
[873831.260714] my_net_link_4:send message 'I am from kernel!'.
[873834.260683] my_net_link_4:send message 'I am from kernel!'.
[873837.260666] my_net_link_4:send message 'I am from kernel!'.
[873840.260632] my_net_link_4:send message 'I am from kernel!'.

參考網址:

 http://blog.csdn.net/wangjingfei/archive/2010/02/05/5288460.aspx

http://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspx

Linux 系統內核空間與用戶空間通信的實現與分析


引言

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

       當然,本文不是分析NetlinkLinux上的實現機制,而是就“什麼是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之間是單播通信,進程BC和子系統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必須提供一種機制用於確保每一對“用戶-內核”空間通信的進程之間的數據交互不會發生紊亂。
    即,進程AB通過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),其主要作用是用於唯一的標識一個基於netlinksocket通道。通常情況下nl_pid都設置爲當前進程的進程號。然而,對於一個進程的多個線程同時使用netlink socket的情況,nl_pid的設置一般採用如下這個樣子來實現:

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

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

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


    今天我們來動手演練一下Netlink的用法,看看它到底是如何實現用戶-內核空間的數據通信的。我們依舊是在2.6.21的內核環境下進行開發。

      在</usr/include/linux/netlink.h>文件裏包含了Netlink協議簇已經定義好的一些預定義協議:
  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST    20 /* 用戶添加的自定義協議 */

      如果我們在Netlink協議簇裏開發一個新的協議,只要在該文件中定義協議號即可,例如我們定義一種基於Netlink協議簇的、協議號是20的自定義協議,如上所示。同時記得,將內核頭文件目錄中的netlink.h也做對應的修改,在我的系統中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h

      接下來我們在用戶空間以及內核空間模塊的開發過程中就可以使用這種協議了,一共分爲三個階段。


Stage 1

      我們首先實現的功能是用戶->內核單向數據通信,即用戶空間發送一個消息給內核,然後內核將其打印輸出,就這麼簡單。用戶空間的示例代碼如下【mynlusr.c

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;
  19.         
  1.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //創建套接字
  2.             perror("can't create netlink socket!");
  3.             return 1;
  4.     }
  5.     memset(&dest_addr, 0, sizeof(dest_addr));
  6.     dest_addr.nl_family = AF_NETLINK;
  7.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  8.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
  9.         
  10.     //將套接字和Netlink地址結構體進行綁定
  1.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  2.           perror("can't bind sockfd with sockaddr_nl!");
  3.           return 1;
  4.     }

  5.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  6.           perror("alloc mem failed!");
  7.           return 1;
  8.     }

  9.     memset(nlh,0,MAX_PAYLOAD);
  10.     /* 填充Netlink消息頭部 */
  11.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  12.     nlh->nlmsg_pid = 0;
  13.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  14.     nlh->nlmsg_flags = 0;

  15.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  16.     strcpy(NLMSG_DATA(nlh), argv[1]);

  17.     /*這個是模板,暫時不用糾結爲什麼要這樣用。有時間詳細講解socket時再說*/
  18.     memset(&iov, 0, sizeof(iov));
  19.     iov.iov_base = (void *)nlh;
  20.     iov.iov_len = nlh->nlmsg_len;
  21.     memset(&msg, 0, sizeof(msg));
  22.     msg.msg_iov = &iov;
  23.     msg.msg_iovlen = 1;

  24.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  25.     /* 關閉netlink套接字 */
  26.     close(sock_fd);
  27.     free(nlh);
  28.     return 0;
  29. }

      上面的代碼邏輯已經非常清晰了,都是socket編程的API,唯一不同的是我們這次編程是針對Netlink協議簇的。這裏我們提前引入了BSD層的消息結構體struct msghdr{},定義在<include/linux/socket.h>文件裏,以及其數據塊struct iovec{}定義在<include/linux/uio.h>頭文件裏。這裏就不展開了,大家先記住這個用法就行。以後有時間再深入到socket的骨子裏去轉悠一番。

      另外,需要格外注意的就是Netlink的地址結構體和其消息頭結構中pid字段爲0的情況,很容易讓人產生混淆,再總結一下:

 

0

netlink地址結構體.nl_pid

1、內核發出的多播報文

2、消息的接收方是內核,即從用戶空間發往內核的消息

netlink消息頭體. nlmsg_pid

來自內核主動發出的消息


     這個例子僅是從用戶空間到內核空間的單向數據通信,所以Netlink地址結構體中我們設置了dest_addr.nl_pid = 0,說明我們的報文的目的地是內核空間;在填充Netlink消息頭部時,我們做了nlh->nlmsg_pid = 0這樣的設置。

     需要注意幾個宏的使用:

     NLMSG_SPACE(MAX_PAYLOAD),該宏用於返回不小於MAX_PAYLOAD4字節對齊的最小長度值,一般用於向內存系統申請空間是指定所申請的內存字節數,和NLMSG_LENGTH(len)所不同的是,前者所申請的空間裏不包含Netlink消息頭部所佔的字節數,後者是消息負載和消息頭加起來的總長度。

     NLMSG_DATA(nlh),該宏用於返回Netlink消息中數據部分的首地址,在寫入和讀取消息數據部分時會用到它。

     它們之間的關係如下:

     內核空間的示例代碼如下【mynlkern.c】:

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <linux/netlink.h>

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static void nl_data_ready (struct sock *sk, int len)
  14. {
  15.     struct sk_buff *skb;
  16.     struct nlmsghdr *nlh = NULL;

  17.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  18.     {
  19.           nlh = (struct nlmsghdr *)skb->data;
  20.           printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
  21.           kfree_skb(skb);
  22.     }
  23.     printk("recvied finished!\n");
  24. }

  25. static int __init myinit_module()
  26. {
  27.     printk("my netlink in\n");
  28.     nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  29.     return 0;
  30. }

  31. static void __exit mycleanup_module()
  32. {
  33.     printk("my netlink out!\n");
  34.     sock_release(nl_sk->sk_socket);
  35. }

  36. module_init(myinit_module);
  37. module_exit(mycleanup_module);

     在內核模塊的初始化函數裏我們用

nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);

創建了一個內核態的socket,第一個參數我們擴展的協議號;第二個參數爲多播組號,目前我們用不上,將其置爲0;第三個參數是個回調函數,即當內核的Netlink socket套接字收到數據時的處理函數;第四個參數就不多說了。

 

      在回調函數nl_data_ready()中,我們不斷的從socket的接收隊列去取數據,一旦拿到數據就將其打印輸出。在協議棧的INET層,用於存儲數據的是大名鼎鼎的sk_buff結構,所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然後通過NLMSG_DATA(nlh)定位到netlink的消息負載。

 

      將上述代碼編譯後測試結果如下:


Stage 2

      我們將上面的代碼稍加改造就可以實現用戶<->內核雙向數據通信。

      首先是改造用戶空間的代碼:
  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  28.           perror("can't bind sockfd with sockaddr_nl!");
  29.           return 1;
  30.     }
  31.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  32.           perror("alloc mem failed!");
  33.           return 1;
  34.     }
  35.     
  36.     memset(nlh,0,MAX_PAYLOAD);
  37.     /* 填充Netlink消息頭部 */
  38.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.     nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.     nlh->nlmsg_flags = 0;

  42.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.     strcpy(NLMSG_DATA(nlh), argv[1]);

  44.     /*這個是模板,暫時不用糾結爲什麼要這樣用。*/
  45.     memset(&iov, 0, sizeof(iov));
  46.     iov.iov_base = (void *)nlh;
  47.     iov.iov_len = nlh->nlmsg_len;
  48.     memset(&msg, 0, sizeof(msg));
  49.     msg.msg_iov = &iov;
  50.     msg.msg_iovlen = 1;

  51.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  52.     //接收內核消息的消息
  53.     printf("waiting message from kernel!\n");
  54.     memset((char*)NLMSG_DATA(nlh),0,1024);
  55.     recvmsg(sock_fd,&msg,0);
  56.     printf("Got response: %s\n",NLMSG_DATA(nlh));

  57.     /* 關閉netlink套接字 */
  58.     close(sock_fd);
  59.     free(nlh);
  60.     return 0;
  61. }

      內核空間的修改如下:

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> /*該文頭文件裏包含了linux/netlink.h,因爲我們要用到net/netlink.h中的某些API函數,nlmsg_pug()*/

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. //向用戶空間發送消息的接口
  14. void sendnlmsg(char *message,int dstPID)
  15. {
  16.     struct sk_buff *skb;
  17.     struct nlmsghdr *nlh;
  18.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  19.     int slen = 0;

  20.     if(!message || !nl_sk){
  21.         return;
  22.     }

  23.     // 爲新的 sk_buffer申請空間
  24.     skb = alloc_skb(len, GFP_KERNEL);
  25.     if(!skb){
  26.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  27.         return;
  28.     }

  29.     slen = strlen(message)+1;

  30.     //用nlmsg_put()來設置netlink消息頭部
  31.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  32.     // 設置Netlink的控制塊
  33.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  34.     NETLINK_CB(skb).dst_group = 0; //如果目的組爲內核或某一進程,該字段也置0

  35.     message[slen] = '\0';
  36.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  37.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  38.     netlink_unicast(nl_sk,skb,dstPID,0);
  39.     printk("send OK!\n");
  40.     return;
  41. }

  42. static void nl_data_ready (struct sock *sk, int len)
  43. {
  44.     struct sk_buff *skb;
  45.     struct nlmsghdr *nlh = NULL;

  46.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  47.     {
  48.         nlh = (struct nlmsghdr *)skb->data;
  49.         printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
  50.         kfree_skb(skb);
  51.         sendnlmsg("I see you",nlh->nlmsg_pid); //發送者的進程ID我們已經將其存儲在了netlink消息頭部裏的nlmsg_pid字段裏,所以這裏可以拿來用。
  52.     }
  53.     printk("recvied finished!\n");
  54. }

  55. static int __init myinit_module()
  56. {
  57.     printk("my netlink in\n");
  58.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  59.     return 0;
  60. }

  61. static void __exit mycleanup_module()
  62. {
  63.     printk("my netlink out!\n");
  64.     sock_release(nl_sk->sk_socket);
  65. }

  66. module_init(myinit_module);
  67. module_exit(mycleanup_module);

      重新編譯後,測試結果如下:


Stage 3

      前面我們提到過,如果用戶進程希望加入某個多播組時才需要調用bind()函數。前面的示例中我們沒有這個需求,可還是調了bind(),心頭有些不爽。在前幾篇博文裏有關於socket編程時幾個常見API的詳細解釋和說明,不明白的童鞋可以回頭去複習一下。

      因爲Netlink是面向無連接的數據報的套接字,所以我們還可以用sendto()recvfrom()來實現數據的收發,這次我們不再調用bind()。將Stage 2的例子稍加改造一下,用戶空間的修改如下:

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     //struct iovec iov;
  17.     int sock_fd=-1;
  18.     //struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.    /*不再調用bind()函數了
  28.    if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  29.           perror("can't bind sockfd with sockaddr_nl!");
  30.           return 1;
  31.    }*/

  32.    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  33.           perror("alloc mem failed!");
  34.           return 1;
  35.    }
  36.    memset(nlh,0,MAX_PAYLOAD);
  37.    /* 填充Netlink消息頭部 */
  38.    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.    nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.    nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.    nlh->nlmsg_flags = 0;

  42.    /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.    strcpy(NLMSG_DATA(nlh), argv[1]);

  44.    /*這個模板就用不上了。*/
  45.    /*
  46.    memset(&iov, 0, sizeof(iov));
  47.    iov.iov_base = (void *)nlh;
  48.    iov.iov_len = nlh->nlmsg_len;
  49.    memset(&msg, 0, sizeof(msg));
  50.    msg.msg_iov = &iov;
  51.    msg.msg_iovlen = 1;
  52.    */

  53.    //sendmsg(sock_fd, &msg, 0); //不再用這種方式發消息到內核
  54.    sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr)); 
  55.         
  56.    //接收內核消息的消息
  57.    printf("waiting message from kernel!\n");
  58.    //memset((char*)NLMSG_DATA(nlh),0,1024);
  59.    memset(nlh,0,MAX_PAYLOAD); //清空整個Netlink消息頭包括消息頭和負載
  60.    //recvmsg(sock_fd,&msg,0);
  61.    recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
  62.    printf("Got response: %s\n",NLMSG_DATA(nlh)); 

  63.    /* 關閉netlink套接字 */
  64.    close(sock_fd);
  65.    free(nlh);
  66.    return 0;
  67. }

      內核空間的代碼完全不用修改,我們仍然用netlink_unicast()從內核空間發送消息到用戶空間。

      重新編譯後,測試結果如下:

      和Stage 2中代碼運行效果完全一樣。也就是說,在開發Netlink程序過程中,如果沒牽扯到多播機制,那麼用戶空間的socket代碼其實是不用執行bind()系統調用的,但此時就需要用sendto()recvfrom()完成數據的發送和接收的任務;如果執行了bind()系統調用,當然也可以繼續用sendto()recvfrom(),但給它們傳遞的參數就有所區別。這時候一般使用sendmsg()recvmsg()來完成數據的發送和接收。大家根據自己的實際情況靈活選擇。


關於Netlink多播機制的用法

        在上一篇博文中我們所遇到的情況都是用戶空間作爲消息進程的發起者,Netlink還支持內核作爲消息的發送方的情況。這一般用於內核主動向用戶空間報告一些內核狀態,例如我們在用戶空間看到的USB的熱插拔事件的通告就是這樣的應用。

       先說一下我們的目標,內核線程每個一秒鐘往一個多播組裏發送一條消息,然後用戶空間所以加入了該組的進程都會收到這樣的消息,並將消息內容打印出來。

        Netlink地址結構體中的nl_groups32位,也就是說每種Netlink協議最多支持32個多播組。如何理解這裏所說的每種Netlink協議?在</usr/include/linux/netlink.h>裏預定義的如下協議都是Netlink協議簇的具體協議,還有我們添加的NETLINK_TEST也是一種Netlink協議。

  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST 20 /* 用戶添加的自定義協議 */

       在我們自己添加的NETLINK_TEST協議裏,同樣地,最多允許我們設置32個多播組,每個多播組用1個比特表示,所以不同的多播組不可能出現重複。你可以根據自己的實際需求,決定哪個多播組是用來做什麼的。用戶空間的進程如果對某個多播組感興趣,那麼它就加入到該組中,當內核空間的進程往該組發送多播消息時,所有已經加入到該多播組的用戶進程都會收到該消息。

       再回到我們Netlink地址結構體裏的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號。如何根據多播組號取得多播組號的掩碼呢?在af_netlink.c中有個函數:
  1. static u32 netlink_group_mask(u32 group)
  2. {
  3.     return group ? 1 << (group - 1) : 0;
  4. }

       也就是說,在用戶空間的代碼裏,如果我們要加入到多播組1,需要設置nl_groups設置爲1;多播組2的掩碼爲2;多播組3的掩碼爲4,依次類推。爲0表示我們不希望加入任何多播組。理解這一點很重要。所以我們可以在用戶空間也定義一個類似於netlink_group_mask()的功能函數,完成從多播組號到多播組掩碼的轉換。最終用戶空間的代碼如下:

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>
  11. #include <errno.h>

  12. #define MAX_PAYLOAD 1024 // Netlink消息的最大載荷的長度

  13. unsigned int netlink_group_mask(unsigned int group)
  14. {
  15.     return group ? 1 << (group - 1) : 0;
  16. }

  17. int main(int argc, char* argv[])
  18. {
  19.     struct sockaddr_nl src_addr;
  20.     struct nlmsghdr *nlh = NULL;
  21.     struct iovec iov;
  22.     struct msghdr msg;
  23.     int sock_fd, retval;

  24.     // 創建Socket
  25.     sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
  26.     if(sock_fd == -1){
  27.         printf("error getting socket: %s", strerror(errno));
  28.         return -1;
  29.     }

  30.     memset(&src_addr, 0, sizeof(src_addr));
  31.     src_addr.nl_family = PF_NETLINK;
  32.     src_addr.nl_pid = 0; // 表示我們要從內核接收多播消息。注意:該字段爲0有雙重意義,另一個意義是表示我們發送的數據的目的地址是內核。
  33.     src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號來自我們執行程序時輸入的第一個參數

  34.     // 因爲我們要加入到一個多播組,所以必須調用bind()
  35.     retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
  36.     if(retval < 0){
  37.         printf("bind failed: %s", strerror(errno));
  38.         close(sock_fd);
  39.         return -1;
  40.     }

  41.     // 爲接收Netlink消息申請存儲空間
  42.     nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
  43.     if(!nlh){
  44.         printf("malloc nlmsghdr error!\n");
  45.         close(sock_fd);
  46.         return -1;
  47.     }

  48.     memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  49.     iov.iov_base = (void *)nlh;
  50.     iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);

  51.     memset(&msg, 0, sizeof(msg));
  52.     msg.msg_iov = &iov;
  53.     msg.msg_iovlen = 1;

  54.     // 從內核接收消息
  55.     printf("waitinf for...\n");
  56.     recvmsg(sock_fd, &msg, 0);
  57.     printf("Received message: %s \n", NLMSG_DATA(nlh));
  58.     
  59.     close(sock_fd);

  60.     return 0;
  61. }

       可以看到,用戶空間的程序基本沒什麼變化,唯一需要格外注意的就是Netlink地址結構體中的nl_groups的設置。由於對它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網上搜集資料。有分析不當之處,還請大家幫我指出。

       內核空間我們添加了內核線程和內核線程同步方法completion的使用。內核空間修改後的代碼如下:
  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> 

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static struct task_struct *mythread = NULL; //內核線程對象

  14. //向用戶空間發送消息的接口
  15. void sendnlmsg(char *message/*,int dstPID*/)
  16. {
  17.     struct sk_buff *skb;
  18.     struct nlmsghdr *nlh;
  19.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  20.     int slen = 0;

  21.     if(!message || !nl_sk){
  22.         return;
  23.     }

  24.     // 爲新的 sk_buffer申請空間
  25.     skb = alloc_skb(len, GFP_KERNEL);
  26.     if(!skb){
  27.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  28.         return;
  29.     }

  30.     slen = strlen(message)+1;

  31.     //用nlmsg_put()來設置netlink消息頭部
  32.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  33.     // 設置Netlink的控制塊裏的相關信息
  34.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  35.     NETLINK_CB(skb).dst_group = 5; //多播組號爲5,但置成0好像也可以。

  36.     message[slen] = '\0';
  37.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  38.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  39.     //netlink_unicast(nl_sk,skb,dstPID,0);
  40.     netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //發送多播消息到多播組5,這裏我故意沒有用1之類的“常見”值,目的就是爲了證明我們上面提到的多播組號和多播組號掩碼之間的對應關係
  41.     printk("send OK!\n");
  42.     return;
  43. }

  44. //每隔1秒鐘發送一條“I am from kernel!”消息,共發10個報文
  45. static int sending_thread(void *data)
  46. {
  47.      int i = 10;
  48.      struct completion cmpl;
  49.      while(i--){
  50.             init_completion(&cmpl);
  51.             wait_for_completion_timeout(&cmpl, 1 * HZ);
  52.             sendnlmsg("I am from kernel!");
  53.      }
  54.      printk("sending thread exited!");
  55.      return 0;
  56. }

  57. static int __init myinit_module()
  58. {
  59.     printk("my netlink in\n");
  60.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);

  61.     if(!nl_sk){
  62.         printk(KERN_ERR "my_net_link: create netlink socket error.\n");
  63.         return 1;
  64.     }

  65.     printk("my netlink: create netlink socket ok.\n");
  66.     mythread = kthread_run(sending_thread,NULL,"thread_sender");
  67.     return 0;
  68. }

  69. static void __exit mycleanup_module()
  70. {
  71.     if(nl_sk != NULL){
  72.         sock_release(nl_sk->sk_socket);
  73. }
  74. printk("my netlink out!\n");
  75. }

  76. module_init(myinit_module);
  77. module_exit(mycleanup_module);

       關於內核中netlink_kernel_create(int unit, unsigned int groups,…)函數裏的第二個參數指的是我們內核進程最多能處理的多播組的個數,如果該值小於32,則默認按32處理,所以在調用netlink_kernel_create()函數時可以不用糾結第二個參數,一般將其置爲0就可以了。

 

       在skbuff{}結構體中,有個成員叫做"控制塊",源碼對它的解釋如下:

  1. struct sk_buff {
  2.     /* These two members must be first. */
  3.     struct sk_buff        *next;
  4.     struct sk_buff        *prev;
  5.     … …
  6.     /*
  7.      * This is the control buffer. It is free to use for every
  8.      * layer. Please put your private variables there. If you
  9.      * want to keep them across layers you have to do a skb_clone()
  10.      * first. This is owned by whoever has the skb queued ATM.
  11.      */
  12.     char            cb[48];

  13.     … …
  14. }
       當內核態的Netlink發送數據到用戶空間時一般需要填充skbuff的控制塊,填充的方式是通過強制類型轉換,將其轉換成struct netlink_skb_parms{}之後進行填充賦值的:
  1. struct netlink_skb_parms
  2. {
  3.     struct ucred        creds;        /* Skb credentials    */
  4.     __u32            pid;
  5.     __u32            dst_group;
  6.     kernel_cap_t        eff_cap;
  7.     __u32            loginuid;    /* Login (audit) uid */
  8.     __u32            sid;        /* SELinux security id */
  9. };

       填充時的模板代碼如下:

  1. NETLINK_CB(skb).pid=xx;
  2. NETLINK_CB(skb).dst_group=xx;

       這裏要注意的是在Netlink協議簇裏提到的skbuffcb控制塊裏保存的是屬於Netlink的私有信息。怎麼講,就是Netlink會用該控制塊裏的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數據。打個比方,以開車爲例,開車的時候我們要做的就是打火、控制方向盤、適當地控制油門和剎車,車就開動了,這就是汽車提供給我們的“功能”。汽車的發動機,輪胎,傳動軸,以及所用到的螺絲螺栓等都屬於它的“私有”數據cb。汽車要運行起來這些東西是不可或缺的,但它們之間的協作和交互對用戶來說又是透明的。就好比我們Netlink的私有控制結構struct netlink_skb_parms{}一樣。

       目前我們的例子中,將NETLINK_CB(skb).dst_group設置爲相應的多播組號和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請Netlink的大蝦們幫我點撥點撥。

       編譯後重新運行,最後的測試結果如下:

       注意,這裏一定要先執行insmod加載內核模塊,然後再運行用戶空間的程序。如果沒有加載mynlkern.ko而直接執行./test 5bind()系統調用時會報如下的錯誤:

       bind failed: No such file or directory

       因爲網上有寫文章在講老版本Netlink的多播時用法時先執行了用戶空間的程序,然後才加載內核模塊,現在(2.6.21)已經行不通了,這一點請大家注意。

       小結:通過這三篇博文我們對Netlink有了初步的認識,並且也可以開發基於Netlink的基本應用程序。但這只是冰山一角,要想寫出高質量、高效率的軟件模塊還有些差距,特別是對Netlink本質的理解還需要提高一個層次,當然這其中牽扯到內核編程的很多基本功,如臨界資源的互斥、線程安全性保護、用Netlink傳遞大數據時的處理等等都是開發人員需要考慮的問題。


Linux中與內核通信的Netlink機制(實例)

      Netlink在2.6版本的內核中變化也是很大的,在最新的2.6.37內核中,其定義已經改成下面這種形式,傳遞的參數已經達到6個。其中第一個參數 和mutex參數都是最新添加的。Mutex也可以爲空。這裏主要是關於內核空間中的netlink函數的使用。

extern struct sock *netlink_kernel_create(struct net *net,
                     int unit,unsigned int groups,
                     void (*input)(struct sk_buff *skb),
                     struct mutex *cb_mutex,
                     struct module *module);

  struct net是一個網絡名字空間namespace,在不同的名字空間裏面可以有自己的轉發信息庫,有自己的一套net_device等等。默認情況下都是使用 init_net這個全局變量,下面是內核中調用netlink_kernel_create()函數的一個示例。
在內核中,

audit_sock = netlink_kernel_create(&init_net, NETLINK_AUDIT, 0,
                                      audit_receive, NULL, THIS_MODULE);

模塊調用函數 netlink_unicast 來發送單播消息:

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)


    參數ssk爲函數 netlink_kernel_create()返回的socket,參數skb存放消息,它的data字段指向要發送的netlink消息結構,而 skb的控制塊保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用於方便設置該控制塊, 參數pid爲接收消息進程的pid,參數nonblock表示該函數是否爲非阻塞,如果爲1,該函數將在沒有接收緩存可利用時立即返回,而如果爲0,該函 數在沒有接收緩存可利用 定時睡眠。
    netlink的內核實現在.c文件 net/core/af_netlink.c中,內核模塊要想使用netlink,也必須包含頭文件linux/netlink.h。內核使用 netlink需要專門的API,這完全不同於用戶態應用對netlink的使用。如果用戶需要增加新的netlink協議類型,必須通過修改 linux/netlink.h來實現,當然,目前的netlink實現已經包含了一個通用的協議類型NETLINK_GENERIC以方便用戶使用,用 戶可以直接使用它而不必增加新的協議類型。前面講到,爲了增加新的netlink協議類型,用戶僅需增加如下定義到linux/netlink.h就可 以:
只要增加這個定義之後,用戶就可以在內核的任何地方引用該協議。
在內核中,爲了創建一個netlink socket用戶需要調用如下函數:

extern struct sock *netlink_kernel_create(struct net *net,
                                     int unit,unsigned int groups,
                                     void (*input)(struct sk_buff *skb),
                                     struct mutex *cb_mutex,
                                     struct module *module);

struct net是一個網絡名字空間namespace,在不同的名字空間裏面可以有自己的轉發信息庫,有自己的一套net_device等等。默認情況下都是使用init_net這個全局變量

    參數unit表示netlink協議類型,如 NETLINK_MYTEST,參數input則爲內核模塊定義的netlink消息處理函數,當有消息到達這個netlink socket時,該input函數指針就會被引用。函數指針input的參數skb實際上就是函數netlink_kernel_create返回的 struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的socket在內核中也會有一個struct sock結構來表示。

     函數input()會在發送進程執行sendmsg()時 被調用,這樣處理消息比較及時,但是,如果消息特別長時,這樣處理將增加系統調用sendmsg()的執行時間,也就是說當用戶的程序調用sendmsg ()函數時,如果input()函數處理時間過長,也就是說input()函數不執行不完,用戶程序調用的sendmsg()函數就不會返回。只有當內核 空間中的input()函數返回時,用戶調用的sendmsg()函數纔會返回。對於這種情況,可以定義一個內核線程專門負責消息接收,而函數input 的工作只是喚醒該內核線程,這樣sendmsg將很快返回。(這裏網上的的說明)不過在查看Linux2.6.37版本的內核時並沒有發現這種處理過程, 一般都是按下面的方法進行處理。

這裏註冊的netlink協議爲NETLINK_XFRM。

nlsk = netlink_kernel_create(net, NETLINK_XFRM, XFRMNLGRP_MAX,
                                 xfrm_netlink_rcv, NULL, THIS_MODULE);
 

static void xfrm_netlink_rcv(struct sk_buff *skb)
{
       mutex_lock(&xfrm_cfg_mutex);

       netlink_rcv_skb(skb, &xfrm_user_rcv_msg);
       mutex_unlock(&xfrm_cfg_mutex);
}
在netlink_rcv_skb()函數中進行接收處理。
 

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
                    u32 group, gfp_t allocation)


    前面的三個參數與 netlink_unicast相同,參數group爲接收消息的多播組,該參數的每一個位代表一個多播組,因此如果發送給多個多播組,就把該參數設置爲 多個多播組組ID的位或。參數allocation爲內核內存分配類型,一般地爲GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用於 原子的上下文(即不可以睡眠),而GFP_KERNEL用於非原子上下文。

NETLINK_CB(skb).pid = 0;

NETLINK_CB(skb).dst_pid = 0;

NETLINK_CB(skb).dst_group = 1;

   字段pid表示消息發送者進程 ID,也即源地址,對於內核,它爲 0, dst_pid 表示消息接收者進程 ID,也即目標地址,如果目標爲組或內核,它設置爲 0,否則 dst_group 表示目標組地址,如果它目標爲某一進程或內核,dst_group 應當設置爲 0。
   下面是參考網上使用netlink寫的和內核通信的兩個程序,一個是用戶空間,一個是內核空間。
內核空間:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/time.h>
#include <linux/types.h>
#include <net/sock.h>
#include <net/netlink.h> 

#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024
int stringlength(char *s);
void sendnlmsg(char * message);
int pid;
int err;
struct sock *nl_sk = NULL;
int flag = 0;

void sendnlmsg(char *message)
{
    struct sk_buff *skb_1;
    struct nlmsghdr *nlh;
    int len = NLMSG_SPACE(MAX_MSGSIZE);
    int slen = 0;
    if(!message || !nl_sk)
    {
        return ;
    }
    skb_1 = alloc_skb(len,GFP_KERNEL);
    if(!skb_1)
    {
        printk(KERN_ERR "my_net_link:alloc_skb_1 error\n");
    }
    slen = stringlength(message);
    nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);

    NETLINK_CB(skb_1).pid = 0;
    NETLINK_CB(skb_1).dst_group = 0;

     message[slen]= '\0';
    memcpy(NLMSG_DATA(nlh),message,slen+1);
    printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh));

    netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);

}

int stringlength(char *s)
{
    int slen = 0;


    for(; *s; s++){
        slen++;
    }

    return slen;
}

void nl_data_ready(struct sk_buff *__skb)
 {
     struct sk_buff *skb;
     struct nlmsghdr *nlh;
     char str[100];
    struct completion cmpl;
    int i=10;
     skb = skb_get (__skb);
     if(skb->len >= NLMSG_SPACE(0))
     {
         nlh = nlmsg_hdr(skb);

         memcpy(str, NLMSG_DATA(nlh), sizeof(str));
           printk("Message received:%s\n",str) ;
             pid = nlh->nlmsg_pid;
    while(i--)
    {
        init_completion(&cmpl);
     wait_for_completion_timeout(&cmpl,* HZ);
        sendnlmsg("I am from kernel!");
    }
        flag = 1;
         kfree_skb(skb);
    }

 }

// Initialize netlink

int netlink_init(void)
{


    nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1,
                                 nl_data_ready, NULL, THIS_MODULE);

    if(!nl_sk){
        printk(KERN_ERR "my_net_link: create netlink socket error.\n");
        return 1;
    }

    printk("my_net_link_3: create netlink socket ok.\n");


    return 0;
}

static void netlink_exit(void)
{
    if(nl_sk != NULL){
        sock_release(nl_sk->sk_socket);
    }

    printk("my_net_link: self module exited\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

MODULE_AUTHOR("frankzfz");
MODULE_LICENSE("GPL");

下面是用戶空間的程序:

#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>

#define NETLINK_TEST 25
#define MAX_PAYLOAD 1024 // maximum payload size

int main(int argc, char* argv[])
{
    int state;
    struct sockaddr_nl src_addr, dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    struct msghdr msg;
    int sock_fd, retval;
    int state_smg = 0;
    // Create a socket

    sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(sock_fd == -1){
        printf("error getting socket: %s", strerror(errno));
        return -1;
    }

    // To prepare binding

    memset(&msg,0,sizeof(msg));
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); // self pid

    src_addr.nl_groups = 0; // multi cast


    retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
    if(retval < 0){
        printf("bind failed: %s", strerror(errno));
        close(sock_fd);
        return -1;
    }

    // To prepare recvmsg

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    if(!nlh){
        printf("malloc nlmsghdr error!\n");
        close(sock_fd);
        return -1;
    }

    memset(&dest_addr,0,sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;
    dest_addr.nl_groups = 0;

    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;
    strcpy(NLMSG_DATA(nlh),"Hello you!");

    iov.iov_base = (void *)nlh;
   iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
    // iov.iov_len = nlh->nlmsg_len;

    memset(&msg, 0, sizeof(msg));
   
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    printf("state_smg\n");
    state_smg = sendmsg(sock_fd,&msg,0);

    if(state_smg == -1)
    {
        printf("get error sendmsg = %s\n",strerror(errno));
    }

    memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
    printf("waiting received!\n");
    // Read message from kernel

    while(1){
        printf("In while recvmsg\n");
        state = recvmsg(sock_fd, &msg, 0);
        if(state<0)
        {
            printf("state<1");
        }
        printf("In while\n");
        printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
    }

    close(sock_fd);

    return 0;
}

下面是Makefile文件:

obj-:= netlink_k.
KERNELBUILD := /lib/modules/`uname -r`/build 
default: 
    @echo "BUILE Kmod" 
    @make -C $(KERNELBUILD) M=$(shell pwd) modules 
    gcc -o netlink_2 netlink_2.
clean: 
    @echo " CLEAN kmod" 
    @rm -rf *.
    @rm -rf .depend .*.cmd *.ko *.mod..tmp_versions *.symvers .*.

其中,netlink_k.c爲內核的空間的程序。
    先運行內核代碼netlink_k.ko,也就是在執行完makefile文件後,會生成一個netlink_k.ko文件,可以使用下面的命令進行安 裝,insmod netlink_k.ko,使用lsmod查看,當安裝成功後,然後,執行./netlink用戶空間程序,可以在另一個終端下執行dmesg命令,查看 內核通信的情況。這裏netlink程序向內核空間發送一個hello you!內核返回給一個I am from kernel!在這裏使用了一個定時器,也就是每3秒中發送一次I am from kernel!只有內核把10個字符串全部發送完畢後,用戶空間的sendmsg()纔會返回,也就是在用戶空間的netlink纔會輸出內核空間發送過 來的數據,這裏只有一個簡單的程序,並沒有什麼實際的意義,因爲,正如前面所說的一般情況下不會在回調函數中處理太多的東西,以免sendmsg()函數 返回不及時。下面是使用dmesg命令輸出的信息。

[873791.498039] my_net_link_3: create netlink socket ok.
[873810.263676] Message received:Hello 
[873813.260848] my_net_link_4:send message 'I am from kernel!'.
[873816.260821] my_net_link_4:send message 'I am from kernel!'.
[873819.260860] my_net_link_4:send message 'I am from kernel!'.
[873822.260762] my_net_link_4:send message 'I am from kernel!'.
[873825.260883] my_net_link_4:send message 'I am from kernel!'.
[873828.260669] my_net_link_4:send message 'I am from kernel!'.
[873831.260714] my_net_link_4:send message 'I am from kernel!'.
[873834.260683] my_net_link_4:send message 'I am from kernel!'.
[873837.260666] my_net_link_4:send message 'I am from kernel!'.
[873840.260632] my_net_link_4:send message 'I am from kernel!'.

參考網址:

 http://blog.csdn.net/wangjingfei/archive/2010/02/05/5288460.aspx

http://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspx

Linux 系統內核空間與用戶空間通信的實現與分析
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章