2017-07-05
上文以一个简单的案例描述了通过Netlink进行用户、内核通信的流程,本节针对流程中的各个要点进行深入分析
- sock的创建
- sock管理结构
- sendmsg源码分析
sock的创建
这点包含用户socket的创建以及内核socket的创建,前者通过socket调用实现,后者通过netlink_kernel_create实现。先看用户层的实现
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
用户层实现较为简单,其目的就是获取一个socket描述符,分为两步,通过sock_create创建socket,通过sock_map_fd关键一个文件描述符。前者最终要调用到__sock_create,该函数仍然主要分为两部分,调用sock_alloc分配一个socket结构,调用对应协议族的create函数。之前有分析到,针对Netlink协议族,对应netlink_create函数。分配sock是通过其inode得到,inode的分配就和文件系统对应的超级快注册的函数有关。这里通过new_inode_pseudo其实分配的是一个socket_alloc结构,其中包含了socket和inode。分配好以后对其做一些初始化,inode的函数操作表i_op对应sockfs_inode_ops。分配好socket之后,调用对应协议族的create函数,协议族由net_proto_family表示,有一个全局的数组net_families记录所有的的协议族。协议族的number就是下标。我们看下Netlink对应的netlink_create函数,只看核心代码
if (nl_table[protocol].registered && try_module_get(nl_table[protocol].module)) module = nl_table[protocol].module; else err = -EPROTONOSUPPORT; cb_mutex = nl_table[protocol].cb_mutex; bind = nl_table[protocol].bind; netlink_unlock_table();
这里我们需要注意下if条件,可以看到如果协议对应的netlink_table结构没有在nl_table中注册,就会返回错误,这就可以解释之前没有加载内核模块创建sock之前,运行用户程序出现创建socket失败的情况了。在已经注册的情况下,才会接着往下,调用__netlink_create,设置socket的操作函数表为netlink_ops,然后分配了一个sock结构,注意sock结构作为netlink_sock内嵌结构,一次性分配的是netlink_sock,调用后sock_init_data对sock结构做初始化,然后初始化了netlink_sock的等待队列等。
创建好socket之后,调用sock_map_fd为其分配一个文件描述符,该函数就比较简单
static int sock_map_fd(struct socket *sock, int flags) { struct file *newfile; int fd = get_unused_fd_flags(flags); if (unlikely(fd < 0)) return fd; newfile = sock_alloc_file(sock, flags, NULL); if (likely(!IS_ERR(newfile))) { fd_install(fd, newfile); return fd; } put_unused_fd(fd); return PTR_ERR(newfile); }
首先获取一个可用的fd,fd即文件描述符数组中的下标,然后创建一个file结构。file结构的private_data字段指向socket。最后调用fd_install把文件file设置到数组中fd下标处。
而内核创建socket,通过netlink_kernel_create创建,该函数直接调用了__netlink_kernel_create函数,这里和socket系统调用有些类似,首先是通过sock_create_lite创建socket结构,其中调用了sock_alloc函数 ,该函数前面也有用到。而后也同样调用__netlink_create创建sock结构并和socket建立关联,只是这里如果参数中包含接收函数,会设置接收函数,最后会调用netlink_insert把sock结构插入到链表中。
if (cfg && cfg->input)//设置接收函数 nlk_sk(sk)->netlink_rcv = cfg->input; /*把sock加入到链表中*/ if (netlink_insert(sk, net, 0)) goto out_sock_release;
由于nl_table是初始化好的,在内核sock加入时无需验证其是否已经注册,所以这里出来后要验证下,如果没有注册,则需要重新注册下,当然在此之前设置netlink_sock的内核sock位NETLINK_KERNEL_SOCKET。
sock管理结构
内核中通过一个全局数组nl_table管理各个协议的netlink sock,该数组会在netlink协议族注册的时候进行初始化,每个表项是一个netlink_table结构,在netlink_proto_init函数中
nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL); if (!nl_table) goto panic;
MAX_LINKS是协议的最大值,定义为32,目前有不少已经使用。
struct netlink_table { struct nl_portid_hash hash; struct hlist_head mc_list; struct listeners __rcu *listeners; unsigned int flags; unsigned int groups; struct mutex *cb_mutex; struct module *module; void (*bind)(int group); int registered; };
hash是通过数组实现的hash表,其本身是一个nl_portid_hash结构,nl_portid_hash中有一个链表头数组table,记录各个protid对应的链表头,大致结构如下,其中实现表示指针指向,虚线表示内嵌结构。registered表明对应的协议是否已经注册。module一般指向当前模块
下面我们在看下netlink_insert函数
static int netlink_insert(struct sock *sk, struct net *net, u32 portid) { struct nl_portid_hash *hash = &nl_table[sk->sk_protocol].hash; struct hlist_head *head; int err = -EADDRINUSE; struct sock *osk; int len; netlink_table_grab(); head = nl_portid_hashfn(hash, portid); len = 0; sk_for_each(osk, head) { if (net_eq(sock_net(osk), net) && (nlk_sk(osk)->portid == portid)) break; len++; } if (osk) goto err; err = -EBUSY; if (nlk_sk(sk)->portid) goto err; err = -ENOMEM; if (BITS_PER_LONG > 32 && unlikely(hash->entries >= UINT_MAX)) goto err; if (len && nl_portid_hash_dilute(hash, len)) head = nl_portid_hashfn(hash, portid); hash->entries++; nlk_sk(sk)->portid = portid; sk_add_node(sk, head); err = 0; err: netlink_table_ungrab(); return err; }
首先就根据sock对应的协议在nl_table表中找到对应的netlink_table结构,然后获取nl_portid_hash,然后通过nl_portid_hashfn函数根据portid计算hash值获取在nl_portid_hash中table的下标,具体计算过程不妨看下
static inline struct hlist_head *nl_portid_hashfn(struct nl_portid_hash *hash, u32 portid) { return &hash->table[jhash_1word(portid, hash->rnd) & hash->mask]; }
可以看到这里通过jhash_1word计算散列值,具体计算过程我们就不深入分析了。获取head之后,对链表进行遍历,通过节点获取 到对应的sock结构,验证是否在同一net下有相同portid的sock存在,如果存在就找到,break,找这个是干什么呢?看下面如,如果最终找到,则goto err,就终止处理了,这也反映了同一命名空间下,portid是不能共享的。如果当前没有相同portid的sock且链表存在,则继续。其实在内核portid统一为0的,如何sock的portid非0则错误。往下走,获取链表头,设置sock的portid,调用sk_add_node把sock加入链表。
sendmsg源码分析
下面从内核空间的sendmsg库函数入手,分析下整个处理流程。sendmsg对应的系统调用同样也是sendmsg函数,在socket.c文件中
SYSCALL_DEFINE3(sendmsg, int, fd, struct msghdr __user *, msg, unsigned int, flags) { if (flags & MSG_CMSG_COMPAT) return -EINVAL; return __sys_sendmsg(fd, msg, flags); }
该函数直接调用了__sys_sendmsg()
long __sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags) { int fput_needed, err; struct msghdr msg_sys; struct socket *sock; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL); fput_light(sock->file, fput_needed); out: return err; }
这里主要有两步,首先通过sockfd_lookup_light函数根据fd查询文件描述符表,获取对应的socket结构,然后再调用___sys_sendmsg函数。先看下前者
static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed) { struct file *file; struct socket *sock; *err = -EBADF; file = fget_light(fd, fput_needed); if (file) { sock = sock_from_file(file, err); if (sock) return sock; fput_light(file, *fput_needed); } return NULL; }
前面我们已经分析创建socket的过程,其中就有和文件描述符建立连接的部分,这里就很容易理解了。通过fget_light查询文件描述符表,其中会检查是否是共享的,如果非共享,则无需枷锁,可以快速的获取,否则需要加rcu lock.其余没什么特殊的,根据文件描述符表结构读取即可。如果找到一个file,则调用sock_from_file函数获取sock,之前提到,socket和file之间的链接是通过file结构的private_data字段联系的,所以这里也很简单
struct socket *sock_from_file(struct file *file, int *err) { if (file->f_op == &socket_file_ops) return file->private_data; /* set in sock_map_fd */ *err = -ENOTSOCK; return NULL; }
不晓得大家是否还记得,在建立连接的时候,有显示的设置file结构的f_op为socket_file_ops。如果sock不为空,就找到了嘛,返回呗赶紧!接下来就是重头戏___sys_sendmsg,代码比较繁琐就不全局列举了,只列举和介绍核心部分。用户层把msghsr的地址作为参数传递到内核(系统调用机制会把参数从用户栈复制到内核栈,并不是直接通过栈传递),然后需要把用户空间的msghdr的内容复制到内核,这是通过copy_from_user实现的,但是现在msghdr记录的还是iov还是用户空间地址,所以需要也iov也进行替换。接下来略过繁琐的验证机制,接下来同样是核心处理
err = sock_sendmsg(sock, msg_sys, total_len);
该函数主要调用了__sock_sendmsg函数,而__sock_sendmsg函数在没有加载安全模块的情况下调用了__sock_sendmsg_nosec函数
static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { struct sock_iocb *si = kiocb_to_siocb(iocb); si->sock = sock; si->scm = NULL; si->msg = msg; si->size = size; return sock->ops->sendmsg(iocb, sock, msg, size); }
可以看到这里实际上调用的是sock->ops->sendmsg(iocb, sock, msg, size);该函数是什么呢?回想下创建socket的时候,已经设置其ops为netlink_ops了,实际对应的sendmsg函数为netlink_sendmsg(af_netlink.c)该函数中首先获取msghdr中的目标地址结构sockaddr_nl,保存在msghdr的msg_name字段,话说这里意义还真是晦涩难懂,不明白的还以为是名字呢!暂且忽略sock_iocb之类的(我也不懂,以后研究)!
if (msg->msg_namelen) { err = -EINVAL; if (addr->nl_family != AF_NETLINK) goto out; dst_portid = addr->nl_pid;//目标端口 dst_group = ffs(addr->nl_groups); err = -EPERM; if ((dst_group || dst_portid) && !netlink_capable(sock, NL_CFG_F_NONROOT_SEND)) goto out; } else { dst_portid = nlk->dst_portid; dst_group = nlk->dst_group; }
如果msg->msg_namelen不为空,则获取地址中的目标端口和组播掩码。当然组播掩码一般为0 的。否则设置netlink_sock中的dst_portid和dst_group,如果nlk->port为空,则随机分配一个。接下来分配一个skb,并对其进行设置,主要是设置portid和dst_group。然后调用memcpy_fromiovec把用户空间的消息内容复制到内核skb中
int memcpy_fromiovec(unsigned char *kdata, struct iovec *iov, int len) { while (len > 0) { if (iov->iov_len) { int copy = min_t(unsigned int, len, iov->iov_len); if (copy_from_user(kdata, iov->iov_base, copy)) return -EFAULT; len -= copy; kdata += copy; iov->iov_base += copy; iov->iov_len -= copy; } iov++; } return 0; }
根据首篇文章介绍的消息格式,这里理解起来就没问题了,这里len是所有iov向量的总长度,一个循环下来数据就拷贝到内核skb中了。接下来在单播的情况下就调用netlink_unicast函数进行发送了。
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 portid, int nonblock) { struct sock *sk; int err; long timeo; skb = netlink_trim(skb, gfp_any()); timeo = sock_sndtimeo(ssk, nonblock); retry: /*根据portid获取目标sork*/ sk = netlink_getsockbyportid(ssk, portid); if (IS_ERR(sk)) { kfree_skb(skb); return PTR_ERR(sk); } if (netlink_is_kernel(sk)) return netlink_unicast_kernel(sk, skb, ssk); if (sk_filter(sk, skb)) { err = skb->len; kfree_skb(skb); sock_put(sk); return err; } err = netlink_attachskb(sk, skb, &timeo, ssk); if (err == 1) goto retry; if (err) return err; return netlink_sendskb(sk, skb); }
函数中首先遍历nl_table获取sock结构,注意参数中的portid是目标socket的端口,需要要到一个网络命名空间相同且portid和参数中的portid相同的sock.此时如果是内核sock,n那么调用netlink_unicast_kernel,意味着这是发往内核的数据。这里实现就很简单了,直接上代码把
static int netlink_unicast_kernel(struct sock *sk, struct sk_buff *skb, struct sock *ssk) { int ret; struct netlink_sock *nlk = nlk_sk(sk); ret = -ECONNREFUSED; if (nlk->netlink_rcv != NULL) { ret = skb->len; netlink_skb_set_owner_r(skb, sk); NETLINK_CB(skb).sk = ssk; nlk->netlink_rcv(skb); consume_skb(skb); } else { kfree_skb(skb); } sock_put(sk); return ret; }
总的来说就是交付给内核sock注册的接收函数了,这点在创建内核套接字部分已经介绍,剩下的就看个人设置的如何接收了!然而如果不是发往内核的,那肯定是发往另一个进程的,调用netlink_sendskb函数,该函数中直接调用了__netlink_sendskb。如果不是mmap的skb,则把skb加入到接收端sock的等待队列中,然后调用sock中的sk_data_ready函数,该函数目前还是空函数。剩下的就等对端从其接受队列中获取skb然后处理了O(∩_∩)O~
以马内利
参考资料:
linux内核3.10.1源码
深入linux内核架构