我要投搞

标签云

收藏小站

爱尚经典语录、名言、句子、散文、日志、唯美图片

当前位置:双彩网 > 执行例程 >

socket网络实现

归档日期:07-05       文本归类:执行例程      文章编辑:爱尚语录

  系统调用的协议无关代码包含几十行代码,它们验证第一个参数是一个有效的socket描述符,并且第二个参数是一个进程中的有效指针。然后调用下层的协议相关代码,协议相关代码可能包含几百行代码。

  协议层包括我们提到的四种协议簇(TCP/IP,XNS,OSI和Unix域)的实现。

  输入处理与输出处理不同,因为输入处理是异步的。就是说,它是通过一个接收完成中断驱动以太网设备程序来接收一个输入分组,而不是通过进程的系统调用。内核处理这个设备中断,并调度设备驱动程序进入运行状态。

  假定它表示一个正常的接收已完成,数据从以太网设备读取到一个mbuf链表中。设备驱动程序把mbuf传给一个通用以太网输入例程,它通过以太网帧中的类型字段来确定哪个协议层接收此分组。

  当接口层在系统的一个接口上收到一个IP数据报时,它就设置这个软中断。当IP输入例程执行它时,循环处理在它的输入队列中的每一个IP数据报,并在整个队列被处理完后返回。

  UDP输入例程验证UDP首部中的各字段(长度与可选的校验和),然后确定是否一个进程应噶接收次数据报。

  UDP输入例程从一个全局变量udb开始,查看所有UDP协议控制块链表PCB,寻找一个本地端口号与接收的UDP数据报的目标端口号相匹配的协议块。(这个PCB是由我们调用socket()创建的,它的成员inp_socket指向相应socket接收,并允许接收的数据在此socket排队).

  因为这个UDP数据报要传送给我们的进程,发送方的IP地址和UDP端口号放置到一个mbuf中,这个mbuf和数据被追加到此socket的接收队列中。

  最后,接收进程被唤醒。如果进程处于睡眠状态等待数据的到达,进程将标志为可运行状态等待内核的调度。也可以通过select系统调用或SIGIO信号来通知进程数据的到达。

  进程可以调用socket 的输入函数将mbuf从socket的接收队列复制到我们程序的缓存中。

  在BSD联网代码设计中的一个基本概念就是存储器缓存,称作为一个mbuf(memory buffer),在整个联网代码中用于存储各种信息。

  网络协议对内核的存储器管理能力提出了很多要求。这些要求包括能方便地操作可变长缓存,能在缓存头部和尾部添加数据(如底层封装来自高层的数据),能从缓存中移去数据(如,当数据分组向上经过协议栈时要去掉首部),并尽量减少为这些操作所做的数据复制。内核中的存储器管理调度直接关系到联网协议的性能。

  mbuf的主要用途是保存在进程和网络接口间互相传递的用户数据。但mbuf也用于保存其它各种数据:源与目的地址、Socket选项等等。

  例如,当Socket层请求分配一个mbuf来存储sendto系统调用的目的地址时,它指定M_WAIT,因为在此阻塞是没有问题的。但是当以太网设备驱动程序请求分配一个mbuf来存储一个接收的帧时,它指定M_DONTWAIT,因为它是作为一个设备中断处理来执行的,不能进入睡眠状态来等待一个mbuf。在这种情况下,若存储器不可用,设备驱动程序丢弃这个帧比较好。

  所有的操作系统都提供服务访问点,程序可以通过它们请求内核中的服务。各种UNIX都提供精心定义的有限个内核入口点,即系统调用。我们不能改变系统调用,除非我们有内核的源代码。

  在各种Unix系统中,每个系统调用在标准C函数库中都有一个相同名字的函数。一个应用程序用标准C的调用序列来调用此函数。这个函数再调用相应的内核服务,所使用的技术依赖于所在的系统。例如,函数可能把一个或多个C参数放到通用寄存器中,并执行几条机器指令产生一个软件中断进入内核。对我们来说,我们可以把系统调用看成C函数。

  在BSD内核中,每一个系统调用均被编号,当进程执行一个系统调用时,硬件被配置成仅传送控制给一个内核函数,即将CPU的使用权转给一个内核函数。将标志系统调用的整数作为参数传送给此内核函数。在i386实现中,此内核函数为syscall(),syscall()利用系统调用的编号在系统调用表中找到请求的系统调用的sysent结构.表中的每一单元均为一个sysent结构。

  例如,recvmsg系统调用在系统调用表中的第27个项,它有2个参数,利用内核中的recvmsg函数实现。

  syscall()负责将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结果。然后,当系统调用执行完成后,syscall将结果返回给进程。syscall将控制交给鱼系统调用相对应的内核函数。

  这里指针callp指向相关的sysent结构;指针p指向调用系统调用的进程的进程表项;args作为参数传给系统调用,它是一个32bit长的字数组;而rval则是一个用来保存系统调用的返回结果的数组,数组有两个元素,每一个元素是一个32bit长的字。当我们用系统调用这个词时,我们指的是被syscall调用的内核中的函数,而是不是应用调用的进程中的函数。

  syscall期望系统调用函数(即sy_call指向的函数)在没有差错时返回0,否则返回非0的差错代码。如果没有差错出现,内核将rval中的值作为系统调用(应用调用的)返回值传送给进程。如果有差错,syscall忽略rval中的值,并以与机器相关的方式返回差错代码给进程,使得进程能从外部变量errno中得到差错代码。应用调用的函数则返回-1或一个空指针表示应用应该查看errno获得差错信息。

  当一个应用调用socket时,进程用系统调用机制将三个独立的整数传给内核。syscall将参数复制到32bit值的数组中,并将数组指针作为第二个参数传给socket的内核版。内核版的socket将第二个参数作为指向socket_args结构的指针。下图描述了上述过程:

  同socket类似,(在i386实现中)每一个实现系统调用的内核函数将args说明称一个与系统调用有关的结构指针,而不是一个指向32bit的子的数组的指针。

  syscall在执行内核系统调用函数之前将返回值设置为0.如果没有差错出现,系统调用函数直接返回而不需要清楚*tetvall,syscall返回0给进程。

  Unix系统中的Socket I/O遵循其一切皆文件的思想,因而可以使用统一的方式对Socket 进行I/O操作。

  socket()的返回值是一个文件描述符,它具有其它Unix文件描述符的所有特性:可以用这个描述符调用read()和write();可以用dup()复制它,在调用了fork()之后,父进程和子进程可以共享它;可以用fcntl()来改变他的属性,可以调用close()来关闭它,等的。

  一个文件描述符是进程对应的进程表项中的一个数组的下标.这个数组项是一个指向打开文件表结构的指针。

  实现系统调用的函数的第一个参数总为p,即指向调用进程的proc结构的指针。内核利用proc结构体记录进程的有关信息。在proc结构体中,p_fd指向filedesc结构,该结构的主要功能是管理fd_ofiles指向的描述符表。描述符表的大小是动态变化的,由一个指向file结构的指针数组组成。每一个file结构体描述一个打开的文件,该结构体可被多个进程共享。

  f_data指向相关I/O对象的专用数据。对于socket而言,f_data指向与描述符相关的socket结构。最后,socket结构中的so_proto指向产生socket时选中的协议的protosw结构。回想一下,每一个protosw结构是由与该协议关联的所有socket共享的。

  Socket代表一条通信链路的一端,存储或指向与链路有关的所有信息。这些信息包括:使用的协议、协议的状态信息(包括源地址和目的地址)、到达的连接队列、数据缓存和可选标识。

  so_type由产生Socket的进程来指定,它指明Socket和相关协议支持的通信语义。

  so_linger表示当关闭一条连接时Socket继续发送数据的时间间隔(单位为一个时钟滴答)。

  例如,当一个进程对一个Socket进行read()系统调用,如果当前没有网络上来的数据,则read系统调用就会被阻塞。同样,当一个进程对一个Socket进行write()系统调用,如果内核中没有缓存来存储发送的数据,则内核将阻塞进程。

  如果设置了SS_NBIO,在对Socket执行I/O操作且请求的资源不能得到时,内核并不阻塞进程,而是返回EWOULDBLOCK.

  如果设置了SS_ASYNC,当因为下列情况之一而使Socket状态发生变化时,内核发送SIGIO信号给so_pgid字段标识的进程或进程组:

  so_pcb指向协议控制块,协议控制块包含与协议有关的状态信息和Socket参数。

  每一种协议都定义了自己的协议控制块结构,因此so_pcb被定义成一个通用的指针。

  so_q0表示还没有完全建立的连接,例如TCP的三次握手还没有完成。队列的长度由so_q0len字段表示

  so_q表示已经建立的,但未被应用层接受的连接,例如TCP的三次握手已经完成。队列的长度由so_qlen字段表示。

  so_error保存错误代码,直到在应引用该socket的下一个系统调用期间错误码能送给应用层

  每一个socket包括两个数据缓存,输入缓存so_rcv和输出缓存so_snd。分别用来缓存接受或发送的数据。

  socket系统调用产生一个新的socket,并将socket同进程在参数domain、type和protocol中指定的协议联系起来。该函数分配一个新的描述符,用来在后续的系统调用中标志socket,并将描述符返回给进程。

  getsock的功能是将一个文件描述符映射到一个文件表项中,即根据一个文件描述符找到起对应的文件表项。。

  getsock函数利用fdp查找文件描述符fdes指定的文件表项,fdp是指向filedesc结构的指针。getsock将打开的文件结构指针赋给fpp,并返回,或者当出现下列情况时返回错误代码:

  sockargs将进程传给系统调用的参数的指针从进程复制到内核而不是复制指向的数据,这样做是因为每一个参数的语义只有相对应的系统调用才知道,而不是针对所有的系统调用。多个系统调用在调用sockargs复制参数指针后,将指针指向的数据从进程复制到内核中新分配的mbuf中。

  例如,sockargs将bind的第二个参数指向的本地socket地址从进程复制到一个mbuf中。

  一般来说,作为客户(client)的进程并不关心它的本地地址是什么。在这种情况下,进程在进行通信之前没有必要调用bind(),内核会自动为其选择一个本地地址。

  但是,服务器进程则总是需要绑定到一个已知的地址。所以,进程在接受TCP连接或接收UDP数据报之前必须调用bind(),因为客户进程需要同已知的地址建立连接或发送数据报到已知的地址。

  listen()系统调用的功能是通知协议进程准备接收socket上的连接请求,并同时指定socket上可以排队等待的连接数的门限制。超过门限制时,socket层将拒绝进入的连接请求排队等待。当这种情况出现时,TCP将忽略进入的连接请求。进程可以通过调用accept来得到队列中的连接。

  当一个在内核中执行的进程得不到内核资源而不能继续执行时,它就调用tsleep等待。tsleep的原型是:

  tsleep()的第一个参数chan,被称之为等待通道。它标志进程等待的特定资源或事件。许多进程能同时在一个等待通道上休眠。

  当资源可用或事件出现时,内核调用wakeup,并将等待通道作为唯一的参数传入。wakeup的原型是:

  所有等待在该通道上的进程均被唤醒,并被设置成运行状态。当每一个进程均恢复执行时,内核安排tsleep返回。

  因为所有等待在同一个等待通道上的进程均被wakeup()唤醒,所以我们总是看到在一个循环中调用tsleep。每一个被唤醒的进程在继续执行之前必须检查等待的资源是否可得到,因为另一个被唤醒的进程可能已经先一步的到了资源。如果仍然得不到资源,进程再调用tsleep等待。

  多个进程在同一个等待通道上休眠的例子是:让多个服务器进程读同一个UDP Socket,每一个服务器都调用recvfrom,并且只要没有数据可读就在tsleep中等待。

  当一个数据报到达socket时,socket层调用wakeup(),所有等待进程均被放入运行队列。

  第一个运行的服务器读取了数据报而其它的服务器则没有数据,就会接着调用tsleep.

  通过上述方式,不需要每一个数据报启动一个新的进程,就可将进入的数据报分发到多个服务器。

  这种技术同样可以用来处理TCP的连接请求,只需让多个进程在同一个socket上调用accept().

  调用listen()后,进程调用accept等待连接请求。accept返回一个新的描述符,指向一个连接到客户的新的socket。原来的socket s仍待是为连接的,并准备接收下一个连接。如果name指向一个正确的缓存,accept就会返回对方的地址。

  处理连接的细节由socket相关联的协议来完成。对于TCP而言,当一条连接已经被建立(即三次握手已经完成)时,就通知socket层。对于其它的协议,如OSI的TP4,只要一个连接请求到达,tsleep就返回。当进程通过在socket上发送或接收数据来显式证实连接后,连接则算完成。

  soaccept()函数通过向协议层发送PRU_ACCEPT请求来获得新的连接的客户端地址

  服务器进程调用listen()和accept()系统调用等待远端进程初始化连接。如果进程想自己初始化一条连接(即作为客户端),则调用connect()。

  上图说明connect如何处理面向连接的协议,如TCP。在这种情况下,协议层开始建立连接,调用soisconnecting指示连接将在某个时刻完成。如果socket是非阻塞的,soconnect调用等待连接完成。对于TCP,当三次握手完成时,协议层调用soisconnected将socket标识为已连接,然后调用wakeup唤醒等待的进程,从而完成connect系统调用。

  上图说明connect()如何处理无连接协议,例如UDP。在这种情况下,协议层调用soisconnected()系统调用后立即返回。

  soconnect()函数确保socket处于正确的连接状态。如果socket没有连接或连接没有被挂起,则连接请求总是正确的。如果socket已经连接或连接正等待处理,则新的连接请求将被面向连接的协议,例如TCP,拒绝。对于无连接协议,例如UDP,多个连接是允许的,但是每一个新的请求中的外部地址会取代原来的外部地址。

  对于读通道,shutdown()丢弃所有进程还没有读走的数据以及调用shutdown()之后到达的数据。

  对于写通道,shutdown()使协议作相应的处理。对于TCP,所有剩余的数据将被发送,发送完成后发送FIN。这就是TCP的半关闭特点。

  为了删除socket和释放文件描述符,我们必须调用close()。可以在没有调用shutdown()的情况下,直接调用close()。同所有文件描述符一样,当进程结束时,内核将调用close(),关闭所有还没有被关闭的socket。

  注意⚠️:连接的读通道的关闭完全由socket层来处理,连接的写通道的关闭通过发送PRU_SHUTDOWN请求交由协议处理。TCP协议收到PRU_SHUTDOWN请求后,发送所有排队的数据,然后发送一个FIN来关闭TCP连接的写通道。

  close()系统调用能用来关闭各类描述符。当fd是引用对象的最后的描述符时,与对象有关的close函数被调用:

  soclose函数取消socket上所有未完成的连接(即,还没有完全被进程接受的连接),等待数据被传输到外部系统,释放不需要的数据结构。

  soclose函数取消socket上所有未完成的连接(即,还没有完全被应用层接收的连接),等待数据被传输到外部系统,释放不需要的数据结构。

本文链接:http://guidoon.com/zhixinglicheng/234.html