Socket与系统调用深度分析
一.系统调用
系统调用使得操作系统从用户态进入了内核态,从而使用了内核中的一些功能,通过查阅书籍得知,linux x86-64系统通过int 80调用syscall中断来进入内核态。在使用socket编程中,系统先通过中断进入内核态,再调用底层功能来使得可以与网络通信协议产生连接,这是此实验的原理。
二.实验准备
本实验需要上次实验准备的menuOS系统,基于linux5.0.1内核,编译方式为x86-64
三.流程分析
首先分析socket函数的参数信息
int socket(int domain, int type, int protocol);
{
unsigned long a[AUDITSC_ARGS];
unsigned long a0, a1;
int err;
unsigned int len;
return -EINVAL;
call = array_index_nospec(call, SYS_SENDMMSG + 1);
if (len > sizeof(a))
return -EINVAL;
if (copy_from_user(a, args, len))
return -EFAULT;
if (err)
return err;
a1 = a[1];
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;
case SYS_GETSOCKNAME:
err =
__sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
err =
__sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_SOCKETPAIR:
err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
break;
case SYS_SEND:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
NULL, 0);
break;
case SYS_SENDTO:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4], a[5]);
break;
case SYS_RECV:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
NULL, NULL);
break;
case SYS_RECVFROM:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4],
(int __user *)a[5]);
break;
case SYS_SHUTDOWN:
err = __sys_shutdown(a0, a1);
break;
case SYS_SETSOCKOPT:
err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
a[4]);
break;
case SYS_GETSOCKOPT:
err =
__sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
(int __user *)a[4]);
break;
case SYS_SENDMSG:
err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_SENDMMSG:
err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
a[3], true);
break;
case SYS_RECVMSG:
err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_RECVMMSG:
if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3],
(struct __kernel_timespec __user *)a[4],
NULL);
else
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3], NULL,
(struct old_timespec32 __user *)a[4]);
break;
case SYS_ACCEPT4:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], a[3]);
break;
default:
err = -EINVAL;
break;
}
return err;
}
这里通过syscall进入了内核中,可以看到除了一些合法性检验外,通过switch语句来实现不通的调用,也是一种函数内部多态的情况
上面我们介绍了进入内核的一些方法,下面我们从socket的参数入手来分析到如何连接TCP/IP协议。
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所要达到的效果。
sock_alloc()函数
总的来说,sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
2.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.3 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.4 创建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的连接了,下面TCP的具体连接就不分析了,此次实验任务结束。
浙公网安备 33010602011771号