linux應(yīng)用程序——netlink的部分使用方法

一溪王、前言

在 嵌入式linux 中沉迹,應(yīng)用程序常常需要和內(nèi)核做通信,其中我們熟悉的方法有系統(tǒng)調(diào)用开睡,異步IO等因苹,但這些只能用于單工通信,即應(yīng)用程序主動跟內(nèi)核通信或者內(nèi)核發(fā)送信號給應(yīng)用程序篇恒,在某些場合中并不使用扶檐。而本文將介紹一種雙工通信方法——netlink,它即可以在內(nèi)核中主動傳輸數(shù)據(jù)胁艰,有可以在應(yīng)用程序中發(fā)送數(shù)據(jù)款筑,如同我們在做網(wǎng)編編程一樣智蝠。本文就講述 netlink 在嵌入式中常用的 2 種使用方法:數(shù)據(jù)傳輸獲取uevent事件信息

二奈梳、netlink

2.1 通信方式總結(jié)

應(yīng)用程序和內(nèi)核通信的常用方式如下:

  • 系統(tǒng)調(diào)用:常見的有 write杈湾、read、ioctl 等等攘须,它需要應(yīng)用程序主動向內(nèi)核寫入或讀取數(shù)據(jù)漆撞,是一種同步的單工數(shù)據(jù)傳輸方式
  • /proc文件系統(tǒng):同 系統(tǒng)調(diào)用 類似
  • 異步IO:可以通過編寫驅(qū)動代碼,使得內(nèi)核在某些時刻主動向應(yīng)用程序發(fā)送信號于宙,但無法傳輸大量數(shù)據(jù)浮驳。
  • netlink:是一種同步或者異步的數(shù)據(jù)傳輸方式,使用 netlink 在應(yīng)用程序和內(nèi)核建立起連接后即可進行雙工數(shù)據(jù)發(fā)送

2.2 netlink 優(yōu)點

  • 支持全雙工捞魁、異步通信
  • 用戶空間使用標準的socket接口即可進行通信
  • 支持多播
  • 在內(nèi)核端可用于進程上下文與中斷上下文

2.3 netlink 常見應(yīng)用

目前 linux內(nèi)核 已經(jīng)使用 netlink 實現(xiàn)了多種功能應(yīng)用至会,關(guān)于功能使用后面會有部分講解。下面羅列出幾個常用的功能署驻,如下:

  • 獲取或修改路由信息
  • 監(jiān)聽TCP協(xié)議數(shù)據(jù)報文
  • 防火墻
  • netfilter子系統(tǒng)
  • 內(nèi)核事件向用戶態(tài)通知

2.4 netlink 協(xié)議

為什么把 netlink 稱之為協(xié)議呢?
按照筆者的理解健霹,netlink 很想網(wǎng)絡(luò)編程旺上,因為它有一定的格式要求,是通過發(fā)送 消息 來完成數(shù)據(jù)傳輸?shù)奶锹瘛D敲此托枰?消息格式協(xié)議流程宣吱。說得簡單一些,就是 netlink 是一種通信方式瞳别,是內(nèi)核提供的一種功能征候。按照內(nèi)核的只提供機制不提供策略的理念,netlink 本身是不具備 協(xié)議流程 這個概念的祟敛。但我們要使用好 netlink疤坝,往往需要我們?nèi)ブ付〝?shù)據(jù)的傳輸流程,所以就需要有一定的 數(shù)據(jù)格式傳輸流程馆铁。其中 數(shù)據(jù)格式 可以看成就是 消息跑揉,是內(nèi)核已經(jīng)做好的固定格式,而 傳輸流程 則是 協(xié)議流程埠巨。當然了历谍,另一方面則是因為 netlink 用就是 socket套接字 那一套編程接口,所以十分像是網(wǎng)絡(luò)協(xié)議辣垒。

2.4.1 消息

消息netlink 的主要發(fā)送單元望侈,也就是 netlink 發(fā)送的數(shù)據(jù)只能是以 消息 為單位。 消息 的組成是 netlink消息頭有效載荷(數(shù)據(jù))勋桶。從內(nèi)存的角度來講脱衙,就是 有效載荷 是直接放在 netlink消息頭 后面的侥猬,也就是前面 16個字節(jié)netlink消息頭,后面的全部都是 有效載荷岂丘。

netlink消息頭 組成如下:

消息組成

  • 總長度:包括 netlink消息頭 在內(nèi)的總字節(jié)數(shù)陵究,長度為 32bit
  • 消息類型:表示 消息 的類型,往往用于協(xié)議流程奥帘。內(nèi)核定義了多個標準的消息類型铜邮。
    內(nèi)核已經(jīng)使用 netlink 實現(xiàn)了多種協(xié)議,每個 協(xié)議都有特定的功能寨蹋,所以每個 協(xié)議 都可能定義了額外的消息類型松蒜。長度為 16bit
  • 消息標志:可以用來更改 消息類型 的行為,某些 協(xié)議 有可能會用到該字段已旧。長度為 16bit
  • 序列號:該項是 可選項秸苗,類似 TCP協(xié)議 中的報文號。長度為 32bit
  • 端口號:表示 消息 發(fā)往的進程运褪。如果 消息 沒有指定端口號惊楼,那么會被發(fā)送給 同一個協(xié)議中第一個匹配的內(nèi)核端套接字〗斩铮可以理解為網(wǎng)絡(luò)編程中用于 尋址 的數(shù)據(jù)檀咙,端口號 一般是當前進程的 進程號。如果 端口號 為 0 璃诀,則是表明消息需要發(fā)往 內(nèi)核

關(guān)于 消息類型消息標志 的信息可以參考附錄中的《libnl庫應(yīng)用詳解(一)》

2.4.2 協(xié)議

這里簡單的說一下內(nèi)核支持的 netlink 協(xié)議弧可,在 include/uapi/linux/netlink.h 中已經(jīng)定義了一些 協(xié)議類型 的宏,每個宏都代表一種協(xié)議劣欢,內(nèi)核最多支持 32 種協(xié)議棕诵。如下所示:

#define NETLINK_ROUTE       0   /* Routing/device hook              */
#define NETLINK_UNUSED      1   /* Unused number                */
#define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
#define NETLINK_FIREWALL    3   /* Unused number, formerly ip_queue     */
#define NETLINK_SOCK_DIAG   4   /* socket monitoring                */
#define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
#define NETLINK_XFRM        6   /* ipsec */
#define NETLINK_SELINUX     7   /* SELinux event notifications */
#define NETLINK_ISCSI       8   /* Open-iSCSI */
#define NETLINK_AUDIT       9   /* auditing */
#define NETLINK_FIB_LOOKUP  10  
#define NETLINK_CONNECTOR   11
#define NETLINK_NETFILTER   12  /* netfilter subsystem */
#define NETLINK_IP6_FW      13
#define NETLINK_DNRTMSG     14  /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */
#define NETLINK_GENERIC     16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
#define NETLINK_ECRYPTFS    19
#define NETLINK_RDMA        20
#define NETLINK_CRYPTO      21  /* Crypto layer */
#define NETLINK_SMC     22  /* SMC monitoring */

#define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG

#define MAX_LINKS 32    

NETLINK_ROUTE 協(xié)議為例子,它可以獲取和修改設(shè)備的路由信息凿将。下面是它支持的一些 消息類型:

RTM_NEWLINK        創(chuàng)建獲取網(wǎng)絡(luò)設(shè)備的信息
RTM_DELLINK        刪除網(wǎng)絡(luò)設(shè)備的信息
RTM_GETLINK        獲取網(wǎng)絡(luò)設(shè)備的信息
RTM_NEWADDR        創(chuàng)建網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_DELADDR        刪除網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_GETADDR        獲取網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_NEWROUTE       創(chuàng)建網(wǎng)絡(luò)設(shè)備的路由信息
RTM_DELROUTE       刪除網(wǎng)絡(luò)設(shè)備的路由信息
RTM_GETROUTE       獲取網(wǎng)絡(luò)設(shè)備的路由信息 
RTM_NEWNEIGH       創(chuàng)建網(wǎng)絡(luò)設(shè)備的相鄰信息
RTM_DELNEIGH       刪除網(wǎng)絡(luò)設(shè)備的相鄰信息
RTM_GETNEIGH       獲取網(wǎng)絡(luò)設(shè)備的相鄰信息 
RTM_NEWRULE        創(chuàng)建路由規(guī)則信息
RTM_DELRULE        刪除路由規(guī)則信息
RTM_GETRULE        獲取路由規(guī)則信息 
RTM_NEWQDISC       創(chuàng)建隊列的原則
RTM_DELQDISC       刪除隊列的原則
RTM_GETQDISC       獲取隊列的原則 
RTM_NEWTCLASS      創(chuàng)建流量的類別
RTM_DELTCLASS      刪除流量的類別
RTM_GETTCLASS      獲取流量的類別 
RTM_NEWTFILTER     創(chuàng)建流量的過慮
RTM_DELTFILTER     刪除流量的過慮
RTM_GETTFILTER     獲取流量的過慮

除了 消息類型 之外校套,有效載荷 也是使用一些指定的數(shù)據(jù)結(jié)構(gòu),比如 ifinfomsg 牧抵、rtattr搔确。因為筆者對此并不熟悉,所以不做過多描述灭忠。只是為了舉例讓讀者門理解 netlink協(xié)議 的概念膳算,有興趣的讀者可以從其他途徑查找資料。

2.5 接口及數(shù)據(jù)結(jié)構(gòu)

在前面講了關(guān)于 netlink 的基本要點中弛作,我們知道 netlink消息頭 是數(shù)據(jù)傳輸?shù)倪^程中需要的基本格式涕蜂,內(nèi)核也已經(jīng)幫我們實現(xiàn)了一些數(shù)據(jù)結(jié)構(gòu)及接口來幫助我們實現(xiàn)相關(guān)功能。
netlink 的接口分為 應(yīng)用層接口內(nèi)核接口映琳,我們分別需要在 應(yīng)用層實現(xiàn)策略机隙,然后在 內(nèi)核實現(xiàn)機制蜘拉。所以一個基本的自定義 netlink協(xié)議 需要分別在 應(yīng)用層內(nèi)核 中有代碼實現(xiàn)

2.5.2 應(yīng)用層接口

前面說了 netlink 可以直接使用 socket套接字 的標準接口直接進行代碼編寫,也就是說 socket 有鹿、 bind 等接口都可以直接使用旭旭。
而按照筆者理解,netlink 的編程與 UDP 編程類似葱跋,但在網(wǎng)絡(luò)編程中我們是使用 IP地址 + 端口號 進行尋址的持寄,而 netlink 則是通過 協(xié)議類型+進程ID 進行尋址的腋颠,其中 協(xié)議類型 會在調(diào)用 socket接口 的時候指定坚弱。
netlink 基本的編程步驟如下:

  1. 使用 socket 聲明套接字
  2. 使用 bind 綁定 本地地址 到套接字
  3. 構(gòu)造 消息
  4. 發(fā)送或接收 消息,而在 netlink 中有 2 套發(fā)送及接收數(shù)據(jù)的接口灶似,分別是 sendto和recvfrom荠卷、sendmsg和recvmsg

2.5.2.1 socket

socket 在網(wǎng)絡(luò)編程中也需要用到模庐,用于聲明一個套接字、但在 netlink 中油宜,它的參數(shù)有所不同掂碱。socket接口 的原型如下:

int socket(int domain, int type, int protocol);

netlink 中的參數(shù)如下:

  • domain:用于聲明協(xié)議簇,在 netlink 中一般為 AF_NETLINK慎冤。
  • type:用于聲明套接字類型疼燥,在 netlink 中一般為 SOCK_RAW
  • protocol:用于聲明 協(xié)議類型粪薛,在 netlink 中可以是內(nèi)核支持的 協(xié)議悴了,也可以是 自定義協(xié)議

例子如下所示:

    #define NETLINK_TEST 23//自定義協(xié)議
    ......
    int skfd = 0;
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);

2.5.2.2 bind

bind 接口用于綁定套接字和地址搏恤,原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

netlink 使用的 地址數(shù)據(jù)結(jié)構(gòu) 與網(wǎng)絡(luò)編程不同违寿,如下所示:

struct sockaddr_nl {
     __kernel_sa_family_t    nl_family;  //一般為AF_NETLINK
     unsigned short          nl_pad;     //無需填充
     __u32                   nl_pid;     //與內(nèi)核通信的進程的進程ID,0 則代表地址為內(nèi)核
     __u32                   nl_groups;  //多播組號熟空,netlink支持多播
};

例子如下:

    struct sockaddr_nl nlsrc_addr = {0};
    /* 設(shè)置本地socket地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;

    /*綁定套接字*/
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
        printf("bind addr error\n");   
        return -1;
    }

2.5.2.1 sendto及recvfrom

上面分別完成了 聲明套接字本地地址綁定套接字 的 2 個前置步驟藤巢,完成之后我們就可以 構(gòu)造和發(fā)送消息netlink 有 2 套接收和發(fā)送消息的接口息罗,本文主要講述 sendto和recvfrom掂咒,對于 sendmsg和recvmsg 有興趣的朋友可以參考附錄中的《內(nèi)核與用戶層通信之netlink》

netlink 的基本數(shù)據(jù)單元是 消息,那么 sendto和recvfromnetlink中 的基本接收單元也就是 消息迈喉。
消息 = netlink消息頭 + 有效載荷绍刮,netlink消息頭 的數(shù)據(jù)結(jié)構(gòu)如下,與 2.4.1節(jié) 中的圖片一一對應(yīng):

struct nlmsghdr {   
    __u32       nlmsg_len;  //包括netlink消息頭在內(nèi)挨摸,整個消息的程度
    __u16       nlmsg_type; //消息類型
    __u16       nlmsg_flags; //消息標志
    __u32       nlmsg_seq;  //消息報文的序列號
    __u32       nlmsg_pid;  //發(fā)送進程的進程ID
};

構(gòu)造消息 免不了需要進行一些 長度地址 的計算孩革,內(nèi)核已經(jīng)提供了一些宏幫助我們進行此類操作。如下所示:

#define NLMSG_ALIGNTO   4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字節(jié)對齊的最小數(shù)值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

/* Netlink 頭部長度 */
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))

/* 計算消息數(shù)據(jù)len的真實消息長度(消息體 + netlink消息頭)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字節(jié)對齊的最小數(shù)值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

/* 宏NLMSG_DATA(nlh)用于取得消息的數(shù)據(jù)部分的首地址得运,設(shè)置和讀取消息數(shù)據(jù)部分時需要使用該宏 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

/* 宏NLMSG_NEXT(nlh,len)用于得到下一個消息的首地址, 同時len 變?yōu)槭S嘞⒌拈L度膝蜈,一般用于分片消息中 */
#define NLMSG_NEXT(nlh,len)  ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
                  (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

/* 判斷消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len <= (len))

/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的長度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

而我們常用的一般有下面幾個:

  • NLMSG_SPACE:參數(shù)是 有效載荷的長度锅移,其計算結(jié)果是包含 netlink消息頭 在內(nèi)的 消息長度
  • NLMSG_DATA:參數(shù)是 netlink消息頭地址,計算結(jié)果是 有效載荷的首地址

sentorecvfrom 的原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

從原型中我們可有看到饱搏,都分別需要傳入 地址 來指定 發(fā)送目的地接收源地址非剃。

實例如下:


#define PAYLOAD_LEN 128
/* 自定義消息類型 */
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

/* 使用結(jié)構(gòu)體將netlink消息頭和有效載荷捆綁為一個結(jié)構(gòu)體,編程時直接對結(jié)構(gòu)體操作即可 */
typedef struct _user_msg_t
{
    struct nlmsghdr hdr;//netlink消息頭
    char   paylaod[PAYLOAD_LEN];//有效載荷
}user_msg_t;

int main()
{
    struct sockaddr_nl nlsrc_addr = {0};
    struct sockaddr_nl nldst_addr = {0};
    user_msg_t user_msg_send = {0};
    user_msg_t user_msg_recv = {0};
    ......

    /* 設(shè)置目的socket地址 */
    nldst_addr.nl_family = AF_NETLINK;
    nldst_addr.nl_pid = 0;//0表示內(nèi)核netlink地址
    nldst_addr.nl_groups = 0;

    /* 構(gòu)造發(fā)送消息 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_REQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;

    /* 發(fā)送消息 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);  

    /* 等待并接收消息 */
    recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
}

2.5.3 內(nèi)核接口

netlink的內(nèi)核實現(xiàn)步驟與應(yīng)用層相似推沸,但相對來說比應(yīng)用層靈活一些备绽。其步驟大致如下:

  1. 創(chuàng)建socket套接字
  2. 構(gòu)造消息
  3. 發(fā)送消息

netlink 內(nèi)核接口成分比較復(fù)雜,涉及到個不同的頭文件坤学,下面簡單的說明一下各個內(nèi)核接口所在的頭文件疯坤。

2.5.3.1 netlink模塊接口

該模塊的接口一般是實現(xiàn) netlink 的功能,比如創(chuàng)建socket套接字深浮、釋放socket套接字压怠、單播多播 等飞苇,其頭文件是 include/linux/netlink.h菌瘫。下面羅列幾個常用的接口及數(shù)據(jù)結(jié)構(gòu)

struct netlink_kernel_cfg {
    unsigned int    groups;
    unsigned int    flags;
    void            (*input)(struct sk_buff *skb);
    struct mutex    *cb_mutex;
    int             (*bind)(struct net *net, int group);
    void            (*unbind)(struct net *net, int group);
    bool            (*compare)(struct net *net, struct sock *sk);
};

static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
void netlink_kernel_release(struct sock *sk);
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid, __u32 group, gfp_t allocation);
  • netlink_kernel_create 用于創(chuàng)建套接字,其參數(shù)如下:

    • net: 一般為 &init_net
    • unit:指定協(xié)議類型布卡,與用戶空間的 socket接口 對應(yīng)
    • cfg :該參數(shù)是 struct netlink_kernel_cfg結(jié)構(gòu)體指針雨让,其常用成員是 inputgroupsinput 是接收到消息時的 回調(diào)函數(shù)忿等,我們一般在這個 回調(diào)函數(shù) 內(nèi)完成數(shù)據(jù)接收栖忠。groups 用于指定 套接字的多播組
  • netlink_kernel_release 用于釋放套接字,參數(shù)如下:

    • sk:是由 netlink_kernel_create 創(chuàng)建的套接字贸街。
  • netlink_unicast 用于單播傳輸數(shù)據(jù)庵寞,參數(shù)如下:

    • ssk:由 netlink_kernel_create 創(chuàng)建的套接字
    • skbstruct sk_buff 結(jié)構(gòu)體,會將需要傳輸?shù)?消息 放在其中薛匪。
    • portid:指定發(fā)送目的進程的PID
    • nonblock:指定阻塞標志捐川,默認情況下為阻塞,可以使用 MSG_DONTWAIT 指定為非阻塞

2.5.3.2 skb模塊接口

struct sk_buff 結(jié)構(gòu)體是內(nèi)核實現(xiàn)的應(yīng)用于網(wǎng)絡(luò)系統(tǒng)的數(shù)據(jù)結(jié)構(gòu)體逸尖,而 netlink 的部分接口也需要使用到古沥。該結(jié)構(gòu)體的實現(xiàn)及其接口比較復(fù)雜,其頭文件在 include/linux/skbuff.h娇跟。筆者對這一部分不甚了解岩齿,所以這里簡單的羅列幾個常用到的接口并簡單講述一下其使用場景,原型如下:

static inline struct sk_buff *skb_get(struct sk_buff *skb)
void kfree_skb(struct sk_buff *skb);
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
  • alloc_skb:用于開辟緩存塊苞俘,一般是在 發(fā)送 時使用

    • size:指定緩存塊大小盹沈,在 netlink 中可以使用宏 NLMSG_SPACE 來計算其大小
    • priority:一般指定為 GFP_KERNEL
  • skb_get:用于增加變量 skb 的計數(shù),一般是在 接收回調(diào) 中對回調(diào)的參數(shù)使用苗胀。

  • kfree_skb:用于減少變量 skb 的計數(shù)襟诸,當計數(shù)為 0 時則釋放緩存塊瓦堵。

2.5.3.3 消息模塊接口

在實現(xiàn) netlink 是需要對消息進行構(gòu)造或者計算等操作,內(nèi)核為我們提供一系列的方法歌亲,其頭文件為inlcude/net/netlink.h
nlmsg_put 可以用于直接構(gòu)造 消息菇用,一般是在 發(fā)送 消息時使用,其原型如下:

static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)
  • skb:指定作為 有效載荷 的 **struct sk_buff ** 結(jié)構(gòu)體陷揪,一般是使用 alloc_skb 開辟
  • portid:與 netlink消息頭 中的 nlmsg_pid 對應(yīng)惋鸥。
  • seq:與 netlink消息頭 中的 nlmsg_seq 對應(yīng)。
  • type:與 netlink消息頭 中的 nlmsg_type 對應(yīng)悍缠。
  • payload:與 netlink消息頭 中的 nlmsg_len 對應(yīng)卦绣。
  • flags:與 netlink消息頭 中的 nlmsg_flags 對應(yīng)。

2.5.3.4 內(nèi)核netlink步驟

看完以上的接口飞蚓,我們就可以大致總結(jié)一下內(nèi)核 netlink 實現(xiàn)的基本步驟:

  1. 實現(xiàn)數(shù)據(jù)的接收回調(diào)
  2. 注冊接收回調(diào)并創(chuàng)建socket
  3. 實現(xiàn)發(fā)送函數(shù)
  4. 根據(jù)協(xié)議需要編寫流程代碼

2.6 代碼例程

下面我們看看示例代碼滤港,示例代碼中實現(xiàn)了一個簡單的協(xié)議,協(xié)議流程如下:

  1. 應(yīng)用程序請求驅(qū)動趴拧,請求消息不需要設(shè)置有效載荷
  2. 驅(qū)動對請求進行應(yīng)答溅漾,成功返回 0,不成功返回 1
  3. 成功后應(yīng)用程序可以一直接收來自驅(qū)動的外部數(shù)據(jù)
  4. 成功請求的程序退出后需要向驅(qū)動釋放請求著榴,以便其他程序可以繼續(xù)獲取驅(qū)動的外部數(shù)據(jù)

可以看出添履,其功能就是:外部程序通過對驅(qū)動寫入數(shù)據(jù),這些數(shù)據(jù)會通過驅(qū)動發(fā)送請求成功的應(yīng)用程序脑又。

PS:驅(qū)動層代碼因為涉及到一些其他代碼暮胧,所以筆者對netlink意外的代碼做了刪減,但整體上不影響閱讀

應(yīng)用層代碼如下:

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

#define NETLINK_TEST 23//自定義協(xié)議
#define PAYLOAD_LEN 128

/* 自定義消息類型 */
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

/* 使用結(jié)構(gòu)體將netlink消息頭和有效載荷捆綁為一個結(jié)構(gòu)體问麸,編程時直接對結(jié)構(gòu)體操作即可 */
typedef struct _user_msg_t
{
    struct nlmsghdr hdr;
    char   paylaod[PAYLOAD_LEN];
}user_msg_t;

int main()
{
    struct nlmsghdr nlmsg_hdr = {0};
    struct sockaddr_nl nlsrc_addr = {0};
    struct sockaddr_nl nldst_addr = {0};
    user_msg_t user_msg_send = {0};
    user_msg_t user_msg_recv = {0};
    int skfd = 0;
    int addr_len = 0;
    int recv_len = 0;
    int send_len = 0;
    char ack = 0;

    addr_len = sizeof(struct sockaddr_nl);
    /* 創(chuàng)建套接字 */
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(skfd < 0)
    {
        printf("can not create a netlink socket\n");
        return 0;
    }

    /* 設(shè)置本地socket地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;

    /*綁定套接字*/
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
        printf("bind addr error\n");   
        return -1;
    }

    /* 設(shè)置目的socket地址 */
    nldst_addr.nl_family = AF_NETLINK;
    nldst_addr.nl_pid = 0;//0表示內(nèi)核netlink地址
    nldst_addr.nl_groups = 0;

    /* 設(shè)置請求消息體, 向驅(qū)動發(fā)送請求往衷,請求消息不需要設(shè)置有效載荷 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_REQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;

    /* 發(fā)送請求 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);  

    /* 接收來自驅(qū)動的應(yīng)答 */
    recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);

    /* 獲取應(yīng)答數(shù)據(jù)并根據(jù)應(yīng)答來判斷 */
    ack = user_msg_recv.paylaod[0];
    if(-1 == ack)
    {
        printf("can't get request\n");
        return -1;
    }

    while(1)
    {
        /* 接收內(nèi)核空間返回的數(shù)據(jù), recvfrom默認阻塞 */
        recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
        printf("recv data = %s\n", user_msg_recv.paylaod);
        /* 如果外部數(shù)據(jù)外exit字符串,則退出程序 */
        if(!strncmp(user_msg_recv.paylaod, "exit", 4))
            break;
    }

    /* 設(shè)置反請求消息體, 讓驅(qū)動釋放清奇 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_UNREQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;    
    /* 發(fā)送反請求消息體 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);

    return 0;  
}

驅(qū)動代碼 如下:

#include <linux/module.h>   
#include <linux/init.h>  
#include <linux/fs.h>   
#include <linux/device.h>   
#include <linux/slab.h>  
#include <linux/cdev.h>  
#include <linux/err.h>  
#include <asm/uaccess.h>  
#include <linux/io.h>  
#include <linux/of.h>  
#include <linux/of_gpio.h>  
#include <linux/gpio.h>  
#include <linux/platform_device.h>
#include <linux/kern_levels.h>
#include <linux/ioport.h>      
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <net/sock.h>
#include <linux/netlink.h>
#include <linux/semaphore.h>

#define NETLINK_TEST 23
#define PAYLOAD_LEN 128
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

typedef struct _gpio_nl_info_t
{
    pid_t user_pid;
    int request_flag;
    struct semaphore flag_lock;
}gpio_nl_info_t;

struct gpio_device
{
    ......
    struct sock* gpio_sock;
    gpio_nl_info_t gpio_nl_info;
};
struct gpio_device* g_gpio_device  = NULL;
int gpio_netlink_send(struct sock* nl_sock, int dst_pid, char* data, int len)
{
    if(NULL == data)
    {
        printk(KERN_INFO"data is NULL\n");
        return -1;
    }
    if(len > PAYLOAD_LEN)
    {
        printk(KERN_INFO"length(%d) is out of range\n", len);
        return -1;
    }
    struct nlmsghdr *nl_msghdr = NULL;
    struct sk_buff  *skb_send  = NULL;
    int data_len = 0;

    /* 使用宏NLMSG_SPACE計算包括消息頭在內(nèi)的消息長度 */
    data_len = NLMSG_SPACE(PAYLOAD_LEN);

    /* 開辟skb緩存塊 */
    skb_send = alloc_skb(data_len, GFP_KERNEL);
    if(NULL == skb_send)
    {
        printk(KERN_INFO"alloc skb error\n");
        return -1;
    }
    
    /* 使用nlmsg_put構(gòu)造消息結(jié)構(gòu)體 */
    nl_msghdr = nlmsg_put(skb_send, 0, 0, 0, data_len - NLMSG_SPACE(0), 0);

    /* 將要發(fā)送的數(shù)據(jù)賦值到消息上 */
    memcpy(NLMSG_DATA(nl_msghdr), data, len);

    /* 發(fā)送消息 */
    netlink_unicast(nl_sock, skb_send, dst_pid, 0);

    return 0;
}


void gpio_netlink_input(struct sk_buff *__skb)
{
    if(__skb == NULL)
    {
        printk(KERN_INFO"sk_buff is NULL\n");
        return;
    }

    struct sk_buff *skb = NULL;
    struct nlmsghdr *nl_msghdr = NULL;
    char* data = NULL;
    char ack = 0;

    /* 獲取__skb的使用權(quán) */
    skb = skb_get(__skb);

    /* 1. 獲取消息頭 */
    nl_msghdr = nlmsg_hdr(skb);
    if(skb->len < NLMSG_SPACE(0))
    {
        printk(KERN_INFO"message length error, len = %d\n", skb->len);
        goto INPUT_EXIT;
    }

    /* 2. 根據(jù)消息類型進行相應(yīng)處理 */
    switch(nl_msghdr->nlmsg_type)
    {
        /* 2.1 請求占用 */
        case GPIO_NLMSG_REQUEST:
            /* 獲取鎖以改變pid占用標志 */
            down(&g_gpio_device->gpio_nl_info.flag_lock);
            /* 如果當前netlink的pid已經(jīng)被占用 */
            if(1 == g_gpio_device->gpio_nl_info.request_flag)
            {
                /* 如果當前進程再一次請求則提示不需要再次請求 */
                if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
                    printk(KERN_INFO"can not get request again\n");
                else/* 如果其他進程請求則提示當前有其他用戶在占用 */
                    printk(KERN_INFO"other user get request\n");
                /* 請求失敗返回 -1 給用戶空間以做其他處理 */
                ack = -1;
                gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1); 
            }
            else
            {
                /* 如果當前netlink的pid沒被占用, 則設(shè)置相關(guān)數(shù)據(jù) */
                g_gpio_device->gpio_nl_info.request_flag = 1;
                g_gpio_device->gpio_nl_info.user_pid = nl_msghdr->nlmsg_pid;
                /* 返回 0 給用戶空間表示成功 */
                ack = 0;
                gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1);
            }
            /* 釋放鎖 */
            up(&g_gpio_device->gpio_nl_info.flag_lock);
            break;
        /* 2.1 釋放占用 */
        case GPIO_NLMSG_UNREQUEST:
            down(&g_gpio_device->gpio_nl_info.flag_lock);
            /* 只有占用標志為 1 的情況下才會執(zhí)行操作 */
            if(1 == g_gpio_device->gpio_nl_info.request_flag)
            {
                /* 只有占用的進程才能釋放請求 */
                if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
                {
                    /* 設(shè)置占用標志和pid以釋放請求 */
                    g_gpio_device->gpio_nl_info.request_flag = 0;
                    g_gpio_device->gpio_nl_info.user_pid = 0;  
                }
                else/* 其他進程無權(quán)釋放請求 */
                    printk(KERN_INFO"you have no right to release the request\n");
            }
            up(&g_gpio_device->gpio_nl_info.flag_lock);                   
            break;
        /* 2.1 默認情況則打印數(shù)據(jù)用于調(diào)試 */
        default:
            data = NLMSG_DATA(nl_msghdr);
            printk(KERN_INFO"netlink get data_len = %d, pid = %d, data = %s\n", (nl_msghdr->nlmsg_len - NLMSG_SPACE(0)), nl_msghdr->nlmsg_pid, data);
            break;
    }
INPUT_EXIT:
    /* 釋放__skb的使用權(quán), 如果此時skb的計數(shù)為 0 則會釋放出內(nèi)存 */
    kfree_skb(skb);
    return;
}

static int gpio_open(struct inode* inode, struct file* filp)
{   
    printk(KERN_INFO"device open, filp = %#x, f_owner.pid = %#p\n", filp, filp->f_owner.pid);
    filp->private_data = (void*)container_of(inode->i_cdev, struct gpio_device, cdev);
    return 0;
}

static ssize_t gpio_write(struct file* filp, const char __user * buf, size_t size, loff_t* ppos)
{
    struct gpio_device* gpio_device = (struct gpio_device*)filp->private_data;
    char gpio_value = 0;
    char* gpio_buf = kzalloc(size, GFP_KERNEL);
    copy_from_user(gpio_buf, buf, size);    

    /* 將數(shù)據(jù)發(fā)往應(yīng)用層 */
    gpio_netlink_send(gpio_device->gpio_sock, gpio_device->gpio_nl_info.user_pid, gpio_buf, size);
    kfree(gpio_buf);

    return size;
}
static struct file_operations gpio_fops = 
{
    .open = gpio_open,
    .write = gpio_write,
};
static int gpio_probe(struct platform_device *pdev)
{
    int ret = 0;

    /* 為設(shè)備數(shù)據(jù)分配空間 */
    struct gpio_device* gpio_device = devm_kzalloc(&pdev->dev, sizeof(struct gpio_device), GFP_KERNEL);

    struct device_node *np = pdev->dev.of_node;
    if(!gpio_device)
    {
        printk(KERN_INFO"can't create gpio_device\n", gpio_device->gpio_num);
        goto ALLOC_FAIL;
    }
    ......

    /* 創(chuàng)建netlink的socket */    
    struct netlink_kernel_cfg nl_cfg = {
        .input = gpio_netlink_input,
    };

    gpio_device->gpio_sock = netlink_kernel_create(&init_net, NETLINK_TEST, &nl_cfg);
    if(NULL == gpio_device->gpio_sock)
    {
        printk(KERN_INFO"create socket error\n");
        goto DEVICE_FAILE;
    }
    /* 初始化信號量 */
    sema_init(&gpio_device->gpio_nl_info.flag_lock, 1);
    g_gpio_device = gpio_device;
    return 0;

    ......
    
}  
static int gpio_remove(struct platform_device *pdev)
{    
    struct gpio_device* gpio_device = (struct gpio_device*)platform_get_drvdata(pdev);
    int n_num = 1;
    
    netlink_kernel_release(gpio_device->gpio_sock);
    ...
    return 0;
}  

static const struct of_device_id of_gpio_match[] = {
    { .compatible = "gpio_node", .data = NULL},
    {},
};
static struct platform_driver gpio_driver = {
    .probe  = gpio_probe,
    .remove = gpio_remove,
    .driver = {
        .name   = "gpio_driver",
        .of_match_table = of_gpio_match,
    },
};

module_platform_driver(gpio_driver);
MODULE_LICENSE("GPL");  

2.7 獲取uevent事件信息

我們前面已經(jīng)講了 netlink 用于通信的使用方法口叙,那么本節(jié)說說如何使用 netlink 捕捉 uevent事件炼绘。

2.7.1 捕獲 uevent事件 的作用

  • 在 嵌入式Linux 中嗅战,我們有時需要檢測設(shè)備是否在進行 熱插播妄田,而內(nèi)核或者某些驅(qū)動實現(xiàn)了 熱插拔 上報 uevent事件信息 的機制。這樣我們就可以在應(yīng)用層監(jiān)聽設(shè)備的 熱插拔狀態(tài) 驮捍,并根據(jù)狀態(tài)執(zhí)行不同操作
  • 當我們使用 insmod 命令加載內(nèi)核時疟呐,常常需要手動使用 mknod 命令手動添加驅(qū)動節(jié)點。內(nèi)核其實是提供了一種機制东且,該機制可以在加載內(nèi)核時启具,通過上報 uevent事件信息,將 主次設(shè)備號 上報給應(yīng)用空間珊泳,拿到設(shè)備號號后我們就可以自動設(shè)備節(jié)點鲁冯,而這也就是 udev 實現(xiàn)的基本原理拷沸。

2.7.2 如何捕獲uevent事件

實現(xiàn)捕獲 uevent事件信息 比較簡單,代碼編寫同前面所講類似薯演,其應(yīng)用層步驟如下:

  1. 創(chuàng)建 socket撞芍,其協(xié)議類型為 NETLINK_KOBJECT_UEVENT
  2. 使用 bind 綁定 socket跨扮。其中地址結(jié)構(gòu)體 struct sockaddr_nlgroup成員 要指定為 NETLINK_KOBJECT_UEVENT
  3. 直接接收來自內(nèi)核的 uevent事件信息

除了 應(yīng)用層 之外序无,內(nèi)核 也需要做一定的工作,就是在設(shè)備加載或者初始化的時候使用接口 device_create衡创,其原型如下:

device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

第一個參數(shù)指定所要創(chuàng)建的設(shè)備所從屬的類帝嗡,第二個參數(shù)是這個設(shè)備的父設(shè)備,如果沒有就指定為NULL璃氢,第三個參數(shù)是設(shè)備號哟玷,第四個參數(shù)是設(shè)備名稱,第五個參數(shù)是從設(shè)備號一也。
device_create 的參數(shù)如下:

  • class:指定所要創(chuàng)建的設(shè)備所從屬的類
  • parent:指定設(shè)備的父設(shè)備碗降,如果沒有就指定為NULL
  • devt:指定設(shè)備號
  • drvdata:設(shè)備的私有數(shù)據(jù)
  • fmt:指定 設(shè)備名稱 (按照筆者的理解)

2.7.3 代碼示例

看了步驟之后,其實會發(fā)現(xiàn)非常簡單塘秦,應(yīng)用層代碼 如下:(PS:別忘了在驅(qū)動中使用device_create)

#define PAYLOAD_LEN 4096
typedef struct _uevent_msg_t
{
    struct nlmsghdr hdr;
    char   paylaod[PAYLOAD_LEN];
}uevent_msg_t;

int main(int argc,char **argv)
{
    uevent_msg_t uevent_msg = {0};
    struct sockaddr_nl nl_sockaddr;
    int sockfd = 0;
    int recv_len = 0;
    int i = 0;
    int addr_len = sizeof(struct sockaddr_nl);

    memset(&nl_sockaddr, 0, sizeof(nl_sockaddr));
    nl_sockaddr.nl_family = AF_NETLINK;
    nl_sockaddr.nl_groups = NETLINK_KOBJECT_UEVENT;//捕獲uevnet時間只能將組設(shè)為NETLINK_KOBJECT_UEVENT
    nl_sockaddr.nl_pid = getpid(); 

    sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);

    if(sockfd == -1)
        printf("socket creating failed:%s\n", strerror(errno));

    if(bind(sockfd, (struct sockaddr *)&nl_sockaddr, sizeof(nl_sockaddr)) == -1)
        printf("bind error:%s\n", strerror(errno));

    while(1)
    {
        memset(&uevent_msg, 0, sizeof(uevent_msg));
        recv_len = recvfrom(sockfd, &uevent_msg, sizeof(uevent_msg_t), 0, (struct sockaddr*)&nl_sockaddr, &addr_len);
        
        if(recv_len < 0)
            printf("receive error\n");
        else if(recv_len < 32 || recv_len > sizeof(uevent_msg.paylaod))
            printf("invalid message");

        for(i = 0; i < recv_len; i++)
        {
            if(uevent_msg.paylaod[i] == '\0')
                uevent_msg.paylaod[i] = '\n';
        }

        printf("received %d bytes\n%s\n", addr_len, uevent_msg.paylaod);
    }
    return 0;
}

驅(qū)動層 代碼做了一些刪減讼渊,主要展示初始化函數(shù)中的代碼實現(xiàn),如下:

static int gpio_probe(struct platform_device *pdev)
{
    ......

    /* 創(chuàng)建設(shè)備尊剔,發(fā)出uevent事件 ,在/sys/class/目錄下創(chuàng)建設(shè)備類別目錄gpio_class */
    gpio_device->gpio_class = class_create(THIS_MODULE, "gpio_class");
    if(IS_ERR(gpio_device->gpio_class)) 
    {
        printk(KERN_INFO"create a class error\n");
        goto CDEV_FAILE;
    }       

    /*在/dev/目錄和/sys/class/gpio_class目錄下分別創(chuàng)建設(shè)備文件gpio_dev*/
    gpio_device->gpio_dev = device_create(gpio_device->gpio_class, NULL, gpio_device->n_dev, NULL, "gpio_dev");
    if(IS_ERR(gpio_device->gpio_dev)) 
    {
        printk(KERN_INFO"create a device error\n");
        goto CLASS_FAILE;
    }

    ......

}  

那么爪幻,以上就是 捕獲uevent事件 的基本原理和簡單實現(xiàn)。

三须误、結(jié)語

本文主要講述了 netlink 的 2 種使用方法:應(yīng)用與內(nèi)核的雙工通信挨稿、捕獲uevent事件。但其實這只是 netlink 的冰山一角京痢,netlink 牽涉到的模塊代碼之多奶甘,之復(fù)雜在筆者學習的過程中深有體會,還有許多知識筆者還沒接觸到祭椰。希望通過本文可以讓讀者們對 netlink 的使用有一些基本的了解臭家,能夠幫助各位讀者在工作生活中解決一些東西。

四方淤、附錄參考鏈接

libnl庫應(yīng)用詳解(一)http://www.reibang.com/p/ab2cd37a9b76
netlink編程介紹https://blog.csdn.net/aabb3575007/article/details/17959199
Netlink編程-數(shù)據(jù)結(jié)構(gòu)http://edsionte.com/techblog/archives/4131
內(nèi)核與用戶層通信之netlinkhttps://blog.csdn.net/stone8761/article/details/72780863
netlink 學習筆記 3.8.13內(nèi)核https://www.cnblogs.com/D3Hunter/archive/2013/07/22/3207670.html
linux內(nèi)核編程之netlinkhttps://blog.csdn.net/shallnet/article/details/17734643
linux協(xié)議棧skb操作函數(shù)https://www.cnblogs.com/x_wukong/p/5924484.html
linux內(nèi)核skb操作https://blog.csdn.net/weixin_30315435/article/details/98735670
sk_buff 結(jié)構(gòu)體以及完全解釋https://blog.csdn.net/shanshanpt/article/details/21024465
Netlink實現(xiàn)熱拔插監(jiān)控http://blog.chinaunix.net/uid-24943863-id-3223000.html
Netlink的簡介及使用方法https://blog.csdn.net/ganshuyu/article/details/30241313/
內(nèi)核kobject上報uevent過濾規(guī)則https://blog.csdn.net/tankai19880619/article/details/11776589

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钉赁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子携茂,更是在濱河造成了極大的恐慌你踩,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異带膜,居然都是意外死亡吩谦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門膝藕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逮京,“玉大人,你說我怎么就攤上這事束莫±撩蓿” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵览绿,是天一觀的道長策严。 經(jīng)常有香客問我,道長饿敲,這世上最難降的妖魔是什么妻导? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮怀各,結(jié)果婚禮上倔韭,老公的妹妹穿的比我還像新娘。我一直安慰自己瓢对,他們只是感情好寿酌,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著硕蛹,像睡著了一般醇疼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上法焰,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天秧荆,我揣著相機與錄音,去河邊找鬼埃仪。 笑死乙濒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的卵蛉。 我是一名探鬼主播颁股,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毙玻!你這毒婦竟也來了豌蟋?” 一聲冷哼從身側(cè)響起廊散,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤桑滩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體运准,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡幌氮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胁澳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片该互。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖韭畸,靈堂內(nèi)的尸體忽然破棺而出宇智,到底是詐尸還是另有隱情,我是刑警寧澤胰丁,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布随橘,位于F島的核電站,受9級特大地震影響锦庸,放射性物質(zhì)發(fā)生泄漏机蔗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一甘萧、第九天 我趴在偏房一處隱蔽的房頂上張望萝嘁。 院中可真熱鬧,春花似錦扬卷、人聲如沸牙言。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嬉挡。三九已至,卻和暖如春汇恤,著一層夾襖步出監(jiān)牢的瞬間庞钢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工因谎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留基括,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓财岔,卻偏偏與公主長得像风皿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匠璧,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容