版權聲明:本文為本文為博主原創文章,轉載請注明出處。如有問題,歡迎指正。博客地址:https://www.cnblogs.com/wsg1100/
1.概述
上篇文章介紹了實時端socket創建和配置的流程,本篇文章來看bind操作,實時端與非實時端是如何關聯起來的?
XDDP通訊的底層設備為xnpipe,是linux任務與xenomai任務通訊的核心,在linux看來是一個字符設備,xnpipe在xenomai內核初始化過程初始化,並完成linux端xnipipe字符設備注冊。
bind的主要操作就是根據socket配置,分配資源,如指定通訊過程中分配釋放的內存池(xnheap)、緩沖區大小等,並根據端口號,分配對應的xnpipe設備,並將rtdm_fd與xnipipe設備通過數組關聯(用次設備號作為數組下標,端口號即次設備號)。下面來看詳細過程。
2. 解析bind函數
與前面函數一樣,用戶空間實時任務對socket調用bind()
函數,先進入實時庫licobalt,再由實時庫libcobalt來發起實時內核系統調用:
saddr.sipc_family = AF_RTIPC;
saddr.sipc_port = XDDP_PORT;
ret = bind(s, (struct sockaddr *)&saddr, sizeof(saddr));
/*lib\cobalt\rtdm.c*/
COBALT_IMPL(int, bind, (int fd, const struct sockaddr *my_addr, socklen_t addrlen))
{
.....
ret = do_ioctl(fd, _RTIOC_BIND, &args);
if (ret != -EBADF && ret != -ENOSYS)
return set_errno(ret);
return __STD(bind(fd, my_addr, addrlen));
}
static int do_ioctl(int fd, unsigned int request, void *arg)
{
....
ret = XENOMAI_SYSCALL3(sc_cobalt_ioctl, fd, request, arg);
....
return ret;
}
進入系統調用后執行__xddp_ioctl()
.
static int __xddp_ioctl(struct rtdm_fd *fd,
unsigned int request, void *arg)
{
struct rtipc_private *priv = rtdm_fd_to_private(fd);
struct sockaddr_ipc saddr, *saddrp = &saddr;
struct xddp_socket *sk = priv->state;
int ret = 0;
switch (request) {
......
COMPAT_CASE(_RTIOC_BIND):/*bind操作*/
ret = rtipc_get_sockaddr(fd, &saddrp, arg);
.......
ret = __xddp_bind_socket(priv, saddrp);
break;
......
}
return ret;
}
前面文章看了__xddp_ioctl()
中的COMPAT_CASE(_RTIOC_SETSOCKOPT)
分支,現在來看COMPAT_CASE(_RTIOC_BIND)
,__xddp_bind_socket()
.
static int __xddp_bind_socket(struct rtipc_private *priv,
struct sockaddr_ipc *sa)
{
struct xddp_socket *sk = priv->state;
struct xnpipe_operations ops;
rtdm_lockctx_t s;
size_t poolsz;
void *poolmem;
...../*參數檢查*/
poolsz = sk->poolsz;
if (poolsz > 0) {
poolsz = xnheap_rounded_size(poolsz);//對齊
poolsz += xnheap_rounded_size(sk->reqbufsz);
poolmem = xnheap_vmalloc(poolsz); //ZONE_NORMAL中分配,分配后使用xnhead方式進行管理
......
ret = xnheap_init(&sk->privpool, poolmem, poolsz);/*初始化內存區*/
.......
sk->bufpool = &sk->privpool;
} else
sk->bufpool = &cobalt_heap;
if (sk->reqbufsz > 0) {
sk->buffer = xnheap_alloc(sk->bufpool, sk->reqbufsz);/*從bufpool 分配sk->buffer*/
......
sk->curbufsz = sk->reqbufsz;
}
/*__xddp_bind_socket()剩余部分*/
.......
}
該函數中先檢查相關參數的合法性,然后配置xddp本地內存池privpool
,上篇文章setsocketopt()
只是設置了內存池的大小poolsz
,但是還沒有真正分配內存,現在開始分配內存,先將內存大小向上頁對齊(PAGE_SIZE為4K),由於xenomai內存池管理緣故,每個內存池至少為(2*PAGE_SIZE);然后看看poolsz
是否夠分配reqbufsz
,不夠的話向reqbufsz
對齊。
大小確定后正式調用linux接口分配,從ZONE_NORMAL中分配,分配后調用xnheap_init()
將該內存初始化(具體流程參見文章xenomai內核解析--實時內存管理--xnheap)。然后將bufpool指向該內存池。接着分配數據緩沖區bufpool
,從bufpool
指向的內存池中分配緩沖區內存。
上面大部分都是關於緩沖區與內存池的設置,到此還沒有看到關於數據真正傳輸控制的東西,__xddp_bind_socket()
接着要完成bind相關工作:
static int __xddp_bind_socket(struct rtipc_private *priv,
struct sockaddr_ipc *sa)
{
struct xnpipe_operations ops;
......
/*接上部分*/
sk->fd = rtdm_private_to_fd(priv);
ops.output = &__xddp_output_handler;
ops.input = &__xddp_input_handler;
ops.alloc_ibuf = &__xddp_alloc_handler;
ops.free_ibuf = &__xddp_free_handler;
ops.free_obuf = &__xddp_free_handler;
ops.release = &__xddp_release_handler;
ret = xnpipe_connect(sa->sipc_port, &ops, sk);//將SK與OPS與sipc_port聯系起來,綁定端口
.......
sk->minor = ret;
sa->sipc_port = ret;
sk->name = *sa;
/*剩余部分*/
}
先取出rtdm_fd,設置struct xnpipe_operations
,struct xnpipe_operations
中的ops為xddp通訊過程中buf分配釋放的函數;
struct xnpipe_operations {
void (*output)(struct xnpipe_mh *mh, void *xstate);
int (*input)(struct xnpipe_mh *mh, int retval, void *xstate);
void *(*alloc_ibuf)(size_t size, void *xstate);
void (*free_ibuf)(void *buf, void *xstate);
void (*free_obuf)(void *buf, void *xstate);
void (*release)(void *xstate);
};
誰會用到這些buf?xnpipe,xnpipe管理收發的數據包時需要動態管理buf,在具體通訊的時候,我們要為每一個數據包在內核空間臨時申請一塊內存來存放數據,這塊內存的申請釋放要足夠快,而且不能影響實時性,所以得從xnheap中申請,也就是前面xddp-socket->bufpool
指向的內存池,對每塊內存的分配釋放就是由這個回調函數來完成。需要注意的是,linux端讀寫數據的時候也是從xddp-socket->bufpool
中分配釋放內存,這會在后面文章中看到;
還有一些場合,執行內核用戶線程需要在數據到來或發送的時候添加一些hook,這通過output()/input()來設置monitor函數。
接下來調用xnpipe_connect(sa->sipc_port, &ops, sk)
將xddp_socket與linux端的xnipipe函數關聯起來,由於xnpipe不是動態分配的,內核配置時確定xnpipe的數量,以數組的形式,這樣確保了確定性,linux啟動時,xenomai內核初始化過程中就已將xnpipe初始化。
2.1 xnpipe介紹
XNPIPE是xenomai內核提供的通訊層,是linux任務與xenomai任務通訊的核心。每個xddp socket對應一個XNPIPE,XNPIPE的個數XNPIPE_NDEVS在內核編譯時配置,內核默認配置為32個XNPIPE對象保存在全局數組xnpipe_states[XNPIPE_NDEVS]
中,全局bitmap xnpipe_bitmap
中記錄着XNPIPE對象分配情況,xnpipe_states[]
內的xpipe對象在xenomai初始化時初始化,在linux VFS下生成對應的設備節點,后一節說明。
內核xnpipe數量配置menuconfig 項如下:
[*] Xenomai/cobalt --->
Sizes and static limits --->
(32) Number of pipe devices
XNPIPE對象結構struct xnpipe_state
如下。
struct xnpipe_state {
struct list_head slink; /* Link on sleep queue */
struct list_head alink; /* Link on async queue */
struct list_head inq; /* in/out是從實時端看的類似USB的端口*/
int nrinq; /*鏈表節點數,代指消息個數*/
struct list_head outq; /* From kernel to user-space */
int nroutq;
struct xnsynch synchbase;/*同步*/
struct xnpipe_operations ops;/*執行一些hook函數,如釋放消息節點的內存,有消息時執行monitor函數等*/
void *xstate; /* xddp是指向 xddp_socket */
/* Linux kernel part */
unsigned long status;/*狀態標識*/
struct fasync_struct *asyncq;
wait_queue_head_t readq; /* linux端讀等待隊列*/
wait_queue_head_t syncq; /*linux端寫同步等待隊列*/
int wcount; /* 這個設備節點的進程數量*/
size_t ionrd; /*緩沖包數據長度*/
};
最為linux任務與xenomai任務通訊的中間人,struct xnpipe_state成員分為兩個部分,首先看xenomai相關成員
- slink、alink 鏈接到xnpipe睡眠隊列 、async 隊列。
- inq 實時端接收數據包隊列,其中的in是相對xenomai端來說的,每個鏈表節點表示一個數據包,包個數用成員
nrinq
記錄。 - outq 實時端發送數據包隊列,其中的out是相對xenomai端來說的,每個鏈表節點表示一個數據包,包個數用成員
nroutq
記錄。 - synchbase xenomai資源同步對象,當沒有數據時會阻塞在
xnsynch
等待資源可用。 - ops 動態發送數據過程中執行的回調函數。
- xstate 指向私有數據,對於xddp指向xddp_socket。
接着是linux相關成員:
-
status linux端收發操作狀態碼,各狀態碼定義如下
#define XNPIPE_KERN_CONN 0x1 /*內核端(rt)已連接*/ #define XNPIPE_KERN_LCLOSE 0x2 /*內核端(rt)關閉*/ #define XNPIPE_USER_CONN 0x4 /*用戶端(nrt)已鏈接*/ #define XNPIPE_USER_SIGIO 0x8 /*用戶(nrt)已設置異步通知*/ #define XNPIPE_USER_WREAD 0x10 /*用戶(nrt)端讀*/ #define XNPIPE_USER_WREAD_READY 0x20 /*用戶端(nrt)讀就緒*/ #define XNPIPE_USER_WSYNC 0x40 /*用戶端(nrt)寫同步*/ #define XNPIPE_USER_WSYNC_READY 0x80 /*rt端已讀數據,待完成寫同步喚醒nrt*/ #define XNPIPE_USER_LCONN 0x100 /*(nrt)端正在執行連接操作*/
-
asyncq 異步通知隊列用於linux端poll操作。
-
readq linux端讀等待隊列,當沒有數據時會在該隊列上阻塞,知道有數據可讀。
-
syncq linux端寫同步隊列,對同步發送的數據包,會在該隊列上阻塞知道數據包被實時端讀取。
-
wcount 使用同一個xnpipe的linux端進程數。
-
ionrd 緩沖區數據包長度。
2.2 xnpipe與xddp_socket關聯
回到__xddp_bind_socket()
接着調用xnpipe_connect()
開始執行bind工作,sa->sipc_port
中保存着我們要使用的rtipc端口(XNPIPE),如果為-1表示自動分配,自動分配后Linux端可通過上節設置的label來找到該xddp。
int xnpipe_connect(int minor, struct xnpipe_operations *ops, void *xstate)
{
struct xnpipe_state *state;
int need_sched = 0, ret;
spl_t s;
minor = xnpipe_minor_alloc(minor);
.....
state = &xnpipe_states[minor];
xnlock_get_irqsave(&nklock, s);
ret = xnpipe_set_ops(state, ops);
.....
state->status |= XNPIPE_KERN_CONN;
xnsynch_init(&state->synchbase, XNSYNCH_FIFO, NULL);
state->xstate = xstate;
state->ionrd = 0;
if (state->status & XNPIPE_USER_CONN) {
if (state->status & XNPIPE_USER_WREAD) {
/*
* Wake up the regular Linux task waiting for
* the kernel side to connect (xnpipe_open).
*/
state->status |= XNPIPE_USER_WREAD_READY;
need_sched = 1;
}
if (state->asyncq) { /* Schedule asynch sig. */
state->status |= XNPIPE_USER_SIGIO;
need_sched = 1;
}
}
if (need_sched)
xnpipe_schedule_request();
xnlock_put_irqrestore(&nklock, s);
return minor;
}
在xnpipe_connect中首先根據傳入的sa->sipc_port
,分配對應的XNPIPE設備號minor
。
static inline int xnpipe_minor_alloc(int minor)
{
......
if (minor == XNPIPE_MINOR_AUTO)//(-1)表示自動分配端口
minor = find_first_zero_bit(xnpipe_bitmap, XNPIPE_NDEVS);
if (minor == XNPIPE_NDEVS ||
(xnpipe_bitmap[minor / BITS_PER_LONG] &
(1UL << (minor % BITS_PER_LONG))))
minor = -EBUSY;
else
xnpipe_bitmap[minor / BITS_PER_LONG] |=
(1UL << (minor % BITS_PER_LONG));
.....
return minor;
}
xnpipe_minor_alloc()
就是去xnpipe_bitmap
中查看我們要bind的rtipc_port
是否已經被使用,指定-1則表示自動分配。得到可用的minor
后,就去xnpipe_states[]
中得到對應的struct xnpipe_state
,配置到xnpipe的ops,初始化xenomai資源同步對象state->synchbase
,設置狀態掩碼為rt已鏈接,如果nrt此時也處於open xddp設備狀態,喚醒 Linux任務,以等待linux內核端連接。
接着__xddp_bind_socket()
剩余部分,如果我們設置的是使用label方式,自動分配的端口號,就調用xnregistry_enter
注冊一個實時對象xnregistry,以便linux端通過路徑/proc/xenomai/registry/rtipc/xddp/%s
來打開通訊端點。
將分配的XNPIPE minor與rddm_fd對應關系保存到portmap[]
中;
static int __xddp_bind_socket(struct rtipc_private *priv,
struct sockaddr_ipc *sa)
{
/* Set default destination if unset at binding time.*/
if (sk->peer.sipc_port < 0)
sk->peer = *sa;
if (poolsz > 0)
xnheap_set_name(sk->bufpool, "xddp-pool@%d", sa->sipc_port);
if (*sk->label) {/*使用xlabel*/
ret = xnregistry_enter(sk->label, sk, &sk->handle,
&__xddp_pnode.node);
.......
}
cobalt_atomic_enter(s);
portmap[sk->minor] = rtdm_private_to_fd(priv);
__clear_bit(_XDDP_BINDING, &sk->status);
__set_bit(_XDDP_BOUND, &sk->status);
if (xnselect_signal(&priv->send_block, POLLOUT))
xnsched_run();
cobalt_atomic_leave(s);
return 0;
}
到此分配好了一個XNPIPE對象,內核所有數據結構初始化好,實時應用可以使用該socket發送接收數據了。
3. xnpipe設備注冊流程
上面僅簡單說明了xnpipe_state,沒有看xnpipe在linux端注冊的具體過程,其實就是注冊一個字符設備,xnpipe在linux端的初始化是在xenomai內核初始化過程中調用xnpipe_mount()
完成初始化。
static int __init xenomai_init(void)
{
......
ret = xnpipe_mount(); /*注冊進程間通訊管道xnpipe*/
......
}
static struct file_operations xnpipe_fops = {
.read = xnpipe_read,
.write = xnpipe_write,
.poll = xnpipe_poll,
.unlocked_ioctl = xnpipe_ioctl,
.open = xnpipe_open,
.release = xnpipe_release,
.fasync = xnpipe_fasync
};
int xnpipe_mount(void)
{
struct xnpipe_state *state;
struct device *cldev;
int i;
for (state = &xnpipe_states[0];
state < &xnpipe_states[XNPIPE_NDEVS]; state++) {
state->status = 0;
state->asyncq = NULL;
INIT_LIST_HEAD(&state->inq); /*初始化數據包鏈表*/
state->nrinq = 0;
INIT_LIST_HEAD(&state->outq);/*初始化數據包鏈表*/
state->nroutq = 0;
}
/*創建class*/
xnpipe_class = class_create(THIS_MODULE, "frtpipe");
if (IS_ERR(xnpipe_class)) {
printk(XENO_ERR "error creating rtpipe class, err=%ld\n",
PTR_ERR(xnpipe_class));
return -EBUSY;
}
/*創建設備*/
for (i = 0; i < XNPIPE_NDEVS; i++) { /*創建rtp1-rtpn*/
cldev = device_create(xnpipe_class, NULL,
MKDEV(XNPIPE_DEV_MAJOR, i),
NULL, "rtp%d", i);
.......
}
/*注冊字符設備*/
if (register_chrdev(XNPIPE_DEV_MAJOR, "rtpipe", &xnpipe_fops)) {
......
}
/*注冊xenomai與linux間異步喚醒虛擬中斷*/
xnpipe_wakeup_apc =
xnapc_alloc("pipe_wakeup", &xnpipe_wakeup_proc, NULL);
return 0;
}
3.1 xnpipe初始化與設備創建
xnpipe_mount()中,內核構建的時候我們在指定了多少個xnipipe就要注冊多少個字符設備
-
將xnpipe_states[]內的xnpipe對象初始化。
-
創建設備類.
-
創建設備.
device_create() ->device_create_vargs() ->device_create_groups_vargs() ->dev = kzalloc(sizeof(*dev), GFP_KERNEL); ->retval = device_add(dev);
設備添加過程中,向用戶空間發出uevent(添加對象)事件,用戶空間的守護進程
systemd-udevd
監聽到該事件后,systemd-udevd
在/dev
下生成設備節點/dev/rtpX
.
3.2注冊rtpipe設備
接着注冊字符設備,將file_operation與cdev實列關聯,其file_operations
為xnpipe_fops
.linux端最終通過這些接口來操作設備/dev/rtpX
來與xenomai 應用通訊。
static struct file_operations xnpipe_fops = {
.read = xnpipe_read,
.write = xnpipe_write,
.poll = xnpipe_poll,
.unlocked_ioctl = xnpipe_ioctl,
.open = xnpipe_open,
.release = xnpipe_release,
.fasync = xnpipe_fasync
};
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
cdev = cdev_alloc();
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
cd->cdev = cdev;
return major ? 0 : cd->major;
}
字符設備在內核設備數據庫中由cdev結構體表示,字符設備驅動程序的主要工作就是創建並向內核注冊cdev實例。注冊的方式是調用 __register_chrdev_region,傳入注冊字符設備的主次設備號和名稱(這里需要注意了,次設備號就是數組下標,也就是我們bind的端口號),然后分配一個 struct cdev
結構,將 cdev 的 ops 成員變量指向這個模塊聲明的 file_operations
。然后,cdev_add 會將這個字符設備添加到內核中一個叫作 struct kobj_map *cdev_map
的結構,來統一管理所有字符設備。
其中,MKDEV(cd->major, baseminor)
表示將主設備號和次設備號生成一個 dev_t
的整數,然后將這個整數 dev_t
和 cdev
關聯起來。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
kobject_get(p->kobj.parent);
return 0;
}
3.3 注冊xnpipe_wakeup_apc
接着注冊一個異步過程調用(Asynchronous Procedure Call)xnpipe_wakeup_apc
,apc基於ipipe虛擬中斷。通過APC,Xenomai域中的活動可以讓在Linux內核重新獲得控制后,讓要延遲處理的程序盡快的在linux域中調度。
xnpipe_wakeup_apc
是ipipe實現的一種虛擬中斷機制,主要用於xenomai內核與linux內核的事件通知,其處理過程和ipipe處理硬件中斷一致,所以實時性好。其具體實現會在ipipe系列文章中詳細解析,敬請關注本博客。
現簡單說明其作用:linux端一個任務\(nrt\)與xenomai實時任務\(rt\)使用xddp進行通訊,此時\(nrt\)讀阻塞等待數據,當\(rt\)向\(nrt\)發送數據后,xenomai內核就會發送一個xnpipe_wakeup_apc
,由於是基於ipipe虛擬中斷實現,相當於給linux發送了一個中斷,發送后會將該虛擬中斷暫時在linux域掛起,當linux得到運行時才會去處理該虛擬中斷的handler,進而知道可以喚醒阻塞的\(nrt\),這個過程中完全是在xenomai域完成的,對xenomai實時性沒有任何影響。
后續文章將從linux端、實時端的數據收發接口進行解析XDDP的詳細通訊過程,請關注本博客。