socket是对协议栈的封装,支持TCP,UDP. protocol+src_addr+dst_addr+src_port+dst_port是socket五元组。Linux支持地址复用和端口复用,需要打开SO_REUSEADDR和SO_REUSEPORT选项�?button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false">
网络实现架构
4.4BSD通过同时对多种通信协议的支持来提供通用的底层基础服务�?code>4.4BSD支持四种不同的通信协议�?
TCP/IP(互联网协议簇)
XNS(Xerox网络系统)
OSI协议
Unix域协�?br>从通信协议是用来在不同的系统之间交换信息的意义上来说,它还不算是一套真正的协议,但它提供了一种进程间通信(//����Ч���ο���http://hnjlyzjd.com/xl/wz_24671.html
IPC)的形式�?/li>4.4BSD内核中的联网代码组织成三层,如下图所�?/p>
Socket层是一个到下面协议相关层的协议无关�?/strong>�?strong>所有系统调用从协议无关的Socket开�?/strong>�?br>例如:在Socket层中�?code>bind()系统调用的协议无关代码包含几十行代码,它们验证第一个参数是一个有效的socket描述符,并且第二个参数是一个进程中的有效指针。然后调用下层的协议相关代码,协议相关代码可能包含几百行代码�?/p>
协议层包括我们提到的四种协议�?TCP/IP,XNS,OSI和Unix�?的实现�?br>每个协议簇可能包含自己的内部结构�?/p>
接口�?br>接口层包括同网络设备通信的设备驱动程序�?/p>
数据传�?button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false">
Socket层中的每一个Socket都具有一个输入队列和一个输出队�?/li>
协议层中的每一个协议都具有一个输入队列和输出队列
接口层中的每个接�?以太网、回环、SLIP、PPP�?都有一个输入队列和输出队列
输入处理
输入处理与输出处理不同,因为输入处理是异步的。就是说,它是通过一个接收完成中�?/strong>驱动以太网设备程�?/strong>来接收一个输入分�?而不是通过进程的系统调用�?strong>内核处理这个设备中断,并调度设备驱动程序进入运行状�?/strong>�?/p>
接口�?以太网输�?button class="cnblogs-toc-button" title="显示目录导航" //����Ч���ο���http://hnjlyzjd.com/xl/wz_24661.html
aria-expanded="false">以太网设备驱动程序处理这个中断�?/p>
假定它表示一个正常的接收已完成,数据从以太网设备读取到一个mbuf链表中。设备驱动程序把mbuf传给一个通用以太网输入例程,它通过以太网帧中的类型字段来确定哪个协议层接收此分组�?/p>
协议层——IP输入
IP输入是异步的,并且通过一�?strong>软中�?/strong>来执行�?/p>
当接口层在系统的一个接口上收到一个IP数据报时,它就设置这个软中断。当IP输入例程执行它时,循环处理在它的输入队列中的每一个IP数据报,并在整个队列被处理完后返回�?/p>
输入�?UDP输入
IP输入历程可能会调用UDP输入例程去处理UDP数据报�?/p>
UDP输入例程验证UDP首部中的各字�?长度与可选的校验�?,然后确定是否一个进程应噶接收次数据报�?/p>
UDP输入例程从一�?strong>全局变量udb开�?查看所有UDP协议控制块链表PCB,寻找一个本地端口号与接收的UDP数据报的目标端口号相匹配的协议块�?这个PCB是由我们调用socket()创建的,它的成员inp_socket指向相应socket接收,并允许接收的数据在此socket排队).
因为这个UDP数据报要传送给我们的进程,发送方的IP地址和UDP端口号放置到一个mbuf中,这个mbuf和数据被追加到此socket的接收队列中�?/p>
最后,接收进程被唤醒。如果进程处于睡眠状态等待数据的到达,进程将标志为可运行状态等待内核的调度。也可以通过select系统调用�?code>SIGIO信号来通知进程数据的到达�?/p>
进程输入
进程可以调用socket 的输入函数将mbuf从socket的接收队列复制到我们程序的缓存中�?/p>
存储器缓�?button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false">
在BSD联网代码设计中的一个基本概念就�?strong>存储器缓�?/strong>,称作为一�?strong>mbuf(memory buffer),在整个联网代码中用于存储各种信息�?/p>
网络协议对内核的存储器管理能力提出了很多要求。这些要求包括能方便地操作可变长缓存,能在缓存头部和尾部添加数据(如底层封装来自高层的数据),能从缓存中移去数据(如,当数据分组向上经过协议栈时要去掉首部),并尽量减少为这些操作所做的数据复制。内核中的存储器管理调度直接关系到联网协议的性能�?/p>
mbuf的主要用途是保存在进程和网络接口间互相传递的用户数据。但mbuf也用于保存其它各种数�?源与目的地址、Socket选项等等�?/p>
指针m_next�?code>mbuf连接在一起,把一个分组形成一�?code>mbuf链表�?/li>
指针m_nextpkt把多个分组链接成一�?code>mbuf链接成一�?code>mbuf链表队列。在队列的每个分组可以是一个单独的mbuf,也可以是一�?code>mbuf链表。每个分组的第一�?code>mbuf包含一个分组首部。如果多个mbuf定义一个分组,只有第一�?code>mbuf的成�?code>m_nextpkt被使用——链表中其它mbuf的成�?code>m_nextpkt全是空指针�?/li>
m_get函数
struct mbuf m_get(int nowait,int type)
{
struct mbuf m;
MGET(m,nowait,type);
return m;
}
nowait的值为M_WAIT�?code>M_DONTWAIT,它取决于在存储器不可用时是否要求等待�?br>例如,当Socket层请求分配一个mbuf来存储sendto系统调用的目的地址时,它指�?code>M_WAIT,因为在此阻塞是没有问题的。但是当以太网设备驱动程序请求分配一个mbuf来存储一个接收的帧时,它指定M_DONTWAIT,因为它是作为一个设备中断处理来执行的,不能进入睡眠状态来等待一个mbuf。在这种情况下,若存储器不可用,设备驱动程序丢弃这个帧比较好�?/p>
type 指定mbuf的类�?/p>
系统调用
所有的操作系统都提供服务访问点,程序可以通过它们请求内核中的服务。各种UNIX都提供精心定义的有限个内核入口点,即系统调用。我们不能改变系统调用,除非我们有内核的源代码�?/p>
在各种Unix系统中,每个系统调用在标准C函数库中都有一个相同名字的函数。一个应用程序用标准C的调用序列来调用此函数。这个函数再调用相应的内核服务,所使用的技术依赖于所在的系统。例如,函数可能把一个或多个C参数放到通用寄存器中,并执行几条机器指令产生一个软件中断进入内核。对我们来说,我们可以把系统调用看成C函数�?/p>
从进程到内核的受保护的环境的转换是与机器和实现相关的�?/p>
�?strong>BSD内核中,每一个系统调用均被编�?/strong>,当进程执行一个系统调用时,硬件被配置成仅传送控制给一个内核函�?即将CPU的使用权转给一个内核函数�?strong>将标志系统调用的整数作为参数传送给此内核函�?/strong>。在i386实现中,此内核函数为syscall(),syscall()利用系统调用的编号在系统调用表中找到请求的系统调用的sysent结构.表中的每一单元均为一个sysent结构�?/p>
struct sysent{
int sy_narg; //参数个数
int (sy_call)();//系统调用的实现函�?/span>
};
表中有几个项是从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系统调用在系统调用表中的�?7个项,它�?个参数,利用内核中的recvmsg函数实现�?/p>
syscall()负责将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结果。然后,当系统调用执行完成后,syscall将结果返回给进程。syscall将控制交给鱼系统调用相对应的内核函数�?/p>
在i386实现中,调用有点�?
struct sysent callp;
error = (callp->syscall)(p,args,rval);
if(error){
errno = error;
return -1;
}else{
return (rval);
}
这里指针callp指向相关的sysent结构;指针p指向调用系统调用的进程的进程表项;args作为参数传给系统调用,它是一�?2bit长的字数�?而rval则是一个用来保存系统调用的返回结果的数组,数组有两个元素,每一个元素是一�?2bit长的字�?strong>当我们用"系统调用"这个词时,我们指的是被syscall调用的内核中的函数,而是不是应用调用的进程中的函�?/strong>�?/p>
syscall期望系统调用函数(即sy_call指向的函�?在没有差错时返回0,否则返回非0的差错代码。如果没有差错出现,内核将rval中的值作为系统调�?应用调用�?返回值传送给进程。如果有差错,syscall忽略rval中的值,并以与机器相关的方式返回差错代码给进程,使得进程能从外部变量errno中得到差错代码。应用调用的函数则返�?1或一个空指针表示应用应该查看errno获得差错信息�?/p>
下表介绍了与网络有关的系统调�?/p>
举例
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说明称一个与系统调用有关的结构指针,而不是一个指�?2bit的子的数组的指针�?/p>
syscall在执行内核系统调用函数之前将返回值设置为0.如果没有差错出现,系统调用函数直接返回而不需要清�?tetvall,syscall返回0给进程�?/p>
进程、描述符和插�?button class="cnblogs-toc-button" title="显示目录导航" aria-expanded="false">
Unix系统中的Socket I/O遵循�?一切皆文件"的思想,因而可以使用统一的方式对Socket 进行I/O操作�?/p>
调用socket()时要求定义socket类型。Internet协议�?PF_INET)和数据报socket(SOCK_DGRAM)组合成一个UDP协议socket�?/p>
socket()的返回值是一个文件描述符,它具有其它Unix文件描述符的所有特�?可以用这个描述符调用read()�?code>write();可以�?code>dup()复制它,在调用了fork()之后,父进程和子进程可以共享�?可以�?code>fcntl()来改变他的属�?可以调用close()来关闭它,等的�?/p>
在每个进程的生存期内都会有一个对应的进程表项存在�?/p>
一个文件描述符是进程对应的进程表项中的一个数组的下标.这个数组项是一个指向打开文件表结构的指针�?/p>
此打开文件表结构有指向一个描述此文件�?code>i-node�?code>v-node结构
实现系统调用的函数的第一个参数总为p,即指向调用进程的proc结构的指针。内核利�?code>proc结构体记录进程的有关信息。在proc结构体中�?code>p_fd指向filedesc结构,该结构的主要功能是管理fd_ofiles指向的描述符�?/strong>�?strong>描述符表的大小是动态变化的,由一个指向file结构的指针数组组�?/strong>�?strong>每一个file结构体描述一个打开的文件,该结构体可被多个进程共享�?/p>
通过p->p_fd->fd_ofiles【fd�?/code>访问到结构。在file结构中,有两个结构成员是我们感兴趣的:f_ops�?code>f_data。I/O系统调用(如read和write)的实现因描述符中的I/O对象类型的不同而不同。f_ops指向fileops结构�?strong>该结构包含一�?code>read�?code>write�?code>ioctl�?code>select�?code>close系统调用的函数指针表。显示f_ops指向一个全局的fileops结构,即socketops,该结构包含指向socket用的函数的指针�?/p>
f_data指向相关I/O对象的专用数据。对于socket而言�?code>f_data指向与描述符相关的socket结构。最后,socket结构中的so_proto指向产生socket时选中的协议的protosw结构。回想一下,每一�?code>protosw结构是由与该协议关联的所有socket共享的�?/p>
Socket结构
Socket代表一条通信链路的一�?存储或指向与链路有关的所有信�?/strong>。这些信息包�?使用的协�?/strong>�?strong>协议的状态信�?包括源地址和目的地址)�?strong>到达的连接队�?/strong>�?strong>数据缓存�?strong>可选标�?/strong>�?/p>
struct socket{
short so_type;//Socket类型,SOCK_STREAM、SOCK_DGRAM或SOCK_RAW
short so_options;//Socket行为的标�?/span>
short so_linger;
short so_state;//Socket状�?/span>
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;//缓存中的实际字节�?/span>
u_long sb_hiwat;
u_long sb_mbcnt;
u_long sb_mbmax;//分配给此socket mbuf缓存的存储器数量的上限�?/span>
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和相关协议支持的通信语义�?/p>
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行为的标志�?/p>
| 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继续发送数据的时间间隔(单位为一个时钟滴�?�?/p>
so_state
so_state表示Socket的内部状态和一些其它的特点�?/p>
so_state描述
SS_NBIO
Socket操作不能阻塞进程
SS_ASYNC
Socket应该I/O事件的异步通知
SS_NBIO
默认情况下,进程在发出I/O请求后会等待资源,并阻塞�?br>例如,当一个进程对一个Socket进行read()系统调用,如果当前没有网络上来的数据,则read系统调用就会被阻塞。同样,当一个进程对一个Socket进行write()系统调用,如果内核中没有缓存来存储发送的数据,则内核将阻塞进程�?/p>
如果设置�?code>SS_NBIO,在对Socket执行I/O操作且请求的资源不能得到时,内核并不阻塞进程,而是返回EWOULDBLOCK.
SS_ASYNC�?code>so_pgid字段
如果设置�?code>SS_ASYNC,当因为下列情况之一而使Socket状态发生变化时,内核发�?code>SIGIO信号�?code>so_pgid字段标识的进程或进程�?
连接请求已经完成
断开连接请求已被启动
断开连接请求已经完成
连接的一个通道已被关闭
Socket上有数据到达
数据已被发�?即输出缓存中有空闲空�?
UDP或TCP Socket上出现了一个异步差�?/li>
so_pcb和协议控制块
so_pcb指向协议控制块,协议控制块包含与协议有关的状态信息和Socket参数�?br>每一种协议都定义了自己的协议控制块结构,因此so_pcb被定义成一个通用的指针�?/p>
协议协议控制�?/th>
UDP
struct inpcb
TCP
struct inpcb、struct tcpcb
ICMP、IGMP和原始IP
struct inpcb
路由
struct rawcb
so_proto
so_proto指向进程�?code>socket()系统调用中选择的协议的protosw结构
连接队列
在so_options字段中设置了SO_ACCEPTCONN标志的socket维护两个连接队列�?/p>
so_q0表示还没有完全建立的连接,例如TCP的三次握手还没有完成。队列的长度由so_q0len字段表示
so_q表示已经建立的,但未被应用层接受的连�?例如TCP的三次握手已经完成。队列的长度由so_qlen字段表示�?/p>
在每一个被排队的socket中,so+heade指向了设置SO_ACCEPTCONN的源socket.
socket上可排队的连接数(so_q和so_q0两个连接队列的总连接数)通过so_qlimit来控制,应用层可以通过listen()系统调用来设置so_qlimit
当下列的不等式成立时,将不再接受任何连接
so_timeo
so_timeo用作accept()�?code>connect()�?code>close()处理期间的等待通道
等待通道
so_error
so_error保存错误代码,直到在应引用该socket的下一个系统调用期间错误码能送给应用�?/p>
数据缓存
每一个socket包括两个数据缓存,输入缓存so_rcv和输出缓存so_snd。分别用来缓存接受或发送的数据�?/p>
socket系统调用
socket系统调用产生一个新的socket,并将socket同进程在参数domain�?code>type�?code>protocol中指定的协议联系起来。该函数分配一个新的描述符,用来在后续的系统调用中标志socket,并将描述符返回给进程�?/p>
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;//获取文件描述�?/span>
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结构�?code>fd_ofiles数组中的一个元素。fp指向新分配的结构,fd则为结构在数�?code>fd_ofiles中的索引。socket将file结构设置成可读、可写,并且作为一个socket。将所有socket共享的全局fileops结构socketopts连接�?code>f_ops指向的file结构中�?code>socketops变量在编译时被初始化�?/p>
getsock函数
getsock的功能是将一个文件描述符映射到一个文件表项中,�?strong>根据一个文件描述符找到起对应的文件表项。�?br>getsock函数利用fdp查找文件描述符fdes指定的文件表项,fdp是指向filedesc结构的指针。getsock将打开的文件结构指针赋给fpp,并返回,或者当出现下列情况时返回错误代�?
描述符的值超过了范围而不是指向一个打开的文�?/li>
描述符没有同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�?/strong>�?/p>
sockargs将进程传给系统调用的参数的指针从进程复制到内核�?strong>不是复制指向的数�?/strong>,这样做是因�?strong>每一个参数的语义只有相对应的系统调用才知�?/strong>,而不是针对所有的系统调用。多个系统调用在调用sockargs复制参数指针后,将指针指向的数据从进程复制到内核中新分配的mbuf中�?/p>
例如,sockargs将bind的第二个参数指向的本地socket地址从进程复制到一个mbuf中�?/p>
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;
//关键代码,实现将进程传入的参数复制到内核�?/span>
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.这一点确保即使进程没有正确地初始化结构,结构内的大小也是正确的�?/span>
}
}
return (error);
};
bind系统调用
bind()系统调用将一个本地的网络传输层地址和socket联系起来�?/p>
一般来说,作为客户(client)的进程并不关心它的本地地址是什么。在这种情况下,进程在进行通信之前没有必要调用bind()�?strong>内核会自动为其选择一个本地地址�?/p>
但是,服务器进程则总是需要绑定到一个已知的地址。所以,进程在接受TCP连接或接收UDP数据报之前必须调�?code>bind(),因为客户进程需要同已知的地址建立连接或发送数据报到已知的地址�?/p>
socket�?strong>外部地址�?code>connect()指定或由允许指定外部地址的写调用,譬�?code>sendto()�?code>sendmsg(),指定�?/p>
struct bind_args{
int s;//socket文件描述�?/span>
caddr_t name;//包含传输地址的缓存指�?/span>
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�?/span>
if(error = sockargs(&nam,uap->name,uap->namelen,MT_SONAME)){
return (error);
}
//sobind将进程指定的地址同socket联系起来�?/span>
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来得到队列中的连接�?/p>
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将请求传递给协议�?/span>
return (solisten((struct socket)fp->f_data,uap->backlog));
}
solisten函数
solisten()是一个封装器,它给与Socket相关联的协议发�?code>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;
}