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 < )
goto out;
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
if (retval < )
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 < ))
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, ))
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 = ;
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 > && 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 = ;
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 > ) {
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 ;
}

根据首篇文章介绍的消息格式,这里理解起来就没问题了,这里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 == )
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内核架构

内核通信之Netlink源码分析-用户内核通信原理2的更多相关文章

  1. 内核通信之Netlink源码分析-用户内核通信原理

    2017-07-05 本节从一个小案例入手,结合源码分析下通过netlink进行内核和用户通信的流程. 内核端 按照传统CS模式,其实内核端可以作为是服务器端,用以接收用户的请求并作出处理,但是从ne ...

  2. 内核通信之Netlink源码分析-用户内核通信原理3

    2017-07-06 上节主讲了用户层通过netlink和内核交互的详细过程,本节分析下用户层接收数据的过程…… 有了之前基础知识的介绍,用户层接收数据只涉及到一个核心调用readmsg(), 其他的 ...

  3. 内核通信之Netlink源码分析-基础架构

    2017-07-04 netlink是一种基于网络的通信机制,一般用于内核内部或者内核与用户层之间的通信.其有一个明显的特点就是异步性,通信的双方不要求同时在线,也就不用阻塞等待.NetLink按照数 ...

  4. v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码

    百篇博客分析|本篇为:(用户态锁篇) | 如何使用快锁Futex(上) 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) ...

  5. jQuery 2.0.3 源码分析Sizzle引擎解析原理

    jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理 声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + ...

  6. wifidog源码分析 - 用户连接过程

    引言 之前的文章已经描述wifidog大概的一个工作流程,这里我们具体说说wifidog是怎么把一个新用户重定向到认证服务器中的,它又是怎么对一个已认证的用户实行放行操作的.我们已经知道wifidog ...

  7. Linux内核2.6.14源码分析-双向循环链表代码分析(巨详细)

    Linux内核源码分析-链表代码分析 分析人:余旭 分析时间:2005年11月17日星期四 11:40:10 AM 雨 温度:10-11度 编号:1-4 类别:准备工作 Email:yuxu97101 ...

  8. SOFA 源码分析 — 自定义线程池原理

    前言 在 SOFA-RPC 的官方介绍里,介绍了自定义线程池,可以为指定服务设置一个独立的业务线程池,和 SOFARPC 自身的业务线程池是隔离的.多个服务可以共用一个独立的线程池. API使用方式如 ...

  9. 【MyBatis源码分析】插件实现原理

    MyBatis插件原理----从<plugins>解析开始 本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyB ...

随机推荐

  1. 基于Redis实现延时队列服务

    背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单 b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论 c.点我达 ...

  2. CSS学习笔记(4)--选择器(w3school)

    CSS3 选择器 在 CSS 中,选择器是一种模式,用于选择需要添加样式的元素. "CSS" 列指示该属性是在哪个 CSS 版本中定义的.(CSS1.CSS2 还是 CSS3.) ...

  3. 示例 - 10行代码在C#中获取页面元素布局信息

    最近研究一个如何在网页定位验证码并截图的问题时, 用SS写了一段C#小脚本可以轻松获取页面任意元素的布局信息 (top, left, width, height). 10行功能代码, 觉得有点用, 现 ...

  4. java---同步与并发概念

    Java中并发的形式无非是多线程和多进程两种形式.这两种形式都是可以利用多核来发挥计算能力的. 先说并发: 多进程意味着同时运行多个JVM,这个代价通常比多线程高,每个JVM都有自己的堆栈.都要分别加 ...

  5. JAVA学习资源网站

    中文java技术网——http://www.cn-java.com/ 灰狐动力(http://www.huihoo.com/)—— 该站点有许多的开源的项目的介绍和学习,涉及操作系统,数据库等许多方向 ...

  6. 借用smtp.qq.com发邮件

    至于sentmail和postfix的配置有多么麻烦学生在这里就不多废话了...反正是配置了N个小时,最终弄的头晕眼花也没弄好... 下面的方法可以让你完全摆脱这两个工具...当然,你要是想做邮件服务 ...

  7. digitalocean --- How To Install Apache Tomcat 8 on Ubuntu 16.04

    https://www.digitalocean.com/community/tutorials/how-to-install-apache-tomcat-8-on-ubuntu-16-04 Intr ...

  8. [openwrt]网络配置

    Network: config interface 'loopback'    option ifname 'lo'    option proto 'static'    option ipaddr ...

  9. Python pycurl使用

    pycurl的学习 (2013-09-26 10:40:31) 转载▼   分类: python pycurl的使用 pycurl是curl的一个python版本. pycurl的使用说明: pycu ...

  10. FCN 分割网络详解

    博客来源于:https://www.cnblogs.com/gujianhan/p/6030639.html: https://blog.csdn.net/sinat_24143931/article ...