注:本文分析基于3.10.107内核版本
1、函数原型
int socket(int domain, int type, int protocol);
参数说明:
domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM等。SOCK_STREAM针对于面向连接的TCP服务应用。SOCK_DGRAM对应于无连接的UDP服务应用。
protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。
- -
2、内核实现源码分析
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
...
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和sock_map_fd这两个函数完成,因此我们主要分析这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket。
2.1 sock_create函数
sock_create() 实际调用的是 __sock_create()。
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;
...
err = security_socket_create(family, type, protocol, kern);//SElinux相关,暂不关注
/*
* 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();//创建struct socket结构体
sock->type = type;//设置套接字的类型
...
pf = rcu_dereference(net_families[family]);//获取对应协议族的协议实例对象
...
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
...
err = security_socket_post_create(sock, family, type, protocol, kern);//SElinux相关,暂不关注
*res = sock;
...
}
这里比较重要的是sock_alloc()和pf->create()这两个函数。其中sock_alloc()里体现了linux一切皆文件(Everything is a file)理念,即使用文件系统来管理socket,这也是VFS所要达到的效果。
2.1.1 sock_alloc()函数
关于sock_alloc()函数的分析详见Linux socket系统调用(二)—-sock_alloc()。
总的来说,sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
2.1.2 pf->create()函数
下面我们继续看下pf->create()这个函数。
pf由net_families[]数组获得,我们先看下net_families[]数组的定义:
#define AF_MAX 41 /* For now.. */
#define NPROTO AF_MAX
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
net_families[]数组的初始化在inet_init()函数,
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
static int __init inet_init(void)
{
...
(void)sock_register(&inet_family_ops);
...
}
int sock_register(const struct net_proto_family *ops)
{
...
rcu_assign_pointer(net_families[ops->family], ops);
...
}
因此,net_families[]数组里存放的是各个协议族的信息,以family字段作为下标。此处我们针对TCP协议分析,因此我们family字段是AF_INET,另外一般会有
#define PF_INET AF_INET
所以pf->create调用的就是inet_create()函数。
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
struct inet_protosw *answer;
struct inet_sock *inet;
struct proto *answer_prot;
unsigned char answer_flags;
char answer_no_check;
int try_loading_module = 0;
int err;
...
sock->state = SS_UNCONNECTED;
/* Look for the requested type/protocol pair. */
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
//根据socket传入的protocal在inetsw[]数组中查找对应的元素
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* 如果我们在socket的protocal传入的是6,即TCP协议,那么走这个分支 */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* 如果socket的protocal传入的是0,那么走这个分支 */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;//重新给protocal赋值,因此socket中protocal传入的是0或者6,都是可以的
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
...
sock->ops = answer->ops;//将查找到的对应协议族的协议函数操作集赋值给我们之前创建的socket
answer_prot = answer->prot;
...
//创建sock结构体,注意这里创建的结构体类型是sock,之前我们通过sock_alloc创建的结构体类型是socket
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
if (sk == NULL)
goto out;
...
sock_init_data(sock, sk);//sock初始化
//另一部分初始化,里面有对各个socket连接定时器的初始化
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
...
}
}
inet_create()函数里有几处是比较重要的,一个是通过inetsw[]数组获取对应协议类型的接口操作集信息,另一个是创建struct sock类型的变量,最后是对创建的sock进行初始化。
2.1.2.1 inetsw[]数组
inetsw[]数组存放的是各个sock_type的信息,它也是在inet_init()函数中初始化。
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
#define SOCK_MAX (SOCK_PACKET + 1)
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_register_protosw(q);
...
}
其中inetsw_array[]存放的就是具体的每种sock的信息,包括操作函数,协议号等,其中prot和ops两个成员是比较重要的,后续很多操作依赖于这两个成员。
static struct inet_protosw inetsw_array[] = {
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
},
...
};
然后通过inet_register_protosw()函数,将上述inetsw_array[]里的元素,按照type字段挂在inetsw[]数组的链表上。因为type字段是有重复的,所以每个inetsw[]元素的链表上可能会有多个成员。
void inet_register_protosw(struct inet_protosw *p)
{
struct list_head *lh;
struct inet_protosw *answer;
int protocol = p->protocol;
struct list_head *last_perm;
...
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;
}
if (answer)
goto out_permanent;
list_add_rcu(&p->list, last_perm);//按照type的值,添加到inetsw[type]数组中的链表中
...
}
2.1.2.2 创建struct sock类型变量
通过sk_alloc()函数创建struct sock类型变量
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_prot这个变量
sk->sk_prot = sk->sk_prot_creator = prot;
...
}
return sk;
}
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) {
//使用对应协议的slab高速缓存分配,减小内存碎片
sk = kmem_cache_alloc(slab, priority & ~__GFP_ZERO);
...
}
由此可见,最后是通过prot->slab,也就是TCP的slab高速缓存里分配的。
关于TCP协议的slab高速缓存建立,详见Linux socket系统调用(三)—-TCP协议的slab高速缓存建立及各结构体关系 。
2.1.2.3 sock初始化
创建完sock结构体,那么自然要做初始化。初始化又分为两个大块,一个在sock_init_data()函数,另一个在sk->sk_prot->init()。
a. sock_init_data()
sock_init_data()函数我认为比较重要的一点就是将之前分配的struct socket和struct sock联系在一起,把逻辑填充完整。
void sock_init_data(struct socket *sock, struct sock *sk)
{
skb_queue_head_init(&sk->sk_receive_queue);
skb_queue_head_init(&sk->sk_write_queue);
skb_queue_head_init(&sk->sk_error_queue);
#ifdef CONFIG_NET_DMA
skb_queue_head_init(&sk->sk_async_wait_queue);
#endif
sk->sk_send_head = NULL;
init_timer(&sk->sk_timer);
sk->sk_allocation = GFP_KERNEL;
//socket的接收和发送缓存,怎么用的,暂时不太清楚,后续再分析
//这两个参数可以在proc下设置,两个分别是/proc/sys/net/core/rmem_default和/proc/sys/net/core/wmem_default
sk->sk_rcvbuf = sysctl_rmem_default;
sk->sk_sndbuf = sysctl_wmem_default;
sk->sk_state = TCP_CLOSE;//socket一开始的状态就是close
sk_set_socket(sk, sock);
sock_set_flag(sk, SOCK_ZAPPED);
if (sock) {
sk->sk_type = sock->type;
sk->sk_wq = sock->wq;
//struct socket里有指针指向对应的struct sock
//加上上面sk_set_socket()的设置,struct socket和struct sock就能通过各自的成员访问到对方
sock->sk = sk;
} else
sk->sk_wq = NULL;
//指定一些回调函数
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
sk->sk_error_report = sock_def_error_report;
...
}
static inline void sk_set_socket(struct sock *sk, struct socket *sock)
{
sk_tx_queue_clear(sk);
//让struct sock的sk_socket成员指向对应的struct socket
sk->sk_socket = sock;
}
b. sk->sk_prot->init()
对于TCP协议,这个init成员指向的是tcp_v4_init_sock()函数,具体参看struct proto tcp_prot结构体的成员。
static int tcp_v4_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
tcp_init_sock(sk);//初始化操作
//设置操作集,这个赋值比较重要,后续icsk->icsk_af_ops的调用要知道最终调用的是什么
icsk->icsk_af_ops = &ipv4_specific;
#ifdef CONFIG_TCP_MD5SIG
tcp_sk(sk)->af_specific = &tcp_sock_ipv4_specific;
#endif
return 0;
}
tcp_v4_init_sock()函数里调用tcp_init_sock()函数完成主要的初始化动作。
void tcp_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
skb_queue_head_init(&tp->out_of_order_queue);
//初始化定时器,包括超时重传定时器,延迟ack定时器,保活定时器等的初始化
tcp_init_xmit_timers(sk);
tcp_prequeue_init(tp);
INIT_LIST_HEAD(&tp->tsq_node);
icsk->icsk_rto = TCP_TIMEOUT_INIT;//设置初始的超时时间,为1HZ,即1s
...
tcp_enable_early_retrans(tp);
icsk->icsk_ca_ops = &tcp_init_congestion_ops;//设置默认拥塞算法,reno
tp->tsoffset = 0;
sk->sk_state = TCP_CLOSE;//这里又再次设置了sock状态,重复了sock_init_data()的动作
...
//socket的接收和发送缓存,sock_init_data()也有设置,不过是设置默认的大小
//这里使用的是/proc/sys/net/ipv4/tcp_wmem和/proc/sys/net/ipv4/tcp_rmem的值
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
...
}
至此,我们已经创建好了socket,也完成了相关的初始化,但是我们知道socket系统调用返回的是一个int型的文件描述符,而目前我们好像还没涉及到。
下面就进入第二部分,fd的创建。
2.2 sock_map_fd()
这个函数主要有两个部分,一个是创建file文件结构,fd文件描述符,另一部分是将file文件结构和fd文件描述符关联,同时将上一步返回的socket也一起绑定,形成一个完整的逻辑。
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;
//分配file结构体
newfile = sock_alloc_file(sock, flags, NULL);
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile);
return fd;
}
...
}
2.2.1 sock_alloc_file()函数
file结构体的创建及初始化,以及将socket和file结构体关联的操作主要在sock_alloc_file()函数中完成,
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
struct qstr name = { .name = "" };
struct path path;
struct file *file;
...
//在sock文件系统上分配一个dentry结构
path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
if (unlikely(!path.dentry))
return ERR_PTR(-ENOMEM);
path.mnt = mntget(sock_mnt);
//使用之前我们分配socket结构体来初始化dentry
d_instantiate(path.dentry, SOCK_INODE(sock));
SOCK_INODE(sock)->i_fop = &socket_file_ops;//设置该inode的函数操作集
file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
&socket_file_ops);//从cache里分配一个file结构体
...
//socket结构体里有file指针,指向我们刚分配的file结构体
sock->file = file;
file->f_flags = O_RDWR | (flags & O_NONBLOCK);
//同样file结构体了也有指针指向socket结构体
//至此file和socket已经做好关联,就差fd和file关联了
file->private_data = sock;
return file;
}
在d_instantiate()函数里我们可以看到dentry和之前创建socket结构体时分配的struct socket_alloc的联系。
void d_instantiate(struct dentry *entry, struct inode * inode)
{
...
__d_instantiate(entry, inode);
...
}
static void __d_instantiate(struct dentry *dentry, struct inode *inode)
{
...
//还记得之前我们分配socket结构体时,其实分配的是struct socket_alloc,
//该结构体里包含了struct socket和struct inode,这里就是将这个struct inode和上面分配的dentry关联
dentry->d_inode = inode;
...
}
而alloc_file()函数则是将file dentry inode socket四者柔和到一起,形成sockfs的整套逻辑。
struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
struct file *file;
file = get_empty_filp();//在filp_cachep缓存中分配file结构体
if (IS_ERR(file))
return file;
//path结构体里包含了刚才struct socket_alloc的inode成员信息
file->f_path = *path;
//file结构体里也有变量指向刚才struct socket_alloc的inode
//至此,file dentry inode socket四者总算是走到一起,实现了完整的sock文件系统逻辑
file->f_inode = path->dentry->d_inode;
file->f_mapping = path->dentry->d_inode->i_mapping;
file->f_mode = mode;
//fop其实就是socket_file_ops,这样就能表明这个file结构体对应的是socket
file->f_op = fop;
...
}
最后就是返回用户使用的文件描述符fd和file的关联了。
2.2.2 fd_install()函数
void fd_install(unsigned int fd, struct file *file)
{
//获得当前进程的打开文件列表files
__fd_install(current->files, fd, file);
}
void __fd_install(struct files_struct *files, unsigned int fd,
struct file *file)
{
struct fdtable *fdt;
spin_lock(&files->file_lock);
fdt = files_fdtable(files);//获得文件描述符位图表
BUG_ON(fdt->fd[fd] != NULL);
//将file指针放入文件描述符位图表中,这样我们之后就能通过fd找回file结构
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}
至此,socket系统调用就结束了,将fd返回用户使用。
我们大概概述一下,socket系统调用的操作:首先在内核生成一个socket_alloc 和tcp_sock类型的对象,其中sock_alloc对象中的socket和tcp_sock对象的sock绑定,sock_alloc对象中的inode和file类型对象绑定。然后将分配的文件描述符fd和file对象关联,最后将这个文件描述符fd返回给用户使用。
经过这一连串操作,用户只要使用fd,内核就能根据这个fd进行网络连接管理的各种操作。
最后上张图,描述从fd到内核sock各个结构体的关系。
参考资料:
1、https://baike.baidu.com/item/socket/281150?fr=aladdin
2、http://blog.csdn.net/u011130578/article/details/44220999
3、http://blog.csdn.net/idwtwt/article/details/50964302