Linux内核协议栈(3) socket调用分析

时间:2021-10-27 11:07:56

我们知道要读写一个文件可以使用open调用,得到文件描述符,然后就可以用read读文件,使用write写文件。而socket就相当于open(事实上linux为了实践万物皆文件实现了一个虚拟文件系统sockfs,socket系统调用之后就是在sockfs文件系统上建立了一个文件,只是这个文件是特殊的socket文件,我们先不去探究sockfs文件系统,我现在只需知道socket之后建立一个特殊的socket文件,我们可以读写这个文件)。

我们在PC上的文件一般有类型的,会占用一定的磁盘空间,如word文档,以xxx.doc文件名视人。那现在这个特殊的文件是什么?到底长什么样?

直接上代码吧:

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);//构造特殊socket文件(其实还有inode,先不探究)
if (retval < 0)
goto out;

retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));//分配文件描述符,构造file,并安装到调用进程
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函数之后,最终会调用到以上函数,至于怎么调用到的,我们也不在这里探究,感兴趣的可以去学习系统调用。回到这个函数,代码不多,表面看起来主要干了两件事,构造socket对象,构造file对象并和socket对象关联。我们open之后,构建了一个和物理磁盘上对应的file结构,而socket之后也构造了一个file,但还多了一个socket对象。构造socket对象使用的是函数sock_create,构造file对象并关联socket对象是在sock_map_fd中实现的,接着我们将分析这两个函数(就这两个函数,还能研究其他吗-_-#)。

先分析sock_create函数:

static int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;

/*
* Check protocol is in range
*/
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;

/* Compatibility.

This uglymoron is moved from INET layer to here to avoid
deadlock in module load.
*/
if (family == PF_INET && type == SOCK_PACKET) {
static int warned;
if (!warned) {
warned = 1;
printk(KERN_INFO "%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
}
family = PF_PACKET;
}

err = security_socket_create(family, type, protocol, kern);
if (err)
return err;

/*
*Allocate the socket and allow the family to set things up. if
*the protocol is 0, the family is instructed to select an appropriate
*default.
*/
sock = sock_alloc();//创建socket_alloc类型的inode --------1
if (!sock) {
if (net_ratelimit())
printk(KERN_WARNING "socket: no more sockets\n");
return -ENFILE;/* Not exactly a match, but its the
closest posix thing */
}

sock->type = type;//设置套接字的类型----------------2

#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
if (net_families[family] == NULL)
request_module("net-pf-%d", family);
#endif

rcu_read_lock();
pf = rcu_dereference(net_families[family]);//获得对应协议族的操作块-----------3
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;

/*
* We will call the ->create function, that possibly is in a loadable
* module, so we have to bump that loadable module refcnt first.
*/
if (!try_module_get(pf->owner))
goto out_release;

/* Now protected by module ref count */
rcu_read_unlock();

err = pf->create(net, sock, protocol);/*----------构造sock对象,然后挂到socket对象上--->*/inet_create------4
if (err < 0)
goto out_module_put;

/*
* Now to bump the refcnt of the [loadable] module that owns this
* socket at sock_release time we decrement its refcnt.
*/
if (!try_module_get(sock->ops->owner))
goto out_module_busy;

/*
* Now that we're done with the ->create function, the [loadable]
* module can have its refcnt decremented
*/
module_put(pf->owner);
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;

return 0;

out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;

out_release:
rcu_read_unlock();
goto out_sock_release;
}

重点的地方有四个,我做了注释,并有编号,我们分别说明。

1.构建socket_alloc类型的inode

socket_alloc类型的inode到底是个什么东西?如下是一个包含socket对象和inode对象的结构体,如果知道linux文件系统,就知道inode是代表文件系统中的文件或目录,如果不了解linux文件系统也没关系,只需知道这个结构体中有一个socket就可以了。

struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};

顺着函数调用sock_alloc()--> new_inode(sock_mnt->mnt_sb)-->alloc_inode(sb)-->sb->s_op->alloc_inode(sb)

static struct inode *alloc_inode(struct super_block *sb)
{
struct inode *inode;

if (sb->s_op->alloc_inode) /**/sock_alloc_inode
inode = sb->s_op->alloc_inode(sb);//调用超级块对应的inode分配函数
else
inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);

if (!inode)
return NULL;

if (unlikely(inode_init_always(sb, inode))) {
if (inode->i_sb->s_op->destroy_inode)
inode->i_sb->s_op->destroy_inode(inode);
else
kmem_cache_free(inode_cachep, inode);
return NULL;
}

return inode;
}


sb->s_op->alloc_inode(sb)这个到底是个什么东西呢?协议栈为了适配VFS虚拟文件系统实现了sockfs,所以很多东西都跟文件系统扯在一块。如果把文件系统扯进来,理解起来似乎比较困难。如果不扯进来,似乎又不知所云。为此,如果开始有熟悉文件系统最好,如果不熟悉,那只需知道协议栈初始化的时候,注册sockfs,实际上是注册代表文件系统的超级块,超级块中有操作函数集,也就是s_op所指的东西,这些操作函数是sockfs文件系统所有的。

static const struct super_operations sockfs_ops = {
.alloc_inode =sock_alloc_inode,
.destroy_inode =sock_destroy_inode,
.statfs =simple_statfs,
};

这个可以类比于file_operations。什么?不知道这个?没关系。知道最终调用到了sock_alloc_inode这个函数就可以了

static struct inode *sock_alloc_inode(struct super_block *sb)
{
struct socket_alloc *ei;

ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
if (!ei)
return NULL;
init_waitqueue_head(&ei->socket.wait);

ei->socket.fasync_list = NULL;
ei->socket.state = SS_UNCONNECTED;
ei->socket.flags = 0;
ei->socket.ops = NULL;
ei->socket.sk = NULL;
ei->socket.file = NULL;

return &ei->vfs_inode;
}


看到了吧,通过着一系列调用,我们得到一个socket_alloc对象,其中包含一个socket对象。撇开文件系统,我们也达到了要得到一个socket对象的目的。

 

2.设置套接字的类型

sock->type = type

这个似乎没什么好讲。就是套接字有好几种,我们常用的SOCK_STREAM,SOCK_DGRAM,SOCK_RAW等,还有Linux自有的SOCK_PACKET

3.获得对应协议族的操作块

pf = rcu_dereference(net_families[family])

我们继续先放出代码,是协议栈初始化时的代码:

static int __init inet_init(void)
{
struct sk_buff *dummy_skb;
struct inet_protosw *q;
struct list_head *r;
int rc = -EINVAL;

BUILD_BUG_ON(sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb));

//将协议挂到全局proto_list,创建slab
rc = proto_register(&tcp_prot, 1);//1分配slab缓存
if (rc)
goto out;

rc = proto_register(&udp_prot, 1);//1分配slab缓存
if (rc)
goto out_unregister_tcp_proto;

rc = proto_register(&raw_prot, 1);//1分配slab缓存
if (rc)
goto out_unregister_udp_proto;

/*
*Tell SOCKET that we are alive...
*/

(void)sock_register(&inet_family_ops);//将INET协议族注册到套接字模块

什么叫做将INET协议族注册到套接字模块?有点混乱?如果没有,感觉你的内功好深厚,不用再看了。如果有点混乱,容我解释。

网络协议栈,BSD 套接字,文件系统三个概念(先读三遍-_-!)。Linux实现的网络协议栈实现了BSD 套接字接口,方便我们使用套接字;实现了VFS虚拟文件系统接口,同样方便我们使用协议栈。BSD 套接字提供一套API函数,方便我们使用协议栈,这个协议栈除了网络协议栈(INET),还有其他协议栈(不知还有哪些-_-!)。这个有点类似C++的抽象类(java的接口更合适),一个父类,很多子类,都实现抽象父类的虚函数。然后通过父类的引用,操作子类对象成员函数。

不扯远,说到底:协议族初始化时,将网络协议族(INET)注册进套接字模块。看注册函数:

int sock_register(const struct net_proto_family *ops)
{
int err;

if (ops->family >= NPROTO) {
printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family,
NPROTO);
return -ENOBUFS;
}

spin_lock(&net_family_lock);
if (net_families[ops->family])
err = -EEXIST;
else {
net_families[ops->family] = ops;
err = 0;
}
spin_unlock(&net_family_lock);

printk(KERN_INFO "NET: Registered protocol family %d\n", ops->family);
return err;
}
再看inet_family_ops这个对象
static struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner= THIS_MODULE,
};
有眼熟的地方不?我们在第二节中的例子建立套接字时:

 fd = socket(PF_INET,SOCK_DGRAM,0);  

第一个参数传入的就是PF_INET。结合代码我想你大概明白这到底是怎么回事了。不过我还要总结一下:

第一.初始化时会将协议族注册到套接字模块,所谓的注册就是将一个操作函数集inet_family_ops,挂到全局指针数据第PF_INET个对象上

第二.我们在调用socket函数时,传入参数PF_INET,取出对应协议族的操作函数集,其实就是要去inet_create,就是这个函数是我们要使用的。


4.构造sock对象,并挂到socket对象上

err = pf->create(net, sock, protocol);

通过3,我们知道实际调用的函数是 inet_create。在分析该函数之前我想强调socket和sock两个对象。细心的同学或许发现了他们相似而不同,在这里讲我的理解。我们知道OSI七层模型,也知道TCP/IP四层模型。在应用层使我们的数据,到了传输层被封装TCP或UDP头的数据包,到了网络层又被封装成IP数据报,到了链路层就成了以太网帧。可知,在不同层次,我们的数据被封装成了不同的形式。对于socket和sock的区别可以类比。在BSD 套接字层面,操作的对象时socket对象,到了INET协议族层,操作的对象成了sock。而INET协议族包含了TCP,UDP协议,之后往下走我们还可以看到udp_sock或tcp_sock。socket和sock一一对应。如果使用UDP协议,我们可以看到socket--->sock---->udp_sock对应。这样讲应该可以理解socket和sock的区别和联系了。

接着我们进入inet_create,该函数构建socket对应的sock对象(代码有点多,不贴出来,在附1有完整代码),该函数主要做了以下几件事:

1.根据传入的套接字类型参数和协议类型参数取出相应的协议操作接口

list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */

//在ifconfig 应用程序中走这个分支
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}

inetsw是个什么东西呢?看图(借用的)

Linux内核协议栈(3) socket调用分析

inetsw是个指针数组,第一个成员指向SOCK_STREAM流式套接字类型的协议链表,第二个成员指向SOCK_DGRAM数据报类型协议的链表,第三个成员指向SOCK_RAW原始套接字类型协议的链表....

在协议初始化的函数inet_init中有这样一段:

static int __init inet_init(void)
{
...
/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r); //初始化表头,套接字类型------------------------>>--->邻接表

for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q) //inet_create
inet_register_protosw(q);//把传输层协议挂到对应类型套接字链表上
...
}

在看下inetsw_array是个什么东西:

static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.capability = -1,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},

{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot, //
.ops = &inet_dgram_ops,//
.capability = -1,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
},


{
.type = SOCK_RAW,
.protocol = IPPROTO_IP,/* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.capability = CAP_NET_RAW,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
}
};

是一个套接字类型与协议的关联数组。什么个意思?我们上面说了,我们建立套接字的时候,要传入套接字类型参数。在网络协议族inet,要对应协议作为基础设施。如:流式套接字,是基于TCP协议的。在这数组中的成员是协议栈内置的协议(言外之意,还可以自己添加自定义的协议),每一个成员都是套接字类型,协议,协议描述块对象,协议操作函数集等的组合。协议栈初始化之后就有如上图的结构。

2.构建sock对象,并初始化

我们看inet_create函数做的第二件事:

sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);//----->对应于tcp,是从tcp cache中分配的,构建对应网络层的sock

注意这个函数构建的sock对象是跟协议相关的。像使用TCP协议和UDP协议构建的sock对象是有差异。怎么实现的?先提醒注意第三个参数。传入的一个描述协议的结构对象。另外要说的是,内核为了减少内存碎片,在分配小量内存时,采用了slab管理方式(相对于伙伴系统管理大内存分配),有兴趣可以去了解,这里要说的是构建sock对象使用的是slab方式。看初始化时的一段代码:

 

static int __init inet_init(void)
{
...
//将协议prot挂到全局proto_list-------->创建slab
rc = proto_register(&tcp_prot, 1);// 1代表分配slab缓存
if (rc)
goto out;

rc = proto_register(&udp_prot, 1);// 1代表分配slab缓存
if (rc)
goto out_unregister_tcp_proto;

rc = proto_register(&raw_prot, 1);// 1代表分配slab缓存
...
}

再看proto_register函数:

int proto_register(struct proto *prot, int alloc_slab)
{
if (alloc_slab) {
prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
SLAB_HWCACHE_ALIGN | prot->slab_flags,
NULL);

if (prot->slab == NULL) {
printk(KERN_CRIT "%s: Can't create sock SLAB cache!\n",
prot->name);
goto out;
}

if (prot->rsk_prot != NULL) {
static const char mask[] = "request_sock_%s";

prot->rsk_prot->slab_name = kmalloc(strlen(prot->name) + sizeof(mask) - 1, GFP_KERNEL);
if (prot->rsk_prot->slab_name == NULL)
goto out_free_sock_slab;

sprintf(prot->rsk_prot->slab_name, mask, prot->name);
prot->rsk_prot->slab = kmem_cache_create(prot->rsk_prot->slab_name,
prot->rsk_prot->obj_size, 0,
SLAB_HWCACHE_ALIGN, NULL);

if (prot->rsk_prot->slab == NULL) {
printk(KERN_CRIT "%s: Can't create request sock SLAB cache!\n",
prot->name);
goto out_free_request_sock_slab_name;
}
}
...


在协议栈初始化时,注册了内置的协议(TCP,UDP,RAW),同时为每种协议申请slab高速缓存,分配单元是prot_obj_size。意思预先分配一块大的内存块,然后在这块大的内存上分配都会分配大小为pro_obj_size的小片内存块。

我们拿tcp协议做例子,看tcp协议描述块tcp_prot

struct proto tcp_prot = {
.name= "TCP",
.owner= THIS_MODULE,
.close= tcp_close,
.connect= tcp_v4_connect,
.disconnect= tcp_disconnect,
.accept= inet_csk_accept,
.ioctl= tcp_ioctl,
.init= tcp_v4_init_sock,
.destroy= tcp_v4_destroy_sock,
.shutdown= tcp_shutdown,
.setsockopt= tcp_setsockopt,
.getsockopt= tcp_getsockopt,
.recvmsg= tcp_recvmsg,
.backlog_rcv= tcp_v4_do_rcv,
.hash= inet_hash,
.unhash= inet_unhash,
.get_port= inet_csk_get_port,
.enter_memory_pressure= tcp_enter_memory_pressure,
.sockets_allocated= &tcp_sockets_allocated,
.orphan_count= &tcp_orphan_count,
.memory_allocated= &tcp_memory_allocated,
.memory_pressure= &tcp_memory_pressure,
.sysctl_mem= sysctl_tcp_mem,
.sysctl_wmem= sysctl_tcp_wmem,
.sysctl_rmem= sysctl_tcp_rmem,
.max_header= MAX_TCP_HEADER,
.obj_size= sizeof(struct tcp_sock),
.slab_flags= SLAB_DESTROY_BY_RCU,
.twsk_prot= &tcp_timewait_sock_ops,
.rsk_prot= &tcp_request_sock_ops,
.h.hashinfo= &tcp_hashinfo,
#ifdef CONFIG_COMPAT
.compat_setsockopt= compat_tcp_setsockopt,
.compat_getsockopt= compat_tcp_getsockopt,
#endif
};

可以看到:

.obj_size  = sizeof(struct tcp_sock)。

结合起来解释这段代码:

prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
SLAB_HWCACHE_ALIGN | prot->slab_flags,
NULL);

意思是建立一块名为“TCP”的大内存块,然后在这块大的内存块上每次分配大小为sizeof(struct tcp_sock)的小内存块。

那tcp_sock是个什么东西?跟之前socket对象,sock对象什么关系?我们之前说过一个socket对象对应一个sock对象。而tcp_sock对象呢?tcp_sock可以说是一个sock对象,更准确说是tcp类型的sock对象。我们联系C++语言中的子类对象。我们说大学生是子类,学生是父类,大学生也是学生,这里也是类似。tcp_sock继承至inet_connection_sock,inet_connection_sock继承至inet_sock,inet_sock继承至sock(内核考虑效率没有使用C++而使用C,但又想利用C++的优点)。也即一个tcp_sock对象包含一个inet_connection_sock对象,一个inet_connection_sock对象包含一个inet_connection_sock对象,一个inet_connection_sock对象包含一个sock对象。(有空画个图看一下就明了)。

总结起来,如果使用的是tcp协议,inet_create函数做的第二件事是:在tcp高速slab中构建一个tcp类型的sock,tcp_sock(本质是sock对象),并初始化。


3.对于RAW协议,将构建的sock对象放到对应协议的hash表中(接受数据时要用到)

sk->sk_prot->hash(sk);//----------------------->将sk添加到相应协议的哈希表中,在udp中该函数是不能被调用的

这里不详解。



至此,socket调用做的两件事中的第一件事——构建socket对象分析到此结束。我们接着分析第二件事——构建file对象并关联socket对象。





未完待续....