Linux内核源代码漫游(2)
时间:2007-06-26 来源:Boatman_yang
一个文件系统类型的快速剖析
一个文件系统类型的任务是执行用于映射相应高层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 数据结构在<linux/tty_driver.h>中有着完整的描述。
上述信息是从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。