首页 > 代码库 > 《TCP/IP详解卷2:实现》笔记--插口层

《TCP/IP详解卷2:实现》笔记--插口层

插口层的主要功能是将进程发送的与协议有关的请求映射到产生插口时指定的与协议有关的实现。下图说明了进程中的插口

接口与内核中的协议实现之间的层次关系。



1.socket结构

插口代表一条通信链路的一端,存储或指向与链路有关的所有信息。这些信息包括:使用的协议、协议的状态信息(包括

源和目的地址)、到达的链接队列、数据缓存和可选标志。下图给出了插口和与插口相关的缓存的定义。


so_type由产生插口的进程指定,指明插口和相关协议支持的通信语义。对于UDP,为SOCK_DGRAM,对于TCP,为

SOCK_STREAM。

so_options是一组改变插口行为的标志。如下图所示:


通过getsockopt和setsockopt系统调用进程能修改除SO_ACCEPTCONN外所有的插口选项。当在插口上发送listen系统调用

时,该选项被内核设置。

so_linger等于当关闭一条连接时插口继续发送数据的时间间隔(单位为一个时钟滴答)

so_state表示插口的内部状态和一些其他的特点。下图是so_state可能的取值。


进程可以通过fcntl和ioctl系统调用直接修改SS_ASYNC和SS_NBIO。

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

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

  • 连接请求已完成
  • 断连请求已被启动
  • 断连请求已完成
  • 连接的一个通道已被关闭
  • 插口上有数据到达
  • 数据已被发送
  • UDP或TCP插口上出现了一个一步差错
so_pcb指向协议控制块,协议控制块包含于协议有关的状态信息和插口参数。每一种协议都定义了自己的控制块结构。所以
so_pcb被定义成一个通用的指针。下图列出了我们讨论的控制块结构。

so_proto指向进程在socket系统调用中选择的协议的protosw结构。
设置了SO_ACCEPTCONN标志的插口维护两个连接队列。还没有完全建立的连接(如TCP的三次握手还没完成)被放在队列
so_q0中。已经建立的连接或将被接收的连接(TCP的三次握手已完成)被放入队列so_q中。队列的长度分别为so_q0len和
so_qlen。每一个被排队的连接由它自己的插口来表示。在一个被排队的插口中,so_head指向设置了SO_ACCEPTCONN的
源插口。
插口上可排队的连接数通过so_qlimit来控制,进程可以通过listen系统调用来设置so_qlimit。
下图说明了有三个连接将被接受、一个连接已被建立的情况下的队列内容。

so_timeo用作accept、connet和close处理期间的等待通道。
so_error保存差错代码,直到在引用该插口的下一个系统调用期间差错代码能送给进程。
so_oobmark标识在输入数据流中最近收到的带外数据的开始点。
每一个插口包含两个数据缓存,so_rcv和so_snd,分别用来缓存接收或发送的数据。
在Net/3中不使用so_tpcb。so_upcall和so_upcallarg也仅用于Net/3中的NFS软件。

2.系统调用

进程同内核交互是通过一组定义好的函数来进行的。这些函数称为系统调用。
从进程到内核中的受保护的环境的转换是与机器和实现相关的。下面的讨论,我们使用Net/3在386上的实现来说明如何实现
有关操作。
在BSD的内核中,每一个系统调用均被编号,当进程执行一个系统调用时,硬件被配置成仅传送控制给一个内核函数。将标识
系统调用的整数作为参数传给该内核函数。在386实现中,这个内核函数为syscall。利用系统调用的编号,syscall在表中找到
请求的系统调用sysent结构。表中的每一个单元均为一个sysent结构。
struct sysent {
int sy_narg; /* number of arguments */
int (*sy_call) (); /* implementing function */
}; /* system call table entry */
表的形态如下图所示:
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 */
/* . . . */
}
syscall将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结果。然后,当系统调用执行完成后,syscall
将结果返回给进程。
syscall将控制交给与系统调用相对应的内核函数。在386实现中,调用有点像:
struct sysent *callp;
error = (*callp->sy_call) (p, args, rval);
这里指针callp指向相关的sysent结构,指针p则指向系统调用的进程的进程表项;args作为参数传给系统调用,它是一个32bit
长的字数组,而rval则是一个用来保存系统调用的返回结果的数组,数组有两个元素,每个元素是一个32bit长的字。当我们用
系统调用这个词时,我们指的是被syscall调用的内核中的函数,而不是应用调用的进程中的函数。
syscall期望系统调用函数在没有差错时返回0,否则返回非0的差错代码。如果没有差错出现,内核将rval中的值作为系统调用
的返回值传给进程。如果有差错,syscall忽略rval中的值,并以与机器相关的方式返回差错代码给进程,使得进程能从外部变量
errno中得到差错代码。应用调用的函数则返回-1或一个空指针表示应用应该查看errno获得差错信息。

2.1.举例

socket系统调用的原型是:
int socket(int domain, int type, int protocol);
实现socket系统调用的内核函数的原型是:
struct socket_args {
int domain;
int type;
int protocol;
};
socket(struct proc *p, struct socket_args *uap, int *retval);
当一个应用调用socket时,进程用系统调用机制将三个独立的整数传给内核。syscall将参数复制到32bit值的数组中,并将数
组指针作为第二个参数传给socket的内核版。内核版的socket将第二个参数作为指向socket_args结构的指针。下图显示了
上述的过程。

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

3.网络系统调用

下图显示了网络系统调用的流程图。


4.进程、描述符和插口

本节介绍进程、描述符和插口的数据结构。下图给出了这些结构以及有关的结构成员。

实现系统调用的函数的第一个参数总是p,即指向调用进程的proc结构的指针。内核利用proc结构记录进程的有关信息。

在proc结构中,p_fd指向filedesc结构,该结构的主要功能是管理fd_ofiles指向的描述表。描述符表的大小是动态变化的,

由一个指向file结构的指针数组组成。每一个file结构描述一个打开的文件,该结构可被多个进程共享。

file结构中,有两个结构成员我们是感兴趣的,f_ops和f_data。I/O系统调用的实现因描述符的I/O对象类型不同而不同。

f_ops指向fileops结构,该结构包含一张实现read、write、ioctl、select和close系统调用的函数指针表。

f_data指向相关I/O对象的专用数据,对于插口而言,f_data指向与描述符相关的socket结构。最后,socket结构中的

so_proto指向产生插口时选中的协议的protosw结构。


5.socket系统调用

socket系统调用产生一个新的插口,并将插口同进程在参数domain、type和protocol中指定的协议联系起来。该函数分配

一个新的描述符,用来在后续的系统调用中标识插口,并将描述符返回给进程。

系统调用的申明如下:


函数的大概处理如下:

1.falloc分配一个新的file结构和fd_ofiles数组中的一个元素,设置file结构的类型,可读、可写并且作为一个插口。

2.调用socreate分配并初始化一个socket结构。


5.1.socreate函数

大多数插口系统调用至少被分成两个函数,与socket和socreate类似,第一个函数从进程哪里获取需要的书序,调用第二个

函数soxxx来完成功能处理,然后返回结果给进程。这种分成多个函数的做法是为了第二个函数能直接被基于内核的网络协议

调用。

socreate函数的大概处理流程如下:

1.发现协议交换表。根据函数参数查找匹配协议的protosw结构的指针或空指针。

2.分配并初始化socket结果。分配一个新的socket结构,初始化相关字段。

3.PRU_ATTACH请求。每一个协议均提供了一个函数来处理从插口层来的通信请求。so->so_proto->pr_usrreq是一个指向

与插口so相关联协议的用户请求函数指针,函数原型是:

int pr_usrreq(struct socket *so, int req, struct mbuf *mo, *m1, *m2);

req是一个标识请求的常数。后三个参数因请求不同而异。下图列出了pr_usrreq函数提供的通信请求。


4.退出处理。返回新建的socket。


6.getsock和sockargs函数

这两个函数重复出现了插口系统调用中。

getsock的功能是将描述符映射到一个文件表项中。

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


7.bind系统调用

bind系统调用将一个本地的网络运输层地址和插口联系起来。一般来说,作为客户的进程并不关心它的本地地址是什么。

在这种情况下,进程在进行通信之前没有必要调用bind;内核会自动为其选择一个本地地址。

服务器进程则总是需要绑定到一个已知的地址上。所以进程在接收连接或者接收数据报之前必须调用bind,因为客户进程

需要同已知的地址建立连接或发送数据报到已知地址。bind系统调用的申明如下:


bind函数的大概处理如下:

1.getsock返回描述符的file结构

2.sockargs将本地地址复制到内核的mbuf中

3.将file结构和mbuf传给sobind函数。


7.1.sobind函数

sobind是一个封装器,它给与插口相关联的协议发送PRU_BIND请求(调用so->so_proto->pr_usrreq函数)。


8.listen系统调用

listen系统调用的功能是通知协议进程准备接收插口上的连接请求。它同时也指定插口上可以排队等待的连接数的门限值。

超过门限值时,插口层将拒绝进入连接请求排队等待。当这种情况出现时,TCP将忽略进入的连接请求。进程可以通过

调用accept来得到队列中的连接。

listen系统调用的申明如下:


listen系统调用的大概处理如下:

1.调用getsock返回描述符的file结构。

2.调用solisten将请求传递给协议层。


8.1.solisten函数

solisten函数发送PRU_LISTEN请求(调用so->so_proto->pr_usrreq函数),并使插口准备接收连接。


9.tsleep和wakeup函数

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

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

chan称之为等待通道,它标志进程等待的特定资源或事件。许多进程能同时在同一个等待通道上睡眠。当资源可用或事件出

现时,内核调用wakeup,并将等待通道作为唯一的参数传入。wakeup的原型是:

void wakeup(caddr_t chan);

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

tsleep的返回值。


因为所有等待在同一个等待通道上的进程均被wakeup唤醒,所有我们总是看到在一个循环中调用tsleep。每一个被唤醒的进程

在继续执行之前必须检查等待的资源是否可得到,因为另一个被唤醒的进程可能已经先一步得到了资源。如果仍然得不到资源,

进程再调用tsleep等待。


10.accept系统调用

调用listen后,进程调用accept等待连接请求。accept返回一个新的描述符,指向一个连接到客户的新的插口。原来的插口

仍然是未连接的,并准备接收下一个连接。如果name指向一个正确的缓存,accept就会返回对方的地址。

处理连接的系统由插口相关联的协议来完成。对于TCP,当一条连接已经被建立(即三次握手已经完成)时,就通知插口层。

accept系统调用的申明如下:


函数的大概处理流程如下:

1.验证参数。

2.等待连接。while循环中调用tsleep函数。当出现下列情况时,while退出:有一条连接到达;出现差错;插口不能再接收

数据。在循环内,进程在tsleep中等待,当有连接到达时,tsleep返回0。如果tsleep被信号中断或插口被设置成非阻塞,

则accept返回EINTR或EWOULDBOLCK。

3.异步差错。如果进程在睡眠期间出现差错,则将插口中的差错代码赋给accept中的返回码,清除插口中的差错码后,accept

返回。所以插口必须在每次被唤醒后检查返回值,查看是否在进程睡眠期间有差错出现。

4.将插口同描述符相关联。falloc函数为新的连接分配一个描述符,将插口从接受队列中删除,放到描述符的fle结构中。

5.协议处理。调用soaccept来完成协议处理。最后调用copyout将地址拷贝到进程空间。


10.1.soaccept函数

soaccept函数确保与一个描述符相连,并发送PRU_ACCEPT请求给协议,pr_surreq返回后,包含了外部插口的名字。


11.sonewconn和soisconnected函数

accept等待协议层处理进入的连接请求,并且将它们放入so_q中。下图利用TCP来说明这个过程。


accept调用tsleep等待进入的连接。tcp_input调用sonewconn为新的连接产生一个插口来处理进入的TCP SYN。sonewconn

将产生的插口放入so_q0排队,因为三次握手还没有完成。‘

当TCP握手协议的最后一个ACK到达时,tcp_input调用soisconnected来更新产生的插口,并将它从so_q0中移到so_q中,

唤醒所有调用accept等待进入的连接的进程。

当tsleep返回时,accept从so_q中得到连接,发送PRU_ATTACH请求。插口同一个新的文件描述符建立联系,accept也

返回到调用进程。


12.connect系统调用

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

对于面向连接的协议如TCP,connect建立一条与指定外部地址的连接。如果进程没有调用bind来绑定地址,则内核选择

并且隐式地绑定一个地址到插口。

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

下图显示了UDP或TCP调用connect时涉及到的函数。


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

右边说明connect如何处理面向连接的协议,如TCP。在这种情况下,协议层开始建立连接,调用soisconnecting指示连接

将在某个时候完成。如果插口是非阻塞的,soconnect调用tsleep等待连接完成。对于TCP,当三次握手完成时,协议层调用

soisconnected将插口标识为已连接。然后调用wakeup唤醒等待的进程,从而完成connect系统调用。

connect系统调用的申明如下:


connect系统调用的大概处理流程如下:

1.开始连接处理。连接是从调用soconnect开始的。

2.等待连接建立。while循环直到连接已建立或出现差错时才退出。

3.清楚“正在连接中”的标志,因为连接已经完成或连接请求已失败。释放存储外部地址的mbuf。


12.1.soconnect函数

soconnect函数确保插口处于正确的连接状态。如果插口没有连接或连接没有被挂起,则连接请求总是正确的。如果插口

已经连接或正等待处理,则新的连接请求将被面向连接的协议(如TCP)拒绝。对于无连接协议,如UDP,多个连接是

允许的,但是每一个新的请求中的外部地址会取代原来的外部地址。

soconnect发出RPU_CONNECT请求启动相应的协议处理来建立连接或关联。


13.shutdown系统调用

shutdown系统调用关闭连接的读通道、写通道或读写通道。对于读通道,shutdown丢弃所有进程还没有读写的数据以及

调用shutdown之后到达的数据。对于写通道,shutdown使协议作相应的处理。对于TCP,所有剩余的数据将被发送,发送

完成后发送FIN。这就是TCP的半关闭特点。

为了删除插口和释放描述符,必须调用close。可以在没有调用shutdown的情况下,直接调用close。同所有描述符一样,

当进程结束时,内核将调用close,关闭所有还没有被关闭的插口。

shutdown系统调用的申明如下:


其中how和how++的期望值如下:


shutdown是函数soshutdown的包装函数(wrapper function)。由getsock返回与描述符相关联的插口,调用soshutdown,

并返回其值。

关闭连接的读通道是由插口层调用sorflush处理的,写通道的关闭是由协议层的PRU_SHUTDOWN请求处理的。


14.close系统调用

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

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

插口的fp->f_ops->fo_close是soo_close函数。

soo_close函数是soclose函数的封装器,soclose函数取消插口上所有未完成的连接(即,还没有完全被进程接受的连接),

等待数据被传输到外部系统,释放不需要的数据结构。

soclose函数的大概处理流程如下:

1.丢弃未完成的连接。遍历so_q0和so_q两个连接队列,调用soabort函数取消每一个挂起的连接。soabort发送PRU_ABORT

请求给协议,并返回结果。

2.断开已建立的连接或关联。如果插口同外部地址相连接,必须断开插口与对等地址之间的连接。

3.释放数据结构。如果插口仍然同协议相连,则发送PRU_DETACH请求断开插口与协议的联系。最后,插口被标识为同任何

描述符没有关联,调用sofree函数释放插口。

《TCP/IP详解卷2:实现》笔记--插口层