网络实现架构
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_next
把mbuf
连接在一起,把一个分组形成一条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_WAIT
或M_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-node
或v-node结构
实现系统调用的函数的第一个参数总为p,即指向调用进程的proc
结构的指针。内核利用proc
结构体记录进程的有关信息。在proc
结构体中,p_fd
指向filedesc
结构,该结构的主要功能是管理fd_ofiles指向的描述符表。描述符表的大小是动态变化的,由一个指向file结构的指针数组组成。每一个file结构体描述一个打开的文件,该结构体可被多个进程共享。
通过p->p_fd->fd_ofiles[fd]
访问到结构。在file
结构中,有两个结构成员是我们感兴趣的:f_ops
和f_data
。I/O系统调用(如read和write)的实现因描述符中的I/O对象类型的不同而不同。f_ops指向fileops结构,该结构包含一张read
、write
、ioctl
、select
和close
系统调用的函数指针表。显示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 | 可靠的数据报服务(未实现) |