TCP协议的初始化及socket创建TCP套接字描述符

1Socket与TCP/IP协议的关系

  Socket并不是TCP/IP协议的一部份,从广义上来讲,socket是Unix/Linux抽像的进程间通讯的一种方法
网络socket通讯仅仅是其若干协议中的一类,而tcp/ip又是网络协议各类中的一种
从tcp/ip的角度看socket,它更多地体现了用户API与协议栈的一个中间层接口层
用户通过调用socket API将报文递交给协议栈,或者从协议栈中接收报文,换句话说,socket是对TCP/IP协议族的封装,并对网络编程开发人员提供可用的接口。

2、Socket原理

  socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

TCP协议的初始化及socket创建TCP套接字描述符_第1张图片

socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的

TCP协议的初始化及socket创建TCP套接字描述符_第2张图片

 

  服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket

  服务器为socket绑定ip地址和端口号

  服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开

  客户端创建socket

  客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket

  服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求

  客户端连接成功,向服务器发送连接状态信息

  服务器accept方法返回,连接成功

  客户端向socket写入信息

  服务器读取信息

  客户端关闭

  服务器端关闭

 3、TCP

TCP/IP协议中的三次握手,TCP协议通过三次握手建立一个可靠的连接。

TCP协议的初始化及socket创建TCP套接字描述符_第3张图片

 

第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

定睛一看,服务器socket与客户端socket建立连接的部分其实就是大名鼎鼎的三次握手

TCP协议的初始化及socket创建TCP套接字描述符_第4张图片

 

TCP编程的服务器端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();
  4、开启监听,用函数listen();
  5、接收客户端上来的连接,用函数accept();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接;
  8、关闭监听;

TCP编程的客户端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt();* 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
  4、设置要连接的对方的IP地址和端口等属性;
  5、连接服务器,用函数connect();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接;

 4实验过程及其分析

  这是紧接着上次的实验进行。

  进入到~/LinuxKernel/menu文件夹下输入以下命令

qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S

 由上节所知,socket()函数、bind()函数、accept()函数等都是调用sys_socketcall()这个函数,首先在内核源码目录/net/socket.c中找到sys_socketcall()这个函数。

TCP协议的初始化及socket创建TCP套接字描述符_第5张图片

 

 首先我们在__sys_socket()这个函数打上断点

TCP协议的初始化及socket创建TCP套接字描述符_第6张图片

 

TCP协议的初始化及socket创建TCP套接字描述符_第7张图片

 

 其三个参数分别表示协议族,协议类型(面向连接或无连接)以及协议

 

TCP协议的初始化及socket创建TCP套接字描述符_第8张图片

 TCP协议的初始化及socket创建TCP套接字描述符_第9张图片

 

 

对于用户态而言, 一个Socket, 就是一个特殊的已经打开的文件,为了对socket抽像出文件的概念,
内核中为socket定义了一个专门的文件系统类型sockfs.

static struct vfsmount *sock_mnt __read_mostly;
static struct file_system_type sock_fs_type = 
{                                                                         
      .name =     "sockfs",
     .get_sb =   sockfs_get_sb,
      .kill_sb =    kill_anon_super,
 };
在模块初始化的时候,安装该文件系统:  
void __init sock_init(void) 
{ 
        …… 
        register_filesystem(&sock_fs_type); 
        sock_mnt = kern_mount(&sock_fs_type);         
}

 有了文件系统后,对内核而言,创建一个socket,就是在sockfs文件系统中创建一个文件节点(inode),并建立起为了实现
socket功能所需的一整套数据结构,包括struct inode和struct socket结构.
struct socket结构在内核中,就代表了一个"Socket",当一个struct socket数据结构被分配空间后,再将其与一个已打开
的文件“建立映射关系”.这样,用户态就可以用抽像的文件的概念来操作socket了

文件系统struct vfsmount中有一个成员指针mnt_sb指向该文件系统的超级块,而超级块结构struct super_lock
有一个重要的成员s_op指向了超级块的操作函数表,其中有函数指针alloc_inode()即为在给定的超级块下创建并初始化
一个新的索引节点对像. 也就是调用:
sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);
当然,连同相关的处理细节一起,这一操作被层层封装至一个上层函数new_inode()

那如何分配一个struct socket结构呢?

内核引入了另一个socket_alloc结构
struct socket_alloc { 
        struct socket socket; 
        struct inode vfs_inode; 
};

显而易见,该结构实现了inode和socket的封装.已知一个inode可以通过宏SOCKET_I来获取
与之对应的 socket:
sock = SOCKET_I(inode); 
static inline struct socket *SOCKET_I(struct inode *inode) 

        return &container_of(inode, struct socket_alloc, vfs_inode)->socket; 
}
但是这样做也同时意味着在分配一个inode后,必须再分配一个socket_alloc结构,并实现对应的封装。

struct vfsmount *kern_mount(struct file_system_type *type)
 {
     return vfs_kern_mount(type, 0, type->name, NULL);
 }

 struct vfsmount *
 vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
 {
     struct vfsmount *mnt;
     char *secdata = NULL;
     int error;
       ....
     mnt = alloc_vfsmnt(name);
     if (!mnt)
         goto out;
     ....
     error = type->get_sb(type, flags, name, data, mnt);
     if (error < 0)
         goto out_free_secdata;
 
     mnt->mnt_mountpoint = mnt->mnt_root;
     mnt->mnt_parent = mnt;
     up_write(&mnt->mnt_sb->s_umount);
     free_secdata(secdata);
     return mnt;
     .....
}

申请文件系统mnt结构, 调用之前注册的sock_fs_type的get_sb成员函数指针, 获取相应的超级块sb. 
并将mnt->mnt_sb指向sock_fs_type中的超级块

static int sockfs_get_sb(struct file_system_type *fs_type,
     int flags, const char *dev_name, void *data, struct vfsmount *mnt)
 {
    return get_sb_pseudo(fs_type, "socket:", &sockfs_ops, SOCKFS_MAGIC,                                                 
                  mnt);
 }

注意其第三个参数 sockfs_ops,它封装了 sockfs 的功能函数表

static struct super_operations sockfs_ops = {
     .alloc_inode =  sock_alloc_inode,
     .destroy_inode =sock_destroy_inode,
    .statfs =   simple_statfs,
 };
struct super_block * 
get_sb_pseudo(struct file_system_type *fs_type, char *name, 
        struct super_operations *ops, unsigned long magic) 

        struct super_block *s = sget(fs_type, NULL, set_anon_super, NULL); 
                …… 
        s->s_op = ops ? ops : &default_ops; 
}

这里就是先获取/分配一个超级块,然后初始化超级块的各成员,包括s_op,它封装了对应的功能函数表.
s_op自然就指向了sockfs_ops,那前面提到的new_inode()函数分配inode时调用的
sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);
这个alloc_inode函数指针也就是sockfs_ops的sock_alloc_inode()函数——转了一大圈,终于指到它了
.
来看看sock_alloc_inode是如何分配一个inode节点的

static kmem_cache_t * sock_inode_cachep __read_mostly;
 
 static struct inode *sock_alloc_inode(struct super_block *sb)
 {
     struct socket_alloc *ei;
     ei = (struct socket_alloc *)kmem_cache_alloc(sock_inode_cachep, SLAB_KERNEL);                                       
     if (!ei)
         return NULL;
     init_waitqueue_head(&ei->socket.wait);
     
     ei->socket.fasync_list = NULL;
     ei->socket.state = SS_UNCONNECTED;
     ei->socket.flags = 0;
     ei->socket.ops = NULL;
     ei->socket.sk = NULL;
     ei->socket.file = NULL;
     ei->socket.flags = 0;
 
     return &ei->vfs_inode;
 }

函数先分配了一个用于封装socket和inode的 ei,然后在高速缓存中为之申请了一块空间.
这样inode和socket就同时都被分配了,接下来初始化socket的各个成员,这些成员在后面都会一一提到

struct socket {
     socket_state        state;
     unsigned long       flags;
     const struct proto_ops  *ops;
     struct fasync_struct    *fasync_list;
     struct file     *file;
     struct sock     *sk;                                                                                                 
    wait_queue_head_t   wait;
     short           type;
};

至目前为止,分配inode,socket以及两者如何关联,都已一一分析了.
最后一个关键问题,就是如何把socket与一个已打开的文件,建立映射关系

在内核中,用struct file结构描述一个已经打开的文件,指向该结构的指针内核中通常用file或filp来描述.
我们知道,内核中可以通过全局项current来获得当前进程,它是一个struct task_struct类型的指针.
tastk_struct有一个成员: 
struct files_struct *files;指向一个已打开的文件.
当然,由于一个进程可能打开多个文件,所以,struct files_struct 结构有
struct file *fd_array[NR_OPEN_DEFAULT]成员,
这是个数组,以文件描述符为下标,即current->files->fd[fd],可以找到与当前进程指定文件描述符的文件

有了这些基础,如果要把一个socket与一个已打开的文件建立映射,首先要做的就是为socket分配一个struct file,
并申请分配一个相应的文件描述符fd. 因为socket并不支持open方法,所以不能期望用户界面通过调用open() API
来分配一个struct file,而是通过调用get_empty_filp来获取

struct file *file = get_empty_filp()
fd = get_unused_fd();获取一个空间的文件描述符

然后让current的files指针的fd数组的fd索引项指向该file

void fastcall fd_install(unsigned int fd, struct file * file) 
{ 
        struct files_struct *files = current->files; 
        spin_lock(&files->file_lock); 
        if (unlikely(files->fd[fd] != NULL)) 
                BUG(); 
        files->fd[fd] = file; 
        spin_unlock(&files->file_lock); 
}

做到这一步,有了一个文件描述符fd和一个打开的文件file,它们与当前进程相连,但是好像与创建的socket并无任何瓜葛.
要做的映射还是没有进展,struct file或者文件描述述fd或current都没有任何能够与 inode或者是socket相关的东东
这需要一个中间的桥梁,目录项:struct dentry结构

因为一个文件都有与其对应的目录项:
struct file { 
        struct list_head        f_list; 
        struct dentry          *f_dentry; 
        …… 
而一个目录项:
struct dentry { 
        …… 
        struct inode *d_inode;                /* Where the name belongs to - NULL is negative */ 

d_inode 成员指向了与之对应的 inode节点

之前已经创建了一个inode节点和与之对应的 socket. 所以现在要做的就是:
“先为当前文件分配一个对应的目录项,再将已创建的 inode节点安装至该目录项” 
这样一个完成的映射关系:
进程,文件描述符,打开文件,目录项,inode节点,socket就完整地串起来了

基本要分析的一些前导的东东都一一罗列了,虽然已尽量避免陷入文件系统的细节分析,但是还是不可避免地进入其中,
因为它们关系实现太紧密了,现在可以来看套接字的创建过程了 

static struct list_head inetsw[SOCK_MAX]; 
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是一个数组,其每一个元素都是一个链表首部,前面一个循环初始化之.后一个循环就值得注意了,
也就是函数

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;
  
      spin_lock_bh(&inetsw_lock);
      if (p->type >= SOCK_MAX)
          goto out_illegal;
      /* If we are trying to override a permanent protocol, bail. */
      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;
      if (answer)
          goto out_permanent;
  
      /* Add the new entry after the last permanent entry if any, so that
       * the new entry does not override a permanent entry when matched with
       * a wild-card protocol. But it is allowed to override any existing
       * non-permanent entry.  This means that when we remove this entry, the 
       * system automatically returns to the old behavior.
       */
      list_add_rcu(&p->list, last_perm);
  out:
      spin_unlock_bh(&inetsw_lock);
  
      synchronize_net();
  
      return;

这个函数完成的工作就是把inetsw_array数组中相同的协议类型下边的协议,
加入到inetsw对应的协议类型的链表中去,因为事实上一对一的关系,所以这个函数要简单得多
因为不存在其它成员,所以每一次list_entry都为空值,所以不存在覆盖和追加的情况,直接调用
list_add_rcu(&p->list, last_perm);
把协议类型节点(struct inet_protosw类型的数组的某个元素)添加到链表(链表首部本身是一个数组,
数组索引是协议对应的协议类型的值)的第一个成员
.

回到inet_create中

static int inet_create(struct socket *sock, int protocol)
  {
      struct sock *sk;
      struct list_head *p;
      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;
socket的初始状态设置为“未连接”,这意味着面向连接的协议类型,如 tcp,在使用之前必须建立连接, 修改状态位.

  
      /* Look for the requested type/protocol pair. */
      answer = NULL;
  lookup_protocol:
      err = -ESOCKTNOSUPPORT;
      rcu_read_lock();
      list_for_each_rcu(p, &inetsw[sock->type]) {
          answer = list_entry(p, struct inet_protosw, list);
  
          /* 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;
          answer = NULL;
      }             

这个循环根据socket(2)调用的protocol把之前在链表中注册的协议节点找出来.
一个问题是,因为一一对应关系的存在,用户态调用socket(2)的时候,常常第三个参数直接就置 0 了.
也就是这里protocol为 0.那内核又如何处理这一默认值呢?
也就是protocol != answer->protocol,而是被if (IPPROTO_IP == protocol) 所匹配了.
这样将protocol置为链表中第一个协议,而当循环结束时,answer自然也是指向这个链表中的第一个注册节点.
假设SOCK_STREAM下同时注册了TCP和123,那么这里默认就取TCP了.当然如果把123在inetsw_array数组中的
位置调前,那么就 默认取123了.

将创建的socket的ops函数指针集指向具体协议类型的.例如创建的是SOCK_STREAM,
 
那么就指向了inet_stream_ops. 

 每一个 Socket 套接字,
都有一个对应的struct socket结构来描述(内核中一般使用名称为sock),但是同时又有一个
struct sock结构(内核中一般使用名称为 sk).两者之间是一一对应的关系.

 socket结构和sock结构实际上是同一个事物的两个方面.不妨说socket结构是面向进程和系统调用界面的侧面,
而sock结构则是面向底层驱动程序的侧面.设计者把socket套接字中与文件系统关系比较密切的那一部份放在
socket结构中而把与通信关系比较密切的那一部份,则单独成为一个数结结构,那就是sock结构.
由于这两部份逻辑上本来就是一体的,所以要通过指针互相指向对方形成一对一的关系
.

inet_create()运行完,一个socket套接字基本上就创建完毕了,剩下的就是与文件系统挂钩,回到最初的sys_socket()
函数中来,它在调用完sock_create()后,紧接着调用sock_map_fd()函数

 TCP协议的初始化及socket创建TCP套接字描述符_第10张图片

 

TCP协议的初始化及socket创建TCP套接字描述符_第11张图片

 

 TCP协议的初始化及socket创建TCP套接字描述符_第12张图片

 这个函数的核心思想在一开始就已经分析过了.从进程的角度来讲一个socket套接字就是一个特殊的已打开的文件.
前面分配好一个socket 后,这里要做的就是将它与文件系统拉上亲戚关系.
首先获取一个空闲的文件描述符号和file结构,然后在文件系统中分配一个目录项(d_alloc),使其指向已经分配的inode节
点(d_add),然后把其目录项挂在sockfs文件系统的根目录之下,并且把目录项的指针d_op设置成

指向sockfs_dentry_operati,这个数据结构通过函数指针提供他与文件路径有关的操作.

static struct dentry_operations sockfs_dentry_operations = { 
        .d_delete =        sockfs_delete_dentry, 
};

最后一步就是将file结构中的f_op和sock结构中的i_fop都指向socket_file_ops,它是一个函数指针集,
指向了socket面向文件系统的用户态调用的一些接口函数
.

static 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, 
        .mmap =                sock_mmap, 
        .open =                sock_no_open,        /* special open code to disallow open via /proc */ 
        .release =        sock_close, 
        .fasync =        sock_fasync, 
        .readv =        sock_readv, 
        .writev =        sock_writev, 
        .sendpage =        sock_sendpage 
};

到这里整个socket套接字的创建工作就宣告完成了

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(TCP协议的初始化及socket创建TCP套接字描述符)