|
本文詳細(xì)介紹了 Linux 2.6 內(nèi)核引入的內(nèi)核空間與用戶空間通信的新機制連接器,并通過典型示例講解了它的使用。
一、引言
連接器是一種新的用戶態(tài)與內(nèi)核態(tài)的通信方式,它使用起來非常方便。本質(zhì)上,連接器是一種netlink,它的 netlink 協(xié)議號為 NETLINK_CONNECTOR,與一般的 netlink 相比,它提供了更容易的使用接口,使用起來更方便。目前,最新穩(wěn)定內(nèi)核有兩個連接器應(yīng)用實例,一個是進程事件連接器,另一個是 CIFS 文件系統(tǒng)。連接器核心實現(xiàn)代碼在內(nèi)核源碼樹的driver/connector/connector.c 和 drivers/connector/cn_queue.c 文件中,文件 drivers/connector/cn_proc.c 是進程事件連接器的實現(xiàn)代碼,而 CIFS 連接器的實現(xiàn)則在該文件系統(tǒng)的實現(xiàn)代碼中。連接器是一個可選模塊,用戶可以在配置內(nèi)核時在設(shè)備驅(qū)動(Device drivers)菜單中選擇或不選它。
任何內(nèi)核模塊要想使用連接器,必須先注冊一個標(biāo)識 ID 和回調(diào)函數(shù),當(dāng)連接器收到 netlink 消息后,會根據(jù)消息對應(yīng)的標(biāo)識 ID 調(diào)用相應(yīng)該 ID 的回調(diào)函數(shù)。
對用戶態(tài)而言,連接器的使用跟普通的 netlink 沒有差別,只要指定 netlink 協(xié)議類型為NETLINK_CONNECTOR 就可以了。
二、連接器相關(guān)數(shù)據(jù)結(jié)構(gòu)和 API
下面是連接器的 API 以及相關(guān)的數(shù)據(jù)結(jié)構(gòu)
:
struct cb_id
{
__u32 idx;
__u32 val;
};
struct cn_msg
{
struct cb_id id;
__u32 seq;
__u32 ack;
__u32 len; /* Length of the following data */
__u8 data[0];
};
int cn_add_callback(struct cb_id *id, char *name, void (*callback) (void *));
void cn_del_callback(struct cb_id *id);
void cn_netlink_send(struct cn_msg *msg, u32 __group, int gfp_mask);
|
結(jié)構(gòu) cb_id 是連接器實例的標(biāo)識 ID,它用于確定 netlink 消息與回調(diào)函數(shù)的對應(yīng)關(guān)系。當(dāng)連接器接收到標(biāo)識 ID 為 {idx,val} 的 netlink 消息時,注冊的回調(diào)函數(shù) void (*callback) (void *) 將被調(diào)用。該回調(diào)函數(shù)的參數(shù)為結(jié)構(gòu) struct cn_msg 的指針。
接口函數(shù) cn_add_callback 用于向連接器注冊新的連接器實例以及相應(yīng)的回調(diào)函數(shù),參數(shù) id 指定注冊的標(biāo)識 ID,參數(shù) name 指定連接器回調(diào)函數(shù)的符號名,參數(shù) callback 為回調(diào)函數(shù)。
接口函數(shù) cn_del_callback 用于卸載回調(diào)函數(shù),參數(shù) id 為注冊函數(shù) cn_add_callback 注冊的連接器標(biāo)識 ID。
接口函數(shù) cn_netlink_send 用于向給定的組發(fā)送消息,它可以在任何上下文安全地調(diào)用。但是,如果內(nèi)存不足,可能會發(fā)送失敗。在具體的連接器實例中,該函數(shù)用于向用戶態(tài)發(fā)送 netlink 消息。
參數(shù) msg 為發(fā)送的 netlink 消息的消息頭。參數(shù) __group 為接收消息的組,如果它為 0,那么連接器將搜索所有注冊的連接器用戶,最終將發(fā)送給用戶 ID 與在 msg 中的 ID 相同的組,但如果 __group 不為 0,消息將發(fā)送給 __group 指定的組。參數(shù) gfp_mask 指定頁分配標(biāo)志。
注意:當(dāng)注冊新的回調(diào)函數(shù)時,連接器將指定它的組為 id.idx。
cn_msg 是連接器定義的消息頭,字段 seq 和 ack 用于確保消息的可靠傳輸,剛才已經(jīng)提到,netlink 在內(nèi)存緊張的情況下可能丟失消息,因此該頭使用順序號和響應(yīng)號來滿足要求可靠傳輸用戶的需求。當(dāng)發(fā)送消息時,用戶需要設(shè)置獨一無二的順序號和隨機的響應(yīng)號,順序號也應(yīng)當(dāng)設(shè)置到 nlmsghdr->nlmsg_seq。注意 nlmsghdr 是類型為結(jié)構(gòu) struct nlmsghdr 的變量,它用于設(shè)置或保存 netlink 的消息頭。每發(fā)送一個消息,順序號應(yīng)當(dāng)加 1,如果需要發(fā)送響應(yīng)消息,那么響應(yīng)消息的順序號應(yīng)當(dāng)與被響應(yīng)的消息的順序號相同,同時響應(yīng)消息的響應(yīng)號應(yīng)當(dāng)為被響應(yīng)消息的順序號加1。如果接收到的消息的順序號不是期望的順序號,那表明該消息是一個新的消息,如果接收到的消息的順序號是期望的順序號,但它的響應(yīng)號不等于上次發(fā)送消息的順序號加1,那么它也是新消息。
三、用戶態(tài)如何使用連接器
內(nèi)核 2.6.14 對 netlink 套接字有新的實現(xiàn),它缺省情況下不允許用戶態(tài)應(yīng)用發(fā)送給組號非 1 的netlink 組,因此用戶態(tài)應(yīng)用要想使用非1的組,必須先加入到該組,這可以通過如下代碼實現(xiàn):
#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif
#ifndef NETLINK_DROP_MEMBERSHIP
#define NETLINK_DROP_MEMBERSHIP 0
#endif
#ifndef NETLINK_ADD_MEMBERSHIP
#define NETLINK_ADD_MEMBERSHIP 1
#endif
int group = 5;
s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
l_local.nl_family = AF_NETLINK;
l_local.nl_groups = group;
l_local.nl_pid = getpid();
if (bind(s, (struct sockaddr *)&l_local, sizeof(struct sockaddr_nl)) == -1) {
perror("bind");
close(s);
return -1;
}
setsockopt(s, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));
|
在不需要使用該連接器時使用語句
setsockopt(s, SOL_NETLINK, NETLINK_DROP_MEMBERSHIP, &group, sizeof(group));
|
退出NETLINK_CONNECTOR的group組。
宏 SOL_NETLINK、NETLINK_ADD_MEMBERSHIP 和 NETLINK_DROP_MEMBERSHIP 在舊的系統(tǒng)中并沒有定義,因此需要用戶顯式定義。
內(nèi)核 2.6.14 的 netlink 代碼只允許選擇一個小于或等于最大組號的組,對于連接器,最大的組號為CN_NETLINK_USERS + 0xf, 即16,因此如果想使用更大的組號,必須修改CN_NETLINK_USERS 到該大值。增加的 0xf 個號碼供非內(nèi)核態(tài)用戶使用。因此,組 0xffffffff目前不能使用。
四、進程事件連接器的使用
進程事件連接器是連接器的第一個使用實例,它通過連接器來報告進程相關(guān)的事件,包括進程 fork、exec、exit 以及進程用戶 ID 與組 ID 的變化。如果用戶想監(jiān)視系統(tǒng)的進程事件,就可以編一個應(yīng)用程序通過 netlink 套接字來獲取進程事件信息。下面將詳細(xì)描述如何編寫一個進程事件監(jiān)視程序。
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <linux/netlink.h>
#include <linux/connector.h>
#define _LINUX_TIME_H
#include <linux/cn_proc.h>
|
上面這些 include 語句包含了進程監(jiān)視程序需要的必要頭文件,其中頭文件 sys/types.h 和sys/socket.h 是編寫套接字程序所必須的,頭文件 signal.h 包含了信號處理相關(guān)的函數(shù),本程序需要信號處理,因此需要包含該頭文件。其余的三個頭文件是內(nèi)核相關(guān)的頭文件,頭文件linux/netlink.h 是編寫netlink套接字程序所必須的,頭文件 linux/connector.h 包含了內(nèi)核實現(xiàn)的連接器的一些結(jié)構(gòu)和宏,使用連接器監(jiān)視系統(tǒng)事件的程序必須包含它,頭文件 linux/cn_proc.h 則定義了進程事件連接器的一些結(jié)構(gòu)和宏,應(yīng)用程序需要包含該頭文件以便正確分析進程事件。注意,在包含頭文件 linux/cn_proc.h 之前定義了宏_LINUX_TIME_H,因為在用戶態(tài)應(yīng)用中包含linux/time.h會導(dǎo)致結(jié)構(gòu)struct timespec 定義沖突,所以該宏避免了頭文件linux/cn_proc.h包含linux/time.h。
#define MAX_MSGSIZE 256
#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif
|
舊的系統(tǒng)并沒有定義 SOL_NETLINK,因此程序必須處理這種情況。宏 MAX_MSGSIZE 定義了最大的進程事件消息大小,它用于指定接收進程事件消息的緩存的大小,這里只是很粗略的大小,實際的消息比這小。
int sd;
struct sockaddr_nl l_local, daddr;
int on;
int len;
struct nlmsghdr *nlhdr = NULL;
struct msghdr msg;
struct iovec iov;
int * connector_mode;
struct cn_msg * cnmsg;
struct proc_event * procevent;
int counter = 0;
int ret;
struct sigaction sigint_action;
|
這些變量用于處理 netlink 消息,其中 sd 為套接字描述符,l_local 和 daddr 分別表示 netlink消息的源地址和目的地址,后面部分將詳細(xì)解釋這種地址的設(shè)置。
void change_cn_proc_mode(int mode)
|
函數(shù) change_cn_proc_mode 用于打開和關(guān)閉進程事件的報告,進程事件連接器初始化時是關(guān)閉進程事件報告的,一個進程要想監(jiān)視進程事件,必須首先打開進程事件連接器的報告開關(guān),在它退出是必須關(guān)閉進程事件連接器的報告開關(guān),否則進程事件連接器將繼續(xù)報告進程事件,盡管沒有一個監(jiān)視進程對這些事件感興趣,這將造成不必要的系統(tǒng)開銷,同時因為緩存這些事件浪費了寶貴的系統(tǒng)內(nèi)存。下面代碼是該函數(shù)的實現(xiàn):
{
memset(nlhdr, 0, sizeof(NLMSG_SPACE(MAX_MSGSIZE)));
memset(&iov, 0, sizeof(struct iovec));
memset(&msg, 0, sizeof(struct msghdr));
cnmsg = (struct cn_msg *)NLMSG_DATA(nlhdr);
connector_mode = (int *)cnmsg->data;
* connector_mode = mode;
nlhdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(enum
proc_cn_mcast_op));
nlhdr->nlmsg_pid = getpid();
nlhdr->nlmsg_flags = 0;
nlhdr->nlmsg_type = NLMSG_DONE;
nlhdr->nlmsg_seq = 0;
|
對于進程事件連接器,netlink 消息包括 netlink 消息頭、連接器消息頭、進程事件或控制操作指令,其中進程事件或控制操作指令部分是變長的,如果是控制指令,僅包含4個字節(jié),如果是進程事件,它應(yīng)當(dāng)為類型 struct proc_event 的結(jié)構(gòu),對于不同的事件,尺寸不同,可能的事件包括控制指令的應(yīng)答、進程 fork、進程 exec、進程 exit、進程用戶 ID 改變以及進程組 ID 的改變。變量 connector_mode 用于設(shè)置控制指令,對于進程事件連接器,只有兩種控制指令,分別是PROC_CN_MCAST_LISTEN 和 PROC_CN_MCAST_IGNORE,對應(yīng)于打開和關(guān)閉進程事件報告。這兩個宏定義在頭文件 linux/cn_proc.h。變量 nlhdr 用于設(shè)置 netlink 的消息頭,nlmsg_len用于指明消息的數(shù)據(jù)部分長度,該消息的數(shù)據(jù)部分包含了固定長度的連接器的消息頭以及進程連接器的消息,nlmsg_pid用于指定消息的來源,一般為進程或線程ID,nlmsg_flags用于指定一些特殊標(biāo)志,一般設(shè)置為0就足夠了。應(yīng)用程序設(shè)置 nlmsg_type 為 NLMSG_DONE,表示該消息是完整的,沒有后續(xù)的消息碎片。一般地,nlmsg_seq 應(yīng)當(dāng)與連接器消息頭的順序號一致。
cnmsg->id.idx = CN_IDX_PROC;
cnmsg->id.val = CN_VAL_PROC;
cnmsg->seq = 0;
cnmsg->ack = 0;
cnmsg->len = sizeof(enum proc_cn_mcast_op);
|
這部分代碼用于設(shè)置連接器消息頭,對于進程事件連接器,cnmsg->id.idx 和 cnmsg->id.val 必須分別設(shè)置為CN_IDX_PROC和CN_VAL_PROC,否則該消息無法派送給進程事件連接器。Seq 和ack 用于指定消息的順序號和響應(yīng)號,對于非響應(yīng)消息,ack 應(yīng)當(dāng)設(shè)置為 0,而順序號應(yīng)當(dāng)為上一個發(fā)送的消息的順序號加1,對于第一個消息可以隨意指定順序號。
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlhdr->nlmsg_len;
msg.msg_name = (void *)&daddr;
msg.msg_namelen = sizeof(daddr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ret = sendmsg(sd, &msg, 0);
if (ret == -1) {
perror("sendmsg error:");
exit(-1);
}
}
|
這部分代碼用于發(fā)送 netlink 消息,為了通過函數(shù) sendmsg 發(fā)送該消息,程序必須填寫類型為結(jié)構(gòu) struct msghdr 的變量 msg,因為該函數(shù)可以一次發(fā)送多個消息,因此通過結(jié)構(gòu) struct iovec 來組織所有要發(fā)送的消息。iov.iov_base 指向消息的開始位置,iov.iov_len 指定消息的大小,msg.msg_name 指定消息的目的地址,msg.msg_namelen 則指定消息的目的地址長度,msg.msg_iov 指向結(jié)構(gòu)為 struct iovec 的數(shù)組開始位置,對于這里的情況,它只包含了一個元素,因此 msg.msg_iovlen 設(shè)置為 1,如果有多個消息,該字段應(yīng)該設(shè)置為實際的消息數(shù),當(dāng)然那時 iov 應(yīng)當(dāng)是一個多元素的數(shù)組,每一個元素都應(yīng)當(dāng)象前面的 iov 結(jié)構(gòu)去設(shè)置。
void sigint_handler(int signo)
{
change_cn_proc_mode(PROC_CN_MCAST_IGNORE);
printf("process event: turn off process event listening.\n");
close(sd);
exit(0);
}
|
這是一個信號處理函數(shù),它用于在該程序退出時關(guān)閉進程事件的報告。
下面是程序的主體部分。
int main(void)
{
memset(&sigint_action, 0, sizeof(struct sigaction));
sigint_action.sa_flags = SA_ONESHOT;
sigint_action.sa_handler = &sigint_handler;
sigaction(SIGINT, &sigint_action, NULL);
|
這段代碼用于設(shè)置信號 SIGINT 的處理函數(shù),該程序是一個無限循環(huán),用戶通過 CTRL + C 來退出,當(dāng)用戶按下 CTRL + C 時,系統(tǒng)將發(fā)送信號 SIGINT 該該程序,相應(yīng)的處理函數(shù)將被執(zhí)行,前面已經(jīng)講過,該信號處理函數(shù)用于關(guān)閉進程事件報告。
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
if (nlhdr == NULL) {
perror("malloc:");
exit(-1);
}
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0;
daddr.nl_groups = CN_IDX_PROC;
|
netlink 消息的地址結(jié)構(gòu)包括三個主要的字段,nl_family 必須設(shè)置為 AF_NETLINK,nl_pid 則用于指定 netlink 消息的接收者或發(fā)送者的地址,一般為進程 ID 或線程 ID,如果該消息的發(fā)送者為內(nèi)核或接收者有多個,它設(shè)置為 0,此時 nl_groups 指定接收者的組號。
sd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
|
該語句創(chuàng)建了一個 netlink 套接字,注意對于使用連接器的應(yīng)用,第三個參數(shù)必須指定為 NETLINK_CONNECTOR。所有使用 netlink 的應(yīng)用程序,函數(shù) socket 的前兩個參數(shù)都是一樣的,應(yīng)當(dāng)分別為 PF_NETLINK 和 SOCK_DGRAM。
l_local.nl_family = AF_NETLINK;
l_local.nl_groups = CN_IDX_PROC;
l_local.nl_pid = getpid();
|
這段代碼用于設(shè)置 netlink 消息的源地址。
if (bind(sd, (struct sockaddr *)&l_local, sizeof(struct sockaddr_nl)) == -1)
{
perror("bind");
close(sd);
return -1;
}
|
使用 bind 函數(shù)主要為了把源地址與套接字 sd 綁定起來,以便后面消息的發(fā)送不必指定源地址。
change_cn_proc_mode(PROC_CN_MCAST_LISTEN);
|
該調(diào)用打開了進程事件的報告。
printf("process event: turn on process event listening.\n");
while (1) {
memset(nlhdr, 0, NLMSG_SPACE(MAX_MSGSIZE));
memset(&iov, 0, sizeof(struct iovec));
memset(&msg, 0, sizeof(struct msghdr));
iov.iov_base = (void *)nlhdr;
iov.iov_len = NLMSG_SPACE(MAX_MSGSIZE);
msg.msg_name = (void *)&daddr;
msg.msg_namelen = sizeof(daddr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ret = recvmsg(sd, &msg, 0);
if (ret == 0) {
printf("Exit.\n");
exit(0);
}
else if (ret == -1) {
perror("recvmsg:");
exit(1);
}
else {
|
這部分代碼用于接收進程事件消息,使用函數(shù) recvmsg 時,用戶也必須設(shè)置 msg,這時各字段的意義與發(fā)送時不一樣,iov 用于指定消息的存放位置以及最大可利用的緩存大小,msg.msgname 則表示該調(diào)用希望接收的消息的目的地址,msg.msg_iovlen 則指定該調(diào)用應(yīng)當(dāng)返回的消息數(shù)。
cnmsg = (struct cn_msg *)NLMSG_DATA(nlhdr);
procevent = (struct proc_event *)cnmsg->data;
switch (procevent->what) {
case PROC_EVENT_NONE:
printf("process event: acknowledge for turning on process
event listening\n\n\n");
break;
case PROC_EVENT_FORK:
printf("process event: fork\n");
printf("parent tid:%d, pid:%d\nchild tid:%d, pid:%d\n\n\n",
procevent->event_data.fork.parent_pid,
procevent->event_data.fork.parent_tgid,
procevent->event_data.fork.child_pid,
procevent->event_data.fork.child_tgid);
break;
case PROC_EVENT_EXEC:
printf("process event: exec\n");
printf("tid:%d, pid:%d\n\n\n",
procevent->event_data.exec.process_pid,
procevent->event_data.exec.process_tgid);
break;
case PROC_EVENT_UID:
printf("process event: uid\n");
printf("process tid:%d, pid:%d, uid:%d->%d\n\n\n",
procevent->event_data.id.process_pid,
procevent->event_data.id.process_tgid,
procevent->event_data.id.r.ruid,
procevent->event_data.id.e.euid);
break;
case PROC_EVENT_GID:
printf("process event: gid\n");
printf("process tid:%d, pid:%d, uid:%d->%d\n\n\n",
procevent->event_data.id.process_pid,
procevent->event_data.id.process_tgid,
procevent->event_data.id.r.rgid,
procevent->event_data.id.e.egid);
break;
case PROC_EVENT_EXIT:
printf("process event: exit\n");
printf("tid:%d, pid:%d, exit code:%d\n\n\n",
procevent->event_data.exit.process_pid,
procevent->event_data.exit.process_tgid,
procevent->event_data.exit.exit_code);
break;
default:
printf("Unkown process action\n\n\n");
break;
}
}
}
}
|
這部分代碼用于處理各種不同的進程事件,并輸出具體的事件信息,對于 fork 事件,輸出父進程和線程的 ID 以及子進程和線程的 ID,對于 exec 事件則輸出執(zhí)行 exec 調(diào)用的進程和線程的 ID,對于用戶 ID 變更事件,則輸出制造該事件的進程和線程的 ID,舊的用戶 ID 以及新的用戶 ID,對于組 ID 變更事件,則輸出制造該事件的進程和線程的 ID,舊的組 ID 以及新的組 ID,對于 exit 事件,則輸出結(jié)束運行的進程和線程的 ID 以及退出碼。
下面是該程序在作者的紅旗 Linux 桌面版 4.1 上的運行結(jié)果示例:
[root@localhost yangyi]# gcc -I linux-2.6.15.4/include cn_proc_user.c -o
cn_proc_user
[root@localhost yangyi]# ./cn_proc_user
process event: turn on process event listening.
process event: acknowledge for turning on process event listening
process event: fork
parent tid:2720, pid:2720
child tid:2775, pid:2775
process event: exec
tid:2775, pid:2775
process event: exit
tid:2775, pid:2775, exit code:0
.
.
.
process event: uid
process tid:2877, pid:2877, uid:500->0
process event: gid
process tid:2877, pid:2877, gid:500->500
process event: uid
process tid:2877, pid:2877, uid:500->0
process event: uid
process tid:2877, pid:2877, uid:500->0
process event: uid
process tid:2877, pid:2877, uid:500->0
process event: uid
process tid:2877, pid:2877, uid:500->0
process event: fork
parent tid:2877, pid:2877
child tid:2878, pid:2878
process event: gid
process tid:2878, pid:2878, gid:500->500
process event: uid
process tid:2878, pid:2878, uid:500->500
process event: exec
tid:2878, pid:2878
process event: exit
tid:2878, pid:2878, exit code:0
process event: turn off process event listening.
[root@localhost yangyi]#
|

 |

|
五、如何實現(xiàn)一個新的連接器實例
要想實現(xiàn)一個新的連接器,必須首先定義個新的連接器標(biāo)識,目前最新的內(nèi)核包括兩個連接器實例,一個是進程事件連接器,另一個為 CIFS 連接器,因此新的連接器標(biāo)識必須不同于現(xiàn)有的任何一個連接器標(biāo)識。例如,用戶可以使用如下語句來定義一個新的連接器標(biāo)識:
#define CN_IDX_NEW 3
#define CN_VAL_NEW 1
|
當(dāng)然連接器必須在內(nèi)核實現(xiàn),因此需要通過內(nèi)核模塊來定義相應(yīng)的回調(diào)函數(shù)并在初始化代碼中注冊該回調(diào)函數(shù),回調(diào)函數(shù)實際上用于處理發(fā)送給該連接器的消息。該模塊也必須實現(xiàn)消息發(fā)送函數(shù)供其它內(nèi)核子系統(tǒng)方便使用該連接器。下面是作者編寫的一個文件系統(tǒng)事件連接器的實現(xiàn)代碼,該代碼根據(jù)進程事件連接器(drivers/connector/cn_proc.c)編寫而成。
頭文件 include/linux/cn_fs.h 定義了文件系統(tǒng)事件處理的數(shù)據(jù)結(jié)構(gòu)、open 消息發(fā)送函數(shù)聲明以及一些相關(guān)的宏定義,結(jié)構(gòu) struct fs_event 定義了文件系統(tǒng)事件連接器消息結(jié)構(gòu)。
#ifndef CN_FS_H
#define CN_FS_H
#include <linux/types.h>
#include <linux/time.h>
#include <linux/connector.h>
#define TASK_NAME_LEN 16
#define FILE_NAME_LEN 256
#define CN_IDX_FS 3
#define CN_VAL_FS 1
/*
* Userspace sends this enum to register with the kernel that it is listening
* for events on the connector.
*/
enum fs_cn_mcast_op {
FS_CN_MCAST_LISTEN = 1,
FS_CN_MCAST_IGNORE = 2
};
struct fs_event {
enum type {
/* Use successive bits so the enums can be used to record
* sets of events as well
*/
FS_EVENT_NONE = 0x00000000,
FS_EVENT_OPEN = 0x00000001
} type;
__u32 cpu;
struct timespec timestamp;
union {
struct {
__u32 err;
} ack;
struct fs_read_event {
char proc_name[TASK_NAME_LEN];
char file_name[FILE_NAME_LEN];
} read;
} event_data;
};
#ifdef __KERNEL__
#ifdef CONFIG_FS_EVENTS
void fs_open_connector(struct dentry * dentryp);
#else
static void fs_open_connector(struct dentry * dentryp)
{}
#endif /* CONFIG_FS_EVENTS */
#endif /* __KERNEL__ */
#endif /* CN_FS_H */
|
下面文件 drivers/connector/cn_fs.c 是文件系統(tǒng)連接器的實現(xiàn)代碼。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cn_fs.h>
#include <asm/atomic.h>
|
這些是必要的內(nèi)核頭文件。
#define CN_FS_MSG_SIZE (sizeof(struct cn_msg) + sizeof(struct fs_event))
|
該宏定義了文件系統(tǒng)消息的大小。
static atomic_t fs_event_listeners = ATOMIC_INIT(0);
|
該變量用于控制文件系統(tǒng) open 事件的報告,初始化時設(shè)置為 0,即不報告 open 事件。用戶態(tài)應(yīng)用可以通過向文件系統(tǒng)連接器發(fā)送控制消息來打開和關(guān)閉 open 事件的報告。
static struct cb_id cn_fs_event_id = { CN_IDX_FS, CN_VAL_FS }; 這是連接器的唯一標(biāo)識,連接器需要它來找到對應(yīng)的連接器實例。
/* fs_event_counts is used as the sequence number of the netlink message */
static DEFINE_PER_CPU(__u32, fs_event_counts) = { 0 };
|
該 PER_CPU 變量用于統(tǒng)計總共的文件系統(tǒng)事件,并通過它來獲得連接器消息的順序號。
static inline void get_seq(__u32 *ts, int *cpu)
{
*ts = get_cpu_var(fs_event_counts)++;
*cpu = smp_processor_id();
put_cpu_var(fs_event_counts);
}
|
該函數(shù)用于得到下一個消息的順序號。
void fs_open_connector(struct dentry * dentryp)
{
struct cn_msg *msg;
struct fs_event *event;
__u8 buffer[CN_FS_MSG_SIZE];
if (atomic_read(&fs_event_listeners) < 1)
return;
printk("cn_fs: fs_open_connector\n");
msg = (struct cn_msg*)buffer;
event = (struct fs_event*)msg->data;
get_seq(&msg->seq, &event->cpu);
getnstimestamp(&event->timestamp);
event->type = FS_EVENT_OPEN;
memcpy(event->event_data.read.proc_name, current->comm,
TASK_NAME_LEN);
memcpy(event->event_data.read.file_name, dentryp->d_name.name,
dentryp->d_name.len);
event->event_data.read.file_name[dentryp->d_name.len] = '\0';
memcpy(&msg->id, &cn_fs_event_id, sizeof(msg->id));
msg->ack = 0; /* not used */
msg->len = sizeof(struct fs_event);
/* If cn_netlink_send() failed, the data is not sent */
cn_netlink_send(msg, CN_IDX_FS, GFP_KERNEL);
}
|
該函數(shù)為 open 事件消息的發(fā)送函數(shù),它被文件系統(tǒng)的 open 操作調(diào)用來向文件系統(tǒng)事件連接器發(fā)送 open 事件。它首先設(shè)置文件系統(tǒng)事件結(jié)構(gòu) struct fs_event 的各個字段,字段event->timestamp為發(fā)生事件的時間,event->type為事件的類型,該模塊只實現(xiàn)了兩個事件,一個為對控制操作的響應(yīng),另一個為 open 事件。字段event->event_data.read.proc_name為打開文件的進程名稱,event->event_data.read.file_name 則為被打開的文件名。消息設(shè)置完畢后可通過連接器接口函數(shù) cn_netlink_send 直接發(fā)送,該發(fā)送函數(shù)不能保證消息發(fā)送成功,因此對于要求可靠傳輸消息的應(yīng)用,必須通過響應(yīng)來最終確認(rèn)是否發(fā)送成功。
static void cn_fs_ack(int err, int rcvd_seq, int rcvd_ack)
{
struct cn_msg *msg;
struct fs_event *event;
__u8 buffer[CN_FS_MSG_SIZE];
if (atomic_read(&fs_event_listeners) < 1)
return;
msg = (struct cn_msg*)buffer;
event = (struct fs_event*)msg->data;
msg->seq = rcvd_seq;
getnstimestamp(&event->timestamp);
event->cpu = -1;
event->type = FS_EVENT_NONE;
event->event_data.ack.err = err;
memcpy(&msg->id, &cn_fs_event_id, sizeof(msg->id));
msg->ack = rcvd_ack + 1;
msg->len = sizeof(struct fs_event);
cn_netlink_send(msg, CN_IDX_FS, GFP_KERNEL);
}
|
該函數(shù)用于給用戶態(tài)發(fā)送響應(yīng)消息。注意,響應(yīng)消息的順序號必須為被響應(yīng)的消息的順序號,響應(yīng)號則為順序號加1。
static void cn_fs_mcast_ctl(void *data)
{
struct cn_msg *msg = data;
enum fs_cn_mcast_op *mc_op = NULL;
int err = 0;
if (msg->len != sizeof(*mc_op))
return;
mc_op = (enum fs_cn_mcast_op*)msg->data;
switch (*mc_op) {
case FS_CN_MCAST_LISTEN:
atomic_inc(&fs_event_listeners);
break;
case FS_CN_MCAST_IGNORE:
atomic_dec(&fs_event_listeners);
break;
default:
err = EINVAL;
break;
}
cn_fs_ack(err, msg->seq, msg->ack);
}
|
該函數(shù)為注冊給連接器的回調(diào)函數(shù),它用于處理用戶態(tài)應(yīng)用發(fā)送給該連接器的消息。因此,實際上它是消息接收函數(shù)。對于該模塊,它實際上用于處理控制命令,用戶態(tài)發(fā)送的控制命令消息最后將由它來處理,它實際上用于打開和關(guān)閉文件系統(tǒng)事件報告開關(guān),同時它也負(fù)責(zé)發(fā)送響應(yīng)消息給用戶態(tài)應(yīng)用。
static int __init cn_fs_init(void)
{
int err;
if ((err = cn_add_callback(&cn_fs_event_id, "cn_fs",
&cn_fs_mcast_ctl))) {
printk(KERN_WARNING "cn_fs failed to register\n");
return err;
}
return 0;
}
|
該函數(shù)在內(nèi)核初始化時調(diào)用,它使用連接器接口函數(shù) cn_add_callback 注冊了一個新的連接器實例。
該語句用于告訴內(nèi)核函數(shù) cn_fs_init 需要在內(nèi)核初始化時調(diào)用。
程序源碼包中的 cn_fs_user.c 是使用該文件系統(tǒng)連接器來監(jiān)視文件系統(tǒng) open 事件的一個示例程序,它的大部分代碼與前面的進程事件監(jiān)視程序示例一樣,只是把進程事件相關(guān)的處理部分替換為文件系統(tǒng)事件對應(yīng)處理。另外一點需要特別注意,對于組號大于 1 的連接器,用戶態(tài)應(yīng)用必須通過第三節(jié)介紹的方式來加入到組中,否則,應(yīng)用無法收到連接器的消息。
下面是作者在紅旗 Linux 桌面版 4.1 上運行 cn_fs_user 的輸出結(jié)果示例:
[root@localhost yangyi]# gcc -I linux-2.6.14.5/include cn_fs_user.c -o
cn_fs_user
[root@localhost yangyi]# ./cn_fs_user
filesystem event: turn on filesystem event listening.
filesystem event: acknowledge for turning on filesystem event listening
filesystem event: open
process 'rfdock' open file 'en.xpm'
filesystem event: open
process 'bash' open file 'passwd'
filesystem event: open
process 'cat' open file 'ld.so.cache'
filesystem event: open
process 'cat' open file 'libc-2.3.2.so'
filesystem event: open
process 'cat' open file 'locale-archive'
filesystem event: open
process 'cat' open file 'test_cn_proc.c'
filesystem event: open
process 'rfdock' open file 'en.xpm'
filesystem event: turn off filesystem event listening.
[root@localhost yangyi]#
|
小結(jié)
連接器是非常便利的用戶態(tài)與內(nèi)核態(tài)的通信方式,內(nèi)核開發(fā)者在編寫內(nèi)核子系統(tǒng)或模塊時可以采用這種方式方便地進行用戶態(tài)與內(nèi)核態(tài)的數(shù)據(jù)交換。本文詳細(xì)地講解了連接器的使用,讀者通過閱讀本文應(yīng)當(dāng)掌握連接器的功能和使用。想了解其它用戶態(tài)與內(nèi)核態(tài)通信方式的讀者,可以參考作者以前的系列文章在 "Linux 下用戶空間與內(nèi)核空間數(shù)據(jù)交換的方式,第 1 部分"和"第 2 部分"。
|
|