版权声明:本文为本文为博主原创文章,转载请注明出处。如有问题,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/
1.概述
上篇文章xenomai内核解析--实时IPC概述中介绍了RTIPC,从这篇文章开始开始深入xenomai内核,解析RTIPC的具体实现。
XDDP、IDDP和BUFP由于应用场景不一样,所以底层不一样,但也区别不大。XDDP用于xenomai任务与普通Linux任务通讯,提供两种方式,一种是每次读写作为一个数据报来操作,对应实时任务间的通讯方式IDDP;另一种为可以将多次读写的数据缓冲最后组成一个大的数据报发送,对应实时任务间的BUFP方式。所以解析了XDDP原理,那么IDDP和BUFP自然也就懂了,后面文章我也会简单说一下IDDP、XDDP。
需要先说明一下 XDDP几乎涉及了xenomai的所有关键组件,通过解析xenomai内核XDDP的实现源码,你会明白:
- xenomai内核:
- XDDP的详细实现。
- 实时设备驱动模型:RTDM是如何管理协议类设备的,应用是如何找到并使用具体的协议设备的(其实和Linux类似),如何为xenomai添加一个自定义协议设备等。
- Linux端:字符类设备管理、虚拟文件系统VFS;
- 通讯过程中的内存分配释放:实时内存堆-xnheap,详见xenomai内核解析--实时内存管理--xnheap
- xenomai资源同步互斥机制:xnsynch
- 如何跨域唤醒指定任务:ipipe虚拟中断xnpipe_wakeup_apc
2.XDDP的使用注意事项
上篇文章已经介绍了具体的使用方法,linux端通过read()、write()
读写/dev/rtp<minor>
或/proc/xenomai/registry/rtipc/xddp/label
来通讯,Xenomai端通过套接字recvfrom()或read()
来接收数据,sendto()或write()
来发送数据。其中需要注意的是:
- XDDP 只能由xenomai应用(使用Libcobalt库编译)创建.
- 由于端口号与Linux端次设备号绑定,所以必须两边都关闭释放了才能再次使用同一个端口(可见文末总框图)。
下面我们就沿着这个流程到内核里面一探究竟,看看在内核里面,都创建了哪些数据结构,做了哪些事情。
3.解析socket函数
从调用socket()函数开始。对于xenomai实时程序,该函数不是直接就执行系统调用,而是由xenomai实时库libcobalt中实现,实时应用编译时会链接到该库。
/*xenomai-3.x.x\lib\cobalt\rtdm.c*/
COBALT_IMPL(int, socket, (int protocol_family, int socket_type, int protocol))
{
int s;
s = XENOMAI_SYSCALL3(sc_cobalt_socket, protocol_family,
socket_type, protocol);
if (s < 0) {
s = __STD(socket(protocol_family, socket_type, protocol));
}
return s;
}
从该函数可以看到,首先执行xenomai内核调用,如果xenomai系统调用返回负值,一种情况时产生了错误,另一种情况说明这些参数不是要实时内核提供服务的,接着才去调用标准库执行linux的系统调用。这就实现了同一接口也可以让linux提供服务。
创建socket的时候有三个参数,一个是protocol_family
表示地址族,在linux中,如下两种是比较熟悉的。
#define AF_UNIX 1/* Unix domain sockets */
#define AF_INET 2/* Internet IP Protocol */
对于xenomai,protocol_family
只有一种,如果自己为xenomai内核添加自定义的协议设备就可以通过该参数识别:
/* Address family */
#define AF_RTIPC 111
/* Protocol family */
#define PF_RTIPC AF_RTIPC
第二个参数是socket_type
,也即 Socket 的类型。类型是比较少的。
第三个参数是protocol
,是协议。协议数目是比较多的,也就是说,多个协议会属于同一种类 型。
常用的 Socket 类型有三种,分别是 SOCK_STREAM
、SOCK_DGRAM
和 SOCK_RAW
。
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
......
};
SOCK_STREAM
是面向数据流的,协议 IPPROTO_TCP属于这种类型。SOCK_DGRAM
是面 向数据报的,协议IPPROTO_UDP 属于这种类型。如果在内核里面看的话,IPPROTO_ICMP 也属于这种类型。SOCK_RAW
是原始的 IP 包,IPPROTO_IP 属于这种类型。
对于socket_type
,在xenomai 中,通讯是在系统内部,统一为数据报即SOCK_DGRAM
,其余无效。xenomai提供的protocol
如下几种:
enum {
/** Default protocol (IDDP) */
IPCPROTO_IPC = 0,
IPCPROTO_XDDP = 1,
IPCPROTO_BUFP = 3,
IPCPROTO_MAX
};
其实xenomai提供的rtipc只作为实时进程间通讯,内部与linux socket一点关系都没有,从上就可以看出,仅是函数接口相同而已。
这一节,我们重点看IPCPROTO_XDDP
协议。实时系统调用sc_cobalt_socket
对应内核代码如下。它会调用__rtdm_dev_socket()
/*kernel\xenomai\posix\io.c*/
COBALT_SYSCALL(socket, lostage,
(int protocol_family, int socket_type, int protocol))
{
return __rtdm_dev_socket(protocol_family, socket_type, protocol);
}
/*kernel\xenomai\rtdm\core.c*/
int __rtdm_dev_socket(int protocol_family, int socket_type,
int protocol)
{
struct rtdm_dev_context *context;
struct rtdm_device *dev;
int ufd, ret;
secondary_mode_only();
dev = __rtdm_get_protodev(protocol_family, socket_type);
......
ufd = __rtdm_anon_getfd("[rtdm-socket]", O_RDWR);
......
ret = create_instance(ufd, dev, &context);
......
if (dev->ops.socket) {
ret = dev->ops.socket(&context->fd, protocol);
......
}
......
ret = rtdm_fd_register(&context->fd, ufd);
......
return ufd;
}
secondary_mode_only()
表示目前上下文必须是linux域,应为涉及到linux一些资源分配,如文件描述符。你可能会疑惑,我们创建调用socket函数的应用已经在实时线程里了,而且我们使用实时库libcobalt发起的系统调用,进内核后应该是haed域,而这里为什么还secondary_mode_only?解答这个问题请移步本博客其他文章xenomai内核解析--双核系统调用(一)小节的权限控制。
先是根据protocol_family
和socket_type
转换为xnkey_t
来查找对应的rtdm_device.
struct rtdm_device *__rtdm_get_protodev(int protocol_family, int socket_type)
{
struct rtdm_device *dev = NULL;
struct xnid *xnid;
xnkey_t id;
secondary_mode_only();
id = get_proto_id(protocol_family, socket_type);
mutex_lock(®ister_lock);
xnid = xnid_fetch(&protocol_devices, id);
if (xnid) {
dev = container_of(xnid, struct rtdm_device, proto.id);
__rtdm_get_device(dev);
}
mutex_unlock(®ister_lock);
return dev;
}
3.1 RTDM Protocol Devices
id类型为longlong,高32位为protocol_family
,低32位为socket_type
,将它作为key在红黑树protocol_devices
上找到对应的设备。
static struct rb_root protocol_devices;
protocol_devices
是一个全局变量,其类型是struct rb_root,我们知道xenomai 实时驱动模型(RTDM)将所有实时设备分为两种Protocol Devices(协议类设备)
和CharacterDevices(字符类设备)
,protocol_devices
作为所有Protocol Devices的根节点,所有Protocol Devices驱动注册时调用rtdm_dev_register()
后都会挂到protocol_devices
上。
xenomai中实时进程间通讯RTDM设备rtipc及其rtipc_driver,在drivers\xenomai\ipc\rtipc.c
中如下:
static struct rtdm_driver rtipc_driver = {
.profile_info = RTDM_PROFILE_INFO(rtipc,
RTDM_CLASS_RTIPC,
RTDM_SUBCLASS_GENERIC,
1),
.device_flags = RTDM_PROTOCOL_DEVICE,
.device_count = 1,
.context_size = sizeof(struct rtipc_private),
.protocol_family = PF_RTIPC, /*地址族*/
.socket_type = SOCK_DGRAM, /*socket类型*/
.ops = {
.socket = rtipc_socket,
.close = rtipc_close,
.recvmsg_rt = rtipc_recvmsg,
.recvmsg_nrt = NULL,
.sendmsg_rt = rtipc_sendmsg,
.sendmsg_nrt = NULL,
.ioctl_rt = rtipc_ioctl,
.ioctl_nrt = rtipc_ioctl,
.read_rt = rtipc_read,
.read_nrt = NULL,
.write_rt = rtipc_write,
.write_nrt = NULL,
.select = rtipc_select,
},
};
static struct rtdm_device device = {
.driver = &rtipc_driver,
.label = "rtipc",
};
从rtipc_driver
中的rtdm_fd_ops
我们就可以看出一二,创建一个rtipc socket后,对该socket的数据收发、读写等操作都会调用到rtdm_fd_ops
内的rtipc_sendmsg()、rtipc_recvmsg()
等函数。
同样,如果需要自定义一个xenomai Protocol Devices,实现这些函数,为该设备设置好protocol_family
和socket_type
后,我们的实时应用就可以通过调用socket(),然后xenomai RTDM通过(protocol_family<<32) | socket_type
作为xnkey_t到该设备及对应的driver,来让该设备为我们服务。
回到__rtdm_dev_socket()
,接下来调用__rtdm_anon_getfd
完成在用户空间定义一个[rtdm-socket]
的文件,将[rtdm-socket]
与rtdm_dumb_fops
结合起来。
int __rtdm_dev_socket(int protocol_family, int socket_type,
int protocol)
{
......
ufd = __rtdm_anon_getfd("[rtdm-socket]", O_RDWR);
......
......
ret = create_instance(ufd, dev, &context);
......
}
为什么要这样做呢?用户空间需要一个文件描述符来与内核rtdm_fd对应起来,ufd作为用户套接字socket,后面的代码会看到ufd成为红黑树上查找rtdm_fd的keyt_t,当使用socket接口对ufd操作时,到了内核里就会用ufd找到对应的rtdm_fd。但是直接对ufd使用read/write等操作是不允许的,所以还需要为ufd设置file_operation rtdm_dumb_fops
,rtdm_dumb_fops
里的函数均打印一条警告信息,直接对ufd使用read/write等操作时就内核就会输出WARNING信息。
static inline void warn_user(struct file *file, const char *call)
{
struct dentry *dentry = file->f_path.dentry;
printk(XENO_WARNING
"%s[%d] called regular %s() on /dev/rtdm/%s\n",
current->comm, task_pid_nr(current), call + 5, dentry->d_name.name);
}
static ssize_t dumb_read(struct file *file, char __user *buf,
size_t count, loff_t __user *ppos)
{
warn_user(file, __func__);
return -EINVAL;
}
.....
const struct file_operations rtdm_dumb_fops = {
.read = dumb_read,
.write = dumb_write,
.poll = dumb_poll,
.unlocked_ioctl = dumb_ioctl,
};
接着调用create_instance()
创建rtdm_dev_context
并初始化对应的结构体,在RTDM中,struct rtdm_driver与struct rtdm_device描述了一个设备的共有抽象信息,但具体的设备有其操作的具体数据,称为实时设备的上下文rtdm_dev_context
,结构如下:
struct rtdm_dev_context {
struct rtdm_fd fd;
/** Set of active device operation handlers */
/** Reference to owning device */
struct rtdm_device *device;
/** Begin of driver defined context data structure */
char dev_private[0];
};
其中成员fd
类型为struct rtdm_fd
,其中记录着该设备的OPS,所属线程等信息。
成员变量dev_private为私有数据的起始地址,至于设备的私有数据有多大,在rtdm_device用context_size
表,对于rtipc,其大小为sizeof(struct rtipc_private)
,所以为rtipc创建rtdm_dev_context时分配的内存大小为sizeof(struct rtdm_dev_context) + rtipc_driver->context_size
。
struct rtdm_fd
如下
struct rtdm_fd {
unsigned int magic; /*RTDM_FD_MAGIC*/
struct rtdm_fd_ops *ops; /*RTDM设备可用的操作,*/
struct cobalt_ppd *owner; /*所属Process*/
unsigned int refs; /*打开计数*/
int minor;
int oflags;
#ifdef CONFIG_XENO_ARCH_SYS3264
int compat;
#endif
struct list_head cleanup;
};
- magic fd的类型 RTDM_FD_MAGIC
- *ops 描述RTDM设备可用的操作。 这些处理程序由RTDM设备驱动程序(rtdm_driver)实现。
- *owner该rtdm_fd所属的应用程序。
- refs 记录该fd的引用次数,当为0时会触发执行
ops->close()
- minor
- oflags
- cleanup
create_instance()
执行完后各结构暂时如下:
接着执行ops.socket()
也就是rtipc_socket()
,传入参数为rtdm_fd
和protocol(IPCPROTO_XDDP)
.
if (dev->ops.socket) {
ret = dev->ops.socket(&context->fd, protocol);
......
}
static int rtipc_socket(struct rtdm_fd *fd, int protocol)
{
struct rtipc_protocol *proto;
struct rtipc_private *priv;
int ret;
if (protocol < 0 || protocol >= IPCPROTO_MAX)
return -EPROTONOSUPPORT;
if (protocol == IPCPROTO_IPC)
/* Default protocol is IDDP */
protocol = IPCPROTO_IDDP;
proto = protocols[protocol - 1];
if (proto == NULL) /* Not compiled in? */
return -ENOPROTOOPT;
priv = rtdm_fd_to_private(fd);
priv->proto = proto;
priv->state = kmalloc(proto->proto_statesz, GFP_KERNEL);
......
xnselect_init(&priv->send_block);
xnselect_init(&priv->recv_block);
ret = proto->proto_ops.socket(fd);
......
return ret;
}
先看协议是不是xenomai支持的,如果协议类型为IPCPROTO_IPC
,那就是默认协议IPCPROTO_IDDP
,接着从数组中取出协议对应的rtipc_protocol* proto
,之前说过rtipc提供三种进程间通讯:IDDP、XDDP、BUFP,用结构体struct rtipc_protocol
来描述它们,保存在数组rtipc_protocol
中:
static struct rtipc_protocol *protocols[IPCPROTO_MAX] = {
#ifdef CONFIG_XENO_DRIVERS_RTIPC_XDDP
[IPCPROTO_XDDP - 1] = &xddp_proto_driver,
#endif
#ifdef CONFIG_XENO_DRIVERS_RTIPC_IDDP
[IPCPROTO_IDDP - 1] = &iddp_proto_driver,
#endif
#ifdef CONFIG_XENO_DRIVERS_RTIPC_BUFP
[IPCPROTO_BUFP - 1] = &bufp_proto_driver,
#endif
};
接着根据rtdm_fd得到rtdm_dev_context
内的dev_private[0]
,这里先看一下struct rtipc_private
各成员变量的意思:
struct rtipc_private {
struct rtipc_protocol *proto;
DECLARE_XNSELECT(send_block);//struct xnselect send_block
DECLARE_XNSELECT(recv_block);//struct xnselect recv_block
void *state;
};
- proto指向具体的rtipc_protocol
- send_block、send_block是链表,发送或接收阻塞时会插入该链表
- state 与
dev_private[0]
类似,指向不同协议所需的而外空间。对于XDDP说指向sizeof(struct xddp_socket)
大小内存。
得到dev_private[0]后,强制类型转换为structr tipc_private *priv
后开始初始化结构体tipc_private
内各成员.最后调用具体协议的下的socket()
,传入参数fd,对应XDDP协议xddp_socket()
;
static int xddp_socket(struct rtdm_fd *fd)
{
struct rtipc_private *priv = rtdm_fd_to_private(fd);
struct xddp_socket *sk = priv->state;
sk->magic = XDDP_SOCKET_MAGIC;
sk->name = nullsa; /* Unbound */
sk->peer = nullsa;
sk->minor = -1;
sk->handle = 0;
*sk->label = 0;
sk->poolsz = 0;
sk->buffer = NULL;
sk->buffer_port = -1;
sk->bufpool = NULL;
sk->fillsz = 0;
sk->status = 0;
sk->timeout = RTDM_TIMEOUT_INFINITE;
sk->curbufsz = 0;
sk->reqbufsz = 0;
sk->monitor = NULL;
rtdm_lock_init(&sk->lock);
sk->priv = priv;
return 0;
}
在xddp_socket()
主要初始化struct xddp_socket
,也很重要,后面会详细解析它。xddp_socket()
执行完毕后回到__rtdm_dev_socket()
,接下来调用rtdm_fd_register()
将rdtm_fd并注册到cobalt_ppd
中。
int __rtdm_dev_socket(int protocol_family, int socket_type,
int protocol)
{
......
ret = rtdm_fd_register(&context->fd, ufd);
.....
return ufd;
}
int rtdm_fd_register(struct rtdm_fd *fd, int ufd)
{
struct rtdm_fd_index *idx;
struct cobalt_ppd *ppd;
spl_t s;
int ret = 0;
ppd = cobalt_ppd_get(0);
idx = kmalloc(sizeof(*idx), GFP_KERNEL);
......
idx->fd = fd;
......
ret = xnid_enter(&ppd->fds, &idx->id, ufd);
.....
return ret;
}
3.2 rtdm_fd_index
首先获取当前进程的struct cobalt_ppd
,然后分配一个struct rtdm_fd_index
,看名字知道rtdm_fd的index结构,怎么索引呢?通过传入的ufd,传入的ufd作为key,构造一个rtdm_fd_index
,然后插入ppd->fds
,ppd->fds
时一颗红黑树,每个实时任务创建或打开的实时设备fd都是由fds来记录着。
将ufd与rtdm_fd联系起来以后,socket函数执行完毕,返回ufd
,用户空间通过ufd发起内核调用时,就可通过ufd找到内核里相关的所有的结构。
完成各数据结构分配关系链接后,下一步就可以对该socket进行配置了。
【原创】xenomai内核解析--xenomai与普通linux进程之间通讯XDDP--实时端socket创建流程(一)