Friday, May 18, 2007

Monolithic kernel and Interfaces - The Linux Networking Architecture

Since Version 2.0, Linux has made a step towards microkernel architectures. More specifically, the possibility was created of moving certain functionalities into modules, which are loaded into the kernel at runtime, from which they can be removed again. This removed an important drawback of monolithic kernels and opened the way to loading drivers or other functionalities at runtime. In addition, modularization offers another benefit: Uniform interfaces are defined.

Table 2-1. Interfaces in the Linux kernel to embed new functionalities.

Functionality

Functions for Dynamic Registration

Character devices

(un)register_chrdev( )

Block devices

(un)register_blkdev( )

Binary formats

(un)register_binfmt( )

File systems

(un)register_filesystem( )

Serial interfaces

(un)register_serial( )

Network adapters

(un)register_netdev( )

Layer-3 protocols

dev_add_pack( ), dev_remove_pack( )

Layer-4 protocols (TCP/IP)

inet_add_protocol( ), inet_del_protocol( )

Console drivers

tty_(un)register_driver( )

Symbol tables

(un)register_symtab( )

Modules

init_module( ), cleanup_module( )


Despite its modularization, Linux has preserved a major benefit of monolithic kernels: All functions implemented in modules run in protected kernel mode, which means that they do not require any context change when called from within the kernel. This can be seen as a clever combination of the benefits from both main operating-system architectures.


Linux 2.4

Huge screenshots using VNC - new.linuxfocus.org

Huge screenshots using VNC

Abstract:

Being between a "tip" and an article in length, this will show you how to produce gigantic screenshots using a VNC virtual desktop. No big magic involved, so it's rather a hint for those who don't know yet the possibilities of VNC.

_________________ _________________ _________________

Motivation

It's a commonplace that an image (or screenshot) says more than thousand words. But imagine that the screenshot you want to make spans multiple pages on your screen. A large graph for example that cannot be exported to a PostScript or PDF.

In my particular case, I wanted to show how I corrected a text, so I wanted the other person to see the changes I've made. Now, that's the classical case for a diff, but what if the partner doesn't know how to read it, or even more, if he got a system without sophisticated graphical diff tools like TkDiff or KDiff3 [1] ? So, I'd prefer to send a screenshot of my nice KDiff3 view – but the window has more than five screen pages, you have to scroll down vertically. Making five screenshots and assembling them in the GIMP would be one possibility. Creating a huge virtual screen and making one screenshot of that huge screen is another, which I'll proceed to describe here.

What is VNC?

I don't want to repeat the good introductions to Virtual Network Computing that exist on the web [2]. In short, a desktop (screen output, mouse and keyboard input) is shared over the network - the computer whose desktop you want to share runs the VNC server, and the computer where you want to see and control the other's desktop inside a window runs the VNC client. Originally developed by the University of Cambridge, UK and AT&T, the VNC headquarter is now at the UK company RealVNC [3]. Fortunately, there are not only free software implementations of the VNC protocol, but it's also cross-platform, so you can control a Mac OS X desktop from a GNU/Linux box and vice versa. And there are many tools and ideas around VNC.

On Microsoft Windows and Mac OS X, you only have one single desktop, so you share your current one; on Unix and GNU/Linux you usually create another desktop (though there are programs to share your current one [4]), the same way as you can have multiple X servers on the same machine. In fact, the new VNC desktop is a X desktop "envelopped" by VNC. If your machines are firewalled or if you want to secure your communications, you can tunnel the VNC connection through SSH (provided that the machine you are connecting to hosts a SSH server) [5].

VNC vs. X11

As you probably know, you can also transfer single X11 windows across the net, having the application run on machine A and see the windows on machine B. The main differences of VNC are:

  • VNC hosts a whole desktop (usually including a window manager)
  • With VNC, if the connection drops, you just can reattach your client. Nothing is lost, like a "screen" session. This is more difficult if a X11 connection drops.
  • There are different modes of data transfer; if your connection is slow, you can reduce the colour depth. Compression is also possible, minimising the data transferred.
  • I found that many applications seem to be more responsive, as they don't wait for me to see the button before they are ready to accept input. However, mapping between different keyboard layouts on server and client can be puzzling. So it may be that you get a better experience with some applications using X11.

Installation, configuration and start of the VNC server

Each major distribution provides packages for VNC server and client. If you're lucky, you already have commands like vncserver or xvncviewer. On Debian and friends, you can get it as usual, being the superuser:

apt-get install vncserver xvncviewer
I'm doing this on a Debian machine, but there shouldn't be major differences between distributions.

Normally, you'd just say:

vncserver :1 -localhost
and you have a new X server running on display :1, accessible by connecting to the VNC server who listens on port 5900 + display number, so in this case on port 5901. Because of the -localhost parameter, you can only connect locally or by using SSH.

You can connect to this server with the client:

xvncviewer :1
If you have secured your VNC connection with a password (using vncpasswd), you have to enter it now. Then, your desktop should appear in a window.

However, probably due to my individual startup scripts in ~/.xsession, the window manager did not show up in the client (or worse, he would continuously create new error windows). So I had to exit the vncserver:

vncserver -kill :1
and to create a file ~/.vnc/xstartup with just a minimal configuration (exec icewm as the only line). That's the configuration for the X server started by VNC, not the VNC configuration itself! That one is located in /etc/vnc.conf and ~/.vncrc. I had to write one line in the latter in order to actually use the xstartup I just created, so my ~/.vncrc looks like:
$vncStartup = "~/.vnc/xstartup";

Now, we actually want to do our huge screenshot, so start the VNC server as big as you need. I find it nice to have the width slightly smaller than your actual screen width, in order to have only one direction to scroll. But maybe you need a larger width for your application.

vncserver :1 -localhost -depth 16 -geometry 950x5000
Connect to it as before (xvncviewer :1).

Capturing the VNC screen

The rest is quite easy: Start your application whose window you want to capture, and make it look the way you want. For capturing the screen, I'm always lazy and use the well-known GIMP image editor. This time, of course, you have to start it inside the VNC desktop, just as your application. Then you can do "File -> Acquire -> Screen Shot..." as normal. You'll probably want just to save the output and do later editing in your normal desktop, to avoid scrolling. Sometimes you have to scroll quite a bit until you find the window that just opened!

Hardware requirements

Don't think that you need lots of video memory in order to do this. My machine is five years old, and the graphics card is even ten years old (4 MB RAM). You just have to wait a little bit, that's all.

My result

This is just an example, I'm comparing two versions of the same section in the German Debian-installer manual in KDiff3. Click on it to view the original resolution.

References

All checked on 15th November 2006.

[1] Two of many graphical diff utilities:
TkDiff: http://tkdiff.sourceforge.net
KDiff3: http://kdiff3.sourceforge.net

[2] VNC in Wikipedia:
http://en.wikipedia.org/wiki/VNC
Some background information, by Tim Waugh for the Red Hat Magazine:
http://cyberelk.net/tim/articles/VNC/index.html

[3] RealVNC homepage:
http://www.realvnc.com

[4] x11vnc (one long page):
http://www.karlrunge.com/x11vnc/
Also, RealVNC version 4 and above claims to be able to share the current X11 session.

[5] Jason's Web Thingy: Securing a VNC connection with OpenSSH
http://www.trekweb.com/~jasonb/articles/vnc_ssh.shtml
From the old AT&T/Cambridge VNC pages: Making VNC more secure using SSH
http://www.cl.cam.ac.uk/research/dtg/attarchive/vnc/sshvnc.html


Thursday, May 17, 2007

What does C(stamp) means? - Ubuntu Forums

Hi, I'm learning Linux Kernel(2.4). I found something like this in source code.
C(stamp);
C(dev);
C(h);
C(nh);
C(mac);
C(dst);
Who can teach me What these mean? Thanks

Code:
/usr/src/linux-source-2.6.20$ grep -nr "\WC(" net
net/core/skbuff.c:449:#define C(x) n->x = skb->x
net/core/skbuff.c:453: C(tstamp);
net/core/skbuff.c:454: C(dev);
net/core/skbuff.c:455: C(h);
net/core/skbuff.c:456: C(nh);
net/core/skbuff.c:457: C(mac);
net/core/skbuff.c:458: C(dst);
net/core/skbuff.c:460: C(sp);
net/core/skbuff.c:465: C(len);
net/core/skbuff.c:466: C(data_len);
net/core/skbuff.c:467: C(csum);
net/core/skbuff.c:468: C(local_df);
net/core/skbuff.c:471: C(pkt_type);
net/core/skbuff.c:472: C(ip_summed);
net/core/skbuff.c:473: C(priority);
net/core/skbuff.c:475: C(ipvs_property);
net/core/skbuff.c:477: C(protocol);
net/core/skbuff.c:479: C(mark);
net/core/skbuff.c:481: C(nfct);
net/core/skbuff.c:483: C(nfctinfo);
net/core/skbuff.c:485: C(nfct_reasm);
net/core/skbuff.c:489: C(nf_bridge);
net/core/skbuff.c:494: C(tc_index);
net/core/skbuff.c:499: C(input_dev);
net/core/skbuff.c:503: C(truesize);
net/core/skbuff.c:505: C(head);
net/core/skbuff.c:506: C(data);
net/core/skbuff.c:507: C(tail);
net/core/skbuff.c:508: C(end);
Does that help?

[经典收藏]Linux内核源代码漫游 - CSDNBlog

inux内核源代码漫游

Alessandro Rubini, rubini@pop.systemy.it

赵炯 译,gohigh@sh163net, (www.plinux.org)

本章试图以顺序的方式来解释Linux源代码,以帮助读者对源代码的体系结构以及很多相关的unix特性的实现有一个很好的理解。目标是帮助对Linux不甚了解的有经验的C程序员对整个Linux的设计有所了解。这也就是为什么内核漫游的入点选择为内核本身的启始点:系统引导(启动)。

这份材料需要对C语言以及对Unix的概念和PC机的结构有很好的了解,然而本章中并没有出现任何的C代码,而是直接参考(指向)实际的代码的。有关内核设计的最佳篇幅是在本手册的其它章节中,而本章仍趋向于是一个非正式的概述。

本章中所参阅的任何文件的路径名都是指主源代码目录树,通常是/usr/src/linux。

这里所给出的大多数信息都是取之于Linux发行版1.0的源代码。虽然如此,有时也会提供对后期版本的参考。这篇漫游中开头有 图标的任何小节都是强调1.0版本后对内核的新的改动。如果没有这样的小节存在,则表示直到版本1.0.9-1.1.76,没有作过改动。

有时候本章中会有象这样的小节,这是指向正确的代码以对刚讨论过的主题取得更多信息的指示符。当然,这里是指源代码

引导(启动)系统

PC 的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC机 的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区读入内存地址0x7C00处,并跳转到这个地方。 启动设备通常是软驱或是硬盘。这里的叙述是非常简单的,但这已经足够理解内核初始化的工作过程了。

Linux 的最最前面部分是用8086汇编语言编写的(boot/bootsect.S),它将由BIOS读入到内存0x7C00处,当它被执行时就会把自己移到绝 对地址0x90000处,并将启动设备(boot/setup.S)的下2kB字节的代码读入内存0x90200处,而内核的其它部分则被读入到地址 0x10000处。在系统加载期间将显示信息"Loading..."。然后控制权将传递给boot/Setup.S中的代码,这是另一个实模式汇编语言 程序。

启动部分识别主机的某些特性以及vga卡的类型。如果需要,它会要求用户为控制台选择显示模式。然后将整个系统从地址0x10000移至0x1000处,进入保护模式并跳转至系统的余下部分(在0x1000处)。

下一步是内核的解压缩。0x1000 处的代码来自于zBoot/head.S,它初始化寄存器并调用decompress_kernel(),它们依次是由zBoot/inflate.c、 zBoot/unzip.c和zBoot/misc.c组成。被解压的数据存放到了地址0x10000处(1兆),这也是为什么Linux不能运行于少于 2兆内存的主要原因。[在1兆内存中解压内核的工作已经完成,见 Memory Savers--ED]

将内核封装在一个gzip文件中的工作是由zBoot目录中的Makefile以及工具完成的。它们是值得一看的有趣的文件。

内核发行版1.1.75将boot和zBoot目录下移到了arch/i386/boot中了,这个改动意味着对不同的体系结构允许真正的内核建造,不过我将仍然只讲解有关i386的信息。

解压过的代码是从地址0x10100处开始执行的[这里我可能忘记了具体的物理地址了,因为我对相应的代码不是很熟],在那里,所有32比特的设置启动被完成: IDT、GDT以及LDT被加载,处理器和协处理器也已确认,分页工作也设置好了;最终调用start_kernel子程序。上述操作的源代码是在boot/head.S中的,这可能是整个内核中最有诀窍的代码了。

注意如果在前述任何一步中出了错,计算机就会死锁。在操作系统还没有完全运转之前是处理不了出错的。

start_kernel()是位于init/main.c中的,并且没有任何返回结果。从现在起的任何代码都是用C语言编制的,除了中断管理和系统调用的入/出代码(当然,还有大多数的宏都嵌入了汇编代码)。

让轮子转动起来

在处理了所有错综复杂的问题之后,start_kernel()初始化了内核的所有部分,尤其是:

  • 设置内存边界和调用paging_init();
  • 初始化中断、IRQ通道和调度;
  • 分析(解析)命令行;
  • 如果需要,就分配一个数据缓冲区(profiling buffer)以及其它一些小部分;
  • 校正延迟循环(计算“BogoMips”数);
  • 检查中断16是否能与协处理器工作。

最后,为了生成初始进程,内核准备好了移至move_to_user_mode(),它的代码也是在同一个源代码文件中的。然后,所谓的空闲任务,进程号0就进入无限的空闲循环中运行。

接着初始进程(init process)尝试着运行/etc/init、/bin/init或者/sbin/init。

如果它们没有一个运行成功的,就会去执行代码“/bin/sh /etc/rc”并且在第一个终端上生成一个根命令解释程序(root shell)。这段代码回溯至Linux 0.01,当时操作系统只有一个内核,并且没有登录进程。

在从一个标准的地方(让我们假定我们有)用exec ()执行了init初始化程序之后,内核就对程序的执行没有了直接的控制。从现在起它的规则是提供对系统调用的处理,以及为异步事件服务(比如硬件中断 等)。多任务的环境已经建立,从现在起是init程序通过fork()派生出的系统进程和登录进程来管理多用户的访问了。

由于内核是负责提供服务的,这个漫游文章将通过观察这些服务(“系统调用”)以及通过提供基本数据结构的原理和代码的组织结构继续讨论下去。

内核是如何看见一个进程的

从内核的观点来看,一个进程只是进程表中的一个条目而已。

而进程表以及各个内存管理表和缓冲存储器则是系统中最为重要的数据结构。进程表中的各个单项是task_struct结构,是定义在include/linux/sched.h中的非常大的数据结构。在task_struct中保留着从低层到高层的信息,范围从某些硬件寄存器的拷贝到进程工作目录的inode信息。

进程表既是一个数组和双链表,也是一个树结构。它的物理实现是一个静态的指针数组,它的长度是定义在include/linux/tasks.h 中的常量NR_TASKS,并且每个结构都位于一个保留内存页中。这个列表结构是通过指针next_task和pre_task构成的,而树结构则是非常 复杂的并且我们在此将不加以讨论。你可能希望改动NR_TASKS的默认值128,但你要保证所有源文件中相关的适当文件都要被重新编译过。

在启动引导过程结束后,内核将总是代表某个进程而工作,并且全局变量current --- 一个指向某个task_struct条目的指针 --- 被用于记录正在运行的进程。current仅能通过在kernel/sched.c中的调度程序来改变。然而,由于所有的进程都必须访问它,所以使用了宏 for_each_task。当系统负荷很轻时,它要比数组的顺序扫描快得多。

进程总是运行于“用户模式”或“内核模式”。用户程序的主体是运行于用户模式而其中的系统调用则运行于内核模式中。在这两种执行模式中进程所用的堆栈是不一样的 -- 常规的堆栈段用于用户模式,而一个固定大小的堆栈(一页,由该进程所有)则用于内核模式。内核堆栈页是从不交换出去的,因为每当一个系统调用进入时它就必须存在着。

内核中的系统调用(system calls)是作为C语言函数存在的,它们的‘正规’名称是以‘sys_’开头的。例如一个名为burnout的系统调用将调用内核函数sys_burnout()

系统调用机制在本手册的第三章中进行了讨论。观看在include/linux/sched.h中的for_each_task和SET_LINKS能够帮助理解进程表中的列表和树结构。

创建和结束进程

unix 系统是通过fork()系统调用创建一个进程的,而进程的终止是通过exit()或收到一个信号来完成的。它们的Linux实现位于 kernel/fork.c和kernel/exit.c中。派生出一个进程是很容易的,所以fork.c程序很短并易于理解。它的主要任务是为新的进程 填写数据结构。除了填写各个字段以外,相关的步骤有:

l 取得一个空闲内存页面来保存task_struct

l 找到一个空闲的进程槽(find_empty_process())

l 为内存堆栈页kernel_stack_page取得另一个空闲的内存页面

l 将父辈的LDT拷贝到子进程

l 复制父进程的mmap信息

sys_fork() 同样也管理文件描述符和inode。

1.0的内核也对线程提供某些不够完善的支持,所以fork()系统调用对此也给出了某些示意。内核的线程是主流内核以外的过程产品。

从一个进程中退出是比较有窍门的,因为父进程必须被通告有关任何子进程的退出。而且,一个进程可以由另外一个进程使用kill()而退出(这些是Unix的特性),所以除了sys_exit()之外,sys_kill()以及sys_wait()的各种特性也存在于exit.c之中了。

这里不对exit.c的代码加以讨论---因为它一点也不令人感兴趣。为了以一致的状态退出系统,它涉及到许多细节。而POSIX标准对于信号则是要求相当严格的,所以这里必须对其加以叙述。

执行程序

在调用了fork ()之后,就有同一个程序的两个拷贝在运行了,通常一个程序使用exec()执行另一个程序。exec()系统调用必须定位该执行文件的二进制映像,加载 并执行它。词语‘加载’并不一定意味着“将二进制映像拷贝进内存”,因为Linux支持按需加载。 exec()的Linux实现支持不同的二进制格式。这是通过linux_binfmt结构来达到的,其中内嵌了两个指向函数的指针--一个是用于加载可 执行文件的,另一个用于加载库函数,每种二进制格式都实现有这两个函数。共享库的加载是在exec()同一个源程序中实现的,但我们只讨论exec()本 身。 Unix系统提供了六种exec()函数。除了一个以外,所有都是以库函数的形式实现的,并且,Linux内核是单独实现sys_execve()调用 的。它执行一个非常简单的任务:加载可执行文件的头部,并试着去执行它。如果头两个字节是“#!”,那么就会解析该可执行文件的第一行并调用一个解释器来 执行它,否则的话,就会顺序地试用各个注册过的二进制格式。 Linux本身的格式是由fs/exec.c直接支持的,并且相关的函数是load_aout_binary和load_aout_library。对于 二进制,函数将加载一个“a.out”可执行文件并以使用mmap()加载磁盘文件或调用read_exec()而结束。前一种方法使用了Linux的按 需加载机理,在程序被访问时使用出错加载方式(fault-in)加载程序页面,而后一种方式是在主机文件系统不支持内存映像时(例如“msdos”文件 系统)使用的。

新近的1.1内核内嵌了一个修订的msdos文件系统,它支持mmap()。而且linux_binfmt结构已是一个链表而不是一个数组了,以允许以一个内核模块的方式加载一个新的二进制格式。最后,结构的本身也已经被扩展成能够访问与格式相关的核心转储程序了。

访问文件系统

众所周知,文件系统是Unix系统中最为基本的资源了,它如此的基本和普遍存在以至于它需要一个更为便利的名字--我将忠于标准的称呼简单地称之为“fs”。

我将假设读者早已知道基本的Unix文件系统的原理--访问(权限)许可、i节点(inode)、超级块、加载(mount)和卸载(umount)文件系统。这些概念在标准的Unix文献中由比我聪明的作者给出了很好的解释,所以我就不重复他们的工作并且我将只专注于有关Linux方面的问题。

早期的Unix 通常只支持一个文件系统(fs)类型,它的代码散布于整个内核中,现今的实现是在内核和fs之间使用一个标准的接口,以便于在不同的体系结构中进行数据的 交换。Linux本身提供了一个标准层以在内核和每种fs模块之间传递数据。这个接口层称为VFS,即“虚拟文件系统”("virtual filesystem")。

因而文件系统的代码被分割成了两层:上层是关于内核表格的管理和数据结构的,而低层是由与各文件系统相关的函数集构成的,并且是由VFS数据结构进行调用的。

所有与文件系统独立的资料都位于fs/*.c文件中。它们涉及如下的问题:

· 管理缓冲寄存器(buffer.c);

· fcntl()和ioctl()系统调用作出响应(fcntl.c和ioctl.c);

· inode和缓冲区上映射管道和fifo(fifo.c,pipe.c);

· 管理文件 - 和inode - 表(file_table.c,inode.c);

· 锁定和解锁文件和记录(lock.c);

· 将名称映射到inode(namei.c,open.c);

· 实现错综复杂的select()函数(select.c);

· 提供信息(stat.c);

· 加载和卸载文件系统(super.c);

· 使用exec()执行可执行程序以及转储核心程序(exec.c);

· 加载各种二进制格式(bin_fmt*.c,如上面所述)。

VFS 接口则由一组相对比较高层次的操作组成,并从与文件系统独立的代码中调用而实际上是由每种文件系统类型执行的。最为相关的数据结构是 inode_operations和file_operations,尽管它们不是独自存在的:同样存在着其它一些数据结构。它们都定义在 include/linux/fs.h文件中。

到实际文件系统的内核入口点是数据结构file_system_type。 file_system_types的一个数组包含在fs/filesystems.c中,并且每当发出了一个加载(mount)命令时都会引用它。然 后,相应fs类型的函数read_super就负责填写结构super_block的一个项,而该项又内嵌了结构super_struct和结构 type_sb_info。前者为当前的fs类型提供了指向一般fs操作的指针,而后者对相应fs类型内嵌了特定的信息。

文件系统类型数组已经转换成了一个链表,以允许用内核模块的形式加载新的fs类型。函数(un-)register_filesystem代码包含在fs/super.c中。

一个文件系统类型的快速剖析

一个文件系统类型的任务是执行用于映射相应高层VFS操作到物理介质(磁盘、网络等等)的低层任务。VFS接口有足够的灵活性来支持传统的Unix文件系统和外来的象msdos和umsdos文件系统类型。

每一个fs类型除了它自己的源代码目录以外,是由下列各项组成的:

· file_systems[]数组中的一个条目(项) (fs/filesystems.c);

· 超级块(superblock)的include文件(include/linux/type_fs_sb.h);

· i节点(inode)的include文件(include/linux/type_fs_i.h);

· 普通自己专用的include文件(include/linux/type_fs.h);

· include/linux.fs.h中的两行#include,以及在结构super_block和inode中的条目。

对于特定fs类型自己的目录,包含有所有的实际代码、inode和数据的管理程序。

本手册中有关procfs的章节,揭示了所有有关那种fs类型的低层代码和VFS接口。在阅读过那个章节之后,fs/procfs中的源代码就显得非常容易理解了。

现在我们来观察VFS 机制的内部工作情况,并以minix文件系统的代码作为一个实际例子。我选择minix类型是因为它比较短小但却是完整的;而且,Linux中的所有其它 的fs类型都衍生于它。在最近Linux安装中的事实上的标准文件系统类型ext2,要比它复杂得多,对ext2这个文件系统的探索就留给聪明的读者作为 一个练习了。

当一个minix-fs被加载后,minix_read_super就会把从被加载的设备中读取的数据添入super_block数据结构中。此时,该结构中的s_op域将保留有一个指向minix_sops的指针,该指针将被一般文件系统代码用于分派超级块的操作。

在全局系统树结构中链接新加载的fs依赖于下列各数据项(假设sb是超级块数据结构,而dir_i是指向加载点的inode的指针):

· sb->s_mounted指向被加载文件系统的根目录i节点(MINIX_ROOT_INO);

· dir_i->i_mount保存有sb->s_mounted;

· sb->s_covered保存有dir_i

卸载操作将最终通过do_umount来执行,而它会依次调用minix_put_super。

每当访问一个文件时,minix_read_inode 就会开始执行;它会使用minix_inode各字段中的数据填写系统范围的inode数据结构。inode->i_op字段是依照inode- >i_mode来填写的,它将负责该文件的任何其它操作。上述minix函数的代码可以从fs/minix/inode.c中找到。

inode_operations 数据结构是用于把inode操作分派给特定fs类型的内核函数;该数据结构的第一项是一个指向file_operations项的指针,它等同于数据管理 的i_op。minix文件系统类型允许有inode操作集中的三种方式(用于目录、文件和符号链接)和文件操作集中的两种(符号链接不需要文件操作)。

目录操作(仅minix_readdir)位于fs/minix/dir.c中;文件操作(读read和写write)位于fs/minix/file.c中而符号操作(读取并跟随着链)位于fs/minix/symlink.c。

minix源代码目录中的其余部分用于实现以下任务:

· bitmap.c用于管理i节点与块的分配和释放(而ext2文件系统却有两个不同的代码文件);

· fsynk.c用于fsync()系统调用--它管理直接、间接和双重间接块(我假定你是知道这些术语的,因为这是Unix的普通知识);

· namei.c内嵌有所有与名字有关的i节点的操作,比如象节点的创建和消除、重命名和链接;

· truncate.c执行文件的截断操作。

控制台驱动程序(console driver

作为大多数Linux系统上的主要I/O设备,控制台驱动程序是应该受到某些关注的。有关控制台和其它字符驱动程序的源代码可以在drivers/char中找到,当我们指称文件时,我们将使用这个特定的目录。

控制台的初始化是由tty_io.c中的tty_init()函数来执行的。这个函数仅仅涉及取得每个设备集的主设备号并调用每个设备集的init函数。而con_init()则是与控制台相关的函数,并存在于console.c中。

在 内核1.1的开发中,控制台的初始化已经有了很大的变化。console_init()已经从tty_init()中脱离出来了,并且是由../.. /main.c直接调用的。现在虚拟控制台是动态分配的,其代码也已有了很大的变化。所以我将跳过初始化、分配等等的详细讨论。

文件操作是如何分派给控制台的

这一节是相当底层的讨论,你可以放心地跳过本节。

毫无疑问,Unix设备是通过文件系统来访问的。本节将详细描述从设备文件到实际控制台函数的所有步骤,而且,以下的信息是从内核的1.1.73源代码中抽取来的,它与1.0的代码可能少许有点不同。

当打开一个设备i 节点时,在../../fs/devices.c中的chrdev_open()函数(或者是blkdev_open(),但我只专注于字符设备)将被执 行。这个函数是通过数据结构def_chr_fops取得的,而它又是被chrdev_inode_operations引用的,是被所有文件系统类型使 用的(见前面有关文件系统的部分)。

chrdev_open 通过在当前操作中替换具体设备的file_operations表并且调用特定的open()函数来管理指定的设备操作的。具体设备的表结构是保存在数组 chrdevs[]中的,并由主设备号作为索引,位于同一个../../devices.c中。

如果该设备是一个tty 类型的(我们不是只关注控制台吗?),我们就来讨论tty的设备驱动程序,它们的函数在tty_io.c之中,由tty_fops作为索引。这样, tty_open()就会调用init_dev(),而init_dev()就会根据次设备号为设备分配任何所需的数据结构。

次设备号也用于检索已经使用tty_register_driver ()注册登记过的设备的实际驱动程序。而且,该驱动程序仍是另一个用于分派计算的数据结构,正如file_ops一样;它是与设备的写操作和控制有关的。 最后一个用于管理tty的数据结构是线路规程,这将在后面叙述。控制台(以及任何其它的tty设备)的线路规程是由 initialize_tty_struct()设置的,并由init_dev调用的。

在这一节中我们所涉及的所有事情都是与设备无关的,仅有与特定控制台相关的是console.c,在con_init()操作期间已经注册了自己的驱动程序。相反,线路规程是与设备无关的。

The tty_driver 数据结构在中有着完整的描述。

上述信息是从1.1.73源代码中取得的。它是有可能与你的内核有所不同的(“如信息有所变动将不另行通知”)。

控制台写操作

当往一个控制台设备进行写操作时,就会调用con_write 函数。这个函数管理所有控制字符和换码字符序列,这些字符给应用程序提供全部的屏幕管理操作。所实现的换码序列是vt102终端的;这意味着当你使用 telnet连接到一台非Linux主机时,你的环境变量应该有TERM=vt102;然而,对于本地操作最佳的选择是设置TERM=console,因 为Linux控制台提供了一个vt102功能的超集。

因而,con_write ()主要是由转换语句组成的,用于处理每一次一个字符的有限长状态自动换码序列的解释。在正常方式下,所打印的字符是使用当前属性直接写到显示内存中的。 在console.c中,数据结构vc的所有域使用宏都是可访问的,所以(例如)任何对attr的引用,只要currcons是所指的控制台的号码,确实 是引证了数据结构vc_cons[currcons]中的域。

实际上,新内核中的vc_cons已不再是一个数据结构数组了,现在它是指针的数组,其内容是用kmalloc()操作的。宏的使用大大地简化了代码修改的工作,因为许多代码都不需要被重写。

控制台内存到屏幕内存的实际映射和非映射是由函数set_scrmem ()(它把控制台缓冲区中的数据拷贝到显示内存中)和get_srcmem()(它把数据拷贝回控制台缓冲区中)执行的。为了减少数据传输的次数,当前控 制台的私有缓冲区是物理地映射到实际显示RAM上的。这意味着console.c中的get-和set-_scrmem()是静态的,并且仅在一个控制台 转换期间才被调用。

控制台读操作

控制台读操作是由线路规程来完成的。Linux 中默认的(也是唯一的)线路规程被称为tty_ldisc_N_TTY。线路规程也就是“通过一线路约束输入”。它是另一个函数表(我们已习惯了这种方 法,不是吗?),它是有关于设备读操作的。在termios标志的帮助下,线路规程也即是从tty上控制输入的规程:未处理过的数据、cbreak和计划 的方式;select();ioctl()等等。

线路规程中的读(read)函数称为read_chan(),它读取tty的缓冲区而不管数据是从哪里来的。原因是通过一个tty来到的字符是由异步硬件中断管理的。

线路规程N_TTY也同样在tty_io.c中,尽管以后出的内核都使用一个不同的n_tty.c源程序。

控制台输入的最底层是键盘管理的一部分,因此它是在keyboard.c的keyboard_interrupt()中处理的。

键盘管理

键盘管理简直是一场噩梦。它限于文件keyboard.c中,里面充满了表示不同厂家键盘的各个键码的十六进制数。

我将不对keyboard.c进行深入讨论,因为其中没有与内核研究者有关的相关信息。

对于那些对Linux的键盘编程确实感兴趣的人,最好的方法是从keyboard.c的最后一行往回看起。最底层的细节是在该文件的上半部分。

转换当前控制台

当前控制台是通过使用函数change_console()来转换的,它位于tty_io.c中由keyboard.c和vt.c调用(前者响应按键的控制台转换,后者是当一个程序通过引用一个ioctl()调用时转换控制台)。

实际的转换过程是分两步来执行的,函数complete_change_console ()处理其中的第二部分。转换的分裂意味着在一个与控制着我们正在离开的tty的进程的可能的握手以后完成任务。如果控制台不在进程控制之下, change_console()就会自己调用complete_change_console()。进程需要足够的能力来成功地完成从图形到文本控制台 或从文本到图形控制台的转换,并且X服务器(例如)是其图形控制台的控制进程。

选择机制

“选择(selection)” 是Linux文本控制台的剪切(cut)与粘贴(paste)功能。这个技巧主要是由用户级的进程来处理的,它可以用selection或gpm的具体例 子说明。用户级的程序在控制台上使用ioctl()通知内核来加亮显示屏幕的一个区域。然后,被选择的文本被拷贝到一个选择缓冲区。该缓冲区是 console.c中的一个静态实体。粘贴文本操作是通过“手工地”将字符放入tty输入队列中完成的。整个选择机制是通过#ifdef受到保护的,所以 用户在内核配置期间可以禁用它以节省几千字节的内存。

选择是一个非常低级的功能,因而它工作是任何其它内核活动所看不见的。这意味着许多的#ifdef只是屏幕在以任何方式作修改之前简单地移动加亮部分。

新内核特性改善了选择的代码,鼠标指针的加亮可以与被选择的文本独立(内核1.1.23或更高)。而且,从1.1.73版起,被选择的文本使用了动态的缓冲区而不是静态的了,使得内核小了4KB。

使用ioctl()操作设备

ioctl ()系统调用是用户进程控制设备文件行为的入口点。Ioctl管理是从../../fs/ioctl.c中产生的,实际上sys_ioctl()就是在这 个ioctl.c中的。标准的ioctl请求就是在那里执行的,其它与文件相关的请求是由file_ioctl()处理的(在同一个源文件中),而其它任 何请求都分派给特定设备的ioctl()函数。

控制台设备的ioctl资料是位于vt.c中的,因为控制台驱动程序要将ioctl请求分派给vt_ioctl()。

上述信息是关于内核1.1.7x的。1.0内核是没有“驱动程序”表的,而且vt_ioctl()是直接由file_operations()表指向的。

Ioctl的资料确实是相当让人混淆的。有些请求是与设备相关的,而有些却是与线路规程相关的。我将试图对1.0和1.1.7x内核之间发生的任何事概要总结一下。

1.1.7x 系列内核有如下的特性:tty_ioctl.c只实现了线路规程请求(也就是n_tty_ioctl(),这是唯一在n_tty.c外面的n_tty函 数),而file_operations字段指向tty_io.c中的tty_ioctl()。如果请求号没有被tty_ioctl()解析出来,它就会 被传到tty->driver.ioctl或者,如果它失败时,就到tty->ldisc.ioctl。控制台的与驱动程序相关的资料可以从 vt.c中找到,而线路规程方面的资料则在tty_ioctl.c中。

1.0内核中,tty_ioctl()是在tty_ioctl.c中的并有一般tty的file_operations所指向。未被解析出的请求将用与1.1.7x相似的方法被传送到特定的ioctl函数或到线路规程代码去。

注意,在这两种情况中,TIOCLINUX 请求是在与设备无关的代码中的,这暗示着控制台选择操作可以通过ioctl对任何tty进行操作来设置(set_selection()总是在控制台前台 上操作的),而这是一个安全上的漏洞。这也是转移到一个更新的内核的很好理由,在新内核中,通过仅允许超级用户来处理选择弥补了这个漏洞。

有很多请求可以被发给控制台设备,而知道它们的最好方法是浏览源程序文件vt.c。

版权所有(c) 1994 Alessandro Rubini, rubini@pop.systemy.it

Linux Kernel 核心中文手册(10)--网络 - CSDNBlog

Networks (网络)
 
Linux 和网络几乎是同义词。实际上 Linux 是 Internet 或 WWW 的产物。它的开
发者和用户使用 web 交换信息、想法、代码而 Linux 自身也常用于支持一些组织的联
网需求。本章描述了 Linux 如何支持统称为 TCP/IP 的网络协议。
TCP/IP 协议设计用来支持连接在 ARPANET 上的计算机之间的通讯。 ARPANET 是美
国政府投资的一个美国的研究网络。 ARPANET 是一些网络概念的先驱,例如报文交换和
协议分层,让一种协议利用其它协议提供的服务。 ARPANET 于 1988 年退出,但是它的
后继者( NSF NET 和 Internet )发展的甚至更大。现在所知的 World Wide Web 是在
ARPANET 中发展的,它本身也是由 TCP/IP 协议支持的。 Unix 在 ARPANET 上大量使
用,第一个发布的网络版的 Unix 是 4.3BSD 。 Linux 的网络实现是基于 4.3BSD 的模
型,它支持 BSD socket (和一些扩展)和全系列的 TCP/IP 网络功能。选择这种编程
接口是因为它的流行程度,而且可以帮助程序在 Linux 和其它 Unix 平台之间移植。


10.1 An Overview of TCP/IP Networking ( TCP/IP 网络概览)
本节为 TCP/IP 网络的主要原理给出了一个概览。这并不是一个详尽的描述。要更
详细的描述,阅读第 10 本参考书(附录)。
在一个 IP 网络中,每一个机器都分配一个 IP 地址,这是一个 32 位的数字,唯
一标识这一台机器。 WWW 是一个非常巨大、不断增长的 IP 网络,每一个连接在上面的
机器都分配了一个独一无二的 IP 地址。 IP 地址用点分隔的四个数字表示,例如, 1
6.42.0.9 。 IP 地址实际上分为两个部分:网络地址和主机地址。这些地址的大小(尺
寸)可能不同(有几类 IP 地址),以 16.42.0.9 为例,网络地址是 16.42 ,主机地
址是 0.9 。主机地址可以进一步划分成为子网( subnetwork )和主机地址。再次以
16.42.0.9 为例,子网地址可以是 16.42.0 ,主机地址为 16.42.0.9 。对于 IP 地址
进行进一步划分允许各个组织划分它们自己的网络。例如,假设 16.42 是 ACME 计算机
公司的网络地址, 16.42.0 可以是子网 0 , 16.42.1 可以是子网 1 。这些子网可以
在分离的大楼里,也许通过电话专线或者甚至通过微波连接。 IP 地址由网络管理员分
配,使用 IP 子网是分散网络管理任务的一个好办法。 IP 子网的管理员可以自由地分
配他们自己子网内的 IP 地址。
但是,通常 IP 地址难于记忆,而名字更容易记忆。 Linux.acme.com 比 16.42.0
.9 更好记。必须使用一种机制把网络名字转换为 IP 地址。这些名字可以静态地存在
/etc/hosts 文件中或者让 Linux 询问一个分布式命名服务器( Distributed Name Se
rver DNS )来解析名字。这种情况下,本地主机必须知道一个或多个 DNS 服务器的 I
P 地址,在 /etc/resolv.conf 中指定。
不管什么时候你连接另外一台机器的时候,比如读取一个 web page ,都要使用它
的 IP 地址和那台机器交换数据。这种数据包括在 IP 报文( packet )中,每一个报
文都有一个 IP 头(包括源和目标机器的 IP 地址,一个校验和和其它有用的信息。这
个校验和是从 IP 报文的数据中得到的,可以让 IP 报文的接收者判断传输过程中 IP
报文是否损坏(可能是一个噪音很大的电话线)。应用程序传输的数据可能被分解成容
易处理的更小的报文。 IP 数据报文的大小依赖于连接的介质而变化:以太网报文通常
大于 PPP 报文。目标主机必须重新装配这些数据报文,然后才能交给接收程序。如果你
通过一个相当慢的串行连接访问一个包括大量图形图像的 web 页,你就可以用图形的方
式看出数据的分解和重组。
连接在同一个 IP 子网的主机可以互相直接发送 IP 报文,而其它的 IP 报文必须
通过一个特殊的主机(网关)发送。网关(或路由器)连接在多于一个子网上,它们会
把一个子网上接收的 IP 报文重新发送到另一个子网。例如,如果子网 16.42.1.0 和
16.42.0.0 通过一个网关连接,那么所有从子网 0 发送到子网 1 的报文必须先发送到
网关,这样才能转发。本地的主机建立一个路由表,让它可以把要转发的 IP 报文发送
到正确的机器。对于每一个 IP 目标,在路由表中都有一个条目,告诉 Linux 要到达目
标需要先把 IP 报文发送到那一台主机。这些路由表是动态的,而且当应用程序使用网
络和网络拓扑变化的时候不断改变。
IP 协议是传输层协议,被其他协议使用,携带它们的数据。传输控制协议( TCP
)是一个可靠的端到端的协议,使用 IP 传送和接收它的报文。象 IP 报文有自己的头
一样, TCP 也有自己的头。 TCP 是一个面向连接的协议,两个网络应用程序通过一个
虚拟的连接连接在一起,甚至它们中间可能会有许多子网、网关和路由器。 TCP 在两个
应用程序之间可靠地传送和接收数据,并且保证不会有丢失和重复的数据。当 TCP 使用
IP 传送它的报文的时候,在 IP 报文中包含的数据就是 TCP 报文自身。每一个通讯的
主机的 IP 层负责传送和接收 IP 报文。用户数据报协议( UDP )也使用 IP 层传送它
的报文,但是不象 TCP , UDP 不是一个可靠的协议,它只提供数据报服务。其它协议
也可以使用 IP 意味着当接收到 IP 报文,接收的 IP 层必须知道把这个 IP 报文中包
含的数据交给哪一个上层协议。为此,每一个 IP 报文的头都有一个字节,包含一个协
议标识符。当 TCP 请求 IP 层传输一个 IP 报文的时候 IP 报文的头就说明它包含一个
TCP 报文。接收的 IP 层,使用这个协议标识符来决定把接收到的数据向上传递给哪一
个协议,在这种情况下,是 TCP 层。当应用程序通过 TCP/IP 通讯的时候,它们不但必
须指定目标的 IP 地址,也要指定目标应用程序的端口( port )地址。一个端口地址
唯一标识一个应用程序,标准的网络应用程序使用标准的端口地址:例如 web 服务器使
用端口 80 。这些已经注册的端口地址可以在 /etc/services 中查到。
协议分层不仅仅停留在 TCP 、 UDP 和 IP 。 IP 协议本身使用许多不同的物理介
质和其它 IP 主机传输 IP 报文。这些介质自己也可能增加它们自己的协议头。这样的
例子有以太网层、 PPP 和 SLIP 。一个以太网允许许多主机同时连接在一个物理电缆上
。每一个传送的以太帧可以被所有连接的主机看到,所以每一个以太网设备都有一个独
一无二的地址。每一个传送到那个地址的以太网帧会被那个地址的主机接收,而被连接
到这个网络的其它主机忽略掉。这个独一无二的地址当每一个以太网设备制造的时候内
建在设备里边,通常保存在以太网卡的 SROM 中。以太地址由 6 个字节长,例如,可能
是 08-00-2b-00-49-4A 。一些以太网地址保留用于多点广播,用这种目标地址发送的以
太网帧会被网络上的所有的主机接收。因为以太网帧中可能运载许多不同的协议(作为
数据),和 IP 报文一样,它们的头中都包含一个协议标识符。这样以太网层可以正确
地接收 IP 报文并把数据传输到 IP 层。
为了通过多种连接协议,例如通过以太网来传输 IP 报文, IP 层必须找出这个 I
P 主机的以太网地址。这是因为 IP 地址只是一个寻址的概念,以太网设备自己有自己
的物理地址。 IP 地址可以由网络管理员根据需要分配和再分配,而网络硬件则只响应
具有它自己物理地址的以太网帧,或者特殊的多点广播地址(所有的机器都必须接收)
。 Linux 使用地址解析协议( ARP )让机器把 IP 地址转换成真实的硬件地址例如以
太网地址。为了得到一个 IP 地址所联系的硬件地址,一个主机会发送一个 ARP 请求包
,包含它希望转换的 IP 地址,发送到一个多点广播地址,让网络上所有的点都可以收
到。具有这个 IP 地址的目标主机用一个 ARP 回应来应答,这中间包括了它的物理硬件
地址。 APR 不仅仅限制在以太网设备,它也可以解析其它物理介质的 IP 地址,例如
FDDI 。不能进行 ARP 的设备会有标记,这样 Linux 就不需要试图对它们进行 ARP 。
也有一个相反的功能,反向 ARP ,或 RARP ,把物理地址转换到 IP 地址。这用于网关
,回应对于代表远端网络的 IP 地址的 ARP 请求。
10.2 The Linux TCP/IP Networking Layers ( Linux TCP/IP 网络分层)
象网络协议一样,图 10.2 显示了 Linux 对于 internet 协议地址族的实现就好像
一系列连接的软件层。 BSD socket 由只和 BSD socket 相关的通用的 socket 管理软
件来支持。支持这些的是 INET socket 层,它管理以 IP 为基础的协议 TCP 和 UDP 的
通讯端点。 UDP 是一个无连接的协议,而 TCP 是一个可靠的端到端的协议。当传送 U
DP 报文的时候, Linux 不知道也不关心它们是否安全到达目的地。 TCP 报文进行了编
号, TCP 连接的每一端都要确保传送的数据正确地接收到。 IP 层包括了网际协议(
Internet Protocol )的代码实现。这种代码在传送的数据前增加 IP 头,而且知道如
何把进来的 IP 报文转送到 TCP 或者 UDP 层。在 IP 层之下,支持 Linux 联网的是网
络设备,例如 PPP 和以太网。网络设备并非总是表现为物理设备:其中一些比如 loop
back 设备只是纯粹的软件设备。不象标准的 Linux 设备用 mknod 命令创建,网络设备
只有在底层的软件找到并且初始化它们之后才出现。你只有在建立俄一个包含恰当的以
太望设备驱动程序的核心之后你才能看到设备文件 /dev/eth0 。 ARP 协议位于 IP 层
和支持 ARP 的协议之间。
10.3 The BSD Socket Interface ( BSD socket 接口)
这是一个通用的接口,不仅仅支持多种形式的联网,也是一种进程间通讯机制。一
个 socket 描述了通讯连接的一端,两个通讯进程每一个都会有一个 socket ,描述它
们之间通讯连接的自己部分。 Socket 可以想象成一种特殊形式的管道,但是和管道不
同, socket 对于可以容纳的数据量没有限制。 Linux 支持几种类型的 socket ,这些
类叫做 address families (地址族)。这是因为每一类都有自己通讯寻址方式。 Lin
ux 支持以下 socket address families 或 domain :
UNIX Unix domain sockets,
INET The Internet address family supports communications via
TCP/IP protocols
AX25 Amateur radio X25
IPX Novell IPX
APPLETALK Appletalk DDP
X25 X25
有几种 socket 类型,每一种都代表了连接上支持的服务的类型。并非所有的 add
ress families 都支持所有类型的服务。 Linux BSD socket 支持以下 socket 类型。
Stream 这种 socket 提供了可靠的、双向顺序的数据流,保证传输过程中数据不会
丢失、损坏或重复。 Stream socket 在 INET address family 中由 TCP 协议支持
Datagram 这种 socket 也提供了双向的数据传输,但是和 stream socket 不同,
它不保证消息会到达。甚至它到达了也不保证它们会顺序到达或没有重复或损坏。这种
类型的 socket 在 Internet address family 中由 UDP 协议支持。
RAW 这允许进程直接(所以叫“ raw ”)访问底层的协议。例如,可以向一个以太
网设备打开一个 raw socket ,观察 raw IP 数据流。
Reliable Delivered Messages 这很象数据报但是数据保证可以到达
Sequenced Packets 象 stream socket 但是数据报文大小是固定的
Packet 这不是标准的 BSD socket 类型,它是 Linux 特定的扩展,允许进程直接
在设备层访问报文
使用 socket 通讯的进程用一个客户服务器的模型。服务器提供服务,而客户使用
这种服务。一个这样的例子是一个 Web 服务器,提供 web page 和一个 web 客户(或
浏览器),读取这些页。使用 socket 的服务器,首先创建一个 socket ,然后为它 b
ind 一个名字。这个名字的格式和 socket 的 address family 有关,它是服务器的本
地地址。 Socket 的名字或地址用 sockaddr 数据结构指定。一个 INET socket 会绑定
一个 IP 端口地址。注册的端口编号可以在 /etc/services 中看到:例如, web 服务
器的端口是 80 。在 socket 上绑定一个地址后,服务器就 listen 进来的对于绑定的
地址的连接请求。请求的发起者,客户,创建一个 socket ,并在上面执行一个连接请
求,指定服务器的目标地址。对于一个 INET socket ,服务器的地址是它的 IP 地址和
它的端口地址。这些进来的请求必须通过大量的协议层,找到它的路径,然后就在服务
器的监听端口等待。一旦服务器接收到了进来的请求,它可以接受( accept )或者拒
绝它。如果要接受进来的请求,服务器必须创建一个新的 socket 来接受它。一旦一个
socket 已经用于监听进来的连接请求,它就不能再用于支持一个连接。连接建立之后
,两端都可以自由地发送和接收数据。最后,当一个连接不再需要的时候,它可以被关
闭。必须小心,保证正确地处理正在传送的数据报文。
一个 BSD socket 上的操作的确切意义依赖于它底层的地址族。建立一个 TCP/IP
连接和建立一个业余无线电 X.25 连接有很大的不同。象虚拟文件系统一样, Linux 在
和独立的地址族相关的软件所支持的 BSD socket 层抽象了 BSD socket 和应用程序之
间的 socket 接口。当核心初始化的时候,建立在核心的地址族就向 BSD socket 接口
登记自己。稍后,当应用程序创建和使用 BSD socket 的时候,在 BSD socket 和它的
支撑地址族之间建立一个联系。这种联系是通过交叉的数据结构和地址族支持例程表实
现的。例如,当应用程序创建一个新的 socket 的时候, BSD socket 接口就使用地址
族相关的 socket 创建例程。
当配置核心的时候,一组地址族和协议都建立到了 protocols 向量表中。每一个都
用它的名称(例如“ INET ”)和它的初始化例程的地址来代表。当启动的时候, soc
ket 接口初始化,每一个协议的初始化代码都要被调用。对于 socket 地址族,它们里
边会登记一系列协议操作。这都是一些例程,每一个都执行一个和地址族相关的特殊操
作。登记的协议操作保存在 pops 向量表中,这个向量表保存指向 proto_ops 数据结构
的指针。 Proto_ops 数据结构包括协议族类型和一批和特定地址族相关的 socket 操作
例程的指针。 Pops 向量表用地址族的标识符作为索引,例如 Internet address fami
ly 的标识符( AF_INET 是 2 )。
参见 include/linux/net.h
10.4 The INET Socket Layer
INET socket 层支持包含 TCP/IP 协议的 internet address family 。象上面讨论
的,这些协议是分层的,每一个协议都使用其它协议的服务。 Linux 的 TCP/IP 代码和
数据结构反映了这种分层。它和 BSD socket 层的接口是通过网络初始化的时候它向 BSD
socket 层登记的 internet address family socket 操作进行的。这些和其它登记
的地址族一起放在 pops 向量表中。 BSD socket 层通过调用在登记的 proto_ops 数据
结构中的 INET 层的 socket 支持例程完成它的工作。例如,一个地址族是 INET 的 B
SD socket 创建请求会使用底层的 INET socket 创建函数。每一次操作 BSD socket 层
都把代表 BSD socket 的 socket 数据结构传递给 INET 层。 INET socket 层使用它自
己的数据结构 socket ,连接到 BSD socket 数据结构,而不是用 TCP/IP 相关的信息
把 BSD socket 搞乱。这种连接参见图 10.3 。它使用 BSD socket 中的 data 指针把
sock 数据结构和 BSD socket 数据结构连接起来。这意味着后续的 INET socket 调用
可以很容易地获取这个 sock 数据结构。在创建的时候 sock 数据结构的协议操作指针
也被建立,这些指针依赖于请求的协议。如果请求 TCP ,则 sock 数据结构的协议操作
指针会指向 TCP 连接所需要的一系列 TCP 协议的操作。
参见 include/net/sock.h
10.4.1 Creating a BSD Socket (创建一个 BSD Socket )
创建一个新的 socket 的系统调用需要传递它的地址族的标识符、 socket 的类型
和协议。首先,用请求的地址族在 pops 向量表中查找一个匹配的地址族。它可能是一
个使用核心模块实现的特殊的地址族,如果这样, kerneld 核心进程必须加载这个模块
,我们才能继续。然后分配一个新的 socket 数据结构来表示这个 BSD socket 。实际
上这个 socket 数据结构物理上是 VFS inode 数据结构的一部分,分配一个 socket 实
际上就是分配一个 VFS inode 。这看起来比较奇怪,除非你考虑让 socket 可以用和普
通文件一样的方式进行操作。象所有文件都用 VFS inode 数据结构表示一样,为了支持
文件操作, BSD socket 也必须用一个 VFS inode 数据结构表示。
这个新创建的 BSD socket 数据结构包括一个指针指向和地址族相关的 socket 例
程,这个指针被设置到从 pops 向量表中取出的 proto_ops 数据结构。它的类型被设置
成请求的 socket 类型: SOCK_STREAM 、 SOCK_DGRAM 等等其中之一,然后用 proto_
ops 数据结构中保存的地址调用和地址族相关的创建例程。
然后从当前进程的 fd 向量表中分配一个空闲的文件描述符,它所指向的 file 数
据结构也被初始化。这包括设置文件操作指针,指向 BSD socket 接口支持的 BSD soc
ket 文件操作例程。所有将来的操作会被定向到 socket 接口,依次通过调用支撑的地
址族的操作例程传递到相应的地址族。
10.4.2 Binding an Address to an INET BSD Socket (为一个 INET BSD socket 绑定
一个地址)
为了监听进来的网际连接请求,每一个服务器必须创建一个 INET BSD socket 并把
自己的地址绑定到它上面。 Bind 的操作大部分由 INET socket 层处理,另一些需要底
层的 TCP 和 UDP 协议层的支持。已经绑定了一个地址的 socket 不能用于其它通讯。
这意味着这个 socket 的状态必须是 TCP_CLOSE 。传递给 bind 操作的 sockaddr 包括
要绑定的 IP 地址和一个端口号(可选)。通常,绑定的地址会是分配给支持 INET 地
址族的网络设备的地址之中的一个,而且接口必须是开启的并能够使用。你可以用 ifc
onfig 命令看系统中哪一个网络接口当前是激活的。 IP 地址也可以是 IP 广播地址(
全是 1 或 0 )。这是意味着“发送给每一个人”的特殊地址。如果这个机器作为一个
透明的 proxy 或者防火墙,这个 IP 地址也可以设置成任何 IP 地址。不过只有具有超
级用户特权的进程可以绑定任意 IP 地址。这个绑定的 IP 地址被存在 sock 数据结构
的 recv_addr 和 saddr 域中。它们分别用于 hash 查找和发送 IP 地址。端口号是可
选的,如果没有设置,会向支撑的网络请求一个空闲的。按照惯例,小于 1024 的端口
号不能被没有超级用户特权的进程使用。如果底层的网络分配端口号,它总是分配一个
大于 1024 的端口。
当底层的网络设备接收报文的时候,这些报文必须被转到正确的 INET 和 BSD soc
ket 才能被处理。为此, UDP 和 TCP 维护 hash table ,用于查找进来的 IP 信息的
地址,把它们转到正确的 socket/sock 对。 TCP 是一个面向连接的协议,所以处理 T
CP 报文比处理 UDP 报文所包括的信息要多。
UDP 维护一个已经分配的 UDP 端口的 hash table , udp_table 。这包括 sock
数据结构的指针,用一个根据端口号的 hash 函数作为索引。因为 UDP hash table 比
允许的端口号要小的多( udp_hash 只有 128 , UDP_HTABLE_SIZE )表中的一些条目
指向一个 sock 数据结构的链表,用每一个 sock 的 next 指针连接在一起。
TCP 更加复杂,因为它维护几个 hast table 。但是,在绑定操作中, TCP 实际上
并不把绑定的 sock 数据结构加到它的 hash table 中,它只是检查请求的端口当前没
有被使用。在 listen 操作中 sock 数据结构才加到 TCP 的 hash table 中。
10.4.3 Making a Connection to an INET BSD Socket
一旦创建了一个 socket ,如果没有用于监听进来的连接请求,它就可以用于建立
向外的连接请求。对于无连接的协议,比如 UDP ,这个 socket 操作不需要做许多,但
是对于面向连接的协议如 TCP ,它涉及在两个应用程序之间建立一个虚拟电路。
一个向外的连接只能在一个正确状态的 INET BSD socket 上进行:就是说还没有建
立连接,而且没有用于监听进来的连接。这意味着这个 BSD socket 数据结构必须在 S
S_UNCONNECTED 状态。 UDP 协议不在两个应用程序之间建立虚拟连接,所有发送的消息
都是数据报,发出的消息可能到到也可能没有到达它的目的地。但是,它也支持 BSD s
ocket 的 connect 操作。在一个 UDP INET BSD socket 上的一个连接操作只是建立远
程应用程序的地址:它的 IP 地址和它的 IP 端口号。另外,它也要建立一个路由表条
目的缓存区,这样,在这个 BSD socket 上发送的 UDP 数据报不需要在检查路由表数据
库(除非这个路由变成无效)。这个缓存的路由信息被 INET sock 数据结构中的 ip_r
oute_cache 指针指向。如果没有给出地址信息,这个 BSD socket 发送的消息就自动使
用这个缓存的路由和 IP 地址信息。 UDP 把 sock 的状态改变成为 TCP_ESTABLISHED

对于在一个 TCP BSD socket 上进行的连接操作, TCP 必须建立一个包括连接信息
的 TCP 消息,并发送到给定的 IP 目标。这个 TCP 消息包括连接的信息:一个独一无
二的起始消息顺序编号、发起主机可以管理的消息的最大尺寸、发送和接收的窗口大小
等等。在 TCP 中,所有的消息都编了号,初始顺序编号用作第一个消息编号。 Linux
选择一个合理的随机数以避免恶意的协议攻击。每一个从 TCP 连接的一端发送,被另一
端成功接收的消息被确认,告诉它成功地到达,而且没有损坏。没有确认的消息会被重
发。发送和接收窗口大小是确认前允许的消息的数目。如果接收端的网络设备支持的最
大消息尺寸比较小,则这个连接会使用两个中间最小的一个。执行向外的 TCP 连接请求
的应用程序现在必须等待目标应用程序的响应,是接受还是拒绝这个连接请求。对于期
望进来的消息的 TCP sock ,它被加到了 tcp_listening_hash ,这样进来的 TCP 消息
可以定向到这个 sock 数据结构。 TCP 也启动计时器,这样如果目标应用程序对于请求
不响应,向外的连接请求会超时。
10.4.4 Listening on an INET BSD Socket
一旦一个 socket 拥有了一个绑定的地址,它就可以监听指定这个绑定地址的进来
的连接请求。一个网络应用程序可以不绑定地址直接在一个 socket 上监听,这种情况
下, INET socket 层找到一个未用的端口号(对于这种协议而言),自动把它绑定到这
个 socket 上。这个 socket 的 listen 函数把 socket 变成 TCP_LISTEN 的状态,并
且执行所需的和网络相关的工作,一边允许进来的连接。
对于 UDP socket ,改变 socket 的状态已经足够,但是 TCP 已经激活它现在要把
socket 的 sock 数据结构加到它的两个 hash table 中。这是 tcp_bound_hash 和 t
cp_listening_hash 表。这两个表都通过一个基于 IP 端口号的 hash 函数进行索引。
不论何时接收到一个对于激活的监听 socket 的进来的 TCP 连接请求, TCP 都要
建立一个新的 sock 数据结构表示它。这个 sock 数据结构在它最终被接受之前成为这
个 TCP 连接的 buttom half 。它也克隆包含连接请求的进来的 sk_buff 并把它排在监
听的 sock 数据结构的 receive_queue 队列中。这个克隆的 sk_buff 包括一个指针,
指向这个新创建的 sock 数据结构。
10.4.5 Accepting Connection Requests
UDP 不支持连接的概念,接受 INET socket 的连接请求只应用于 TCP 协议,在一
个监听的 sock 上进行接受( accept )操作会从原来的监听的 socket 克隆出一个新
的 socket 数据结构。然后这个 accept 操作传递给支撑的协议层,在这种情况下,是
INET 去接受任何进来的连接请求。如果底层的协议,比如 UDP 不支持连接, INET 协
议层的 accept 操作会失败。否则,连接的请求会传递到真正的协议,在这里,是 TCP
。这个 accept 操作可能是阻塞,也可能是非阻塞的。在非阻塞的情况下,如果没有需
要 accept 的进来的连接,这个 accept 操作会失败,而新创建的 socket 数据结构会
被废弃。在阻塞的情况下,执行 accept 操作的网络应用程序会被加到一个等待队列,
然后挂起,直到接收到一个 TCP 的连接请求。一旦接收到一个连接请求,包含这个请求
的 sk_buff 会被废弃,这个 sock 数据结构被返回到 INET socket 层,在这里它被连
接到先前创建的新的 socket 数据结构。这个新的 socket 的文件描述符( fd )被返
回给网络应用程序,应用程序就可以用这个文件描述符对这个新创建的 INET BSD sock
et 进行 socket 操作。
10.5 The IP Layer ( IP 层)
10.5.1 Socket Buffers
使用分成许多层,每一层使用其它层的服务,这样的网络协议的一个问题是,每一
个协议都需要在传送的时候在数据上增加协议头和尾,而在处理接收的数据的时候需要
删除。这让协议之间传送数据缓冲区相当困难,因为每一层都需要找出它的特定的协议
头和尾在哪里。一个解决方法是在每一层都拷贝缓冲区,但是这样会没有效率。替代的
, Linux 使用 socket 缓冲区或者说 sock_buffs 在协议层和网络设备驱动程序之间传
输数据。 Sk_buffs 包括指针和长度域,允许每一协议层使用标准的函数或方法操纵应
用程序数据。
 图 10.4 显示了 sk_buff 数据结构:每一个 sk_buff 都有它关联的一块数据。 Sk_bu
ff 有四个数据指针,用于操纵和管理 socket 缓冲区的数据
参见 include/linux/skbuff.h
head 指向内存中的数据区域的起始。在 sk_buff 和它相关的数据块被分配的时候确定
的。
Data 指向协议数据的当前起始为止。这个指针随着当前拥有这个 sk_buff 的协议层不
同而变化。
Tail 指向协议数据的当前结尾。同样,这个指针也随拥有的协议层不同而变化。
End 指向内存中数据区域的结尾。这是在这个 sk_buff 分配的时候确定的。
另有两个长度字段 len 和 truesize ,分别描述当前协议报文的长度和数据缓冲区
的总长度。 Sk_buff 处理代码提供了标准的机制用于在应用程序数据上增加和删除协议
头和尾。这种代码安全地操纵了 sk_buff 中的 data 、 tail 和 len 字段。
Push 这把 data 指针向数据区域的起始移动,并增加 len 字段。用于在传送的数据前
面增加数据或协议头
参见 include/linux/skbuff.h skb_push()
Pull 把 data 指针从数据区域起始向结尾移动,并减少 len 字段。用于从接收的数据
中删除数据或协议头。
参见 include/linux/skbuff.h skb_pull()
Put 把 tail 指针向数据区域的结尾移动并增加 len 字段,用于在传输的数据尾部增加
数据或协议信息
参见 include/linux/skbuff.h skb_put()
trim 把 tail 指针向数据区域的开始移动并减少 len 字段。用于从接收的数据中删除
数据或协议尾
参见 include/linux/skbuff.h skb_trim()
sk_buff 数据结构也包括一些指针,使用这些指针,在处理过程中这个数据结构可以存
储在 sk_buff 的双向环形链表中。有通用的 sk_buff 例程,在这些列表的头和尾中增
加 sk_buffs 和删除其中的 sk_buff 。
10.5.2 Receiving IP Packets
第 8 章描述了 Linux 的网络设备驱动程序如何建立到核心以及被初始化。这产生
了一系列 device 数据结构,在 dev_base 列表中链接在一起。每一个 device 数据结
构描述了它的设备并提供了一组回调例程,当需要网络驱动程序工作的时候网络协议层
可以调用。这些函数大多数和传输数据以及网络设备的地址有关。当一个网络设备从它
的网络上接收到数据报文的时候,它必须把接收到的数据转换到 sk_buff 数据结构。这
些接收的 sk_buff 在接收的时候被网络驱动程序增加到 backlog 队列。如果 backlog
队列增长的太大,那么接收的 sk_buff 就被废弃。如果有工作要执行,这个网络的 b
utton half 标记成准备运行。
参见 net/core/dev.c netif_rx()
当网络的 bottom half 处理程序被调度程序调用的时候,它首先处理任何等待传送
的网络报文,然后才处理 sk_buff 的 backlog backlo 队列,确定接收到的报文需要
传送到那个协议层。当 Linux 网络层初始化的时候,每一个协议都登记自己,在 ptyp
e_all 列表或者 ptype_base hash table 中增加一个 packet_type 的数据结构。这个
packet_type 数据结构包括协议类型,一个网络驱动设备的指针,一个协议的数据接收
处理例程的指针和一个指针,指向这个列表或者 hash table 下一个 packet_type 数据
类型。 Ptype_all 链表用于探测( snoop )从任意网络设备上接收到的所有的数据报
文,通常不使用。 Ptype_base hash table 使用协议标识符 hash ,用于确定哪一种协
议应该接收进来的网络报文。网络的 bottom half 把进来的 sk_buff 的协议类型和任
一表中的一个或多个 packet_type 条目进行匹配。协议可能会匹配一个或多个条目,例
如当窥测所有的网络通信的时候,这时,这个 sk_buff 会被克隆。这个 sk_buff 被传
递到匹配的协议的处理例程。
参见 net/core/dev.c net_bh()
参见 net/ipv4/ip_input.c ip_recv()
10.5.3 Sending IP Packets
报文在应用程序交换数据的过程中传送,或者也可能是为了支持已经建立的连接或
为了建立连接而由网络协议产生产生。不管数据用什么方式产生,都建立一个包含数据
的 sk_buff ,并当它通过协议层的时候增加许多头。
这个 sk_buff 需要传递到进行传输的网络设备。但是首先,协议,例如 IP ,需要
决定使用哪一个网络设备。这依赖于这个报文的最佳路由。对于通过 modem 连接到一个
网络的计算机,比如通过 PPP 协议,这种路由选择比较容易。报文应该要么通过 loop
back 设备传送给本地主机,要么传送到 PPP modem 连接的另一端的网关。对于连接到
以太网的计算机而言,这种选择比较困难,因为网络上连接了许多计算机。
对于传送的每一个 IP 报文, IP 使用路由表解析目标 IP 地址的路由。对于每一
个 IP 目标在路由表中进行的查找,成功就会返回一个描述要使用的路由的 rtable 数
据结构。包括使用的源 IP 地址,网络 device 数据结构的地址,有时候还会有一个预
先建立的硬件头。这个硬件头和网络设备相关,包含源和目的物理地址和其它同介质相
关的信息。如果网络设备是以太网设备,硬件头会在图 10.1 中显示,其中的源和目的
地址会是物理的以太网地址。硬件头和路由缓存在一起,因为在这个路由传送的每一个
IP 报文都需要追加这个头,而建立这个头需要时间。硬件头可能包含必须使用 ARP 协
议才能解析的物理地址。这时,发出的报文会暂停,直到地址解析成功。一旦硬件地址
被解析,并建立了硬件头,这个硬件头就被缓存,这样以后使用这个接口的 IP 报文就
不需要进行 ARP 。
参见 include/net/route.h
10.5.4 Data Fragmentation
每一个网络设备都有一个最大的报文尺寸,它无法传送或接收更大的数据报文。 I
P 协议允许这种数据,会把数据分割成网络设备可以处理的报文大小的更小的单元。 I
P 协议头包含一个分割字段,包含一个标记和分割的偏移量。
当要传输一个 IP 报文的时候, IP 查找用来发送 IP 报文的网络设备。通过 IP
路由表来查找这个设备。每一个设备都有一个字段描述它的最大传输单元(字节),这
是 mtu 字段。如果设备的 mtu 比等待传送的 IP 报文的报文尺寸小,那么这个 IP 报
文必须被分割到更小的碎片( mtu 大小)。每一个碎片用一个 sk_buff 代表:它的 I
P 头标记了它被分割,以及这个 IP 报文在数据中的偏移量。最后一个报文被标记为最
后一个 IP 碎片。如果在分割成碎片的过程中, IP 无法分配一个 sk_buff ,这次传送
就失败。
接收 IP 碎片比发送更难,因为 IP 碎片可能以任意顺序被接收,而且它们必须在
重组之前全部接收到。每一次一个 IP 报文被接收的时候,都检查它是否是一个 IP 碎
片。收到一个消息的第一个碎片, IP 就建立一个新的 ipq 数据结构,并连接到等待组
装的 IP 碎片的 ipqueue 列表中。当更多的 IP 碎片接收到的时候,就查到正确的 ip
q 数据结构并建立一个新的 ipfrag 数据结构来描述这个碎片。每一个 ipq 数据结构都
唯一描述了一个成为碎片的 IP 接收帧,包括它的源和目标 IP 地址,上层协议标识符
和这个 IP 帧的标识符。当接收到所有的碎片的时候,它们被组装在一起成为一个单一
的 sk_buff ,并传递到下一个协议层去处理。每一个 ipq 包括一个计时器,每一次接
收到一个有效的碎片的时候就重新启动。如果这个计时器过期,这个 ipq 数据结构和它
的 ipfrag 就被去除,并假设这个消息在传输过程中丢失了。然后由高层的协议负责重
新传输这个消息。
参见 net/ipv4/ip_input.c ip_rcv()
10.6 The Address Resolution Protocol (ARP)
地址解析协议的任务是提供 IP 地址到物理硬件地址的转换,例如以太网地址。 I
P 在它把数据(用一个 sk_buff 的形式)传送到设备驱动程序进行传送的时候才需要这
种转换。它进行一些检查,看这个设备是否需要一个硬件头,如果是,这个报文的硬件
头是否需要重建。 Linux 缓存硬件头以免频繁地重建。如果硬件头需要重建,它就调用
和设备相关的硬件头重建例程。所有的一台设备使用相同的通用的头重建例程,然后使
用 ARP 服务把目标的 IP 地址转换到物理地址。
参见 net/ipv4/ip_output.c ip_build_xmit()
参见 net/ethernet/eth.c rebuild_header()
ARP 协议本身非常简单,包含两种消息类型: ARP 请求和 ARP 应答。 ARP 请求包
括需要转换的 IP 地址,应答(希望)包括转换的 IP 地址和硬件地址。 ARP 请求被广
播到连接到网络的所有的主机,所以,对于一个以太网所有连在以太网上的机器都可以
看到这个 ARP 请求。拥有这个请求中包括的 IP 地址的机器会回应这个 ARP 请求,用
包含它自己物理地址的 ARP 应答。
Linux 中的 ARP 协议层围绕着一个 arp_table 数据结构的表而建立。每一个描述
一个 IP 和物理地址的对应。这些条目在 IP 地址需要转换的时候创建,随着时间推移
变得陈旧的时候被删除。每一个 arp_table 数据结构包含以下域:
Last used 这个 ARP 条目上一次使用的时间
Last update 这个 ARP 条目上一次更新的时间
Flags 描述这个条目的状态:它是否完成等等
IP address 这个条目描述的 IP 地址
Hardware address 转换(翻译)的硬件地址
Hardware header 指向一个缓存的硬件头的指针
Timer 这是一个 timer_list 的条目,用于让没有回应的 ARP 请求超时
Retries 这个 ARP 请求重试的次数
Sk_buff queue 等待解析这个 IP 地址的 sk_buff 条目的列表
ARP 表包含一个指针( arp_tables 向量表)的表,把 arp_table 的条目链接在一
起。这些条目被缓存,以加速对它们的访问。每一个条目用它的 IP 地址的最后两个字
节做表的索引进行查找,然后跟踪这个条目链,直到找到正确的条目。 Linux 也缓存从
arp_table 条目预先建立的硬件头,用 hh_cache 数据结构的形式进行缓存。
当请求一个 IP 地址转换的时候,没有对应的 arp_table 条目, ARP 必须发送一
个 ARP 请求消息。它在表中创建一个新的 arp_table 条目,并把需要地址转换的包括
了网络报文的 sk_buff 放到这个新的条目的 sk_buff 队列。它发出一个 ARP 请求并让
ARP 过时计时器运行。如果没有回应, ARP 会重试几次。如果仍旧没有回应, ARP 会
删除这个 arp_table 条目。任何排队等待这个 IP 地址进行转换的 sk_buff 数据结构
会被通知,由传输它们的上层协议负责处理这种失败。 UDP 不关心丢失的报文,但是
TCP 会在一个建立的 TCP 连接上试图重新发送。如果这个 IP 地址的属主用它的硬件地
址应答,这个 arp_table 条目标记为完成,任何排队的 sk_buff 会被从对队列中删除
,继续传送。硬件地址被写到每一个 sk_buff 的硬件头中。
ARP 协议层也必须回应指明它的 IP 地址的 ARP 请求。它登记它的协议类型( ETH_P_
ARP ),产生一个 packet_type 数据结构。这意味着网络设备接收到的所有的 ARP 报
文都会传给它。象 ARP 应答一样,这也包括 ARP 请求。它使用接收设备的 device 数
据结构中的硬件地址产生 ARP 应答。
网络拓扑结构不断变化, IP 地址可能被重新分配到不同的硬件地址。例如,一些
拨号服务为它建立的每一个连接分配一个 IP 地址。为了让 ARP 表中包括最新的条目,
ARP 运行一个定期的计时器,检查所有的 arp_table 条目,看哪一个超时了。它非常
小心,不删除包含包含一个或多个缓存的硬件头的条目。删除这些条目比较危险,因为
其它数据结构依赖它们。一些 arp_table 条目是永久的,并被标记,所以它们不会被释
放。 ARP 表不能增长的太大:每一个 arp_table 条目都要消耗一些核心内存。每当需
要分配一个新的条目而 ARP 表到达了它的最大尺寸的时候,就查找最旧的条目并删除它
们,从而修整这个表。
10.7 IP Routing
IP 路由功能确定发向一个特定的 IP 地址的 IP 报文应该向哪里发送。当传送 IP
报文的时候,会有许多选择。目的地是否可以到达?如果可以,应该使用哪一个网络设
备来发送?是不是有不止一个网络设备可以用来到达目的地,哪一个最好? IP 路由数
据库维护的信息可以回答这些问题。有两个数据库,最重要的是转发信息数据库( For
warding Information Database )。这个数据库是已知 IP 目标和它们最佳路由的详尽
的列表。另一个小一些,更快的数据库,路由缓存( route cache )用于快速查找 IP
目标的路由。象所有缓存一样,它必须只包括最常访问的路由,它的内容是从转发信息
数据库中得来的。
路由通过 BSD socket 接口的 IOCTL 请求增加和删除。这些请求被传递到具体的协
议去处理。 INET 协议层只允许具有超级用户权限的进程增加和删除 IP 路由。这些路
由可以是固定的,或者是动态的,不断变化的。多数系统使用固定路由,除非它们本身
是路由器。路由器运行路由协议,不断地检查所有已知 IP 目标的可用的路由。不是路
由器的系统叫做末端系统( end system )。路由协议用守护进程的形式来实现,例如
GATED ,它们也使用 BSD socket 接口的 IOCTL 来增加和删除路由。
10.7.1 The Route Cache
不论何时查找一个 IP 路由的时候,都首先在路由缓存中检查匹配的路由。如果在
路由缓存中没有匹配的路由,才查找转发信息数据库。如果这里也找不到路由, IP 报
文发送会失败,并通知应用程序。如果路由在转发信息数据库而不在路由缓存中,就为
这个路由产生一个新的条目并增加到路由缓存中。路由缓存是一个表( ip_rt_hash_ta
ble ),包括指向 rtable 数据结构链的指针。路由表的索引是基于 IP 地址最小两字
节的 hash 函数。这两个字节通常在目标中有很大不同,让 hash value 可以最好地分
散。每一个 rtable 条目包括路由的信息:目标 IP 地址,到达这个 IP 地址要使用的
网络设备( device 结构),可以使用的最大的信息尺寸等等。它也有一个引用计数器
( refrence count ),一个使用计数器( usage count )和上次使用的时间戳(在
jiffies 中)。每一次使用这个路由的时候这个引用计数器就增加,显示利用这个路由
的网络连接数目,当应用程序停止使用这个路由的时候就减少。使用计数器每一次查找
路由的时候就增加,用来让这个 hash 条目链的 rtable 条目变老。路由缓存中所有条
目的最后使用的时间戳用于定期检查这个 rtable 是否太老。如果这个路由最近没有使
用,它就从路由表中废弃。如果路由保存在路由缓存中,它们就被排序,让最常用的条
目在 hash 链的前面。这意味着当查找路由的时候找到这些路由会更快。
参见 net/ipv4/route.c check_expire()
 
10.7.2 The Forwarding Information Database
 
转发信息数据库(图 10.5 显示)包含了当时从 IP 的观点看待系统可用的路由。
它是非常复杂的数据结构,虽然它已经进行了合理有效的安排,但是它对于参考而言并
不是一个快速的数据库。特别是如果每一个传输的 IP 报文都在这个数据库中查找目标
会非常慢。这也是为什么要有路由缓存:加速已经知道最佳路由的 IP 报文的传送。路
由缓存从这个转发信息数据库得到,表示了它最常用的条目。
表指向。 Hash 索引取自 IP 子网掩码。所有通向同一子网的路由都用排在每一个 fib
_zone 数据结构的 fz_list 队列中得的成对的 fib_node 和 fib_info 数据结构来描述
。如果这个子网的路由数目变得太大,就生成一个 hash table ,让 fib_node 数据结
构的查找更容易。
对于同一个 IP 子网,可能存在多个路由,这些路由可能穿过多个网关之一。 IP
路由层不允许使用相同的一个网关对于一个子网有多于一个路由。换句话说,如果对于
一个子网有多个路由,那么要保证每一个路由都是用不同的网关。和每一个路由关联的
是它的量度( metric ),这是用来衡量这个路由的益处。一个路由的量度,基本上,
是它在到达目标子网之前必须跳过的子网数目。这个量度越高,路由越差。

如何发掘出更多退休的钱?

如何发掘出更多退休的钱? http://bbs.wenxuecity.com/bbs/tzlc/1328415.html 按照常规的说法,退休的收入必须得有退休前的80%,或者是4% withdrawal rule,而且每年还得要加2-3%对付通胀,这是一个很大...