1、socket()到系统调用
Linux下,用户空间的调用的socket()接口由glibc实现,man socket如下,三个参数domain,type和protocol:
1
2
3
4
5
6
7
|
NAME
socket - create an endpoint
for
communication
SYNOPSIS
#include <sys/socket.h>
int
socket(
int
domain,
int
type,
int
protocol);
|
libc实现系统调用同名函数通常使用INT 0x80 + 系统调用号的方式陷入内核,一般来说,read对应sys_read,write对应sys_write.但是,socket系列却不是这样,为了节约系统调用号,将所有的socket系列的接口使用同一个系统调用号陷入内核(叫socketcall),glibc中通过socket.S陷入内核:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
.globl __socket
ENTRY (__socket)
/* Save registers. */
movl %ebx, %edx
cfi_register (3, 2)
/* System call number in %eax. */
movl $SYS_ify(socketcall), %eax
/* Use ## so `socket' is a separate
token that might be #define'd. */
/* Subcode is first arg to syscall. */
movl $P(SOCKOP_,socket), %ebx
/* Address of args is 2nd arg. */
lea 4(%esp), %ecx
/* Do the system call trap. */
ENTER_KERNEL
/* Restore registers. */
movl %edx, %ebx
cfi_restore (3)
/* %eax is < 0 if there was an error. */
cmpl $-125, %eax
jae SYSCALL_ERROR_LABEL
/* Successful; return the syscall's value. */
ret
|
eax存系统调用号,应该是198,ebx存参数1,ecx存参数2(这个地方的参数1和参数2不是domian/type/protocol,而是参数1是socket systemcall的类型,参数二是栈顶减4,ecx存domain/type/protocol的整体指针),ENTER_KERNEL即通过INT 0x80陷入内核:
1
|
# define ENTER_KERNEL int $0x80
|
2、socketcall到协议簇的挂接
内核中实现的socketcall代码片段如下,一个简单的逻辑分支将socketcall引向sys_socket():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
SYSCALL_DEFINE2(socketcall,
int
, call,
unsigned
long
__user *, args)
{
...
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
;
...
}
|
sys_socket()的实现从代码上有些曲折(net/socket.c),通过SYSCALL_DEFINE3实现sys_socket:
1
|
SYSCALL_DEFINE3(socket,
int
, family,
int
, type,
int
, protocol)
|
SYSCALL_DEFINE3即定义一个3个参数的系统调用,具体细节不研究了。
sys_socket()主要有两个操作,sock_create()和sock_map_fd(),前者用于创建具体的socket结构,后者用于为socket结构分配file sttuct和file fd。我们主要关注sock_create()。sock_create()又调用了__sock_create(),__sock_creat()仅列出主要代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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);
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();
...
sock->type = type;
...
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
...
rcu_read_unlock();
err = pf->create(net, sock, protocol, kern);
...
err = security_socket_post_create(sock, family, type, protocol, kern);
...
}
|
security_socket_*用于SELinux增强系统安全,此处不讨论了。那么__sock_create主要就调用了pf->create,因此此处也是挂接具体的内核协议簇的位置,如PF_INET,PF_NETLINK等。net_family定义如下(3.10.9内核共有39中协议簇,27,28空):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
static
const
struct
net_proto_family __rcu *net_families[NPROTO] __read_mostly;
struct
net_proto_family {
int
family;
int
(*create)(
struct
net *net,
struct
socket *sock,
int
protocol,
int
kern);
struct
module *owner;
};
#define NPROTO AF_MAX
/* Supported address families. */
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_AX25 3 /* Amateur Radio AX.25 */
#define AF_IPX 4 /* Novell IPX */
#define AF_APPLETALK 5 /* AppleTalk DDP */
#define AF_NETROM 6 /* Amateur Radio NET/ROM */
#define AF_BRIDGE 7 /* Multiprotocol bridge */
#define AF_ATMPVC 8 /* ATM PVCs */
#define AF_X25 9 /* Reserved for X.25 project */
#define AF_INET6 10 /* IP version 6 */
...
#define AF_NETLINK 16
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
...
#define AF_MAX 41 /* For now.. */
|
协议簇,Protocol Family,在内核中使用net_familes[]树组维护,目前3.10.9内核中共有39个协议簇。协议簇代表socket的第一个参数domain,而不是第二个type或者第三个protocol,协议簇的意思是一簇协议的集合,根据ISO/OSI的7层模型,应该都包含,而非其中某一层上的一种协议。而对于具体的create函数,就是会调用具体的协议簇自己的create,如unix_create,inet_craete,netlink_create等。至此,完成了协议簇和应用层socket的挂接工作:
3、socket结构体初始化
sock_create()函数的目的是为了socket结构分配空间,并根据family/type/protocol对其进行初始化。先看一下socket结构(include/linux/net.h):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* 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
* @file: File back pointer for gc
* @sk: internal networking protocol agnostic socket representation
* @wq: wait queue for several uses
*/
struct
socket {
socket_state state;
kmemcheck_bitfield_begin(type);
short
type;
kmemcheck_bitfield_end(type);
unsigned
long
flags;
struct
socket_wq __rcu *wq;
struct
file *file;
struct
sock *sk;
const
struct
proto_ops *ops;
};
|
其中后三个指针比较重要,分别为文件指针、sock指针、协议ops指针,sock_create()主要也就是初始化这三个指针。*file指针用于指向VFS创建的文件结构体,而*ops则为具体的协议的操作结构体,内含有bind、accept、recvmsg、sendmsg、setsockopt、getsockopt等具体与协议相关的函数指针。而*sk,具体负责传输层的工作,非常复杂。
1)socket->file的初始化
首先,前面提到sock_map_fd()主要用于对socket的*file指针初始化,经过sock_map_fd()操作后,socket就通过其*file指针与VFS管理的文件进行了关联,便可以进行文件的各种操作,如read、write、lseek、ioctl等,sock_map_fd()主要有两个调用:get_unused_fd_flags用于获取一个无用的fd号,sock_alloc_file用于将分配并初始化*file,通过将file->fop指针挂接到socket_file_ops的地址完成该操作(通过alloc_file接口完成,但此时其实仍为完全完成对file->fop的赋值,因为socket_file_ops本身也有需要挂接的地方):
当然了,alloc_file同时还做了很多其他的工作,这里就不关心了,那么socket_file_ops结构体指针就存储了socket的文件操作指针(任何内核对象要想以文件的方式进行处理,都需要定义file_operations结构题变量):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
static
const
struct
file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.open = sock_no_open,
/* special open code to disallow open via /proc */
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
|
具体的sock_*函数就是将socket当成文件时的具体操作,对于一些没有的操作,例如open,就定义为sock_no_xxx,直接返回错误。
至此,socket->file->fop已经初始化为socket_file_ops。当然,一个fop指针的初始化并不能算是一个file结构体变量完成了初始化(inode等等都需要做初始化,VFS一系列的),但是这里暂时就不关注了。
另外,VFS的三个比较重要的结构inode,file,dentry也相互关联,而file结构又与socket结构相互管理,因此通过socket也可以索引到相关的file结构,反之也可以;通过socket索引到的file结构是一个通用的file结构,具备文件的通用功能和属性,这也是为什么在Linux操作系统中socket能够被当作文件使用的原因:
此处应该有一张inode,dentry,file,socket相互关联的图,等到写VFS时再补充吧。
2)socket->ops的初始化
除了socket->file->fop初始化为socket_file_ops,socket自身的操作集也需要初始化,即socket作为一个socket理所应当的操作(BSD socket)。由于socket的具体操作,如send,recv,setsockopt,getsockopt等与具体的协议簇、类型、协议有关系,那么这个socket->ops的初始化过程也应该是一个逐层挂接的过程,即种协议簇自己的xxx_create()接口实现的socket->ops的初始化。以PF_INET为例,具体分析一下inet_create如何初始化socket->ops指针。
首先,有几种与协议簇相关的结构先介绍一下:
a)struct net_proto_family,用于描述协议簇,也即glibc的socket()接口的第一个参数domain,目前内核工支持39个协议簇,PF_INET协议簇为其定义了变量inet_family_ops
1
2
3
4
5
6
|
struct
net_proto_family {
int
family;
int
(*create)(
struct
net *net,
struct
socket *sock,
int
protocol,
int
kern);
struct
module *owner;
};
|
b)struct proto_ops,用于描述每种协议簇的不同类型协议集合,即glibc的socket()接口的第二个参数type,PF_INET协议簇工支持三种type:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW,Linux内核中关于sock_type的定义如下,共7种(include/linux/net.h):
1
2
3
4
5
6
7
8
9
|
enum
sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
|
proto_ops结构体定义如下,INET定义了它的变量inet_stream_ops、inet_dgram_ops和inet_sockraw_ops,分别代表SOCK_STREAM、SOCK_DGRAM、SOCK_RAW类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
struct
proto_ops {
int
family;
struct
module *owner;
int
(*release) (
struct
socket *sock);
int
(*bind) (...);
int
(*connect) (...);
int
(*socketpair) (...);
int
(*accept) (...);
int
(*getname) (...);
int
(*ioctl) (...);
...
int
(*listen) (...);
int
(*setsockopt) (...);
int
(*getsockopt) (...);
int
(*sendmsg) (...);
int
(*mmap) (...);
...
}
|
其次,PF_INET为了维护方便定义了自己的一些结构体和变量:
a)struct inet_protosw,用于INET注册自己的子协议使用,也即代表glibc的socket()的第三个参数,protocol。使用type表示该protocol所属的类型,使用protocol表示该协议,使用ops指针用于具体的该protocol的操作集。相关变量为inetsw_array[]和inetsw[](inetsw[]为inet_init时将inetsw_array[]按照type作为数组下标重新赋值的变量):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* This is used to register socket interfaces for IP protocols. */
struct
inet_protosw {
struct
list_head list;
/* These two fields form the lookup key. */
unsigned
short
type;
/* This is the 2nd argument to socket(2). */
unsigned
short
protocol;
/* This is the L4 protocol number. */
struct
proto *prot;
const
struct
proto_ops *ops;
char
no_check;
/* checksum on rcv/xmit/none? */
unsigned
char
flags;
/* See INET_PROTOSW_* below. */
};
|
struct proto结构与struct proto_ops有些类似,不过定义的是传输层的操作集,比较多,这里就不列举了。
写到这个位置,Linux系统对网络这部分的分层是非常明显的,一些理解应该是这样的:
- linux kernel遵从ISO/OSI的7层模型,但是仅仅实现其中的三层链路层、网络层、传输层(物理层由硬件负责,绘画层、表示层、应用层由用户空间的具体应用负责)。
- socket的三个参数domain,type,protocol分别对应协议簇、协议类型、具体协议号;而这个协议号,是指传输层的协议号。三者之间属于包含关系,即协议簇下面分为具体不同类型的子协议集合,不同类型的子协议集合中又包含具体的协议。
- Linux系统有一些结构体分别用于定义不同层次的通信句柄,用户空间使用int fd作为通信句柄,Linux内核中的socket层使用struct socket作为通信的句柄,Linux内核中传输层使用struct sock作为通信的句柄。
- 不同层上对应了不同的操作集合,用户空间应用使用glibc的接口,如socket().bind(),recv(),send()等;socket层使用struct proto_ops指针作为操作集,传输层使用struct proto指针作为具体的协议相关的操作集合。
OK,回到inet_create()。inet_create()通过以sock->type作为数组inetsw[]的下标,找到相关inet_protosw指针,并为socket->ops赋值为inet_protosw->ops,具体过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
static
int
inet_create(
struct
net *net,
struct
socket *sock,
int
protocol,
int
kern)
{
struct
sock *sk;
struct
inet_protosw *answer;
...
rcu_read_lock();
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. */
if
(IPPROTO_IP == protocol) {
protocol = answer->protocol;
break
;
}
if
(IPPROTO_IP == answer->protocol)
break
;
}
err = -EPROTONOSUPPORT;
}
...
sock->ops = answer->ops;
...
rcu_read_unlock();
...
}
|
当然,inet_create()接口还做了很多其他工作,在这里就不讨论了。
3)socket->sk的初始化
socket->sk指针为struct sock指针,而struct sock是传输层的的句柄。在PF_INET协议簇中,仍然使用inet_create()接口为socket->sk指针做初始化的工作,下面选择另一种协议簇PF_UNIX进行socket->sk指针初始化的分析,类似与PF_INET,PF_UNIX也定义了协议簇变量和具体协议类型变量,他们分别是unix_family_ops和unix_stream_ops, unix_dgram_ops, unix_seqpacket_ops(PF_UNIX也支持三种类型的协议,分别是SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET)。
unix_create()接口调用了unix_create1()为socket->sk赋值,摘录unix_create1()接口代码片段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
static
struct
sock *unix_create1(
struct
net *net,
struct
socket *sock)
{
struct
sock *sk = NULL;
...
sk = sk_alloc(net, PF_UNIX, GFP_KERNEL, unix_proto);
...
sock_init_data(sock, sk);
...
}
void
sock_init_data(
struct
socket *sock,
struct
sock *sk)
{
some init operations;
...
sk_set_socket(sk, sock);
...
other init operations;
}
static
inline
void
sk_set_socket(
struct
sock *sk,
struct
socket *sock)
{
sk_tx_queue_clear(sk);
sk->sk_socket = sock;
}
|
大量的具体的初始化工作都在sock_init_data()接口中,本文中就不讨论了,重点在sock_init_data()为struct sock结构分配了内存,并做了初始化工作,sock_init_data()接口负责与具体协议簇无关的初始化,而其他初始化在family_create接口中其他代码完成,如:
1
2
3
4
5
6
|
lockdep_set_class(&sk->sk_receive_queue.lock,
&af_unix_sk_receive_queue_lock_key);
sk->sk_write_space = unix_write_space;
sk->sk_max_ack_backlog = net->unx.sysctl_max_dgram_qlen;
sk->sk_destruct = unix_sock_destructor;
|