Socket层的核心是两个函数:sosend()
和soreceive()
.这两个函数负责处理所有Socket层和协议层之间的I/O操作。
select()
系统调用的作用是监控文件描述符的状态。一般用于Socket I/O操作,也可以用于其它文件I/O操作。
Socket缓存
我们知道,每一个Socket都有一个发送缓存和一个接收缓存。缓存的类型为sockbuf
。
struct sockbuf{ struct mbuf * sb_mb;//mbuf链,用于存储用户数据 u_long sb_mbcnt;//mbuf链的长度 u_long sb_mbmax;//mbuf链的最大长度。 u_long sb_cc;//缓存中的实际字节数 u_long sb_hiwat; long sb_lowat; struct selinfo sb_sel; short sb_flags; short sb_timeo;//read/write超时时间 }
sb_mb
为指向存储数据的mbuf链的指针,指向的是mbuf链的第一个mbufsb_cc
的值等于存储在mbuf链中的数据字节数,即mbuf链中存储的有效数据的字节数sb_hiwat
和sb_lowat
用来调整Socket的流控算法(不是TCP的流量控制)sb_mbcnt
为分配给缓存中的所有mbuf的存储器数量,即mbuf链的长度sb_mbmax
为分配给该Socket mbuf缓存的存储器数量的上限,即mbuf链的最大长度。
默认的上限在socket系统调用中发送PRU_ATTACH
请求时由协议设置。只要内核要求的每个socket缓存的大小不超过262 144个字节的限制(sb_max,这是一个全局变量),进程就可以修改缓存的上限和下限。
Internet协议的默认的Socket缓存限制
因为每一个收到的UDP数据报的源地址和其携带的数据一起排队,所以UDP协议的sb_hiwat的默认值设置为能容纳40个1K字节长的数据报和相应的sockaddr_in结构(每个16个字节)。
sb_sel是用来实现
select()
系统调用的selinfo结构sb_timeo用来限制一个进程在读写调用中被阻塞的时间。
下表列出了sb_flags的所有可能的值
sb-flags | 说明 |
---|---|
SB_LOCK | 一个进程已经锁定了socket缓存 |
SB_WANT | 一个进程正在等待给socket缓存加锁 |
SB_WAIT | 一个进程正在等待接收数据或发送数据所需的缓存 |
SB_SEL | 一个或多个进程正在选择这个缓存 |
SB_ASYNC | 为这个缓存产生异步I/O信号 |
SB_NOINTR | 信号不取消加锁请求 |
SB_NOTIFY | (SB_WAIT|SB_SEL|SB_ASYNC ) 一个进程正在等待缓存的变化,如果缓存发送任何变化,用wakeup通知该进程 |
write、writev、sendto和sendmsg系统调用
我们将write()
、writev
、sendto()
和sendmsg()
四个系统调用统称为"写系统调用",它们的作用是往网络连接上发送数据。相对于最一般的调用sendmsg()
而言,前三个系统调用是比较简单的接口。
所有的写系统调用都要直接或间接地调用sosend
。sosend
的功能是将进程来的数据复制到内核,并将数据传递给与socket相关的协议。
函数 | 描述符类型 | 缓存数量 | 是否指明目的地址 | 标志? | 控制信息? |
---|---|---|---|---|---|
write | 任何类型 | 1 | |||
writev | 任何类型 | [1..UIO_MAXIOV] | |||
send | socket | 1 | . | ||
sendto | socket | 1 | . | . | |
sendmsg | socket | [1..UIO_MAXIOV] | . | . | . |
write()
和writev()
系统调用适用于任何描述符,而其它的系统调用只适用于socket描述符。
writev()
和sendmsg()
系统调用可以接收从多个(应用层)缓存中来的数据。从多个缓存中写数据称为"收集(gathering)",同它相对应的读操作称为"分散(scattering)"。执行收集操作时,内核按序接收类型为iovec
的数据中指定的缓存中的数据。数组最多有UIO_MAXIOV
个单元。
struct iovec { char * iov_base;//基线地址,指向长度为iov_len个字节的缓存的开始 size_t iov_len;//长度};
如果没有这种接口(writev),一个进程将不得不将多个缓存复制到一个大的缓存中,或调用多个写系统调用来发送多个缓存中的数据。对于数据报协议而言,调用一次write()
就是发送一个数据报,数据报的发送不能用多个写动作来实现。
数据报协议要求每一个写调用必须指定一个目的地址。因为write()
、writev()
和send()
调用接口不支持对目的地址的指定,因此这些调用只能在调用connect()
将目的地址同一个无连接的socket联系起来后才能被调用。调用sendto()
或sendmsg()
时必须提供目的地址,或在调用它们之前调用connect()
来指定目的地址。
sendmsg()系统调用
只有通过sendmsg()
系统调用才能访问到与socket API的输出有关的所有功能。sendmsg()
和sendit()
函数准备sosend()
系统调用所需的数据结构,然后由sosend()
系统调用将报文发送给相应的协议。
对于SOCK_DGRAM
协议而言,报文就是数据报。对于SOCK_STREAM
协议而言,报文是一串字节流。
sendmsg()
有三个参数:socket描述符、指向msghdr结构的指针、几个控制标志。函数copyin()
将msghdr
结构从用户空间复制到内核空间。
struct msghdr { caddr_t msg_name;//可选的目的地址 u_int msg_namelen;//msg_name的长度 struct iovec * msg_iov;//分散/收集数组 u_int msg_iovlen;//msg_iov数组长度 caddr_t msg_control;//控制信息 u_int msg_controllen;//控制信息长度 int msg_flags;//接收标志};
控制信息(msg_control字段)的类型为cmsghdr
结构:
struct cmsghdr { u_int cmsg_len; int cmsg_level; int cmsg_type; };
struct sendmsg_args { int s; caddr_t msg; int flags; }; sendmsg(struct proc * p,struct sendmsg_args * uap,int * retval) { struct msghdr msg; struct iovec aiov[UIO_SMALLOV],*iov; int error; /** *一个有8个元素(UIO_SMALLIOV)的iovec数组从栈中自动分配。 *如果分配的数组不够大,sendmsg将调用MALLOC分配更大的数组。如果进程指定的数组单元大于1024(UIO_MAXIOV),则返回EMSGSIZE。 *copyin将iovec数组从用户空间复制到栈中的数组或一个更大的动态分配的数组中。 *这种技术避免了调用malloc带来的高代价,因为大多数情况下,数组的单元数小于等于8 */ //将msg数据从用户空间复制到内核空间 if(error = copyin(uap->msg,(caddr_t)&msg,sizeof(msg))){ return (error); } if((u_int)msg.msg_iovlen >= UIO_SMALLIOV){ if((u_int)msg.msg_iovlen >= UIO_MAXIOV){ return (EMSGSIZE); } MALLOC(iov,struct iovec *,sizeof(struct iovec)*(u_int)msg.msg_iovlen,M_IOV,M_WAITOK); }else{ iov = aiov; } if(msg.msg_iovlen && (error = copyin((caddr_t)msg.msg_iov,(caddr_t)iov,(unsigned)(msg.msg_iovlen * sizeof(struct iovec))))){ goto done; } msg.msg_iov = iov; //如果sendit返回,则表明数据已经发送给相应的协议或出现差错 error = sendit(p,uap->s,&msg,uap->flags,retval); done: if(iov != aiov){ FREE(iov,M_IOV); } return (error); }
sendit系统调用
sendit(struct proc * p,int s,struct msghdr * mp,int flags,int * retsize) { struct file * fp; struct uio auio; struct iovec * iov; int i; struct mbuf * to, * control; int len,error; if(error = getsock(p->p_fd,s,&fp)){ return error; } /** *初始化uio结构,并将应用层的输出缓存中的数据收集到内核缓存中 */ auio.uio_iov = mp->msg_iov; auio.uio_iovcnt = mp->msg_iovlen; auio.uio_segflg = UIO_USERSPACE; auio.uio_rw = UIO_WRITE; auio.uio_procp = p; auio.uio_offset = 0; auio.uio_resid = 0; iov = mp->msg_iov; /** *将传送的数据的长度通过一个for循环来计算 */ for(i = 0;i < mp->msg_iovlen;i++,iov++){ /** *保证缓存的长度非负 */ if(iov->iov_len < 0){ return (EINVAL); } /** *保证uio_resid不溢出,因为uio_resid是一个**有符号的**整数,且iov_len要求非负 */ if((auio.uio_resid += iov->iov_len) < 0){ return (EINVAL); } } /** *如果进程提供了地址和控制信息,则sockargs将地址和控制信息复制到内核缓存中 */ if(mp->msg_name){//如果进程提供了地址 //将地址复制到内核中 if(error = sockargs(&to,mp->msg_name,mp->msg_namelen,MT_SONAME)){ return (error); } }else{ top = 0; } if(mp->msg_control){//如果进程提供了控制信息 if(mp->msg_controllen < sizeof(struct cmsghdr)){ error = EINVAL; goto bad;; } //将控制信息复制到内核中 if(error = sockargs(&control,mp->msg_control,mp->msg_controllen,MT_CONTROL)){ goto bad; } }else{ control = 0; } /** *发送数据和清除缓存 */ len = auio.uio_resid;//为了防止sosend不接受所有数据而无法计算传送的字节数,将uio_resid的值保存在len中 //将socket、目的地址、uio结构(包含了要发送的数据)、控制信息和标志全部传给函数sosend if(error = sosend((struct socket *)fp->f_data,to,&auio,(struct mbuf*)0,control,fkags)){ if(auio.uio_resid != len && (error == ERESTART || error == EINTR || error == EWOULDBLOCK)){ error = 0; } if(error == EPIPE){ psignal(p,SIGPIPE); } } if(0 == error){ /** *如果没有差错出现(或差错被丢弃),则计算传送的字节数,并将其保存在*retsize中。 *如果sendit返回0,syscall返回*retsize给进程而不是返回差错代码 */ *retsize = len - auio.uio_resid; } bad://释放包含目的地址的缓存。 if(to){ m_freem(to); } return (error); }
sosend系统调用
sosend()
是socket层中最复杂的函数之一。前面提到的所有五个写系统调用最终都要调用sosend()
。sosend()
的功能就是:根据socket指明的协议支持的语义和缓存的限制,将数据和控制信息传递给socket指明的协议的pr_usrreq函数.sosend从不将数据放在发送缓存(输出缓存)中,存储和移走数据应由协议来完成。
sosend()
对发送缓存的sb_hiwat
和sb_lowat
值的解释,取决于对应的协议是否实现可靠或不可靠的数据传送功能。
可靠的协议缓存
对于提供可靠的数据传输协议,例如TCP,发送缓存保存了还没有发送的数据和已经发送但还没有被确认的数据。sb_cc
等于发送缓存的数据的字节数,且0 <= sb_cc <= sb_hiwat
。
如果有带外数据发送,则sb_cc有可能暂时超过sb_hiwat
so_send应该确保在通过pr_usrreq()
函数将数据传递给协议层之前有足够的发送缓存。
协议层会将数据放到发送缓存中。sosend通过下面两种方式之一将数据传送给协议层:
TCP应用程序对外出的TCP报文段的大小没有控制。例如,在TCP Socket上发送一个长度为4096字节的报文,假定发送缓存中有足够的缓存,则Socket层将该报文分成两部分,每一部分长度为2048个字节,分别存放在一个带外部簇的mbuf中。然后,在协议处理时,TCP将根据连接上的MSS将数据分段,通常情况下,MSS为2048个字节。
当一个报文因为太大而没有足够的缓存时,协议允许报文被分成多段。但sosend仍然不将数据传送给协议层知道发送缓存中的空闲空间大小大于sb_lowat。对于TCP而言,sb_lowat的默认值是2048,从而阻止Socket层在发送缓存快满时用小块数据干扰TCP.
不可靠的协议缓存
对于提供不可靠的数据传输的协议而言,例如UDP,发送缓存不需要保存任何数据,也不等待任何确认。
每一个报文一旦被排队等待发送到相应的网络设备,Socket层立即将它传送到协议。在这种情况下,sb_cc总是等于0,sb_hiwat指定每一次写的最大长度,间接指明数据报的最大长度。
UDP协议的sb_hiwat
的默认值为9216(9 x 1024)。如果进程没有通过SO_SNDBUF
Socket选项改变sb_hiwat
的值,则发送长度大于9216个字节的数据报将导致差错。不仅如此,其它的协议限制也可能不允许一个进程发送大的数据报。
对于NFS写而言,9216已足够大,NFS写的数据加上协议首部的长度一般默认为8192个字节。
实现
sosend(struct socket * so,struct mbuf * addr,struct uio * uio,struct mbuf * top,struct mbuf * control,int flags) { /** *初始化 */ struct proc * p = curproc; struct mbuf **mp; struct mbuf * m; long space,len,resid; int clen = 0,error,s,dontroute,mlen; /** * 如果sosendallatonce等于true(任何设置了PR_ATOMIC的协议)或数据已经通过top中的mbuf链传送给sosend,则将设置atomic。这个标志[控制数据是作为一个mbuf链还是作为独立的mbuf传送给协议]. */ int automic = sosendallatonce(so) || top; /** *resid等于iovec缓存中的数据字节数或top中的mbuf链中的数据字节数 */ if(uio){ resid = uio->uio_resid; }else{ resid = top->m_pkthdr.len; } if(resid < 0){ return (EINVAL); } /** *如果仅仅要求对这个报文不通过路由表进行路由选择,则设置dontroute */ dontroute = (flags & MSG_DONTROUTE) && (so->so_options & SO_DONTROUTE) == 0 && (so->so_proto->pr_flags & PR_ATOMIC); p->p_stats->p_ru.ru_msgsnd++; if(control){ /** * clen等于在可选的控制缓存中的字节数 */ clen = control->m_len; } #define snderr(errno) {error = errno;splx(s);goto release;} /** *sosend的主循环从restart开始,在循环的开始调用sblock()给发送缓存加锁。通过加锁确保多个进程按序互斥访问socket缓存 */restart: if(error = sblock(&so->so_snd,SBLOCKWAIT(flags))) { goto out; } /** *主循环直到[将所有数据都传送给协议层](即resid = 0)时才会退出 */ do{ /** * 等待发送缓存有空闲空间 */ s = splnet(); /** * 如果Socket输出被禁止,即TCP连接的写通道已经关闭,则返回EPIPE */ if(so->so_state & SS_CANTSENDMORE){ snderr(EPIPE); } /** * 如果Socket正处于差错状态(例如,前一个数据报可能已经产生了一个ICMP不可达的差错),则返回so->so_error */ if(so->so_error){ snderr(so->so_error); } /** * 如果协议请求连接且连接还没有建立或连接请求还没有启动,则返回EMOTCONN. * sosend允许只有控制信息但没有数据的写操作 */ if((so->so_state & SS_ISCONNECTED) == 0){ if(so->so_proto->pr_flags & PR_CONNREQUIRED){ if((so->so_state & SS_ISCONFIRMING) == 0 && !(resid == 0 && clen != 0)){ snderr(ENOTCONN); } }else if(addr == 0){ snderr(EDESTADDRREQ); }
http://www.cnblogs.com/kakawater/p/7112817.html