《TCP/IP详解 卷2:实现》 —1.8 描述符
1.8 描述符
图1-2中,一开始调用socket,这要求定义插口类型。Internet协议族(PF_INET)和数据报插口(SOCK_DGRAM)组合成一个UDP协议插口。
socket的返回值是一个描述符,它具有其他Unix描述符的所有特性:可以用这个描述符调用read和write;可以用dup复制它;在调用了fork后,父进程和子进程可以共享它;可以调用fcntl来改变它的属性;可以调用colse来关闭它;等等。在我们的例子中可以看到插口描述符是函数sendto和recvfrom的第一个参数。当程序终止时(通过调用exit),所有打开的描述符,包括插口描述符都会被内核关闭。
我们现在介绍在进程调用socket时被内核创建的数据结构。在后面的几章中会更详细地描述这些数据结构。
首先从进程的进程表表项开始。在每个进程的生存期内都会有一个对应的进程表表项存在。
一个描述符是进程的进程表表项中的一个数组的下标。这个数组项指向一个打开文件表结构,这个结构又指向一个描述此文件的i-node或v-node结构。图1-4说明了这种关系。
图1-4 从一个描述符开始的内核数据结构的基本关系
在这个图中,我们还显示了一个涉及插口的描述符,它是本书的焦点。由于进程表表项是由以下C语言定义的,我们把记号proc{}放在进程表表项的上面。并且在本书所有的图中都用它来标注这个结构。
struct proc {
...
}
[Stevens 1992,3.10节]显示了当进程调用dup和fork时描述符、文件表结构和i-node或v-node之间的关系是如何改变的。这三种数据结构的关系存在于所有版本的Unix中,但不同的实现细节有所变化。在本书中我们感兴趣的是socket结构和它所指向的Internet专用数据结构。但是既然插口系统调用以一个描述符开始,我们就需要理解如何从一个描述符导出一个socket结构。
如果程序如此执行:
a.out
不重定向标准输入(描述符0)、标准输出(描述符1)和标准错误处理(描述符2),图1-5显示了程序示例中的Net/3数据结构的更多细节。在这个例子中,描述符0、1和2连接到我们的终端,并且当socket被调用时未用描述符的最小编号是3。
当进程执行了一个系统调用,如socket,内核就访问进程表结构。在这个结构中的项p_fd指向进程的filedesc结构。在这个结构中有两个我们现在关心的成员:一个是fd_ofileflags,它是一个字符数组指针(每个描述符有一个描述符标志);一个是fd_ofiles,它是一个指向文件表结构的指针数组的指针。描述符标志有8位,只有2位是任何描述符都可设置的:close-on-exec标志和mapped-from-device标志。在这里我们显示的所有标志都是0。
由于Unix描述符与很多东西有关,除了文件外,还有插口、管道、目录、设备等等,因此,我们有意把本节叫作“描述符”而不是“文件描述符”。但是很多Unix文献在谈到描述符时总是加上“文件”这个修饰词,其实没有必要。虽然我们要说明的是插口描述符,但这个内核数据结构叫filedesc{}。我们尽可能使用描述符这个未加修饰的术语。
项fd_ofiles指向的数据结构用*file{}[]表示。它是一个指向file结构的指针数组。这个数组及描述符标志数组的下标就是描述符本身—0、1、2等等,是非负整数。在图1-5中我们可以看到描述符0、1、2对应的项指向图底部的同一个file结构(由于这三个描述符都对应终端设备)。描述符3对应的项指向另外一个file结构。
结构file的成员f_type指示描述符的类型是DTYPE_SOCKET和DTYPE_VNODE。v-node是一个通用机制,允许内核支持不同类型的文件系统—磁盘文件系统、网络文件系统(如NFS)、CD-ROM文件系统、基于存储器的文件系统等等。在本书中关心的不是v-node,因为TCP/IP插口的类型总是DTYPE_SOCKET。
结构file的成员f_data指向一个socket结构或者一个vnode结构,根据描述符类型而定。成员f_ops指向一个有5个函数指针的向量。这些函数指针用在read、readv、write、writev、ioctl、select和close系统调用中,这些系统调用需要一个插口描述符或非插口描述符。这些系统调用每次被调用时都要查看f_type的值,然后做出相应的跳转,实现者选择了直接通过fileops结构的相应项跳转的方式。
我们用等宽字体(fo_read)来醒目地表示结构成员的名称,用斜体等宽字体(soo_read)来表示结构成员的内容。注意,有时我们用一个箭头指向一个结构的左上角(如结构filedesc),有时用一个箭头指向一个结构的右上角(如结构file和fileops)。我们用这些方法来简化图例。
下面我们来查看结构socket,当描述符的类型是DTYPE_SOCKET时,结构file指向结构socket。在我们的例子中,socket的类型(数据报插口的类型是SOCK_DGRAM)保存在成员so_type中。还分配了一个Internet协议控制块(PCB):一个inpcb结构。结构socket的成员so_pcb指向inpcb,并且结构inpcb的成员inp_socket指向结构socket。对于一个给定插口,操作可能来自“上”或“下”两个方向,因此需要有指针来互相指向。
1) 当进程执行一个系统调用时,如sendto,内核从描述符值开始,使用fd_ofiles索引到file结构指针向量,直到描述符所对应的file结构。结构file指向socket结构,结构socket带有指向结构inpcb的指针。
2) 当一个UDP数据报到达一个网络接口时,内核搜索所有UDP协议控制块,寻找一个合适的,至少要根据目标UDP端口号寻找,可能还要根据目标IP地址、源IP地址和源端口号寻找。一旦定位了所找的inpcb,内核就能通过inp_socket指针来找到相应的socket结构。
成员inp_faddr和inp_laddr包含远地和本地IP地址,而成员inp_fport和inp_lport包含远地和本地端口号。IP地址和端口号的组合经常叫作插口。
在图1-5的左边,我们用名称udb来标注另一个inpcb结构。这是一个全局结构,它是由所有UDP PCB组成的链表表头。我们可以看到两个成员inp_next和inp_prev把所有的UDP PCB组成了一个双向环形链表。为了简化此图,我们用两条平行的水平箭头来表示两条链,而不是用箭头指向PCB的顶角。右边的inpcb结构的成员inp_prev指向结构udb,而不是它的成员inp_prev。来自udb.inp_prev和另一个PCB成员inp_next的虚线箭头表示这里还有其他PCB在这个双向链表上,但我们没有画出。
在本章我们已看到不少内核数据结构,其中大多数还要在后续章节中说明。现在要理解的关键是:
1) 我们的进程调用socket,最后分配了最小未用的描述符(在我们的例子中是3)。在后面,所有针对此socket的系统调用都要用这个描述符。
2) 以下内核数据结构是一起被分配和链接起来的:一个DTYPE_SOCKET类型file结构、一个socket结构和一个inpcb结构。这些结构的很多初始化过程我们并没有说明:file结构的读写标志(因为调用socket总是返回一个可读或可写的描述符);默认的输入和输出缓存大小被设置在socket结构中;等等。
3) 我们显示了标准输入、输出和标准错误处理等非插口描述符,目的是说明所有描述符最后都对应一个file结构,虽然插口描述符和其他描述符之间有所不同。
- 点赞
- 收藏
- 关注作者
评论(0)