NOTE: 这篇文章假定读者熟悉 UNIX 下的 socket 和网络编程. 对用户级编程知识, 本文不作叙述.
socket 机制在系统中的地位
socket 机制是作为一个通用的, 跨机器, 跨平台的进程间通讯机制出现在系统中的. 我们通常把它作为网络通讯之用. 它使用了与文件系统相同的接口, 提供了协议无关的进程间通讯功能.
因此, 我们可以想到. 一个 socket 的机构应该被连接在 file 结构的 f_data 域中, 这样才能与上层的 VFS 相连, 并且应该逐个实现 fileops 中的函数接口 , 连在 file 结构的 f_ops 之中, 这样我们才可以如我们实际所做的那样, 用文件系统的 read(2), write(2) 函数进行 socket 通信. 同时, socket 的机构也应该包含一个域, 它是一个函数跳转表, 以跳转到底层协议, 如 TCP 的实现函数中. 在这个角度上看, socket 的作用也跟 VFS 是类似的. 但它与 VFS 大部分操作直接转接不同, 它还有大量需要自己处理的工作.
数据结构
内核结构
socket 的基本结构在 sys/socketvar.h 中, 包括 struct socket 以及描述 socket 缓冲区的结构 struct sockbuf. 上文所说的 socket 应有的机构都可以得到确认.
在 sys/protosw.h 中, 我们可以看到由 socket 结构 so_proto 域指向的 protosw 结构. 这就是 socket 与底层具体协议实现的接口. 我们需要大概记忆接口函数.
sys/socketvar.h 的 145 行是 socket 的状态, 需牢记.
用户接口数据结构
XXX.
mbuf
在 sockbuf 结构中, 我们可以看到, 一个 socket 的缓冲是一个 mbuf 链 (sb_mb 域). mbuf 是 socket 及以下的协议实现的基本数据封装, 传输和控制单位. 其结构定义在 sys/mbuf.h. 其中 m_hdr 是各种 mbuf 共同的头部信息, 最为重要.
我们首先来看一个注释, 弄明白一些重要的 size 问题. 我们使用 i386 的标准 . 即 MSIZE = 256, MCLBYTES = 2048.
/*
* Mbufs are of a single size, MSIZE (machine/param.h), which
* includes overhead. An mbuf may add a single "mbuf cluster" of size
* MCLBYTES (also in machine/param.h), which has no additional overhead
* and is used instead of the internal data area; this is done when
* at least MINCLSIZE of data must be stored.
*/
也就是说, 一个 mbuf 有 256 字节大, 如果它使用扩展空间, 可以用到 2048 个字节. 又, mbuf 携带的数据分两种: 正常数据和报文头部. 那么, 如果不使用扩展空间 (m_ext), mbuf 在携带正常数据的时候应可以使用 256 - sizeof(m_hdr) 的空间, 而携带报文头部时这个大小应该是 256 - sizeof(m_hdr) - sizeof(pkthdr).
定义 MBUF 结构在 269 行, 真正的结构描述在 225 行. 可以看到, 我们最终得出的是, 在不使用扩展空间时. 一个 mbuf 能携带 256 - 30 = 226bytes 的普通数据 和 256 - 30 - 20 = 206 bytes 的协议头.
我们要对 m_hdr 结构很熟悉. 也应该看看 kern/uipc_mbuf.c 的函数接口.
连接建立
socket(2)
sys_socket() 在 uipc_syscalls.c 中. 它要完成两个工作: 建立文件结构和相应的接口; 调用 socreate 完成真正的 socket 建立工作. 其中第一项工作是我们熟悉的, 注意 89 行起三行填写参数的代码, 还要注意 socketops 的内容, 这是我们对 socket 调用 read(2) 等调用时将要进入的地方. 我们现在来看 socreate 是怎么完成第二项工作的.
socreate 在 uipc_socket.c 中. 在这里, 我们首先要利用 pffindproto() /pffindtype() 根据参数找到相应的 protosw 结构, 即到底层协议实现的接口 . 然后申请 socket 结构的内存空间, 填写参数. 上层机构建立完毕后, 我们要通知协议实现加入这个 socket, 并真正创建协议相关的通讯机构. 我们使用其 pr_usrreq 的 PRU_ATTACH 操作完成这一点.
至此, 从顶层的文件接口, 到底层的协议实现机构, 整个 socket 创建完成.