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. Python解析xml文件遇到的编码解析的问题

    使用python对xml文件进行解析的时候,假设xml文件的头文件是utf-8格式的编码,那么解析是ok的,但假设是其它格式将会出现例如以下异常: xml.parsers.expat.ExpatErr ...

  2. PS中进程状态

    PROCESS STATE CODES       Here are the different values that the s, stat and state output       spec ...

  3. phpadmin 装了6666端口只能在IE打开,在阿里云改了 开放端口85好了

    phpadmin 装了6666端口只能在IE打开,在阿里云改了 开放端口85好了 非常用端口谷歌浏览器识别不了phpadmin

  4. ContextLoader,ContextLoaderListener解读

    一.ServletContext 有 addListener(..) 方法,也有创建的方法 createListener(Class<T> c) . 有addFilter(..) 方法,也 ...

  5. 如果可能的话,使用 PC-Lint、LogiScope 等工具进行代码审查

    如果可能的话,使用 PC-Lint.LogiScope 等工具进行代码审查. #include <iostream> #include <algorithm> #include ...

  6. Python使用pycurl获取http的响应时间

    最近需要对节点到源站自己做个监控,简单的ping可以检测到一些东西,但是http请求的检查也要进行,于是就研究了下pycurlpycurl是个用c语言实现的python 库,虽然据说不是那么pytho ...

  7. LAMP环境如何配置多个域名访问

    背景: 公司有多个项目想要挂载在一个服务器上,因此需要多个域名来访问不同的网站,其实就是一个阿里云服务器,一个ip对应于多个域名 lamp环境: centos版本:命令查看centos的版本号:rpm ...

  8. IOS 开发之 -- 过滤掉字符串里面所有的非法字符 字典和json之间的互转

    比如一个字符串: NSString * hmutStr = @"(010)*(123)E6(234)**150-1111-^^-1234#" 很多时候,数据之间的传输,我们仅仅只想 ...

  9. GIS Cesium地图数据配置

    1.打开IIS,点击站点 2.跨域配置 配置方式: 在地图数据目录之中放置web.config文件,里面存放 <?xml version="1.0" encoding=&qu ...

  10. virtualbox虚拟机Linux系统与本地windows系统共享文件方法

    转自:http://jingyan.baidu.com/article/2fb0ba40541a5900f2ec5f07.html