Socket与内核调用深度分析

Posted n-p-2019-blogs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与内核调用深度分析相关的知识,希望对你有一定的参考价值。

1 概念

Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念,即与系统相关的一些特别关键的操作必须由最高特权的程序来完成。
Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态(Kernel Mode)用户态(User Mode)

  • 内核态:CPU可以访问内存所有数据,包括外围设备(硬盘、网卡),CPU也可以将自己从一个程序切换到另一个程序;
  • 用户态:只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其他程序获取;

Linux中任何一个用户进程被创建时都包含2个栈:内核栈,用户栈,并且是进程私有的,从用户态开始运行。内核态和用户态分别对应内核空间与用户空间,内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。

2 内核空间相关

  • 内核空间:存放的是内核代码和数据,处于虚拟空间;
  • 内核态:当进程执行系统调用而进入内核代码中执行时,称进程处于内核态,此时CPU处于特权级最高的0级内核代码中执行,当进程处于内核态时,执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈;
  • CPU堆栈指针寄存器指向:内核栈地址;
  • 内核栈:进程处于内核态时使用的栈,存在于内核空间;
  • 处于内核态进程的权利:处于内核态的进程,当它占有CPU的时候,可以访问内存所有数据和所有外设,比如硬盘,网卡等等;

3 用户空间相关

  • 用户空间:存放的是用户程序的代码和数据,处于虚拟空间;
  • 用户态:当进程在执行用户自己的代码(非系统调用之类的函数)时,则称其处于用户态,CPU在特权级最低的3级用户代码中运行,当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态,因为中断处理程序将使用当前进程的内核栈;
  • CPU堆栈指针寄存器指向:用户堆栈地址;
  • 用户堆栈:进程处于用户态时使用的堆栈,存在于用户空间;
  • 处于用户态进程的权利:处于用户态的进程,当它占有CPU的时候,只可以访问有限的内存,而且不允许访问外设,这里说的有限的内存其实就是用户空间,使用的是用户堆栈;

4 scoket创建过程分析

  在用户进程中,socket(int domain, int type, int protocol) 函数用于创建socket并返回一个与socket关联的fd,该函数实际执行的是系统调用 sys_socketcall,sys_socketcall几乎是用户进程socket所有操作函数的入口:

1 /** sys_socketcall (linux/syscalls.h)*/
2 asmlinkage long sys_socketcall(int call, unsigned long __user *args);

 

  sys_socketcall 实际调用的是 SYSCALL_DEFINE2:

 1 /** SYSCALL_DEFINE2 (net/socket.c)*/
 2 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
 3 {
 4     unsigned long a[AUDITSC_ARGS];
 5     unsigned long a0, a1;
 6     int err;
 7     unsigned int len;
 8     // 省略...
 9     a0 = a[0];
10     a1 = a[1];
11 
12     switch (call) {
13     case SYS_SOCKET:
14         // 与 socket(int domain, int type, int protocol) 对应,创建socket
15         err = sys_socket(a0, a1, a[2]);  
16         break;
17     case SYS_BIND:
18         err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); 
19         break;
20     case SYS_CONNECT:
21         err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
22         break;
23 // 省略...
24 }

 

  在 SYSCALL_DEFINE2 函数中,通过判断call指令,来统一处理 socket 相关函数的事务,对于socket(…)函数,实际处理是在 sys_socket 中,也是一个系统调用,对应的是 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol):

 1 /** SYSCALL_DEFINE3 net/socket.c*/
 2 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
 3 {
 4     int retval;
 5     struct socket *sock;
 6     int flags;
 7     // SOCK_TYPE_MASK: 0xF; SOCK_STREAM等socket类型位于type字段的低4位
 8     // 将flag设置为除socket基本类型之外的值
 9     flags = type & ~SOCK_TYPE_MASK;
10 
11     // 如果flags中有除SOCK_CLOEXEC或者SOCK_NONBLOCK之外的其他参数,则返回EINVAL
12     if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
13         return -EINVAL;
14 
15     // 取type中的后4位,即sock_type,socket基本类型定义
16     type &= SOCK_TYPE_MASK;
17 
18     // 如果设置了SOCK_NONBLOCK,则不论SOCK_NONBLOCK定义是否与O_NONBLOCK相同,
19     // 均将flags中的SOCK_NONBLOCK复位,将O_NONBLOCK置位
20     if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
21         flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
22 
23     // 创建socket结构,(重点分析)
24     retval = sock_create(family, type, protocol, &sock);
25     if (retval < 0)
26         goto out;
27 
28     if (retval == 0)
29         sockev_notify(SOCKEV_SOCKET, sock);
30 
31     // 将socket结构映射为文件描述符retval并返回,(重点分析)
32     retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
33     if (retval < 0)
34         goto out_release;
35 out:
36     return retval;
37 out_release:
38     sock_release(sock);
39     return retval;
40 }

 

  SYSCALL_DEFINE3 中主要判断了设置的socket类型type,如果设置了除基本sock_type,SOCK_CLOEXEC和SOCK_NONBLOCK之外的其他参数,则直接返回;同时调用 sock_create 创建 socket 结构,使用 sock_map_fd 将socket 结构映射为文件描述符并返回。在分析 sock_create 之前,先看看socket结构体:

 1 /** socket结构体 (linux/net.h)*/
 2 struct socket {
 3     socket_state        state;       // 连接状态:SS_CONNECTING, SS_CONNECTED 等
 4     short           type;            // 类型:SOCK_STREAM, SOCK_DGRAM 等
 5     unsigned long       flags;       // 标志位:SOCK_ASYNC_NOSPACE(发送队列是否已满)等
 6     struct socket_wq __rcu  *wq;     // 等待队列
 7     struct file     *file;           // 该socket结构体对应VFS中的file指针
 8     struct sock     *sk;             // socket网络层表示,真正处理网络协议的地方
 9     const struct proto_ops  *ops;    // socket操作函数集:bind, connect, accept 等
10 };

 

  socket结构体中定义了socket的基本状态,类型,标志,等待队列,文件指针,操作函数集等,利用 sock 结构,将 socket 操作与真正处理网络协议相关的事务分离。
回到 sock_create 继续看socket创建过程,sock_create 实际调用的是 __sock_create:

 1 /** __sock_create (net/socket.c)*/
 2 int __sock_create(struct net *net, int family, int type, int protocol,
 3              struct socket **res, int kern)
 4 {
 5     int err;
 6     struct socket *sock;
 7     const struct net_proto_family *pf;
 8 
 9     // 检查是否是支持的地址族,即检查协议
10     if (family < 0 || family >= NPROTO)
11         return -EAFNOSUPPORT;
12     // 检查是否是支持的socket类型
13     if (type < 0 || type >= SOCK_MAX)
14         return -EINVAL;
15 
16     // 省略...
17 
18     // 检查权限,并考虑协议集、类型、协议,以及 socket 是在内核中创建还是在用户空间中创建
19     // 可以参考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
20     err = security_socket_create(family, type, protocol, kern);
21     if (err)
22         return err;
23 
24     // 分配socket结构,这其中创建了socket和关联的inode (重点分析)
25     sock = sock_alloc();
26     if (!sock) {
27         net_warn_ratelimited("socket: no more sockets
");
28         return -ENFILE; /* Not exactly a match, but its the
29                    closest posix thing */
30     }
31     sock->type = type;
32     // 省略...
33 }

 

  __socket_create 检查了地址族协议和socket类型,同时,调用 security_socket_create 检查创建socket的权限(如:创建不同类型不同地址族socket的SELinux权限也会不同)。接着,来看看 sock_alloc:

  

 1 /** sock_alloc (net/socket.c)*/
 2 static struct socket *sock_alloc(void)
 3 {
 4     struct inode *inode;
 5     struct socket *sock;
 6 
 7     // 在已挂载的sockfs文件系统的super_block上分配一个inode
 8     inode = new_inode_pseudo(sock_mnt->mnt_sb);
 9     if (!inode)
10         return NULL;
11 
12     // 获取inode对应socket_alloc中的socket结构指针
13     sock = SOCKET_I(inode);
14 
15     inode->i_ino = get_next_ino();
16     inode->i_mode = S_IFSOCK | S_IRWXUGO;
17     inode->i_uid = current_fsuid();
18     inode->i_gid = current_fsgid();
19 
20     // 将inode的操作函数指针指向 sockfs_inode_ops 函数地址
21     inode->i_op = &sockfs_inode_ops;
22 
23     this_cpu_add(sockets_in_use, 1);
24     return sock;
25 }

 

  new_inode_pseudo 函数实际调用的是 alloc_inode(struct super_block *sb) 函数:

  
 1 /** alloc_inode (fs/inode.c)*/
 2 static struct inode *alloc_inode(struct super_block *sb)
 3 {
 4     struct inode *inode;
 5 
 6     // 如果文件系统的超级块已经指定了alloc_inode的函数,则调用已经定义的函数去分配inode
 7     // 对于sockfs,已经将alloc_inode指向sock_alloc_inode函数指针
 8     if (sb->s_op->alloc_inode)
 9         inode = sb->s_op->alloc_inode(sb);
10     else
11         // 否则在公用的 inode_cache slab缓存上分配inode
12         inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);
13 
14     if (!inode)
15         return NULL;
16 
17     // 编译优化,提高执行效率,inode_init_always正常返回0
18     if (unlikely(inode_init_always(sb, inode))) {
19         if (inode->i_sb->s_op->destroy_inode)
20             inode->i_sb->s_op->destroy_inode(inode);
21         else
22             kmem_cache_free(inode_cachep, inode);
23         return NULL;
24     }
25 
26     return inode;
27 }

 

  从前文 “socket文件系统注册” 提到的:”.alloc_inode = sock_alloc_inode” 可知,alloc_inode 实际将使用 sock_alloc_inode 函数去分配 inode:

  
 1 /** sock_alloc_inode (net/socket.c)*/
 2 static struct inode *sock_alloc_inode(struct super_block *sb)
 3 {
 4     // socket_alloc 结构体包含一个socket和一个inode,将两者联系到一起
 5     struct socket_alloc *ei;
 6     struct socket_wq *wq;
 7 
 8     // 在sock_inode_cachep缓存上分配一个socket_alloc
 9     // sock_inode_cachep: 前文"socket文件系统注册"中已经提到,专用于分配socket_alloc结构
10     ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
11     if (!ei)
12         return NULL;
13     // 分配socket等待队列结构
14     wq = kmalloc(sizeof(*wq), GFP_KERNEL);
15     if (!wq) {
16         kmem_cache_free(sock_inode_cachep, ei);
17         return NULL;
18     }
19     // 初始化等待队列
20     init_waitqueue_head(&wq->wait);
21     wq->fasync_list = NULL;
22     wq->flags = 0;
23     // 将socket_alloc中socket的等待队列指向wq
24     RCU_INIT_POINTER(ei->socket.wq, wq);
25 
26     // 初始化socket的状态,标志,操作集等
27     ei->socket.state = SS_UNCONNECTED;
28     ei->socket.flags = 0;
29     ei->socket.ops = NULL;
30     ei->socket.sk = NULL;
31     ei->socket.file = NULL;
32 
33     // 返回socket_alloc中的inode
34     return &ei->vfs_inode;
35 }

 

5 内核跟踪

  我们首先在 sys_socketcall 处建立断点
技术图片
  捕获到sys_socketcall,对应的内核处理函数为SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
  在net/socket.c中查看SYSCALL_DEFINE2相关的源代码如下:

  1 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
  2 {
  3     unsigned long a[AUDITSC_ARGS];
  4     unsigned long a0, a1;
  5     int err;
  6     unsigned int len;
  7 
  8     if (call < 1 || call > SYS_SENDMMSG)
  9         return -EINVAL;
 10     call = array_index_nospec(call, SYS_SENDMMSG + 1);
 11 
 12     len = nargs[call];
 13     if (len > sizeof(a))
 14         return -EINVAL;
 15 
 16     /* copy_from_user should be SMP safe. */
 17     if (copy_from_user(a, args, len))
 18         return -EFAULT;
 19 
 20     err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
 21     if (err)
 22         return err;
 23 
 24     a0 = a[0];
 25     a1 = a[1];
 26 
 27     switch (call) {
 28     case SYS_SOCKET:
 29         err = __sys_socket(a0, a1, a[2]);
 30         break;
 31     case SYS_BIND:
 32         err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
 33         break;
 34     case SYS_CONNECT:
 35         err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
 36         break;
 37     case SYS_LISTEN:
 38         err = __sys_listen(a0, a1);
 39         break;
 40     case SYS_ACCEPT:
 41         err = __sys_accept4(a0, (struct sockaddr __user *)a1,
 42                     (int __user *)a[2], 0);
 43         break;
 44     case SYS_GETSOCKNAME:
 45         err =
 46             __sys_getsockname(a0, (struct sockaddr __user *)a1,
 47                       (int __user *)a[2]);
 48         break;
 49     case SYS_GETPEERNAME:
 50         err =
 51             __sys_getpeername(a0, (struct sockaddr __user *)a1,
 52                       (int __user *)a[2]);
 53         break;
 54     case SYS_SOCKETPAIR:
 55         err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
 56         break;
 57     case SYS_SEND:
 58         err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
 59                    NULL, 0);
 60         break;
 61     case SYS_SENDTO:
 62         err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
 63                    (struct sockaddr __user *)a[4], a[5]);
 64         break;
 65     case SYS_RECV:
 66         err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
 67                      NULL, NULL);
 68         break;
 69     case SYS_RECVFROM:
 70         err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
 71                      (struct sockaddr __user *)a[4],
 72                      (int __user *)a[5]);
 73         break;
 74     case SYS_SHUTDOWN:
 75         err = __sys_shutdown(a0, a1);
 76         break;
 77     case SYS_SETSOCKOPT:
 78         err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
 79                        a[4]);
 80         break;
 81     case SYS_GETSOCKOPT:
 82         err =
 83             __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
 84                      (int __user *)a[4]);
 85         break;
 86     case SYS_SENDMSG:
 87         err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
 88                     a[2], true);
 89         break;
 90     case SYS_SENDMMSG:
 91         err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
 92                      a[3], true);
 93         break;
 94     case SYS_RECVMSG:
 95         err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
 96                     a[2], true);
 97         break;
 98     case SYS_RECVMMSG:
 99         if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
100             err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
101                          a[2], a[3],
102                          (struct __kernel_timespec __user *)a[4],
103                          NULL);
104         else
105             err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
106                          a[2], a[3], NULL,
107                          (struct old_timespec32 __user *)a[4]);
108         break;
109     case SYS_ACCEPT4:
110         err = __sys_accept4(a0, (struct sockaddr __user *)a1,
111                     (int __user *)a[2], a[3]);
112         break;
113     default:
114         err = -EINVAL;
115         break;
116     }
117     return err;
118 }

 

  SYSCALL_DEFINE2根据不同的call来进入不同的分支,从而调用不同的内核处理函数,如__sys_bind, __sys_listen等等内核处理函数。

  在SYSCALL_DEFINE2调用的与socket相关的函数们都打上断点,再进行调试
技术图片
技术图片
  在Qemu中输入replyhi,发现gdb捕获到以上断点,一直到sys_accept4函数停止。说明此时服务器处于阻塞状态,一直在等待客户端连接,在Qemu中输入hello,同样捕获断点,可以看到客户端发起连接,发送接收数据,整个过程与TCP socket通信流程完全相同,至此,整个追踪过程结束。

以上是关于Socket与内核调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析