消息在以流的形式在程序之間進行傳遞,一個流中可能包含多個消息。
對於每一個消息消息來講,爲了便於維護和方便使用,還須要一些有關記錄消息的信息。一個 netlink message 結構以下:
其中 nlmsghdr 結構裏記錄了該條 netlink message 的以下信息:linux
struct nlmsghdr { __u32 nlmsg_len; //該 netlink message 的長度(包括 nlmsghdr 部分自己) __u16 nlmsg_type; //該 netlink message 的種類 __u16 nlmsg_flags; //一個附加的標記 __u32 nlmsg_seq; //該 netlink message 在消息流中的序列號 __u32 nlmsg_pid; //發送該 netlink message 的進程的進程號 pid };
爲了使數據對齊,在 payload 兩端可能會塞入了一些填充字段。netlink message 的 payload 結構以下:
這個結構稱做 family。進程間通訊的目的一般是但願信息接收方可以執行某些動做,而不少命令都是圍繞同一個事件的(如對某個表的增、刪、改、查)。所以爲了便於管理,netlink 通訊協議將這些相關的一套命令和其對應的操做組織起來,歸於一個family管轄,每一個family都有個獨有的名字,該名字需公示出來,以便其餘程序與該 family 通訊,當 family 在內核完成註冊後,內核會給其分配一個獨有的 id。若是用戶須要結構化的信息,能夠在 netlink message 的 payload 中添加可自選的信息頭 user specific header。剩下的 attributes 結構以下:
每一個 attribute 便是一個不可再分的最基本信息單元,結構以下:
其中 nlattr 是管理 payload 的信息頭,記錄了 payload 的如下信息:數組
struct nlattr { __u16 nla_len;//attribute 長度,包括 nlattr 自己 __u16 nla_type;//attribute 的類型 };
payload 部分便是想要傳遞的信息了,如傳遞一句話 「 hello world 」。爲了對齊,payload 兩端可能會塞入 pad。
netlink message 協議使用的信息結構便是以上這些,但對着使用愈來愈多,內核能夠用來分配給 family 的 id 資源開始緊張了。所以 generic netlink 協議在 netlink 協議上又作了一些拓展,即在 netlink message 的 payload 上又作了一層封裝:
該結構即爲 generic netlink message。該信息頭 genlmsghdr 中記錄以下內容。socket
struct genlmsghdr { __u8 cmd; //命令號 __u8 version; //命令版本號 __u16 reserved; //保留字段 };
故最終 generic netlink message 的結構以下:函數
/* generic netlink message attributes */ enum { MY_ATTR_UNSPEC, MY_ATTR_MSG, __MY_ATTR_MAX, }; #define MY_ATTR_MAX (__MY_ATTR_MAX - 1)
你可能有多種消息須要傳遞,使用枚舉類型來管理消息種類是個不錯的辦法。這裏實際上只定義了一個消息種類,即 MY_ATTR_MSG。 根據枚舉類型的特色,在枚舉體最後定義的 __MY_ATTR_MAX 實際上表示了該屬性集的大小,而 MY_ATTR_MAX 則表示了該屬性集中最後一類屬性(即 MY_MTTR_MSG)的編號 1。 spa
(在 linux 內核中大量的枚舉體的首個元素名均是以 UNSPEC 結尾,在這裏 MY_ATTR_UNSPEC 做爲該屬性集的「通常屬性」,可能只是用來對應一些「空操做」,而並不特指哪一消息類型。——做者猜想).net
/* commands 定義命令類型,用戶空間以此來代表須要執行的命令 */ enum { MY_CMD_UNSPEC, MY_CMD_1, //命令 __MY_CMD_MAX, //表示命令集的大小,並非命令 }; #define MY_CMD_MAX (__MY_CMD_MAX - 1)
與消息屬性的定義相似,這裏一樣使用了枚舉類型來定義命令集,但其實這裏只定義了一個命令,即 MY_CMD_1。根據枚舉類型的特色,在枚舉體最後定義的 __MY_CMD_MAX 實際上表示了該命令集的大小,而 MY_CMD_MAX 則表示了該命令集中最後一個命令(即 MY_CMD_1)的編號 1。 指針
generic netlink協議要求命令相應函數的類型是:code
static int my_callback_function(struct sk_buff *skb, struct genl_info *info);
static struct genl_ops my_cmd_ops = { .cmd = MY_CMD_1, //命令 .flags = 0, .policy = my_cmd_policy, .doit = my_callback_function, //響應函數 .dumpit = NULL, };
經過定義一個 genl_ops, 咱們能夠將一個命令和其對應的響應函數關聯起來。成員 policy 指明瞭該響應函數對其可以處理的信息的要求。具體以下:blog
static struct nla_policy my_cmd_policy[MY_ATTRIBUTE_MAX + 1] = { [MY_ATTRIBUTE_MSG] = { .type = NLA_NUL_STRING }, };
這裏使用了比較難懂的語法,先來看一下 struct nla_policy 的定義:接口
struct nla_policy { u16 type; u16 len; };my_cmd_policy 是一個 nal_policy 結構體數組,數組長度爲2(即 MY_ATTRIBUTE_MAX+1),即前文定義的 generic netlink message 屬性集的長度(my_cmd_policy[0] 對應 MY_ATTRIBUTE_UNSPEC;my_cmd_policy[1] 對應 MY_ATTRIBUTE_MSG)。在大括號中將my_cmd_policy[1] 的 type 屬性賦值爲 NLA_NUL_STRING,該宏的意思是除去NULL外的最大字符串長度。
/* family definition */ static struct genl_family my_cmd_family = { .id = GENL_ID_GENERATE, //這裏不指定family ID,由內核進行分配 .hdrsize = 0, //自定義的頭部長度,參考genl數據包結構 .name = "MY_CMDS", //這裏定義family的名稱,user program須要根據這個名字來找到對應的family ID。 .version = 1, //版本號1 .maxattr = ATTRIBUTES_MAX,//最大屬性數 };
該命令族的名爲 MY_CMD_FAMILY。這個名字須要事先告知用戶態程序的編寫者,用戶態程序便經過這個名字來代表想跟內核中的哪一個命令族進行通訊。
在具體的用戶態向內核命令族發送消息的 API 函數中,用戶須要使用 id 來指定想要與之進行通訊的內核命令族,而不能直接使用命令族的名字。解決這個問題的辦法是,當一個命令族在內核中註冊完成時,內核會爲其分配一個id。而內核中還存在一個默認的命令族 GENL_ID_CTRL,用戶態程序即可以先向該命令族發送 CTRL_CMD_GETFAMILY 命令來獲取某個命令族的 id(這裏便是內核分配給 MY_CMDS 命令族的 id)。
genl_register_family(&my_cmd_family);
genl_register_ops(&my_cmd_family, &my_cmd_ops);
//申請一個generic netlink協議的socket int sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); //<1>填寫netlink 通訊地址 nladdr struct sockaddr_nl nladdr; nladdr.nl_family = AF_NETLINK;//填寫要使用的協議 nladdr.nl_pid = getpid();//填寫當前進程的進程號 /*這個是mask值,若是family ID & nl_groups爲0, *則這個family的廣播就接收不到, *因此這裏設爲0xffffffff就能夠接收全部的family消息*/ nladdr.nl_groups = 0xffffffff; //</1> //將填寫好的nladdr綁定到申請的sock上 bind(sockfd, (struct sockaddr *)&nladdr, sizeof(nladdr));
此時,這裏用戶態程序要先與命令族GENL_ID_CRTL 進行通訊,使用命令族 id 查詢命令 CTRL_CMD_GETFAMILY,發送的 netlink message 的類型是 CTRL_ATTR_FAMILY_NAME,信息的內容即便上文定義的命令族名 "MY_CMDS",信息的長度爲 strlen("MY_CMDS")+1。 具體過程以下
char* my_family_name = "MY_CMDS"; int len = strlen(my_family_name) + 1; //<1>計算長度爲 lne 的消息封裝成 netlink message 後的長度 //加上 netlink message attributes header 的長度 len = nla_total_size(len); //加上自定義消息體頭的長度 len += 0; //加上 generic netlink message header 的長度 len += GENL_HDRLEN; //加上 netlink message header 的長度,netlink message 是消息的最後一層封裝 len = NLMSG_SPACE(len); //</1> //申請內存空間用於存儲此條 netlink message unsigned char *buf = genlmsg_alloc(&len);
struct nlmsghdr *nlh; nlh = (struct nlmsghdr *)buf;//獲取buf開頭的 netlink message header 部分 nlh->nlmsg_len = len;//填寫此netlink message的長度 nlh->nlmsg_type = GENL_ID_CTRL;//填寫此netlink message的類型,即命令族 DOC_EXMPL 的 id nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = 0;//這是第0個 netlink message nlh->nlmsg_pid = 0;//發送這條 netlink message 的進程的進程號
struct genlmsghdr *glh; /*generic netlink message是netlink message的負載, *在該負載中獲取generic netlink message header*/ glh = (struct genlmsghdr *)NLMSG_DATA(nlh); glh->cmd = CTRL_CMD_GETFAMILY;//填寫 generic netlink 命令 glh->version = 1;//填寫 generic netlink命令版本號
struct nlattr *nla; //generic netlink message 便是真實的消息部分,消息的格式是 nlattr nla = (struct nlattr *)GENLMSG_DATA(glh); //填寫消息類型,即 DOC_EXMPL_A_MSG nla->nla_type = CTRL_ATTR_FAMILY_NAME; //填寫消息長度(包括消息頭和負載) nla->nla_len = nla_attr_size(strlen(my_family_name)+1); //將想要傳遞的數據 nla_data = 「hello from user space!」填入消息體 nalttr 的負載部分 memcpy(NLA_DATA(nla), my_family_name, nla_len);
struct sockaddr_nl nladdr; memset(&nladdr, 0, sizeof(nladdr)); nladdr.nl_family = AF_NETLINK;//使用 netlink 通訊協議
int ret; int count = 0; do { ret = sendto(sockfd, &buf[count], len - count, 0, (struct sockaddr *)&nladdr, sizeof(nladdr)); count += ret; } while (count < len);
使用套接字sockfd(地址爲nladdr),嘗試傳遞存儲在以 buf[count] 爲起始位置,長度爲 len 的消息。但在實際傳送過過程當中可能沒法一次性傳遞完存儲在 buf 區的全部內容,sendto 函數會返回這次調用實際傳送的消息長度,咱們可使用上面這個循環直至將 buf 區的消息所有送出。
這裏通常給待接收的信息預留 len = 256 字節的空間,建立 netlink message 的過程見上文
struct sockaddr_nl nladdr; nladdr.nl_family = AF_NETLINK;//使用 netlink 通訊協議 nladdr.nl_pid = getpid(); //填寫當前進程的進程號 nladdr.nl_groups = 0xffffffff;//此廣播號意味着能夠接收全部內核命令族發來的信息
struct iovec iov; iov.iov_base = buf; iov.iov_len = len; struct msghdr msg; msg.msg_name = (void *)&nladdr; msg.msg_namelen = sizeof(nladdr); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_flags = 0; //</1> //從套接字sockfd中接收消息,存儲到msg recvmsg(sockfd, &msg, 0);
//讀取到的數據流裏面,可能會包含多條nlmsg for (struct nlmsghdr *nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { /* The end of multipart message. */ struct genlmsghdr *glh = (struct genlmsghdr *)NLMSG_DATA(nlh); struct nlattr *nla = (struct nlattr *)GENLMSG_DATA(glh); //the first attribute int nla_len = nlh->nlmsg_len - GENL_HDRLEN; //len of attributes //一條nlmsg裏面,可能會包含多個attr for (int i = 0; NLA_OK(nla, nla_len); nla = NLA_NEXT(nla, nla_len), ++i) { //若是消息裏的nla_type與調用者指定的nla_type相同 if (nla_type == nla->nla_type) { int l = nla->nla_len - NLA_HDRLEN; *len = *len > l ? l : *len; //將該 nlattr 對應的數據拷貝給到調用者指定的 buf 中 memcpy(buf, NLA_DATA(nla), *len); break; } } }
此時 buf 中存儲的就是內核分配給命令族 MY_CMDS 的 id 號
該通訊過程與上文所述的用戶態程序與命令族 GENL_ID_CRTL 的通訊過程相同,再也不贅述。不一樣的是:
注:爲了程序易讀好懂,這裏改變了函數定義的順序,刪去了差錯校驗代碼。完整程序可參考:
generic netlink 介紹與例子
int main(int argc, char *argv[]) { test_netlink_unicast(); return 0; }
test_netlink_unicast
#define BUF_SIZE 256 void test_netlink_unicast(void) { //獲取用於與內核通訊的socket,並綁定上本身的netlink通訊地址nladdr int sockfd = genlmsg_open(); //<1>向kernel發送信息 //這裏必須先經過family的名字獲取到family ID,名字須要與驅動裏的一致 int id = genlmsg_get_family_id(sockfd, "DOC_EXMPL"); //獲取本身的進程號pid pid_t pid = getpid(); //向內核發送genl消息 /*經過sockfd套接字,向 DOC_EXMPL 命令族, *本進程號是 pid,該命令是 DOC_EXMPL_C_ECHO(命令版本號爲1), *命令負載的信息種類是 DOC_EXMPL_A_MSG, *信息內容爲MESSAGE_TO_KERNEL,長度爲strlen(MESSAGE_TO_KERNEL) + 1*/ genlmsg_send(sockfd, id, pid, DOC_EXMPL_C_ECHO, 1, DOC_EXMPL_A_MSG, MESSAGE_TO_KERNEL, strlen(MESSAGE_TO_KERNEL) + 1); //</1> //<2>接收從kernel傳來的信息 /*申請一個長度爲len的內存空間,用於存儲netlink message, *但以netlink header類型指針指示這段空間的起始位置*/ int len = BUF_SIZE; struct nlmsghdr *nlh = genlmsg_alloc(&len); //使用sockfd進行通訊,將接收到的內核消息存儲在剛申請的內存空間中 nlh_len = genlmsg_recv(sockfd, (unsigned char *)nlh, len); //</2> unsigned char buf[BUF_SIZE]; genlmsg_dispatch(nlh, nlh_len, DOC_EXMPL_A_MSG, buf, &len); }
genlmsg_open
static int genlmsg_open(void) { //申請一個generic netlink協議的socket int sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); //<1>填寫netlink 通訊地址 nladdr struct sockaddr_nl nladdr; nladdr.nl_family = AF_NETLINK;//填寫要使用的協議 nladdr.nl_pid = getpid();//填寫當前進程的進程號 /*這個是mask值,若是family ID & nl_groups爲0, *則這個family的廣播就接收不到, *因此這裏設爲0xffffffff就能夠接收全部的family消息*/ nladdr.nl_groups = 0xffffffff; //</1> //將填寫好的nladdr綁定到申請的sock上 bind(sockfd, (struct sockaddr *)&nladdr, sizeof(nladdr)); return sockfd; }
genlmsg_get_family_id
static int genlmsg_get_family_id(int sockfd, const char *family_name) { //<1>向GENL_ID_CTRL命令族查詢內核給family_name分配的id /*經過sockfd套接字,向GENL_ID_CTRL命令族, *進程號是 0,該命令是CTRL_CMD_GETFAMILY(命令版本號爲1), *命令負載的信息種類是CTRL_ATTR_FAMILY_NAME, *信息內容爲family_name,長度爲strlen(family_name) + 1*/ genlmsg_send(sockfd, GENL_ID_CTRL, 0, CTRL_CMD_GETFAMILY, 1, CTRL_ATTR_FAMILY_NAME, family_name, strlen(family_name) + 1); //</1> //<2>接收sockfd上收到的內核返回的信息,存儲在buf中 int len = 256; void *buf = genlmsg_alloc(&len); len = genlmsg_recv(sockfd, buf, len); //</2> //<3>拆解從內核傳來的信息,獲得family_name對應的id __u16 id = 0; int l = sizeof(id); genlmsg_dispatch((struct nlmsghdr *)buf, len, 0, CTRL_ATTR_FAMILY_ID, (unsigned char *)&id, &l); //</3> return id; }
genlmsg_send
static int genlmsg_send(int sockfd, unsigned short nlmsg_type, unsigned int nlmsg_pid, unsigned char genl_cmd, unsigned char genl_version, unsigned short nla_type, const void *nla_data, unsigned int nla_len) { /*申請一個長度爲nla_len的內存空間,用於存儲netlink message, *但以最簡單的unsigned char類型指針指示這段空間的起始位置*/ int len = nla_len; unsigned char *buf = genlmsg_alloc(&len); //<1>填寫netlink message header struct nlmsghdr *nlh; nlh = (struct nlmsghdr *)buf;//獲取buf開頭的 netlink message header 部分 nlh->nlmsg_len = len;//填寫此netlink message的長度 nlh->nlmsg_type = nlmsg_type;//填寫此netlink message的類型,即命令族 DOC_EXMPL 的 id nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = nlmsg_pid; //</1> //<2>填寫generic netlink message header struct genlmsghdr *glh; /*generic netlink message是netlink message的負載, *在該負載中獲取generic netlink message header*/ glh = (struct genlmsghdr *)NLMSG_DATA(nlh); glh->cmd = genl_cmd;//填寫 generic netlink 命令,即 DOC_EXMPL_C_ECHO glh->version = genl_version;//這裏 generic netlink命令版本號是 1 //</2> //<3>netlink attribute header struct nlattr *nla; //generic netlink message便是真實的消息部分,消息的格式是nlattr nla = (struct nlattr *)GENLMSG_DATA(glh); //填寫消息類型,即 DOC_EXMPL_A_MSG nla->nla_type = nla_type; //填寫消息長度(包括消息頭和負載) nla->nla_len = nla_attr_size(nla_len); //將想要傳遞的數據 nla_data = 「hello from user space!」填入消息體 nalttr 的負載部分 memcpy(NLA_DATA(nla), nla_data, nla_len); //</3> //<4>填寫消息發送的netlink socket address struct sockaddr_nl nladdr; memset(&nladdr, 0, sizeof(nladdr)); nladdr.nl_family = AF_NETLINK; //</4> int ret; int count = 0; do { /*使用套接字sockfd(地址爲nladdr),嘗試傳遞存儲在buf區,長度爲len的消息, *sendto會返回實際傳送的消息長度,使用循環直至將buf區的消息所有送出*/ ret = sendto(sockfd, &buf[count], len - count, 0, (struct sockaddr *)&nladdr, sizeof(nladdr)); count += ret; }while (count < len); return count; }
genlmsg_alloc
static void *genlmsg_alloc(int *size) { //<1>計算長度爲size的消息封裝成netlink message後的長度 int len; //加上nlattr的長度(nalttr是最基本的消息體頭,其後跟的負載部分再無任何封裝) len = nla_total_size(*size); //加上自定義消息體頭的長度 len += 0; //加上generic netlink message header 的長度 len += GENL_HDRLEN; //加上netlink message header 的長度,netlink message 是消息的最後一層封裝 len = NLMSG_SPACE(len); //</1> //告知申請者封裝以後的netlink message的長度 *size = len; //申請netlink message所需的內存空間 unsigned char *buf = malloc(len); return buf; }
genlmsg_recv
//從套接字sockfd接收從內核傳來的消息,存儲在buf(長度爲len)中 static int genlmsg_recv(int sockfd, unsigned char *buf, unsigned int len) { //<1>設置好message header,準備接收消息 //填寫「收件地址」 struct sockaddr_nl nladdr; nladdr.nl_family = AF_NETLINK; nladdr.nl_pid = getpid(); nladdr.nl_groups = 0xffffffff; //設置好數據接收區 struct iovec iov; iov.iov_base = buf; iov.iov_len = len; struct msghdr msg; msg.msg_name = (void *)&nladdr; msg.msg_namelen = sizeof(nladdr); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_flags = 0; //</1> //從套接字sockfd中接收消息,存儲到msg recvmsg(sockfd, &msg, 0); }
genlmsg_dispatch
static int genlmsg_dispatch(struct nlmsghdr *nlmsghdr, unsigned int nlh_len, int nlmsg_type, int nla_type, unsigned char *buf, int *len) { //讀取到的數據流裏面,可能會包含多條nlmsg for (struct nlmsghdr *nlh = nlmsghdr; NLMSG_OK(nlh, nlh_len); nlh = NLMSG_NEXT(nlh, nlh_len)) { /* The end of multipart message. */ struct genlmsghdr *glh = (struct genlmsghdr *)NLMSG_DATA(nlh); struct nlattr *nla = (struct nlattr *)GENLMSG_DATA(glh); //the first attribute int nla_len = nlh->nlmsg_len - GENL_HDRLEN; //len of attributes //一條nlmsg裏面,可能會包含多個attr for (int i = 0; NLA_OK(nla, nla_len); nla = NLA_NEXT(nla, nla_len), ++i) { //若是消息裏的nla_type與調用者指定的nla_type相同 if (nla_type == nla->nla_type) { int l = nla->nla_len - NLA_HDRLEN; *len = *len > l ? l : *len; //將該nlattr對應的數據拷貝給到調用者指定的buf中 memcpy(buf, NLA_DATA(nla), *len); break; } } } }
static int genetlink_init(void) { //註冊一個命令族,linux 3.12纔開始引入generic netlink機制 genl_register_family(&doc_exmpl_genl_family); //給註冊的命令族綁定上對應的操做集 genl_register_ops(&doc_exmpl_genl_family, &doc_exmpl_genl_ops_echo); /* * for multicast */ genl_register_mc_group(&doc_exmpl_genl_family, &doc_exmpl_genl_mcgrp); }
doc_exmpl_genl_family
/* family definition */ static struct genl_family doc_exmpl_genl_family = { .id = GENL_ID_GENERATE, //這裏不指定family ID,由內核進行分配 .hdrsize = 0, //自定義的頭部長度,參考genl數據包結構 .name = "DOC_EXMPL", //這裏定義family的名稱,user program須要根據這個名字來找到對應的family ID。 .version = 1, .maxattr = DOC_EXMPL_A_MAX, };
doc_exmpl_genl_ops_echo
/* operation definition 將命令command echo和具體的handler對應起來 */ static struct genl_ops doc_exmpl_genl_ops_echo = { .cmd = DOC_EXMPL_C_ECHO, .flags = 0, .policy = doc_exmpl_genl_policy, .doit = doc_exmpl_echo, .dumpit = NULL, };
doc_exmpl_genl_policy
/* netlink attributes */ enum { DOC_EXMPL_A_UNSPEC, DOC_EXMPL_A_MSG, __DOC_EXMPL_A_MAX, }; #define DOC_EXMPL_A_MAX (__DOC_EXMPL_A_MAX - 1) /* attribute policy */ static struct nla_policy doc_exmpl_genl_policy[DOC_EXMPL_A_MAX + 1] = { [DOC_EXMPL_A_MSG] = { .type = NLA_NUL_STRING }, };
nla_policy
struct nla_policy { u16 type; u16 len; };
doc_exmpl_echo
//echo command handler, 命令處理函數,當接收到user program發出的命令後,這個函數會被內核調用 #define MY_TEST_MSG "Hello from kernel space!!!" #define MSG_LEN (strlen(MY_TEST_MSG) + 1) static int doc_exmpl_echo(struct sk_buff *skb, struct genl_info *info) { //新建一個 netlink message /* total length of attribute including padding */ size_t size = nla_total_size(MSG_LEN); struct sk_buff *skbp = genlmsg_new(size, GFP_KERNEL); /* Add a new netlink message to an skb */ struct nlmsghdr *nlhdr = nlmsg_hdr(skb); pid_t pid = nlhdr->nlmsg_pid; genlmsg_put(skbp, pid, 0, &doc_exmpl_genl_family, 0, DOC_EXMPL_C_ECHO); nla_put(skbp, DOC_EXMPL_A_MSG, MSG_LEN, MY_TEST_MSG); void *head = genlmsg_data(nlmsg_data(nlmsg_hdr(skbp))); // genlmsg_end(skbp, head); genlmsg_unicast(&init_net, skbp, pid); }
doc_exmpl_genl_genl_mcgrp
static struct genl_multicast_group doc_exmpl_genl_mcgrp = { .name = "DOC_EXMPL_GRP", };
內核態程序
接收:
等待用戶態程序向該 genl_family 發送命令,由內核調用註冊好的響應函數。
發送:genlmsg_unicast / genlmsg_multicast
用戶態程序
接收:recvmsg發送:sendto / sendmsg