Socket实现

网络实现架构

4.4BSD通过同时对多种通信协议的支持来提供通用的底层基础服务。4.4BSD支持四种不同的通信协议簇:

  • TCP/IP(互联网协议簇)
  • XNS(Xerox网络系统)
  • OSI协议
  • Unix域协议
    从通信协议是用来在不同的系统之间交换信息的意义上来说,它还不算是一套真正的协议,但它提供了一种进程间通信(IPC)的形式。

4.4BSD内核中的联网代码组织成三层,如下图所示

  • Socket层是一个到下面协议相关层的协议无关层所有系统调用从协议无关的Socket开始
    例如:在Socket层中的bind()系统调用的协议无关代码包含几十行代码,它们验证第一个参数是一个有效的socket描述符,并且第二个参数是一个进程中的有效指针。然后调用下层的协议相关代码,协议相关代码可能包含几百行代码。

  • 协议层包括我们提到的四种协议簇(TCP/IP,XNS,OSI和Unix域)的实现。
    每个协议簇可能包含自己的内部结构。

  • 接口层
    接口层包括同网络设备通信的设备驱动程序。

数据传递

  • Socket层中的每一个Socket都具有一个输入队列和一个输出队列
  • 协议层中的每一个协议都具有一个输入队列和输出队列
  • 接口层中的每个接口(以太网、回环、SLIP、PPP等)都有一个输入队列和输出队列

输入处理

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

接口层-以太网输入

以太网设备驱动程序处理这个中断。

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

协议层——IP输入

IP输入是异步的,并且通过一个软中断来执行。

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

输入层-UDP输入

IP输入历程可能会调用UDP输入例程去处理UDP数据报。

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选项等等。

  • 指针m_nextmbuf连接在一起,把一个分组形成一条mbuf链表。
  • 指针m_nextpkt把多个分组链接成一个mbuf链接成一个mbuf链表队列。在队列的每个分组可以是一个单独的mbuf,也可以是一个mbuf链表。每个分组的第一个mbuf包含一个分组首部。如果多个mbuf定义一个分组,只有第一个mbuf的成员m_nextpkt被使用——链表中其它mbuf的成员m_nextpkt全是空指针。

m_get函数

struct mbuf * m_get(int nowait,int type) { struct mbuf * m; MGET(m,nowait,type); return m; }
  • nowait的值为M_WAITM_DONTWAIT,它取决于在存储器不可用时是否要求等待。
    例如,当Socket层请求分配一个mbuf来存储sendto系统调用的目的地址时,它指定M_WAIT,因为在此阻塞是没有问题的。但是当以太网设备驱动程序请求分配一个mbuf来存储一个接收的帧时,它指定M_DONTWAIT,因为它是作为一个设备中断处理来执行的,不能进入睡眠状态来等待一个mbuf。在这种情况下,若存储器不可用,设备驱动程序丢弃这个帧比较好。

  • type 指定mbuf的类型

系统调用

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

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

从进程到内核的受保护的环境的转换是与机器和实现相关的。

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

struct sysent{
    int sy_narg;  //参数个数 int (*sy_call)();//系统调用的实现函数 };

表中有几个项是从sysent数据中来的,概述组是在kern/init_sysent.c中定义的:

struct sysent sysent[] = {
  {3,recvmsg},    /* 27 = recvmsg */ {3,sendmsg}, /* 28 = sendmsg */ {6,recvfrom}, /* 29 = recvfrom */ {3,accept}, /* 30 = accept */ {3,getpeername},/* 31 = getpeername */ {3,getsockname},/* 32 = getsockname */ };

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

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

在i386实现中,调用有点像:

struct sysent * callp;
error = (*callp->syscall)(p,args,rval);

if(error){
    errno = error;
    return -1; }else{ return (rval); }

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

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

下表介绍了与网络有关的系统调用



举例

socket系统调用的函数原型是:

int socket(int domain,int type,int protocol);

实现socket系统调用的内核函数原型是:

struct socket_args{
  int domain;
  int type; int protocl; }; socket(struct proc * p,struct socket_args * uap,int * retvall);

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

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

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

进程、描述符和插口

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

调用socket()时要求定义socket类型。Internet协议族(PF_INET)和数据报socket(SOCK_DGRAM)组合成一个UDP协议socket。

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

在每个进程的生存期内都会有一个对应的进程表项存在。

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

此打开文件表结构有指向一个描述此文件的i-nodev-node结构

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

通过p->p_fd->fd_ofiles[fd]访问到结构。在file结构中,有两个结构成员是我们感兴趣的:f_opsf_data。I/O系统调用(如read和write)的实现因描述符中的I/O对象类型的不同而不同。f_ops指向fileops结构,该结构包含一张readwriteioctlselectclose系统调用的函数指针表。显示f_ops指向一个全局的fileops结构,即socketops,该结构包含指向socket用的函数的指针。

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

Socket结构

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

struct socket{

    short so_type;//Socket类型,SOCK_STREAM、SOCK_DGRAM或SOCK_RAW short so_options;//Socket行为的标志 short so_linger; short so_state;//Socket状态 caddr_t so_pcb;//协议控制块(Protocol Control Block) struct protosw * sp_proto;//协议处理函数 /**  * Socket连接队列相关  */ struct socket * so_head; struct socket * so_qo; struct socket * so_q; short so_q0len; short so_qlen; short so_qlimit; short so_timeo; u_short so_error; pid_t so_pgid; u_long so_oobmasrk; /**  * Socket缓存相关变量  */ struct sockbuf{ struct mbuf * sb_mb;//mbuf链,用于存储用户数据 u_long sb_cc;//缓存中的实际字节数 u_long sb_hiwat; u_long sb_mbcnt; u_long sb_mbmax;//分配给此socket mbuf缓存的存储器数量的上限。 long sb_lowat; struct selinfo sb_sel; short sb_flags; short sb_timeo;//read/write超时时间 } so_rcv,so_snd; //Socket的输入缓存和输出缓存 caddr_t so_tpcb; void (*so_upcall)(struct socket * so,caddr_t arg,int waitf); caddr_t so_upcallarg; };

通用字段

so_type

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

pr_type协议语义Internet协议
SOCK_STREAM可靠的双向字节流服务TCP
SOCK_DGRAM最好的传输层数据报服务UDP
SOCK_RAW最好的网络层数据报服务ICMP、IGMP、原始IP
SOCK_RDM可靠的数据报服务(未实现)?
SOCK_SEQPACKET可靠的双向记录流服务?

对于UDP,so_type等于SOCK_DGRAM,而对于TCP,so_type等于SOCK_STREAM

so_options

so_options是一组改变Socket行为的标志。

| so_options | 描述 |
| SO_ACCEPTCONN | Socket接收进入的连接(仅用于内核) |
| SO_BROADCAST | Socket能够发送广播报文 |
| SO_DEBUG | Socket记录排错信息 |
| SO_DONTROUTE | 输出操作旁路选路表 |
| SO_KEEPALIVE | Socket查询空闲的连接 |
| SO_OOBINLINE | Socket将带外数据同正常数据存放在一起 |
| SO_REUSEADDR | Socket能重新使用一个本地地址 |
| SO_REUSEPORT | Socket能重新使用一个本地地址和端口 |
| SO_USELOOPBACK | 仅针对选路域Socket,发送进程收到它自己的选路请求 |

so_linger

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

so_state

so_state表示Socket的内部状态和一些其它的特点。

so_state描述
SS_NBIOSocket操作不能阻塞进程
SS_ASYNCSocket应该I/O事件的异步通知
SS_NBIO

默认情况下,进程在发出I/O请求后会等待资源,并阻塞。
例如,当一个进程对一个Socket进行read()系统调用,如果当前没有网络上来的数据,则read系统调用就会被阻塞。同样,当一个进程对一个Socket进行write()系统调用,如果内核中没有缓存来存储发送的数据,则内核将阻塞进程。

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

SS_ASYNC和so_pgid字段

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

  • 连接请求已经完成
  • 断开连接请求已被启动
  • 断开连接请求已经完成
  • 连接的一个通道已被关闭
  • Socket上有数据到达
  • 数据已被发送(即输出缓存中有空闲空间)
  • UDP或TCP Socket上出现了一个异步差错

so_pcb和协议控制块

so_pcb指向协议控制块,协议控制块包含与协议有关的状态信息和Socket参数。
每一种协议都定义了自己的协议控制块结构,因此so_pcb被定义成一个通用的指针。

协议协议控制块
UDPstruct inpcb
TCPstruct inpcb、struct tcpcb
ICMP、IGMP和原始IPstruct inpcb
路由struct rawcb

so_proto

so_proto指向进程在socket()系统调用中选择的协议的protosw结构

连接队列

在so_options字段中设置了SO_ACCEPTCONN标志的socket维护两个连接队列。

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

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

在每一个被排队的socket中,so+heade指向了设置SO_ACCEPTCONN的源socket.

socket上可排队的连接数(so_q和so_q0两个连接队列的总连接数)通过so_qlimit来控制,应用层可以通过listen()系统调用来设置so_qlimit

当下列的不等式成立时,将不再接受任何连接

so_timeo

so_timeo用作accept()connect()close()处理期间的等待通道

等待通道

so_error

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

数据缓存

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

socket系统调用

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

struct socket_args {
  int domain;
  int type; int protocol; }; socket(struct proc * p,struct socket_args * uap,int * retval) { struct filedesc * fdp = p->p_fd;//获取文件描述符 struct socket * so; struct file * fp; int fd,error; if (error = falloc(p,&fp,&fd)){ return (error); }; fp->f_flag = FREAD | FWRITE; fp->f_type = DTYPE_SOCKET; fp->f_ops = &socketops; if(error = socreate(uap->domain,&so,uap->type,uap->protocol)){ fdp->fd_ofiles[fd] = 0; ffree(fp); }else{ fp->f_data = (caddr_t)so; *retval = fd; }; return error; };

falloc分配一个新的file结构和fd_ofiles数组中的一个元素。fp指向新分配的结构,fd则为结构在数组fd_ofiles中的索引。socket将file结构设置成可读、可写,并且作为一个socket。将所有socket共享的全局fileops结构socketopts连接到f_ops指向的file结构中。socketops变量在编译时被初始化。

getsock函数

getsock的功能是将一个文件描述符映射到一个文件表项中,即根据一个文件描述符找到起对应的文件表项。。
getsock函数利用fdp查找文件描述符fdes指定的文件表项,fdp是指向filedesc结构的指针。getsock将打开的文件结构指针赋给fpp,并返回,或者当出现下列情况时返回错误代码:

  • 描述符的值超过了范围而不是指向一个打开的文件
  • 描述符没有同socket建立联系
getsock(struct filedesc * fdp,int fdes,struct file **fpp) { struct file * fp; //文件描述符的值超过了范围。 文件描述符不存在 if((unsigned) fdes >= fdp->fd_nfiles || (fp = fdp->fd_ofiles[fdes] == NULL)){ return (EBADF); } //文件描述符指向的不是socket if(fp->f_type != DTYPE_SOCKET){ return (ENOTSOCK); } *fp = fp; return (0); };

sockargs函数

sockargs将进程传入的参数复制到内核中的一个新分配的mbuf中

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

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

sockargs(struct mbuf **mp,caddr_t buf,int buflen,int type) { struct sockaddr * sa; struct mbuf * m; int error; if((u_int)buflen > MLEN){ return (EINVAL); } m = m_get(M_WAIT,type); if(NULL == m){ return (ENOBUFS); } m->m_len = buflen; //关键代码,实现将进程传入的参数复制到内核中 error = copyin(buf,mtod(m,caddr_t),(u_int)buflen); if(error){ (void)m_free(m); }else{ *mp = m; if(MT_SONAME == type){//如果type等于MT_SONAME,则进程传入的是一个sockaddr结构 sa = mtod(m,struct sockaddr*); sa->sa_len = buflen;//sockargs将刚复制的参数的长度赋给内部长度变量sa_len.这一点确保即使进程没有正确地初始化结构,结构内的大小也是正确的。 } } return (error); };

bind系统调用

bind()系统调用将一个本地的网络传输层地址和socket联系起来。

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

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

socket的外部地址由connect()指定或由允许指定外部地址的写调用,譬如sendto()sendmsg(),指定。

struct bind_args{
    int s;//socket文件描述符 caddr_t name;//包含传输地址的缓存指针 int namelen;//缓存大小 }; bind(struct proc * p,struct bind_args * uap,int * retval) { struct file * fp; struct mbuf * nam; int error; if(error = getsock(p->p_fd,uap->s,&fp)){ return (error); } //将uap参数复制到内核mbuf中 if(error = sockargs(&nam,uap->name,uap->namelen,MT_SONAME)){ return (error); } //sobind将进程指定的地址同socket联系起来。 error = sobind((struct*)fp->f_data,nam); //释放内核中的mbuf m_free(nam); return (error); }

sobind函数

sobind()是一个封装器,它给与Socket相关联的协议层发送PRU_BIND请求

sobind(struct socket so,struct mbuf * nam)
{
    int s = splnet(); int error; error = (*so->so_proto->pr_usrreq)(so,PRU_BIND,(struct mbuf * )0,nam,(struct mbuf*)0); splx(s); return (error); }

listen系统调用

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

struct listen_args{
    int s;
    int backlog; }; listen(struct proc * p,struct listen_args * uap,int * retval) { struct file * fp; int error; if(error = getsock(p->p_fd,uap->s,&fp)){ return (error); } //solisten将请求传递给协议层 return (solisten((struct socket*)fp->f_data,uap->backlog)); }

solisten函数

solisten()是一个封装器,它给与Socket相关联的协议发送PRU_LISTEN请求

solisten(struct socket * so,int backlog)
{
    int s = splnet(); int error; error = (*so->so_proto->pr_usrreq)(so,PRU_LISTEN,(struct mbuf *)0,(struct mbuf*)0,(struct mbuf*)9); if(error){ splx(s); return (error); } if(so->so_q == 0){ so->so_options |= SO_ACCEPTCONN; } if(backlog < 0){ backlog = 0; } so->so_qlimit = min(backlog,SOMAXCONN);//BSD中SOMAXCONN为5 splx(s); return 0; }

tsleep和wakeup函数

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

int tsleep(caddr_t chan,int ppri,char * mesg,int timeo);

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

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

void wakeup(caddr_t chan);

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

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

示例

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

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

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

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

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

accept系统调用

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

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

struct accept_args
{
    int s;
    caddr_t name; int * anamelen; }; accept(struct proc * p,struct accept_args * uap,int * retval) { struct file * fp; struct mbuf * name; int namelen,error,s; struct socket * so; if(uap->name && (error = copyin((caddr_t)uap->anamlen),(caddr_t)&namelen,sizeof(namelen))){ return (error); } if(error = getsock(p->p_fd,uap->s,&fp)){ return (error); } s = splnet(); so = (struct socket *)fp->f_data; //判断此socket是否能够接受连接 if(0 == (SO_ACCEPTCONN & so->so_options)){ splx(s); return (EINVAL); } //如果连接队列为空,且此Socket设置为非阻塞,则立即返回 if((so->so_state && SS_NBIO) && so->so_qlen == 0){ splx(s); return (EWOULDBLOCK); } //如果连接队列为空并且没有错误产生,则等待 while((0 == so->so_qlen) && (0 == so->so_error)){ if(so->so_state && SS_CANTRCVMORE){//如果此socket不能再接收数据,则设置错误代码并跳出等待 so->so_error = ECONNABORTED; break; } if(error = tsleep(caddr_t)&so->so_timeo,PSOCK | PCATCH,netcon,0){ splx(s); return (error); } } if(so->so_error){ error = so->so_error; so->so_error = 0; splx(s); return (error); } //在文件描述符表中添加一个文件描述符 if(error = falloc(p,&fp,retval)){ splx(s); return (error); } //从指定的socket连接队列中获取一个socket { struct socket * aso = so->so_q; if(0 == soqremque(aso,1)){ panic("accept"); } so = aso; } fp->f_type = PTYPE_SOCKET;//将file设置为socket类型中 fp->f_flag = FREAD | FWRITE;//将file设置为可读、可写 fp->f_ops = &socketops;//设置file的系统调用函数表 fp->f_data = (caddr_t)sp;//将socket绑定到f_data中 nam = m_get(M_WAIT,MT_SONAME); //协议处理 (void)soaccept(so,name); if(uap->name){ if(namelen > nam->m_len){ namelen = nam->m_len; } if(0 == (error = copyout(mtod(name,caddr_t),(caddr_t)uap->name,(u_int)namlen))){ error = copyout((caddr_t)*namelen,(caddr_t)uap->anamelen,sizeof(*uap->anamelen)) } } m_freem(nam); splx(s); return (error); }

soaccept函数

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

soaccept(struct socket * so,struct mbuf * nam)
{
    int s = splnet(); int error; if((so->so_state & SS_NOFDREF) == 0){ panic("soaccept: !NOFDREF"); } so->so_state &= ~SS_NOFDREF; error = (*so->so_proto->pr_usrreq)(so,PRU_ACCEPT,(struct mbuf *)0,nam,(struct mbuf*)0); splx(s); return error; }

connect系统调用

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

  • 对于面向连接的协议,例如TCP,connect()系统调用建立一条与指定的外部地址的连接。如果进程没有调用bind()来绑定地址,则内核自动选择并且隐式地绑定一个地址到socket

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

  • 对于无连接的协议,例如UDP或ICMP,connect()系统调用记录外部地址,以便发送数据报时使用。任何以前的外部地址均被新的地址所代替

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

struct connect_args
{
    int s;
    int caddr_t name; int namelen; }; connect(struct proc * p,struct connect_args * uap,int * retval) { struct file * fp; struct socket * so; struct mbuf * nam; int error ,s; if(error = getsock(p->p_fd,uap->s,&fp)){ return error; } so = (struct socket *)fp->f_data; //如果socket是非阻塞并且正在连接,则返回 if((so->so_state & SS_NBIO) && (so->so_state & SS_ISCONNECTING)){ return (EALREADY); } //将进程传递的参数复制到内核区 if(error = sockargs(&nam,uap->nam,uap->namelen,MT_SONAME)){ return (error); } //开始进行连接处理 error = soconnect(so,nam); if(error){ goto bad; } //如果socket是非阻塞并且正在连接,则 if((so->so_state & SS_NBIO) && (so->so_state && SS_ISCONNECTING)){ m_freem(nam); return (EINPROGRESS); } s = splnet(); //如果socket正处于连接状态,则等待连接建立 while((so->so_state & SS_ISCONNECTING) && (0 == so->so_error)){ if(error = tsleep((caddr_t)&so->so_timeo,PSOCK | PCATCH,netcon,0)){ break; } } if(0 == error){ error = so->so_error; so->so_error = 0; } splx(s); bad: //去掉SS_ISCONNECTING标志 so->so_state &= ~SS_ISCONNECTING; m_freem(nam); if(error == ERESTART){ error = EINTR; } return (error); }

soisconnected函数

soisconnected(struct socket * so)
{
  struct socket * head = so->so_head;
  
  //通过修改socket的状态来表明连接已经完成。 so->so_state &= ~(SS_ISCONNECTING | SS_ISDISCONNECTING | SS_ISCONFIRMING); so->so_state |= SS_ISCONNECTED; //当对进入的连接调用soisconnected(即,本地进程正在调用accept)时,head为非空 if(head && soqremque(so,0)){ /**  *如果soqremque返回1,就将socket放入so_q排队。sorwakeup唤醒通过调用select测试socket的可读性来监控socket上连接到达的进程。如果进程在accept中因等待连接而阻塞,则wakeup使得相应的tsleep返回。  */ soqinsque(head,so,1); sorwakeup(head); wakeup((caddr_t)&head->so_timeo); }else{ /**  * 如果head为空,就不需要调用soqremque,因为进程调用connect系统调用初始化连接,而且socket不再队列中。如果head非空,且so_qremque返回0,则socket已经在so_q队列中。  */ wakeup((caddr_t)&so->so_timeo); sorwakeup(so); sowwakeup(so); } }

soconnect函数

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

soconnect(struct socket * so,struct mbuf * nam)
{
   int s; int error; //如果socket已经被标识准备接收连接,则返回EOPNOTSUPP。因为如果已经对socket调用了listen,则进程不能再初始化连接。 if(so->so_options * SO_ACCEPTCONN){ return (EOPNOTSUPP); } s = splnet(); /**  *如果协议是基于连接的,则只能连接一次。否者,如果已经连接了,则会尝试先断开连接。这使得用户能够调用connecting到一个null的地址来断开连接  */ // 如果协议是面向连接的,并且一条连接已经被初始化,则返回EISCONN.对于无连接协议,任何已有的同外部地址的联系都被so_disconnect切断。 if((so->so_state & (SS_ISCONNECTED | SS_ISCONNECTING)) && ((so->so_proto->pr_flags & PR_CONNREQUIRED) || (error = sodisconnect(so)))){ error = EISCONN; }else{ //通过PRU_CONNECT请求启动相应的协议处理来建立连接或关联。 error = (*so->so_proto->pr_usrreq)(so,PRU_CONNECT,(struct mbuf*)0,nam,(struct mbuf*)0); } splx(s); return error; }

shutdown系统调用

shutdown()系统调用关闭连接的读通道、写通道或读写通道。

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

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

struct shutdown_args
{
    int s;
    int how;//指明关闭连接的方式 }; shutdown(struct proc * p,struct shutdown_args * uap,int * retval) { struct file * fp; int error; if(error = getsock(p->p_fd,uap->s,&fp)){ return (error); } return (soshutdown((struct socket * )fp->f_data,uap->how)); }

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

shutdown系统调用选项

howhow++描述
0FREAD关闭连接的读通道
1FWRITE关闭连接的写通道
2FREAD | FWRITE关闭连接的读写通道

soshutdown函数

soshutdown(struct socket * so,int how)
{
    struct protosw * pr = so->so_proto; how++; if(how & FREAD){//关闭读通道 sorflush(so);//丢弃socket接收缓存中的数据 } if(how & FWRITE){//关闭写通道 //给协议层发送PRU_SHUTDOWN请求。 return ((*pr_pr_usrreq)(so,PRU_SHUTDOWN,(struct mbuf*)0,(struct mbuf*)0,(struct mbuf*)0)); } return 0; }

sorflush函数

sorflush(struct socket * so)
{
    struct sockbuf * sb = &si->rcv;//获取此socket的接收缓存 struct protosw * pr = so->so_proto; int s; struct sockbuf asb; sb->sb_flags |= SB_NOINTR; (void)sblock(sb,M_WAITOK); s = splimp(); //标识此socket拒收接受进入的分组 socantrcvmore(so); sbunlock(sb); asb = * sb; bzero((caddr_t)sb,sizeof(*sb));//清空输入缓存 splx(s); //当shutdown被调用时,存储在输入缓存中的控制信息可能引用了一些内核资源。通过sockbuf结构的副本中的sb_mb仍然可以访问mbuf链 if((pr->pr_flags & PR_RIGHTS) && (pr->pr_domain->dom_dispose)){ (*pr->pr_domain->dom_dispose)(asb,sb_mb); } //释放输入缓存中的所有mbuf,丢弃所有调用shutdown时还没有被处理的数据 abrelease(&asb); }

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

close系统调用

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

error = (*fp->f_ops->fo_close)(fp,p);

socket的fp->f_ops->fo_closesoo_close函数

soo

soo_close函数是soclose函数的封装器:

soo_close(struct file * fp,struct proc * p)
{
    int error = 0; //如果socket结构与file相关联,则调用soclose,清楚f_data if(fp->f_data){ error = soclose((struct socket*)fp->f_data); } fp->f_data = 0; return (error); }

soclose函数

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

soclose(struct socket * so)
{
    int s = splnet();
    
    int error = 0; if(so->so_options & SO_ACCEPTCONN){ while(so->so_q0){ (void)soabort(so->so_q0); } while(so->so_q){ (void)soabort(so->q); } } if(so->so_pcb == 0){ goto discard; } if(so->so_state & SS_ISCONNECTED){ if((so->so_state & SS_ISDISCONNECTING) == 0){ error = sodisconnect(so); if(error){ goto drop; } } if(so->so_options & SO_LINGER){ if((so->so_state & SS_ISDISCONNECTING) && (so->so_state & SS_NBIO)){ goto drop; } while(so->so_state & SS_ISCONNECTED){ if(error = tsleep((caddr_t)&so->timeo,PSOCK | PCATCH ,netcls,so->linger)){ break; } } } } drop: if(so->so_pcb){ int error2 = (*so->so_proto->pr_usrreq)(so,PRU_DETACH,(struct mbuf*)0,(struct mbuf*)0,(struct mbuf*)0); if(error == 0){ error = error2; } } discard: if(so->so_state & SS_NOFDREF){ panic("soclose: NOFDREF"); } so->so_state |= SS_NOFDREF; sofree(so); spx(s); return error; }

sofree函数

sofree(struct socket * so)
{
    if(so->so_pcb || (so->so_state & SS_NOFDREF) == 0){ return ; } if(so->so_head){ if(!soqremque(so,0) && !soqremque(so,1)){ panic("sofree dq"); } so->so_head = 0; } sbrelease(&so->so_snd); soflush(so); FREE(so,M_SOCKET); }

fclose函数

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

soclose(struct socket * so)
{
    int s = splnet();
    
    int error = 0; if(so->so_options & SO_ACCEPTCONN){//如果socket正在接收连接,则调用soabort释放每一个挂起的连接 /**  *soabort发送PRU_ABORT请求给协议层,并发挥结果  */ while(so->so_q0){ (void)soabort(so->so_q0); } while(so->so_1){ (void)soabort(so->q); } } if(0 == so->so_pcb){//如果协议已同socket分离,则跳转到discard进行退出处理 goto discard; } if(so->so_state & SS_ISCONNECTED){//如果连接已经建立 if(0 == (so->so_state & SS_ISDISCONNECTING)){ error = sodisconnect(so); if(error){ goto drop; } } if(so->so_options & SO_LINGER){ if((so->so_state & SS_ISDISCONNECTING) && (so->so_state & SS_NBIO)){ goto drop; } while(so->so_state & SS_ISCONNECTED){ if(error = tsleep(caddr_t)&so->so_timeo,PSOCK|PCATCH,netcls,so->so_linger){ break; } } } } drop: if(so->so_pcb){ int error2 = (*so->so_proto->pr_usrreq)(so,PRU_DETACH,(struct mbuf*)0,(struct mbuf*)0,(struct mbuf*)0); if(0 == error){ error = error2; } } discard: if(so->so_state & SS_NOFDREF){ panic("soclose: NOFDREF"); } so->so_state |= SS_NOFDREF; sofree(so); splx(s); return (error); }

 

 
 https://www.cnblogs.com/kakawater/p/7085122.html

转载于:https://www.cnblogs.com/feng9exe/p/8206756.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值