目录
3.1、sock_create(__sock_create)
3.2、socket 节点分配以及初始化(sock_alloc)
3.3.1、inet_create注册(inet_init)
1、socket 概述
为了建立Socket,程序可以调用Socket函数,函数返回一个文件描述符,原型为:
int socket(int domain, int type, int protocol);
domain:指定协议簇,常见的有AF_IENT, AF_INET6, AF_UNIX/AF_LOCAL, AF_NETLINK等。
type:指定类型,常见的有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等。
protocol:指定传输协议,如TCP/UDP等,不同协议簇都对应了默认的协议,可以填充 0 使用默认值。
关于 socket 函数的用法,参见《socket编程:socket接口创建套接字(原始套接字)使用说明》
以 linux 内核 2.6.38 为例,简要分析socket创建内核的操作流程,完整调用栈如下:
sys_socketcall
sys_socket
sock_create
__sock_create
inet_create//err = pf->create(net, sock, protocol);
当用户态执行socket创建时,内核从sys_socketcall开始执行到inet_create执行完,差不就完成了socket的内核创建,返回给用户态。当然其中还会涉及一些参数检查、安全检查以及映射等操作。socket创建相关函数基本都在socket.c文件中
2、socket 创建入口
2.1、sys_socketcall
在 sys_socketcall 函数中,通过判断call指令,来统一处理 socket 相关函数的事务,对于socket(…)函数,实际处理是在 sys_socket 中。
/*
* System call vectors.
*
* Argument checking cleaned up. Saved 20% in size.
* This function doesn't need to set the kernel lock because
* it is set by the callees.
*/
asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
unsigned long a[6];
unsigned long a0, a1;
int err;
if (call < 1 || call > SYS_ACCEPT4)
return -EINVAL;
/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;
err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
if (err)
return err;
a0 = a[0];
a1 = a[1];
switch (call) {
case SYS_SOCKET:
err = sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
....
default:
err = -EINVAL;
break;
}
return err;
}
2.2、sys_socket
进入 sys_socket 函数就开始了 socket 的真正创建过程。这里主要完成三件事情:
- 校验type的合法性,如果设置了除基本sock_type,SOCK_CLOEXEC和SOCK_NONBLOCK之外的其他参数,则直接返回err。
- 调用 sock_create 创建 socket 结构。
- 使用 sock_map_fd 将socket 结构映射为文件描述符并返回给用户空间。
sys_socket 函数如下:
asmlinkage long sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
...
// SOCK_TYPE_MASK: 0xF; SOCK_STREAM等socket类型位于type字段的低4位
// 将flag设置为除socket基本类型之外的值
flags = type & ~SOCK_TYPE_MASK;
// 如果flags中有除SOCK_CLOEXEC或者SOCK_NONBLOCK之外的其他参数,则返回EINVAL
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
// 取type中的后4位,即sock_type,socket基本类型定义
type &= SOCK_TYPE_MASK;
// 如果设置了SOCK_NONBLOCK,则不论SOCK_NONBLOCK定义是否与O_NONBLOCK相同,
// 均将flags中的SOCK_NONBLOCK复位,将O_NONBLOCK置位
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
//根据协议族、套接字类型、协议栈类创建socket
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
goto out;
// 将socket结构映射为文件描述符retval并返回,
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;
}
3、socket 创建过程
3.1、sock_create(__sock_create)
socket结构体中定义了socket 的基本状态,类型,标志,等待队列,文件指针,操作函数集等,利用 sock 结构,将 socket 操作与真正处理网络协议相关的事务分离。具体结构如下:
/**
* struct socket - general BSD socket
* @state: socket state (%SS_CONNECTED, etc)
* @type: socket type (%SOCK_STREAM, etc)
* @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
* @ops: protocol specific socket operations
* @fasync_list: Asynchronous wake up list
* @file: File back pointer for gc
* @sk: internal networking protocol agnostic socket representation
* @wait: wait queue for several uses
*/
struct socket {
socket_state state;// 连接状态:SS_CONNECTING, SS_CONNECTED 等
short type; // 类型:SOCK_STREAM, SOCK_DGRAM 等
unsigned long flags;// 标志位:SOCK_ASYNC_NOSPACE(发送队列是否已满)等
const struct proto_ops *ops;// socket操作函数集:bind, connect, accept 等
struct fasync_struct *fasync_list;
struct file *file; // 该socket结构体对应VFS中的file指针
struct sock *sk; // socket网络层表示,真正处理网络协议的地方
wait_queue_head_t wait;// 等待队列
};
回到 sock_create 继续看socket创建过程,sock_create 实际调用的是__sock_create。该接口主要完成以下几件事:
- 校验协议族类型、套接字类型的合法性。
- 操作安全权限检查(SElinux),如果该scoket是由用户态创建来触发创建则需要检查,内核态触发创建则跳过。
- 调用sock_alloc配scoket节点,从socket_fs文件系统中申请。
- 根据传入的协议族类型创建哪一层的套接字(ipv4、ipv6、以太网层)。
- 通过RCU机制来保证,创建过程的同步互斥。
__sock_create代码如下:
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
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;
if (family < 0 || family >= NPROTO)// 检查是否是支持的地址族,即检查协议
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX) // 检查是否是支持的socket类型
return -EINVAL;
// 检查权限,并考虑协议集、类型、协议,以及 socket 是在内核中创建还是在用户空间中创建
// 可以参考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
...
sock = sock_alloc();// 分配socket结构,这其中创建了socket和关联的inode (重点分析)
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 */
}
...
rcu_read_lock();//socket通过RCU机制实现同步互斥操作。
根据API传入的family信息确定是那个协议簇,很据协议族在socket创建此处分流
pf = rcu_dereference(net_families[family]);
...
rcu_read_unlock();
/*
* Now to bump the refcnt of the [loadable] module that owns this
* socket at sock_release time we decrement its refcnt.
*/
/*调用协议簇对应的create函数
在IPv4协议族中调用inet_create()对已创建的socket继续进行初始化,同时创建网络层socket*/
err = pf->create(net, sock, protocol);
if (err < 0)
goto out_module_put;
/*如果proto_ops结构实例所在模块以内核模块方式动态加载进内核,
则增加该模块的引用计数,在sock_release时,减小该计数。*/
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
*/
//调用完inet_create函数后,对此模块的引用计数减一。
module_put(pf->owner);
//安全模块对创建后的socket做安全检查
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;
return 0;
....
out_release:
rcu_read_unlock();
goto out_sock_release;
}
3.2、socket 节点分配以及初始化(sock_alloc)
sock_mnt 是在 socket 初始化向系统挂载sock_fs时保存的vfsmount结构,其mnt_sb指向了该文件系统对应的超级块。new_inode 函数用于从sock_fs文件系统中申请一个节点。然后通过SOCKET_I(inode)获取节点指针返回,实现是取地址偏移。
static struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
inode = new_inode(sock_mnt->mnt_sb);//向文件系统申请节点
if (!inode)
return NULL;
sock = SOCKET_I(inode);//获取指针返回
kmemcheck_annotate_bitfield(sock, type);
inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
percpu_add(sockets_in_use, 1);
return sock;
}
3.3、根据指定协议族继续初始化
调用了sock_alloc申请socket后,申请socket的过程已经全部结束,继续分析__sock_create函数,则是根据API传入的family信息确定是那个协议簇,然后调用协议簇对应的create函数:err = pf->create(net, sock, protocol)这里不同的协议簇走的流程开始不一样了。
3.3.1、inet_create注册(inet_init)
static int __init inet_init(void)
{
......
(void)sock_register(&inet_family_ops);//注册协议簇
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);//注册 inet_protosw
......
}
inet_init是在网络模块初始化要调用的初始化函数,这个函数完成的任务不少,暂时我们先关注上面两个部分,其余部分后续还会再回头来看。
static struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
int sock_register(const struct net_proto_family *ops)
{
......
spin_lock(&net_family_lock);
if (net_families[ops->family])
err = -EEXIST;
else {
net_families[ops->family] = ops;
err = 0;
}
......
}
3.3.2、inet_create调用(核心)
sock_register中会把inet_family_ops注册到net_families数组中,根据具体协议簇的类型进行索引调用,我们回想一下在__sock_create时,按照如下方式调用
pf = rcu_dereference(net_families[family]);
err = pf->create(net, sock, protocol);
inet_create 函数是内核创建socket的核心,代码如下:
static int inet_create(struct net *net, struct socket *sock, int protocol)
{
......
sock->state = SS_UNCONNECTED;
......
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
......
}
......
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_no_check = answer->no_check;
answer_flags = answer->flags;
......
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
if (sk == NULL)
goto out;
err = 0;
sk->sk_no_check = answer_no_check;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = 1;
inet = inet_sk(sk);
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
if (SOCK_RAW == sock->type) {
inet->num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1;
}
if (ipv4_config.no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->id = 0;
sock_init_data(sock, sk);
sk->sk_destruct = inet_sock_destruct;
sk->sk_protocol = protocol;
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
inet->uc_ttl = -1;
inet->mc_loop = 1;
inet->mc_ttl = 1;
inet->mc_all = 1;
inet->mc_index = 0;
inet->mc_list = NULL;
sk_refcnt_debug_inc(sk);
if (inet->num) {
/* It assumes that any protocol which allows
* the user to assign a number at socket
* creation time automatically
* shares.
*/
inet->sport = htons(inet->num);
/* Add to protocol hash chains. */
sk->sk_prot->hash(sk);
}
if (sk->sk_prot->init) {//具体协议的初始化,例如:tcp_ipv4:tcp_v4_init_sock
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
}
首先、sock->state设置为未连接状态SS_UNCONNECTED。
次之、根据用户指定的 type() 在 inetsw 查找对应的 inet_protosw 结构。这里需要先确认一下inetsw里面对应了哪些内容以及使如何注册上的,现在可以回头看看 inet_init了,本文的最上面部分代码,先遍历 inetsw_array 数组,然后对每个元素调用inet_register_protosw 函数进行注册。看看 inetsw_array 存储了什么。
/* Upon startup we insert all the elements in inetsw_array[] into
* the linked list inetsw.
*/
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,
}
};
对应了常用的三种类型,(SOCK_STREAM/IPPROTO_TCP), (SOCK_DGRAM/IPPROTO_UDP), (SOCK_RAW/IPPROTO_IP),用户通过type指定。以TCP为例看一下相关的socket 操作接口集合 tcp_prot 和inet_stream_ops,该集合中包含了后期tcp建链、传输、断链等系统调用对应的内核操作。具体如下:
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),
.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
};
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = tcp_sendmsg,
.recvmsg = sock_common_recvmsg,
.mmap = sock_no_mmap,
.sendpage = tcp_sendpage,
.splice_read = tcp_splice_read,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_sock_common_setsockopt,
.compat_getsockopt = compat_sock_common_getsockopt,
#endif
};
以以获取cp的全链接系统调用accept为例,当任务阻塞在accept处时,内核堆栈如下:
[root@localhost tcp]# cat /proc/2591/stack
[<ffffffff8147a431>] inet_csk_accept+0x1c1/0x260
[<ffffffff814a07e4>] inet_accept+0x34/0xf0
[<ffffffff81427a95>] sys_accept4+0x155/0x2b0
[<ffffffff81427c00>] sys_accept+0x10/0x20
[<ffffffff8100b0d2>] system_call_fastpath+0x16/0x1b
[<ffffffffffffffff>] 0xffffffffffffffff
inet_init 中的循环遍历数组 inetsw_array 已经 分别把这三种type进行注册,再来看看注册到哪里去了。
void inet_register_protosw(struct inet_protosw *p)
{
......
answer = NULL;
last_perm = &inetsw[p->type];
list_for_each(lh, &inetsw[p->type]) {
answer = list_entry(lh, struct inet_protosw, list);
/* Check only the non-wild match. */
if (INET_PROTOSW_PERMANENT & answer->flags) {
if (protocol == answer->protocol)
break;
last_perm = lh;
}
answer = NULL;
}
}
很明显,注册到了 inetsw 中,现在可以回头继续分析 inet_create 函数了,answer实际对应了TCP/UDP/RAW的inet_protosw结构,进行一系列赋值,然后调用sk_alloc函数。
struct sock *sk_alloc(struct net *net, int family, gfp_t priority, struct proto *prot)
{
struct sock *sk;
sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family;
sk->sk_prot = sk->sk_prot_creator = prot;
sock_lock_init(sk);
sock_net_set(sk, get_net(net));
atomic_set(&sk->sk_wmem_alloc, 1);
}
return sk;
}
sk_prot_alloc 实际上是在 prot 对应的 slab 中或者直接 kmalloc 申请一个sock结构,然后设置协议簇,以及 proto 结构信息。这里需要注意一下,目前返回的是一个struct sock的结构,而实际上并不仅仅是这个简单的结构。我们先回到inet_create函数中继续向下分析。可以看到申请完sock之后,inet_create函数中使用了inet = inet_sk(sk)对sock进行了强制转换,然后对转换得到的inet结构进行一系列赋值。而inet_sock是INET域专用的一个sock表示,在struct sock上扩展而来,除基本sock属性,提供了INET域专用的属性,如TTL、IP地址、端口等。这里是怎么对应上的呢。还得再回头研究一下,申请sock的最终实现,申请sock时最终走的是下面的函数,前面简单提到过,但没深入。
static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority,int family)
{
struct sock *sk;
struct kmem_cache *slab;
slab = prot->slab;
if (slab != NULL) {
.......
else
sk = kmalloc(prot->obj_size, priority);
sock的申请是通过 kmalloc 实现的,申请的大小由prot->obj_size指定,而prot->obj_size是由上面static struct inet_protosw inetsw_array[] 数组成员结构中的 proto 结构中指定的,而这个Proto结构区分了TCP/UDP/RAW协议,见上面数组定义。其中tcp_prot的.obj_size = sizeof(struct tcp_sock),
udp_prot的.obj_size = sizeof(struct udp_sock), raw_prot的.obj_size = sizeof(struct raw_sock)。再看一下这几个结构的具体定义:
struct tcp_sock {
/* inet_connection_sock has to be the first member of tcp_sock */
struct inet_connection_sock inet_conn;
u16 tcp_header_len; /* Bytes of tcp header to send */
u16 xmit_size_goal_segs; /* Goal for segmenting output packets */
/*
* Header prediction flags
* 0x5?10 << 16 + snd_wnd in net byte order
*/
__be32 pred_flags;
........
struct udp_sock {
/* inet_sock has to be the first member */
struct inet_sock inet;
int pending; /* Any pending frames ? */
unsigned int corkflag; /* Cork is required */
__u16 encap_type; /* Is this an Encapsulation socket? */
/*
......
struct raw_sock {
/* inet_sock has to be the first member */
struct inet_sock inet;
struct icmp_filter filter;
};
除了tcp_sock第一个对应的是inet_connection_sock外,udp和raw对应的都是inet_sock。我们看一下inet_connection_sock
struct inet_connection_sock {
/* inet_sock has to be the first */
struct inet_sock icsk_inet;
struct request_sock_queue icsk_accept_queue;
struct inet_bind_bucket *icsk_bind_hash;
unsigned long icsk_timeout;
......
}
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct sock sk;
......
}
inet_connection_sock 第一个成员也是inet_sock,而 inet_sock 的第一个成员是struct sock。这样就比较清晰了,可以这么理解struct sock 是一个基类是 Linux socket 的最基础的面向协议栈的结构,而 inet_sock 在继承 sock 的基础上进行了扩展,除了基本socket属性外提供了INET域专用的属性,如TTL等。简单的,udp_sock和raw_sock在inet_sock的基础上进行了私有扩展。inet_connection_sock在继承inet_sock的基础上,扩展了所有面向连接相关的一些协议信息,而tcp_sock则在inet_connection_sock基础上进一步扩展,增加了TCP的一些私有属性,比如滑动窗口,拥塞控制等。至此,上述inet_create函数中inet = inet_sk(sk);的转换就没有任何疑问了。
回到 inet_create,除了对inet_sk结构的赋值外,也对struct sock结构进行了一系列的初始化,主要涉及收发包队列/缓冲区等。最后调用具体协议类型init函数,这里主要是对应了tcp_v4_init_sock的函数,而udp没有定义init,raw的init函数仅仅是把filter字段给清0了。即inet_create中的结尾部分:
static int inet_create(struct net *net, struct socket *sock, int protocol)
{
...
if (sk->sk_prot->init) {//具体协议的初始化,tcp_ipv4:tcp_v4_init_sock
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
...
}