【原創】xenomai內核解析--xenomai與普通linux進程之間通訊XDDP(二)--實時與非實時關聯(bind流程)

1.概述

上篇文章介紹了實時端socket創建和配置的流程,本篇文章來看bind操作,實時端與非實時端是如何關聯起來的?

XDDP通訊的底層設備爲xnpipe,是linux任務與xenomai任務通訊的核心,在linux看來是一個字符設備,xnpipe在xenomai內核初始化過程初始化,並完成linux端xnipipe字符設備註冊。

rtipc-arch

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指向的內存池中分配緩衝區內存。

pripool

上面大部分都是關於緩衝區與內存池的設置,到此還沒有看到關於數據真正傳輸控制的東西,__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-bitmap

內核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;
}

xddp-shawd

到此分配好了一個XNPIPE對象,內核所有數據結構初始化好,實時應用可以使用該socket發送接收數據了。

3. xnpipe設備註冊流程

上面僅簡單說明了xnpipe_state,沒有看xnpipe在linux端註冊的具體過程,其實就是註冊一個字符設備,xnpipe在linux端的初始化是在xenomai內核初始化過程中調用xnpipe_mount()完成初始化。

static int __init xenomai_init(void)
{
    ......
    ret = xnpipe_mount(); /*註冊進程間通訊管道xnpipe*/  
    ......
}
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()中先將xnpipe_states[]內的xnpipe對象初始化。然後調用linux驅動相關接口註冊到linux設備管理器,在linux 下生成設備節點/dev/rtpX.

3.2註冊rtpipe設備

接着註冊字符設備,其file_operationsxnpipe_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;
}

註冊的方式是調用 __register_chrdev_region,註冊字符設備的主次設備號和名稱,然後分配一個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的詳細通訊過程,請關注本博客。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章