zoukankan      html  css  js  c++  java
  • [置顶] Linux协议栈代码阅读笔记(一)

    Linux协议栈代码阅读笔记(一)
    (基于linux-2.6.21.7)

    (一)用户态通过诸如下面的C库函数访问协议栈服务

    int socket(int domain, int type, int protocol);
    int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
    int connect(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
    ……

    (二)上述C库函数如何与内核交互
    C库代码准备好相应的工作后(例如,设置系统调用号啦、参数构造啦、栈啦、寄存器设置啦),通过系统调用指令,进入内核态。从内核返回后,C库函数再做相应的善后工作,然后将结果返回给用户程序。

    这部分代码,不同架构的处理器,有不同的实现。
    可以参考Glibc的源码。
    下面以X86为例,简要描述一下这个过程。
    另外,后续的内容,如无特殊说明,均是针对X86架构。

    对于X86架构,一般是通过“int  $0x80”指令进入内核,即触发128号中断。
    内核中断向量表的定义如下(源码文件archi386kernel Traps.c):

    struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };

    函数trap_init(源码文件archi386kernel Traps.c)对此表进行了初始化。
    其中,对128号中断的初始化方式为:
    set_system_gate(SYSCALL_VECTOR,&system_call);
    SYSCALL_VECTOR宏的值为0x80,即128。

    因此,128号中断,即对应中断向量表的第128个条目,其中断服务程序为system_call这段代码。
    system_call这段代码,是用汇编实现的。
    其代码在archi386kernelentry.S中。
    这个代码,主要是根据系统调用号,索引系统调用表中的一个条目进行执行。

    (三)内核态如何处理用户的网络通讯请求
    上一步,C库发起了系统调用,进入了内核128号中断,即系统调用软中断。
    128号中断处理程序,根据系统调用号,进入系统调用表的相应表目。系统调用表如下,每个表目是一个函数指针。(源码文件:archi386kernel syscall_table.S)

    ENTRY(sys_call_table)
     .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
     .long sys_exit
     .long sys_fork
     .long sys_read
     .long sys_write
     .long sys_open  /* 5 */
     .long sys_close
     .long sys_waitpid
     .long sys_creat
     .long sys_link
     .long sys_unlink /* 10 */
     .long sys_ni_syscall /* old lock syscall holder */
     …
     .long sys_statfs
     .long sys_fstatfs /* 100 */
     .long sys_ioperm
    .long sys_socketcall
     .long sys_syslog
     …
     .long sys_tee   /* 315 */
     .long sys_vmsplice
     .long sys_move_pages
     .long sys_getcpu
     .long sys_epoll_pwait

    对于上述的几个socket库函数,全部对应同一个系统调用,即102号系统调用,即sys_socketcall函数。

    sys_socketcall函数如何处理用户的socket请求
    所有的socket相关的C库函数,如socket、bind、connect、listen、accept、send、recv、sendto、sendmsg等,全部都属于同一个系统调用(即102号系统调用),全部由这一个函数处理。
    此函数的代大致如下(源码文件netSocket.c)
    long sys_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_accept(a0, (struct sockaddr __user *)a1,
              (int __user *)a[2]);
      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_send(a0, (void __user *)a1, a[2], a[3]);
      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_recv(a0, (void __user *)a1, a[2], a[3]);
      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 msghdr __user *)a1, a[2]);
      break;
     case SYS_RECVMSG:
      err = sys_recvmsg(a0, (struct msghdr __user *)a1, a[2]);
      break;
     default:
      err = -EINVAL;
      break;
     }
     return err;
    }

    (四)socket的创建
    使用上述C库函数,第一步当然是使用socket库函数创建一个socket。后续的操作,则都是针对这一步创建出的socket而进行了。因此,我们先来看看socket的创建。
    创建socket,主要就是分配一个struct socket结构变量,并适当的初始化。
    这个工作由sys_socket 通过如下形式调用sock_create完成。其中的参数family、type、protocol都是用户调用socket库函数时传入的。

    sock_create(family, type, protocol, &sock);

    sock_create成功返回后,就创建了一个struct socket结构变量。
    后续的数据收发,状态维护,差不多都基于这个结构变量了。

    struct socket结构如下(源码文件:netSocket.c)
    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;
    };

    这个结构的初始化,主要依赖于family, type, protocol这三项信息,查找到相应的协议栈模块,填充struct socket结构中相应的成员。
    这个初始化过程的层次比较深,涉及较多细节。在下也没有深入阅读理解。

    不过,我们可以简单看看大的数据结构。
    内核中的协议栈,也是按family, type, protocol这三项信息进行了组织。

    a) 固定的协议信息(netipv4 Af_inet.c)
    Socket的创建,需要根据family, type, protocol确定一个协议。
    内核中固定的协议信息,都在inetsw_array中进行了登记。

    Static  struct  inet_protosw  inetsw_array[] =
    {
     {
      .type =       SOCK_STREAM,
      .protocol =   IPPROTO_TCP,
      .prot =       &tcp_prot,
      .ops =        &inet_stream_ops,
      .capability = -1,
      .no_check =   0,
      .flags =      INET_PROTOSW_PERMANENT |
             INET_PROTOSW_ICSK,
     },

     {
      .type =       SOCK_DGRAM,
      .protocol =   IPPROTO_UDP,
      .prot =       &udp_prot,
      .ops =        &inet_dgram_ops,
      .capability = -1,
      .no_check =   UDP_CSUM_DEFAULT,
      .flags =      INET_PROTOSW_PERMANENT,
           },


           {
            .type =       SOCK_RAW,
            .protocol =   IPPROTO_IP, /* wild card */
            .prot =       &raw_prot,
            .ops =        &inet_sockraw_ops,
            .capability = CAP_NET_RAW,
            .no_check =   UDP_CSUM_DEFAULT,
            .flags =      INET_PROTOSW_REUSE,
           }
    };

    数组中的每个元素,对应一个协议。
    其中,每个元素的prot成员,指向相应的协议(如TCP、UDP等)提供的proto结构变量,其中含有大量的函数指针,指向相应的函数,这些函数用于实现各种协议的交互、收发、控制等。这样一来,每一个协议,在这个数组中都能查到了,如何操作使用他们,也都有了相应的信息。
    每个协议的ops成员,也指向一个proto_ops结构变量,其中也包含大量函数指针。这些操作,可以认为是包装后的,更抽象的操作。是更接近用户的socket操作函数。例如,这些操作函数包括:bind、connect、listen等。
    具体包含哪些操作,是由family, type决定的(例如,family=PF_INET,type=SOCK_DGRAM时,则使用sendmsg接收数据)。对于多个协议,即使实现不同,但是如果他们的family, type相同,那么对于用户来说,操作都是一样的。
    初始化完成后,最终的情况是:

    a)    socket.sk.__sk_common.skc_prot指向具体的协议提供的proto结构变量。内含大量函数,实现具体的协议操作。
    b)    socket. ops 指向相应的proto_ops结构变量,实现各种socket操作。
    c)   最终的流程是:socket. ops包装了socket操作,但是socket. ops中的函数是利用socket.sk.__sk_common.skc_prot中的函数完成最终的操作。

    最后,inetsw_array中的元素(协议),不是遍历查找的。他们被按照type分类组织到中inetsw了。Inetsw包含了PF_INET协议族中的全部协议。
    Inetsw是个链表数组,定义如下(源码文件netipv4 Af_inet.c)。
    static  struct  list_head  inetsw[SOCK_MAX];

    (五)使用socket进行收发
    上一步已经完成了相关的初始化工作。
    后续的建链、收发、断链等操作,也还都是由socketcall这一个函数完成的。
    有兴趣的朋友可以自己研习研习相关的代码了。
    在下对这方面也没有深入阅读理解:)

  • 相关阅读:
    Java的synchronized的同步代码块和同步方法的区别
    关于java字节码框架ASM的学习
    说说cglib动态代理
    Java虚拟机-JVM各种参数配置大全详细
    Permanent Space 和 Heap Space
    OOM三种情况
    Java性能调优(一):调优的流程和程序性能分析
    数学之路-python计算实战(17)-机器视觉-滤波去噪(中值滤波)
    使用 Pascal 脚本编写网页, PWP 项目
    android 4.0主线程訪问网络问题
  • 原文地址:https://www.cnblogs.com/suncoolcat/p/3343476.html
Copyright © 2011-2022 走看看