小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

用戶空間和內(nèi)核空間通訊之Netlink

 waston 2013-09-12
引言

         Alan Cox在內(nèi)核1.3版本的開發(fā)階段最先引入了Netlink,剛開始時(shí)Netlink是以字符驅(qū)動(dòng)接口的方式提供內(nèi)核與用戶空間的雙向數(shù)據(jù)通信;隨后,在2.1內(nèi)核開發(fā)過程中,Alexey KuznetsovNetlink改寫成一個(gè)更加靈活、且易于擴(kuò)展的基于消息通信接口,并將其應(yīng)用到高級(jí)路由子系統(tǒng)的基礎(chǔ)框架里。自那時(shí)起,Netlink就成了Linux內(nèi)核子系統(tǒng)和用戶態(tài)的應(yīng)用程序通信的主要手段之一。

       2001年,ForCES IETF委員會(huì)正式對(duì)Netlink進(jìn)行了標(biāo)準(zhǔn)化的工作。Jamal Hadi Salim提議將Netlink定義成一種用于網(wǎng)絡(luò)設(shè)備的路由引擎組件和其控制管理組件之間通信的協(xié)議。不過他的建議最終沒有被采納,取而代之的是我們今天所看到的格局:Netlink被設(shè)計(jì)成一個(gè)新的協(xié)議域,domain。

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

       當(dāng)然,本文不是分析NetlinkLinux上的實(shí)現(xiàn)機(jī)制,而是就“什么是Netlink”以及“如何用好Netlink”的話題和大家做個(gè)分享,只有在遇到問題時(shí)才需要去閱讀內(nèi)核源碼弄清個(gè)所以然。


什么是Netlink

       關(guān)于Netlink的理解,需要把握幾個(gè)關(guān)鍵點(diǎn):

       1、面向數(shù)據(jù)報(bào)的無連接消息子系統(tǒng)

       2、基于通用的BSD Socket架構(gòu)而實(shí)現(xiàn)

      關(guān)于第一點(diǎn)使我們很容易聯(lián)想到UDP協(xié)議,能想到這一點(diǎn)就非常棒了。按著UDP協(xié)議來理解Netlink不是不無道理,只要你能觸類旁通,做到“活學(xué)”,善于總結(jié)歸納、聯(lián)想,最后實(shí)現(xiàn)知識(shí)遷移這就是學(xué)習(xí)的本質(zhì)。Netlink可以實(shí)現(xiàn)內(nèi)核->用戶以及用戶->內(nèi)核的雙向、異步的數(shù)據(jù)通信,同時(shí)它還支持兩個(gè)用戶進(jìn)程之間、甚至兩個(gè)內(nèi)核子系統(tǒng)之間的數(shù)據(jù)通信。本文中,對(duì)后兩者我們不予考慮,焦點(diǎn)集中在如何實(shí)現(xiàn)用戶<->內(nèi)核之間的數(shù)據(jù)通信。

      看到第二點(diǎn)腦海中是不是瞬間閃現(xiàn)了下面這張圖片呢?如果是,則說明你確實(shí)有慧根;當(dāng)然,不是也沒關(guān)系,慧根可以慢慢長(zhǎng)嘛,呵呵。

    在后面實(shí)戰(zhàn)Netlink套接字編程時(shí)我們主要會(huì)用到socket(),bind(),sendmsg()
recvmsg()等系統(tǒng)調(diào)用,當(dāng)然還有socket提供的輪訓(xùn)(polling)機(jī)制。       


Netlink通信類型

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

      單播:經(jīng)常用于一個(gè)用戶進(jìn)程和一個(gè)內(nèi)核子系統(tǒng)之間1:1的數(shù)據(jù)通信。用戶空間發(fā)送命令到內(nèi)核,然后從內(nèi)核接受命令的返回結(jié)果。

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


Netlink的消息格式

       Netlink消息由兩部分組成:消息頭和有效數(shù)據(jù)載荷,且整個(gè)Netlink消息是4字節(jié)對(duì)齊,一般按主機(jī)字節(jié)序進(jìn)行傳遞。消息頭為固定的16字節(jié),消息體長(zhǎng)度可變:

Netlink的消息頭

      消息頭定義在<include/linux/netlink.h>文件里,由結(jié)構(gòu)體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:整個(gè)消息的長(zhǎng)度,按字節(jié)計(jì)算。包括了Netlink消息頭本身。

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

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

     NLMSG_ERROR-指明該消息中包含一個(gè)錯(cuò)誤;

     NLMSG_DONE-如果內(nèi)核通過Netlink隊(duì)列返回了多個(gè)消息,那么隊(duì)列的最后一條消息的類型為NLMSG_DONE,其余所有消息的nlmsg_flags屬性都被設(shè)置NLM_F_MULTI位有效。

     NLMSG_OVERRUN-暫時(shí)沒用到。

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

標(biāo)記

作用及說明

NLM_F_REQUEST

如果消息中有該標(biāo)記位,說明這是一個(gè)請(qǐng)求消息。所有從用戶空間到內(nèi)核空間的消息都要設(shè)置該位,否則內(nèi)核將向用戶返回一個(gè)EINVAL無效參數(shù)的錯(cuò)誤

NLM_F_MULTI

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

NLM_F_ACK

該消息是內(nèi)核對(duì)來自用戶空間的NLM_F_REQUEST消息的響應(yīng)

NLM_F_ECHO

如果從用戶空間發(fā)給內(nèi)核的消息中該標(biāo)記為1,則說明用戶的應(yīng)用進(jìn)程要求內(nèi)核將用戶發(fā)給它的每條消息通過單播的形式再發(fā)送給用戶進(jìn)程。和我們通常說的“回顯”功能類似。


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

nlmsg_seq:消息序列號(hào)。因?yàn)?span lang="EN-US">Netlink是面向數(shù)據(jù)報(bào)的,所以存在丟失數(shù)據(jù)的風(fēng)險(xiǎn),但是Netlink提供了如何確保消息不丟失的機(jī)制,讓程序開發(fā)人員根據(jù)其實(shí)際需求而實(shí)現(xiàn)。消息序列號(hào)一般和NLM_F_ACK類型的消息聯(lián)合使用,如果用戶的應(yīng)用程序需要保證其發(fā)送的每條消息都成功被內(nèi)核收到的話,那么它發(fā)送消息時(shí)需要用戶程序自己設(shè)置序號(hào),內(nèi)核收到該消息后對(duì)提取其中的序列號(hào),然后在發(fā)送給用戶程序回應(yīng)消息里設(shè)置同樣的序列號(hào)。有點(diǎn)類似于TCP的響應(yīng)和確認(rèn)機(jī)制。

注意:當(dāng)內(nèi)核主動(dòng)向用戶空間發(fā)送廣播消息時(shí),消息中的該字段總是為0。


nlmsg_pid:當(dāng)用戶空間的進(jìn)程和內(nèi)核空間的某個(gè)子系統(tǒng)之間通過Netlink建立了數(shù)據(jù)交換的通道后,Netlink會(huì)為每個(gè)這樣的通道分配一個(gè)唯一的數(shù)字標(biāo)識(shí)。其主要作用就是將來自用戶空間的請(qǐng)求消息和響應(yīng)消息進(jìn)行關(guān)聯(lián)。說得直白一點(diǎn),假如用戶空間存在多個(gè)用戶進(jìn)程,內(nèi)核空間同樣存在多個(gè)進(jìn)程,Netlink必須提供一種機(jī)制用于確保每一對(duì)“用戶-內(nèi)核”空間通信的進(jìn)程之間的數(shù)據(jù)交互不會(huì)發(fā)生紊亂。
    即,進(jìn)程A、B通過Netlink向子系統(tǒng)1獲取信息時(shí),子系統(tǒng)1必須確保回送給進(jìn)程A的響應(yīng)數(shù)據(jù)不會(huì)發(fā)到進(jìn)程B那里。主要適用于用戶空間的進(jìn)程從內(nèi)核空間獲取數(shù)據(jù)的場(chǎng)景。通常情況下,用戶空間的進(jìn)程在向內(nèi)核發(fā)送消息時(shí)一般通過系統(tǒng)調(diào)用getpid()將當(dāng)前進(jìn)程的進(jìn)程號(hào)賦給該變量,即用戶空間的進(jìn)程希望得到內(nèi)核的響應(yīng)時(shí)才會(huì)這么做。從內(nèi)核主動(dòng)發(fā)送到用戶空間的消息該字段都被設(shè)置為0

Netlink的消息體

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


Netlink提供的錯(cuò)誤指示消息

      當(dāng)用戶空間的應(yīng)用程序和內(nèi)核空間的進(jìn)程之間通過Netlink通信時(shí)發(fā)生了錯(cuò)誤,Netlink必須向用戶空間通報(bào)這種錯(cuò)誤。Netlink對(duì)錯(cuò)誤消息進(jìn)行了單獨(dú)封裝,<include/linux/netlink.h>
  1. struct nlmsgerr
  2. {
  3.     int        error; //標(biāo)準(zhǔn)的錯(cuò)誤碼,定義在errno.h頭文件中??梢杂胮error()來解釋
  4.     struct nlmsghdr msg; //指明了哪條消息觸發(fā)了結(jié)構(gòu)體中error這個(gè)錯(cuò)誤值
  5. };


Netlink編程需要注意的問題

      基于Netlink的用戶-內(nèi)核通信,有兩種情況可能會(huì)導(dǎo)致丟包:

      1、內(nèi)存耗盡;

      2、用戶空間接收進(jìn)程的緩沖區(qū)溢出。導(dǎo)致緩沖區(qū)溢出的主要原因有可能是:用戶空間的進(jìn)程運(yùn)行太慢;或者接收隊(duì)列太短。

      如果Netlink不能將消息正確傳遞到用戶空間的接收進(jìn)程,那么用戶空間的接收進(jìn)程在調(diào)用recvmsg()系統(tǒng)調(diào)用時(shí)就會(huì)返回一個(gè)內(nèi)存不足(ENOBUFS)的錯(cuò)誤,這一點(diǎn)需要注意。換句話說,緩沖區(qū)溢出的情況是不會(huì)發(fā)送在從用戶->內(nèi)核的sendmsg()系統(tǒng)調(diào)用里,原因前面我們也說過了,請(qǐng)大家自己思考一下。

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


Netlink的地址結(jié)構(gòu)體

      在TCP博文中我們提到過在Internet編程過程中所用到的地址結(jié)構(gòu)體和標(biāo)準(zhǔn)地址結(jié)構(gòu)體,它們和Netlink地址結(jié)構(gòu)體的關(guān)系如下:

    struct sockaddr_nl{}的詳細(xì)定義和描述如下:

  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:該屬性為發(fā)送或接收消息的進(jìn)程ID,前面我們也說過,Netlink不僅可以實(shí)現(xiàn)用戶-內(nèi)核空間的通信還可使現(xiàn)實(shí)用戶空間兩個(gè)進(jìn)程之間,或內(nèi)核空間兩個(gè)進(jìn)程之間的通信。該屬性為0時(shí)一般適用于如下兩種情況:

        第一,我們要發(fā)送的目的地是內(nèi)核,即從用戶空間發(fā)往內(nèi)核空間時(shí),我們構(gòu)造的Netlink地址結(jié)構(gòu)體中nl_pid通常情況下都置為0。這里有一點(diǎn)需要跟大家交代一下,在Netlink規(guī)范里,PID全稱是Port-ID(32bits),其主要作用是用于唯一的標(biāo)識(shí)一個(gè)基于netlinksocket通道。通常情況下nl_pid都設(shè)置為當(dāng)前進(jìn)程的進(jìn)程號(hào)。然而,對(duì)于一個(gè)進(jìn)程的多個(gè)線程同時(shí)使用netlink socket的情況,nl_pid的設(shè)置一般采用如下這個(gè)樣子來實(shí)現(xiàn):

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

       第二,從內(nèi)核發(fā)出的多播報(bào)文到用戶空間時(shí),如果用戶空間的進(jìn)程處在該多播組中,那么其地址結(jié)構(gòu)體中nl_pid也設(shè)置為0,同時(shí)還要結(jié)合下面介紹到的另一個(gè)屬性。

nl_groups:如果用戶空間的進(jìn)程希望加入某個(gè)多播組,則必須執(zhí)行bind()系統(tǒng)調(diào)用。該字段指明了調(diào)用者希望加入的多播組號(hào)的掩碼(注意不是組號(hào),后面我們會(huì)詳細(xì)講解這個(gè)字段)。如果該字段為0則表示調(diào)用者不希望加入任何多播組。對(duì)于每個(gè)隸屬于Netlink協(xié)議域的協(xié)議,最多可支持32個(gè)多播組(因?yàn)?span lang="EN-US">nl_groups的長(zhǎng)度為32比特),每個(gè)多播組用一個(gè)比特來表示。 


    今天我們來動(dòng)手演練一下Netlink的用法,看看它到底是如何實(shí)現(xiàn)用戶-內(nèi)核空間的數(shù)據(jù)通信的。我們依舊是在2.6.21的內(nèi)核環(huán)境下進(jìn)行開發(fā)。

      在</usr/include/linux/netlink.h>文件里包含了Netlink協(xié)議簇已經(jīng)定義好的一些預(yù)定義協(xié)議:
  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 /* 用戶添加的自定義協(xié)議 */

      如果我們?cè)?span lang="EN-US">Netlink協(xié)議簇里開發(fā)一個(gè)新的協(xié)議,只要在該文件中定義協(xié)議號(hào)即可,例如我們定義一種基于Netlink協(xié)議簇的、協(xié)議號(hào)是20的自定義協(xié)議,如上所示。同時(shí)記得,將內(nèi)核頭文件目錄中的netlink.h也做對(duì)應(yīng)的修改,在我的系統(tǒng)中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h

      接下來我們?cè)谟脩艨臻g以及內(nèi)核空間模塊的開發(fā)過程中就可以使用這種協(xié)議了,一共分為三個(gè)階段。


Stage 1

      我們首先實(shí)現(xiàn)的功能是用戶->內(nèi)核單向數(shù)據(jù)通信,即用戶空間發(fā)送一個(gè)消息給內(nèi)核,然后內(nèi)核將其打印輸出,就這么簡(jiǎn)單。用戶空間的示例代碼如下【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 /*消息最大負(fù)載為1024字節(jié)*/

  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(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //創(chuàng)建套接字
  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; /*我們的消息是發(fā)給內(nèi)核的*/
  8.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
  9.         
  10.     //將套接字和Netlink地址結(jié)構(gòu)體進(jìn)行綁定
  1.     if(-1 == 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是消息負(fù)載是一條空消息
  14.     nlh->nlmsg_flags = 0;

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

  17.     /*這個(gè)是模板,暫時(shí)不用糾結(jié)為什么要這樣用。有時(shí)間詳細(xì)講解socket時(shí)再說*/
  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向內(nèi)核發(fā)送消息

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

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

      另外,需要格外注意的就是Netlink的地址結(jié)構(gòu)體和其消息頭結(jié)構(gòu)中pid字段為0的情況,很容易讓人產(chǎn)生混淆,再總結(jié)一下:

 

0

netlink地址結(jié)構(gòu)體.nl_pid

1、內(nèi)核發(fā)出的多播報(bào)文

2、消息的接收方是內(nèi)核,即從用戶空間發(fā)往內(nèi)核的消息

netlink消息頭體. nlmsg_pid

來自內(nèi)核主動(dòng)發(fā)出的消息


     這個(gè)例子僅是從用戶空間到內(nèi)核空間的單向數(shù)據(jù)通信,所以Netlink地址結(jié)構(gòu)體中我們?cè)O(shè)置了dest_addr.nl_pid = 0,說明我們的報(bào)文的目的地是內(nèi)核空間;在填充Netlink消息頭部時(shí),我們做了nlh->nlmsg_pid = 0這樣的設(shè)置。

     需要注意幾個(gè)宏的使用:

     NLMSG_SPACE(MAX_PAYLOAD),該宏用于返回不小于MAX_PAYLOAD4字節(jié)對(duì)齊的最小長(zhǎng)度值,一般用于向內(nèi)存系統(tǒng)申請(qǐng)空間是指定所申請(qǐng)的內(nèi)存字節(jié)數(shù),和NLMSG_LENGTH(len)所不同的是,前者所申請(qǐng)的空間里不包含Netlink消息頭部所占的字節(jié)數(shù),后者是消息負(fù)載和消息頭加起來的總長(zhǎng)度。

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

     它們之間的關(guān)系如下:

     內(nèi)核空間的示例代碼如下【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);

     在內(nèi)核模塊的初始化函數(shù)里我們用

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

創(chuàng)建了一個(gè)內(nèi)核態(tài)的socket,第一個(gè)參數(shù)我們擴(kuò)展的協(xié)議號(hào);第二個(gè)參數(shù)為多播組號(hào),目前我們用不上,將其置為0;第三個(gè)參數(shù)是個(gè)回調(diào)函數(shù),即當(dāng)內(nèi)核的Netlink socket套接字收到數(shù)據(jù)時(shí)的處理函數(shù);第四個(gè)參數(shù)就不多說了。

 

      在回調(diào)函數(shù)nl_data_ready()中,我們不斷的從socket的接收隊(duì)列去取數(shù)據(jù),一旦拿到數(shù)據(jù)就將其打印輸出。在協(xié)議棧的INET層,用于存儲(chǔ)數(shù)據(jù)的是大名鼎鼎的sk_buff結(jié)構(gòu),所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然后通過NLMSG_DATA(nlh)定位到netlink的消息負(fù)載。

 

      將上述代碼編譯后測(cè)試結(jié)果如下:


Stage 2

      我們將上面的代碼稍加改造就可以實(shí)現(xiàn)用戶<->內(nèi)核雙向數(shù)據(jù)通信。

      首先是改造用戶空間的代碼:
  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 /*消息最大負(fù)載為1024字節(jié)*/

  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(-1 == (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; /*我們的消息是發(fā)給內(nèi)核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.     if(-1 == 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();//我們希望得到內(nèi)核回應(yīng),所以得告訴內(nèi)核我們ID號(hào)
  40.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
  41.     nlh->nlmsg_flags = 0;

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

  44.     /*這個(gè)是模板,暫時(shí)不用糾結(jié)為什么要這樣用。*/
  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向內(nèi)核發(fā)送消息

  52.     //接收內(nèi)核消息的消息
  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.     /* 關(guān)閉netlink套接字 */
  58.     close(sock_fd);
  59.     free(nlh);
  60.     return 0;
  61. }

      內(nèi)核空間的修改如下:

  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,因?yàn)槲覀円玫絥et/netlink.h中的某些API函數(shù),nlmsg_pug()*/

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

  12. struct sock *nl_sk = NULL;
  13. //向用戶空間發(fā)送消息的接口
  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申請(qǐng)空間
  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()來設(shè)置netlink消息頭部
  31.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  32.     // 設(shè)置Netlink的控制塊
  33.     NETLINK_CB(skb).pid = 0; // 消息發(fā)送者的id標(biāo)識(shí),如果是內(nèi)核發(fā)的則置0
  34.     NETLINK_CB(skb).dst_group = 0; //如果目的組為內(nèi)核或某一進(jìn)程,該字段也置0

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

  37.     //通過netlink_unicast()將消息發(fā)送用戶空間由dstPID所指定了進(jìn)程號(hào)的進(jìn)程
  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); //發(fā)送者的進(jìn)程ID我們已經(jīng)將其存儲(chǔ)在了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);

      重新編譯后,測(cè)試結(jié)果如下:


Stage 3

      前面我們提到過,如果用戶進(jìn)程希望加入某個(gè)多播組時(shí)才需要調(diào)用bind()函數(shù)。前面的示例中我們沒有這個(gè)需求,可還是調(diào)了bind(),心頭有些不爽。在前幾篇博文里有關(guān)于socket編程時(shí)幾個(gè)常見API的詳細(xì)解釋和說明,不明白的童鞋可以回頭去復(fù)習(xí)一下。

      因?yàn)?span lang="EN-US">Netlink是面向無連接的數(shù)據(jù)報(bào)的套接字,所以我們還可以用sendto()recvfrom()來實(shí)現(xiàn)數(shù)據(jù)的收發(fā),這次我們不再調(diào)用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 /*消息最大負(fù)載為1024字節(jié)*/

  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(-1 == (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; /*我們的消息是發(fā)給內(nèi)核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.    /*不再調(diào)用bind()函數(shù)了
  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();//我們希望得到內(nèi)核回應(yīng),所以得告訴內(nèi)核我們ID號(hào)
  40.    nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
  41.    nlh->nlmsg_flags = 0;

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

  44.    /*這個(gè)模板就用不上了。*/
  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); //不再用這種方式發(fā)消息到內(nèi)核
  54.    sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
  55.         
  56.    //接收內(nèi)核消息的消息
  57.    printf("waiting message from kernel!\n");
  58.    //memset((char*)NLMSG_DATA(nlh),0,1024);
  59.    memset(nlh,0,MAX_PAYLOAD); //清空整個(gè)Netlink消息頭包括消息頭和負(fù)載
  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.    /* 關(guān)閉netlink套接字 */
  64.    close(sock_fd);
  65.    free(nlh);
  66.    return 0;
  67. }

      內(nèi)核空間的代碼完全不用修改,我們?nèi)匀挥?span lang="EN-US">netlink_unicast()從內(nèi)核空間發(fā)送消息到用戶空間。

      重新編譯后,測(cè)試結(jié)果如下:

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


關(guān)于Netlink多播機(jī)制的用法

        在上一篇博文中我們所遇到的情況都是用戶空間作為消息進(jìn)程的發(fā)起者,Netlink還支持內(nèi)核作為消息的發(fā)送方的情況。這一般用于內(nèi)核主動(dòng)向用戶空間報(bào)告一些內(nèi)核狀態(tài),例如我們?cè)谟脩艨臻g看到的USB的熱插拔事件的通告就是這樣的應(yīng)用。

       先說一下我們的目標(biāo),內(nèi)核線程每個(gè)一秒鐘往一個(gè)多播組里發(fā)送一條消息,然后用戶空間所以加入了該組的進(jìn)程都會(huì)收到這樣的消息,并將消息內(nèi)容打印出來。

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

  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 /* 用戶添加的自定義協(xié)議 */

       在我們自己添加的NETLINK_TEST協(xié)議里,同樣地,最多允許我們?cè)O(shè)置32個(gè)多播組,每個(gè)多播組用1個(gè)比特表示,所以不同的多播組不可能出現(xiàn)重復(fù)。你可以根據(jù)自己的實(shí)際需求,決定哪個(gè)多播組是用來做什么的。用戶空間的進(jìn)程如果對(duì)某個(gè)多播組感興趣,那么它就加入到該組中,當(dāng)內(nèi)核空間的進(jìn)程往該組發(fā)送多播消息時(shí),所有已經(jīng)加入到該多播組的用戶進(jìn)程都會(huì)收到該消息。

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

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

  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消息的最大載荷的長(zhǎng)度

  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.     // 創(chuàng)建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; // 表示我們要從內(nèi)核接收多播消息。注意:該字段為0有雙重意義,另一個(gè)意義是表示我們發(fā)送的數(shù)據(jù)的目的地址是內(nèi)核。
  33.     src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號(hào)來自我們執(zhí)行程序時(shí)輸入的第一個(gè)參數(shù)

  34.     // 因?yàn)槲覀円尤氲揭粋€(gè)多播組,所以必須調(diào)用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消息申請(qǐng)存儲(chǔ)空間
  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.     // 從內(nèi)核接收消息
  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地址結(jié)構(gòu)體中的nl_groups的設(shè)置。由于對(duì)它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網(wǎng)上搜集資料。有分析不當(dāng)之處,還請(qǐng)大家?guī)臀抑赋觥?/font>

       內(nèi)核空間我們添加了內(nèi)核線程和內(nèi)核線程同步方法completion的使用。內(nèi)核空間修改后的代碼如下:
  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; //內(nèi)核線程對(duì)象

  14. //向用戶空間發(fā)送消息的接口
  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申請(qǐng)空間
  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()來設(shè)置netlink消息頭部
  32.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  33.     // 設(shè)置Netlink的控制塊里的相關(guān)信息
  34.     NETLINK_CB(skb).pid = 0; // 消息發(fā)送者的id標(biāo)識(shí),如果是內(nèi)核發(fā)的則置0
  35.     NETLINK_CB(skb).dst_group = 5; //多播組號(hào)為5,但置成0好像也可以。

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

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

  44. //每隔1秒鐘發(fā)送一條“I am from kernel!”消息,共發(fā)10個(gè)報(bào)文
  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);

       關(guān)于內(nèi)核中netlink_kernel_create(int unit, unsigned int groups,…)函數(shù)里的第二個(gè)參數(shù)指的是我們內(nèi)核進(jìn)程最多能處理的多播組的個(gè)數(shù),如果該值小于32,則默認(rèn)按32處理,所以在調(diào)用netlink_kernel_create()函數(shù)時(shí)可以不用糾結(jié)第二個(gè)參數(shù),一般將其置為0就可以了。

 

       在skbuff{}結(jié)構(gòu)體中,有個(gè)成員叫做"控制塊",源碼對(duì)它的解釋如下:

  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. }
       當(dāng)內(nèi)核態(tài)的Netlink發(fā)送數(shù)據(jù)到用戶空間時(shí)一般需要填充skbuff的控制塊,填充的方式是通過強(qiáng)制類型轉(zhuǎn)換,將其轉(zhuǎn)換成struct netlink_skb_parms{}之后進(jìn)行填充賦值的:
  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. };

       填充時(shí)的模板代碼如下:

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

       這里要注意的是在Netlink協(xié)議簇里提到的skbuffcb控制塊里保存的是屬于Netlink的私有信息。怎么講,就是Netlink會(huì)用該控制塊里的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數(shù)據(jù)。打個(gè)比方,以開車為例,開車的時(shí)候我們要做的就是打火、控制方向盤、適當(dāng)?shù)乜刂朴烷T和剎車,車就開動(dòng)了,這就是汽車提供給我們的“功能”。汽車的發(fā)動(dòng)機(jī),輪胎,傳動(dòng)軸,以及所用到的螺絲螺栓等都屬于它的“私有”數(shù)據(jù)cb。汽車要運(yùn)行起來這些東西是不可或缺的,但它們之間的協(xié)作和交互對(duì)用戶來說又是透明的。就好比我們Netlink的私有控制結(jié)構(gòu)struct netlink_skb_parms{}一樣。

       目前我們的例子中,將NETLINK_CB(skb).dst_group設(shè)置為相應(yīng)的多播組號(hào)和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請(qǐng)Netlink的大蝦們幫我點(diǎn)撥點(diǎn)撥。

       編譯后重新運(yùn)行,最后的測(cè)試結(jié)果如下:

       注意,這里一定要先執(zhí)行insmod加載內(nèi)核模塊,然后再運(yùn)行用戶空間的程序。如果沒有加載mynlkern.ko而直接執(zhí)行./test 5bind()系統(tǒng)調(diào)用時(shí)會(huì)報(bào)如下的錯(cuò)誤:

       bind failed: No such file or directory

       因?yàn)榫W(wǎng)上有寫文章在講老版本Netlink的多播時(shí)用法時(shí)先執(zhí)行了用戶空間的程序,然后才加載內(nèi)核模塊,現(xiàn)在(2.6.21)已經(jīng)行不通了,這一點(diǎn)請(qǐng)大家注意。

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


Linux中與內(nèi)核通信的Netlink機(jī)制(實(shí)例)

      Netlink在2.6版本的內(nèi)核中變化也是很大的,在最新的2.6.37內(nèi)核中,其定義已經(jīng)改成下面這種形式,傳遞的參數(shù)已經(jīng)達(dá)到6個(gè)。其中第一個(gè)參數(shù) 和mutex參數(shù)都是最新添加的。Mutex也可以為空。這里主要是關(guān)于內(nèi)核空間中的netlink函數(shù)的使用。

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是一個(gè)網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫(kù),有自己的一套net_device等等。默認(rèn)情況下都是使用 init_net這個(gè)全局變量,下面是內(nèi)核中調(diào)用netlink_kernel_create()函數(shù)的一個(gè)示例。
在內(nèi)核中,

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

模塊調(diào)用函數(shù) netlink_unicast 來發(fā)送單播消息:

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


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

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是一個(gè)網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫(kù),有自己的一套net_device等等。默認(rèn)情況下都是使用init_net這個(gè)全局變量

    參數(shù)unit表示netlink協(xié)議類型,如 NETLINK_MYTEST,參數(shù)input則為內(nèi)核模塊定義的netlink消息處理函數(shù),當(dāng)有消息到達(dá)這個(gè)netlink socket時(shí),該input函數(shù)指針就會(huì)被引用。函數(shù)指針input的參數(shù)skb實(shí)際上就是函數(shù)netlink_kernel_create返回的 struct sock指針,sock實(shí)際是socket的一個(gè)內(nèi)核表示數(shù)據(jù)結(jié)構(gòu),用戶態(tài)應(yīng)用創(chuàng)建的socket在內(nèi)核中也會(huì)有一個(gè)struct sock結(jié)構(gòu)來表示。

     函數(shù)input()會(huì)在發(fā)送進(jìn)程執(zhí)行sendmsg()時(shí) 被調(diào)用,這樣處理消息比較及時(shí),但是,如果消息特別長(zhǎng)時(shí),這樣處理將增加系統(tǒng)調(diào)用sendmsg()的執(zhí)行時(shí)間,也就是說當(dāng)用戶的程序調(diào)用sendmsg ()函數(shù)時(shí),如果input()函數(shù)處理時(shí)間過長(zhǎng),也就是說input()函數(shù)不執(zhí)行不完,用戶程序調(diào)用的sendmsg()函數(shù)就不會(huì)返回。只有當(dāng)內(nèi)核 空間中的input()函數(shù)返回時(shí),用戶調(diào)用的sendmsg()函數(shù)才會(huì)返回。對(duì)于這種情況,可以定義一個(gè)內(nèi)核線程專門負(fù)責(zé)消息接收,而函數(shù)input 的工作只是喚醒該內(nèi)核線程,這樣sendmsg將很快返回。(這里網(wǎng)上的的說明)不過在查看Linux2.6.37版本的內(nèi)核時(shí)并沒有發(fā)現(xiàn)這種處理過程, 一般都是按下面的方法進(jìn)行處理。

這里注冊(cè)的netlink協(xié)議為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()函數(shù)中進(jìn)行接收處理。
 

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


    前面的三個(gè)參數(shù)與 netlink_unicast相同,參數(shù)group為接收消息的多播組,該參數(shù)的每一個(gè)位代表一個(gè)多播組,因此如果發(fā)送給多個(gè)多播組,就把該參數(shù)設(shè)置為 多個(gè)多播組組ID的位或。參數(shù)allocation為內(nèi)核內(nèi)存分配類型,一般地為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表示消息發(fā)送者進(jìn)程 ID,也即源地址,對(duì)于內(nèi)核,它為 0, dst_pid 表示消息接收者進(jìn)程 ID,也即目標(biāo)地址,如果目標(biāo)為組或內(nèi)核,它設(shè)置為 0,否則 dst_group 表示目標(biāo)組地址,如果它目標(biāo)為某一進(jìn)程或內(nèi)核,dst_group 應(yīng)當(dāng)設(shè)置為 0。
   下面是參考網(wǎng)上使用netlink寫的和內(nèi)核通信的兩個(gè)程序,一個(gè)是用戶空間,一個(gè)是內(nèi)核空間。
內(nèi)核空間:

#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,3 * 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-m := netlink_k.o
KERNELBUILD := /lib/modules/`uname -r`/build
default:
    @echo "BUILE Kmod"
    @make -C $(KERNELBUILD) M=$(shell pwd) modules
    gcc -o netlink_2 netlink_2.c
clean:
    @echo " CLEAN kmod"
    @rm -rf *.o
    @rm -rf .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers .*.d

其中,netlink_k.c為內(nèi)核的空間的程序。
    先運(yùn)行內(nèi)核代碼netlink_k.ko,也就是在執(zhí)行完makefile文件后,會(huì)生成一個(gè)netlink_k.ko文件,可以使用下面的命令進(jìn)行安 裝,insmod netlink_k.ko,使用lsmod查看,當(dāng)安裝成功后,然后,執(zhí)行./netlink用戶空間程序,可以在另一個(gè)終端下執(zhí)行dmesg命令,查看 內(nèi)核通信的情況。這里netlink程序向內(nèi)核空間發(fā)送一個(gè)hello you!內(nèi)核返回給一個(gè)I am from kernel!在這里使用了一個(gè)定時(shí)器,也就是每3秒中發(fā)送一次I am from kernel!只有內(nèi)核把10個(gè)字符串全部發(fā)送完畢后,用戶空間的sendmsg()才會(huì)返回,也就是在用戶空間的netlink才會(huì)輸出內(nèi)核空間發(fā)送過 來的數(shù)據(jù),這里只有一個(gè)簡(jiǎn)單的程序,并沒有什么實(shí)際的意義,因?yàn)?,正如前面所說的一般情況下不會(huì)在回調(diào)函數(shù)中處理太多的東西,以免sendmsg()函數(shù) 返回不及時(shí)。下面是使用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!'.

參考網(wǎng)址:

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

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

Linux 系統(tǒng)內(nèi)核空間與用戶空間通信的實(shí)現(xiàn)與分析

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多