学而不思则罔,思而不学则殆

有其事必有其理, 有其理必有其事

  IT博客 :: 首页 :: 联系 :: 聚合  :: 管理
  85 Posts :: 12 Stories :: 47 Comments :: 0 Trackbacks

#


/**********************************************************
*
*  测试的主函数
*
***********************************************************/

#include <cppunit/CompilerOutputter.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>



int main(int argc, char* argv[])
{
  // Get the top level suite from the registry
  CPPUNIT_NS::Test *suite = CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest();

  // Adds the test to the list of test to run
  CPPUNIT_NS::TextUi::TestRunner runner;
  runner.addTest( suite );

  // Change the default outputter to a compiler error format outputter
  runner.setOutputter( new CPPUNIT_NS::CompilerOutputter( &runner.result(),
                                                       std::cerr ) );
  // Run the test.
  bool wasSucessful = runner.run();

  // Return error code 1 if the one of test failed.
  return wasSucessful ? 0 : 1;
}



/**********************************************************
*
*  测试的类的头文件
*  testex.h
***********************************************************/
#include <cppunit/extensions/HelperMacros.h>


class CTestEx : public CPPUNIT_NS::TestFixture
{
    // 开始
    CPPUNIT_TEST_SUITE( CTestEx );
    // 申明自动测试的函数
    CPPUNIT_TEST( test_f );
    // 结束
    CPPUNIT_TEST_SUITE_END();
public:
    CTestEx ();
    virtual ~CTestEx ();
    void test_f() ;
}

/**********************************************************
*
*  测试的类的实现
*  testex.cpp
***********************************************************/


#include "testex.h"


CPPUNIT_TEST_SUITE_REGISTRATION( CTestEx );

void CTestEx::test_f()
{
    ....;
}

posted @ 2006-05-31 16:42 易道 阅读(401) | 评论 (0)编辑 收藏

用户选用ARM处理器开发嵌入式系统时,选择合适的开发工具可以加快开发进度,节省开发成本。因此一套含有编辑软件、编 译软件、汇编软件、链接软件、调试软件、工程管理及函数库的集成开发环境(IDE)一般来说是必不可少的,至于嵌入式实时操作系统、评估板等其他开发工具 则可以根据应用软件规模和开发计划选用。 
    使用集成开发环境开发基于ARM的应用软件,包括编辑、编译、汇编、链接等工作全部在PC机上即可完成,调试工作则需要配合其他的模块或产品方可完成,目前常见的调试方法有以下几种: 
1、指令集模拟器 
     部分集成开发环境提供了指令集模拟器,可方便用户在PC机上完成一部分简单的调试工作,但是由于指令集模拟器与真实的硬件环境相差很大,因此即使用户使用 指令集模拟器调试通过的程序也有可能无法在真实的硬件环境下运行,用户最终必须在硬件平台上完成整个应用的开发。 
2、驻留监控软件 
     驻留监控软件(Resident Monitors)是一段运行在目标板上的程序,集成开发环境中的调试软件通过以太网口、并行端口、串行端口等通讯端口 与驻留监控软件进行交互,由调试软件发布命令通知驻留监控软件控制程序的执行、读写存储器、读写寄存器、设置断点等。 
    驻留监控软件是一种比较低廉有效的调试方式,不需要任何其他的硬件调试和仿真设备。ARM公司的Angel就是该类软件,大部分嵌入式实时操作系统也是采用该类软件进行调试,不同的是在嵌入式实时操作系统中,驻留监控软件是作为操作系统的一个任务存在的。 
    驻留监控软件的不便之处在于它对硬件设备的要求比较高,一般在硬件稳定之后才能进行应用软件的开发,同时它占用目标板上的一部分资源,而且不能对程序的全速运行进行完全仿真,所以对一些要求严格的情况不是很适合。 
3、JTAG仿真器 
    JTAG 仿真器也称为JTAG调试器,是通过ARM芯片的JTAG边界扫描口进行调试的设备。JTAG仿真器比较便宜,连接比较方便,通过现有的JTAG边界扫描 口与 ARM CPU 核通信,属于完全非插入式(即不使用片上资源)调试,它无需目标存储器,不占用目标系统的任何端口,而这些是驻留监控软件所必需 的。另外,由于JTAG调试的目标程序是在目标板上执行,仿真更接近于目标硬件,因此,许多接口问题,如高频操作限制、AC和DC参数不匹配,电线长度的 限制等被最小化了。使用集成开发环境配合JTAG仿真器进行开发是目前采用最多的一种调试方式。 
4、在线仿真器 
    在线仿真器使 用仿真头完全取代目标板上的CPU,可以完全仿真ARM芯片的行为,提供更加深入的调试功能。但这类仿真器为了能够全速仿真时钟速度高于100MHz的处 理器,通常必须采用极其复杂的设计和工艺,因而其价格比较昂贵。在线仿真器通常用在ARM的硬件开发中,在软件的开发中较少使用,其价格高昂也是在线仿真 器难以普及的因素。
posted @ 2006-05-31 16:21 易道| 编辑 收藏

级别: 初级

Anand K Santhanam, 软件工程师, IBM Global Services
Vishal Kulkarni, 软件工程师, IBM Global Services

2002 年 3 月 01 日

如 果您刚接触嵌入式开发,那么大量可用的引导装载程序(bootloader)、规模缩小的分发版(distribution)、文件系统和 GUI 看起来可能太多了。但是这些丰富的选项实际上是一种恩赐,允许您调整开发或用户环境以完全符合您的需要。对 Linux 嵌入式开发的概述将帮助您理解所有这些选项。

Linux 正在嵌入式开发领域稳步发展。因为 Linux 使用 GPL(请参阅本文后面的 参考资料), 所以任何对将 Linux 定制于 PDA、掌上机或者可佩带设备感兴趣的人都可以从因特网免费下载其内核和应用程序,并开始移植或开发。许多 Linux 改良品种迎合了嵌入式/实时市场。它们包括 RTLinux(实时 Linux)、uclinux(用于非 MMU 设备的 Linux)、Montavista Linux(用于 ARM、MIPS、PPC 的 Linux 分发版)、ARM-Linux(ARM 上的 Linux)和其它 Linux 系统(请参阅 参考资料以链接到本文中提到的这些和其它术语及产品。)

嵌 入式 Linux 开发大致涉及三个层次:引导装载程序、Linux 内核和图形用户界面(或称 GUI)。在本文中,我们将集中讨论涉及这三层的一些基本概念;深入了解引导装载程序、内核和文件系统是如何交互的;并将研究可用于文件系统、GUI 和引导装载程序的众多选项中的一部分。

引导装载程序

引 导装载程序通常是在任何硬件上执行的第一段代码。在象台式机这样的常规系统中,通常将引导装载程序装入主引导记录(Master Boot Record,(MBR))中,或者装入 Linux 驻留的磁盘的第一个扇区中。通常,在台式机或其它系统上,BIOS 将控制移交给引导装载程序。这就提出了一个有趣的问题:谁将引导装载程序装入(在大多数情况中)没有 BIOS 的嵌入式设备上呢?

解决这个问题有两种常规技术:专用软件和微小的引导代码(tiny bootcode)。

专用软件可以直接与远程系统上的闪存设备进行交互并将引导装载程序安装在闪存的给定位置中。 闪存设备是与存储设备功能类似的特殊芯片,而且它们能持久存储信息 ― 即,在重新引导时不会擦除其内容。

这个软件使用目标(在嵌入式开发中,嵌入式设备通常被称为 目标)上的 JTAG 端口,它是用于执行外部输入(通常来自主机机器)的指令的接口。JFlash-linux 是一种用于直接写闪存的流行工具。它支持为数众多的闪存芯片;它在主机机器(通常是 i386 机器 ― 本文中我们把一台 i386 机器称为 主机)上执行并通过 JTAG 接口使用并行端口访问目标的闪存芯片。当然,这意味着目标需要有一个并行接口使它能与主机通信。Jflash-linux 在 Linux 和 Windows 版本中都可使用,可以在命令行中用以下命令启动它:

												Jflash-linux <bootloader>
										

某些种类的嵌入式设备具有 微小的引导代码― 根据几个字节的指令 ― 它将初始化一些 DRAM 设置并启用目标上的一个串行(或者 USB,或者以太网)端口与主机程序通信。然后,主机程序或装入程序可以使用这个连接将引导装载程序传送到目标上,并将它写入闪存。

在安装它并给予其控制后,这个引导装载程序执行下列各类功能:

  • 初始化 CPU 速度
  • 初始化内存,包括启用内存库、初始化内存配置寄存器等
  • 初始化串行端口(如果在目标上有的话)
  • 启用指令/数据高速缓存
  • 设置堆栈指针
  • 设置参数区域并构造参数结构和标记(这是重要的一步,因为内核在标识根设备、页面大小、内存大小以及更多内容时要使用引导参数)
  • 执行 POST(加电自检)来标识存在的设备并报告任何问题
  • 为电源管理提供挂起/恢复支持
  • 跳转到内核的开始

带有引导装载程序、参数结构、内核和文件系统的系统典型内存布局可能如下所示:


清单 1. 典型内存布局
												   /* Top Of Memory */ 

Bootloader
Parameter Area
Kernel
Filesystem

/* End Of Memory */

嵌入式设备上一些流行的并可免费使用的 Linux 引导装载程序有 Blob、Redboot 和 Bootldr(请参阅 参考资料获得链接)。所有这些引导装载程序都用于基于 ARM 设备上的 Linux,并需要 Jflash-linux 工具用于安装。

一旦将引导装载程序安装到目标的闪存中,它就会执行我们上面提到的所有初始化工作。然后,它准备接收来自主机的内核和文件系统。一旦装入了内核,引导装载程序就将控制转给内核。





回页首


设置工具链

设置工具链在主机机器上创建一个用于编译将在目标上运行的内核和应用程序的构建环境 ― 这是因为目标硬件可能没有与主机兼容的二进制执行级别。

工具链由一套用于编译、汇编和链接内核及应用程序的组件组成。 这些组件包括:

  • Binutils ― 用于操作二进制文件的实用程序集合。它们包括诸如 arasobjdumpobjcopy 这样的实用程序。
  • Gcc― GNU C 编译器。
  • Glibc― 所有用户应用程序都将链接到的 C 库。避免使用任何 C 库函数的内核和其它应用程序可以在没有该库的情况下进行编译。

构建工具链建立了一个交叉编译器环境。 本地编译器编译与本机同类的处理器的指令。 交叉编译器运 行在某一种处理器上,却可以编译另一种处理器的指令。重头设置交叉编译器工具链可不是一项简单的任务:它包括下载源代码、修补补丁、配置、编译、设置头文 件、安装以及很多很多的操作。另外,这样一个彻底的构建过程对内存和硬盘的需求是巨大的。如果没有足够的内存和硬盘空间,那么在构建阶段由于相关性、配置 或头文件设置等问题会突然冒出许多问题。

因此能够从因特网上获得已预编译的二进制文件是一件好事(但不太好的一点是,目前它们大多数只限于基于 ARM 的系统,但迟早会改变的)。一些比较流行的已预编译的工具链包括那些来自 Compaq(Familiar Linux )、LART(LART Linux)和 Embedian(基于 Debian 但与它无关)的工具链 ― 所有这些工具链都用于基于 ARM 的平台。

内核设置

Linux 社区正积极地为新硬件添加功能部件和支持、在内核中修正错误并且及时地进行常规改进。这导致大约每 6 个月(或 6 个月不到)就有一个稳定的 Linux 树的新发行版。不同的维护者维护针对特定体系结构的不同内核树和补丁。当为一个项目选择了一个内核时,您需要评估最新发行版的稳定性如何、它是否符合项目 要求和硬件平台、从编程角度来看它的舒适程度以及其它难以确定的方面。还有一点也非常重要:找到需要应用于基本内核的所有补丁,以便为特定的体系结构调整 内核。

内核布局

内 核布局分为特定于体系结构的部分和与体系结构无关的部分。内核中特定于体系结构的部分首先执行,设置硬件寄存器、配置内存映射、执行特定于体系结构的初始 化,然后将控制转给内核中与体系结构无关的部分。系统的其余部分在这第二个阶段期间进行初始化。内核树下的目录 arch/ 由不同的子目录组成,每个子目录用于一个不同的体系结构(MIPS、ARM、i386、SPARC、PPC 等)。每一个这样的子目录都包含 kernel/ 和 mm/ 子目录,它们包含特定于体系结构的代码来完成象初始化内存、设置 IRQ、启用高速缓存、设置内核页面表等操作。一旦装入内核并给予其控制,就首先调用这些函数,然后初始化系统的其余部分。

根据可用的系统资源和引导装载程序的功能,内核可以编译成 vmlinux、Image 或 zImage。vmlinux 和 zImage 之间的主要区别在于 vmlinux是实际的(未压缩的)可执行文件,而 zImage是或多或少包含相同信息的自解压压缩文件 ― 只是压缩它以处理(通常是 Intel 强制的)640 KB 引导时间的限制。有关所有这些的权威性解释,请参阅 Linux Magazine的文章“Kernel Configuration: dealing with the unexpected”(请参阅 参考资料)。

内核链接和装入

一 旦为目标系统编译了内核后,通过使用引导装载程序(它已经被装入到目标的闪存中),内核就被装入到目标系统的内存(在 DRAM 中或者在闪存中)。通过使用串行、USB 或以太网端口,引导装载程序与主机通信以将内核传送到目标的闪存或 DRAM 中。在将内核完全装入目标后,引导装载程序将控制传递给装入内核的地址。

内核可执行文件由许多链接在一起的对象文件组成。对象文件有许多节,如文本、数据、init 数据、bass 等等。这些对象文件都是由一个称为 链接器脚本的文件链接并装入的。这个链接器脚本的功能是将输入对象文件的各节映射到输出文件中;换句话说,它将所有输入对象文件都链接到单一的可执行文件中,将该可执行文件的各节装入到指定地址处。 vmlinux.lds是存在于 arch/<target>/ 目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。典型的 vmlinux.lds 看起来象这样:


清单 2. 典型的 vmlinux.lds 文件
												OUTPUT_ARCH(<arch>)      /* <arch> includes architecture type */ 
ENTRY(stext) /* stext is the kernel entry point */
SECTIONS /* SECTIONS command describes the layout
of the output file */
{
. = TEXTADDR; /* TEXTADDR is LMA for the kernel */
.init : { /* Init code and data*/
_stext = .; /* First section is stext followed
by __init data section */
__init_begin = .;
*(.text.init)
__init_end = .;
}
.text : { /* Real text segment follows __init_data section */
_text = .;
*(.text)
_etext = .; /* End of text section*/
}
.data :{
_data=.; /* Data section comes after text section */
*(.data)
_edata=.;
} /* Data section ends here */
.bss : { /* BSS section follows symbol table section */
__bss_start = .;
*(.bss)
_end = . ; /* BSS section ends here */
}
}

LMA 是装入模块地址;它表示将要装入内核的目标虚拟内存中的地址。 TEXTADDR 是内核的虚拟起始地址,并且在 arch/<target>/ 下的 Makefile 中指定它的值。这个地址必须与引导装载程序使用的地址相匹配。

一旦引导装载程序将内核复制到闪存或 DRAM 中,内核就被重新定位到 TEXTADDR — 它通常在 DRAM 中。然后,引导装载程序将控制转给这个地址,以便内核能开始执行。

参数传递和内核引导

stext 是内核入口点,这意味着在内核引导时将首先执行这一节下的代码。它通常用汇编语言编写,并且通常它在 arch/<target>/ 内核目录下。这个代码设置内核页面目录、创建身份内核映射、标识体系结构和处理器以及执行分支 start_kernel (初始化系统的主例程)。

start_kernel 调用 setup_arch 作为执行的第一步,在其中完成特定于体系结构的设置。这包括初始化硬件寄存器、标识根设备和系统中可用的 DRAM 和闪存的数量、指定系统中可用页面的数目、文件系统大小等等。所有这些信息都以参数形式从引导装载程序传递到内核。

将参数从引导装载程序传递到内核有两种方法:parameter_structure 和标记列表。在这两种方法中,不赞成使用参数结构,因为它强加了限制:指定在内存中,每个参数必须位于 param_struct 中的特定偏移量处。最新的内核期望参数作为标记列表的格式来传递,并将参数转化为已标记格式。 param_struct 定义在 include/asm/setup.h 中。它的一些重要字段是:


清单 3. 样本参数结构
												struct param_struct  { 
unsigned long page_size; /* 0: Size of the page */
unsigned long nr_pages; /* 4: Number of pages in the system */
unsigned long ramdisk /* 8: ramdisk size */
unsigned long rootdev; /* 16: Number representing the root device */
unsigned long initrd_start; /* 64: starting address of initial ramdisk */
/* This can be either in flash/dram */
unsigned long initrd_size; /* 68: size of initial ramdisk */
}

请注意:这些数表示定义字段的参数结构中的偏 移量。这意味着如果引导装载程序将参数结构放置在地址 0xc0000100,那么 rootdev 参数将放置在 0xc0000100 + 16,initrd_start 将放置在 0xc0000100 + 64 等等 ― 否则,内核将在解释正确的参数时遇到困难。

正 如上面提到的,因为从引导装载程序到内核的参数传递会有一些约束条件,所以大多数 2.4.x 系列内核期望参数以已标记的列表格式传递。在已标记的列表中,每个标记由标识被传递参数的 tag_header 以及其后的参数值组成。标记列表中标记的常规格式可以如下所示:


清单 4. 样本标记格式。内核通过 <ATAG_TAGNAME> 头来标识每个标记。
												#define <aTAG_TAGNAME>  <Some Magic number> 

struct <tag_tagname> {
u32 <tag_param>;
u32 <tag_param>;
};

/* Example tag for passing memory information */

#define ATAG_MEM 0x54410002 /* Magic number */

struct tag_mem32 {
u32 size; /* size of memory */
u32 start; /* physical start address of memory*/
};

setup_arch 还需要对闪存存储库、系统寄存器和其它特定设备执行内存映射。一旦完成了特定于体系结构的设置,控制就返回到初始化系统其余部分的 start_kernel 函数。这些附加的初始化任务包含:

  • 设置陷阱
  • 初始化中断
  • 初始化计时器
  • 初始化控制台
  • 调用 mem_init ,它计算各种区域、高内存区等内的页面数量
  • 初始化 slab 分配器并为 VFS、缓冲区高速缓存等创建 slab 高速缓存
  • 建立各种文件系统,如 proc、ext2 和 JFFS2
  • 创建 kernel_thread ,它执行文件系统中的 init 命令并显示 lign 提示符。 如果在 /bin、/sbin 或 /etc 中没有 init 程序,那么内核将执行文件系统的 /bin 中的 shell。




回页首


设备驱动程序

嵌 入式系统通常有许多设备用于与用户交互,象触摸屏、小键盘、滚动轮、传感器、RA232 接口、LCD 等等。除了这些设备外,还有许多其它专用设备,包括闪存、USB、GSM 等。内核通过所有这些设备各自的设备驱动程序来控制它们,包括 GUI 用户应用程序也通过访问这些驱动程序来访问设备。本节着重讨论通常几乎在每个嵌入式环境中都会使用的一些重要设备的设备驱动程序。

帧缓冲区驱动程序

这 是最重要的驱动程序之一,因为通过这个驱动程序才能使系统屏幕显示内容。帧缓冲区驱动程序通常有三层。最底层是基本控制台驱动程序 drivers/char/console.c,它提供了文本控制台常规接口的一部分。通过使用控制台驱动程序函数,我们能将文本打印到屏幕上 ― 但图形或动画还不能(这样做需要使用视频模式功能,通常出现在中间层,也就是 drivers/video/fbcon.c 中)。这个第二层驱动程序提供了视频模式中绘图的常规接口。

帧缓冲区是显卡上的内存,需要将它内存映射到用户空间以便可以将图形 和文本能写到这个内存段上:然后这个信息将反映到屏幕上。帧缓冲区支持提高了绘图的速度和整体性能。这也是顶层驱动程序引人注意之处:顶层是非常特定于硬 件的驱动程序,它需要支持显卡不同的硬件方面 ― 象启用/禁用显卡控制器、深度和模式的支持以及调色板等。所有这三层都相互依赖以实现正确的视频功能。与帧缓冲区有关的设备是 /dev/fb0(主设备号 29,次设备号 0)。

输入设备驱动程序

可触摸板是用于嵌入式设备的最基本的用户交互设备之一 ― 小键盘、传感器和滚动轮也包含在许多不同设备中以用于不同的用途。

触摸板设备的主要功能是随时报告用户的触摸,并标识触摸的坐标。这通常在每次发生触摸时,通过生成一个中断来实现。

然后,这个设备驱动程序的角色是每当出现中断时就查询触摸屏控制器,并请求控制器发送触摸的坐标。一旦驱动程序接收到坐标,它就将有关触摸和任何可用数据的信号发送给用户应用程序,并将数据发送给应用程序(如果可能的话)。然后用户应用程序根据它的需要处理数据。

几乎所有输入设备 ― 包括小键盘 ― 都以类似原理工作。

闪存 MTD 驱动程序

MTD 设备是象闪存芯片、小型闪存卡、记忆棒等之类的设备,它们在嵌入式设备中的使用正在不断增长。

MTD 驱动程序是在 Linux 下专门为嵌入式环境开发的新的一类驱动程序。相对于常规块设备驱动程序,使用 MTD 驱动程序的主要优点在于 MTD 驱动程序是专门为基于闪存的设备所设计的,所以它们通常有更好的支持、更好的管理和基于扇区的擦除和读写操作的更好的接口。Linux 下的 MTD 驱动程序接口被划分为两类模块:用户模块和硬件模块。

用户模块
这些模块提供从用 户空间直接使用的接口:原始字符访问、原始块访问、FTL(闪存转换层,Flash Transition Layer ― 用在闪存上的一种文件系统)和 JFS(即日志文件系统,Journaled File System ― 在闪存上直接提供文件系统而不是模拟块设备)。用于闪存的 JFS 的当前版本是 JFFS2(稍后将在本文中描述)。

硬件模块
这些模块提供对内存设备的物理访问,但并不直接使用它们。通过上述的用户模块来访问它们。这些模块提供了在闪存上读、擦除和写操作的实际例程。

MTD 驱动程序设置
为了访问特定的闪存设备并将文件系统置于其上,需要将 MTD 子系统编译到内核中。这包括选择适当的 MTD 硬件和用户模块。当前,MTD 子系统支持为数众多的闪存设备 ― 并且有越来越多的驱动程序正被添加进来以用于不同的闪存芯片。

有两个流行的用户模块可启用对闪存的访问: MTD_CHARMTD_BLOCK

MTD_CHAR 提供对闪存的原始字符访问,而 MTD_BLOCK 将闪存设计为可以在上面创建文件系统的常规块设备(象 IDE 磁盘)。与 MTD_CHAR 关联的设备是 /dev/mtd0、mtd1、mtd2(等等),而与 MTD_BLOCK 关联的设备是 /dev/mtdblock0、mtdblock1(等等)。由于 MTD_BLOCK 设备提供象块设备那样的模拟,通常更可取的是在这个模拟基础上创建象 FTL 和 JFFS2 那样的文件系统。

为了进行这个操作,可能需要创建分区表将闪存设备分拆到引导装载程序节、内核节和文件系统节中。样本分区表可能包含以下信息:


清单 5. MTD 的简单闪存设备分区
												struct mtd_partition sample_partition = { 
{
/* First partition */
name : bootloader, /* Bootloader section */
size : 0x00010000, /* Size */
offset : 0, /* Offset from start of flash- location 0x0*/
mask_flags : MTD_WRITEABLE /* This partition is not writable */
},
{ /* Second partition */
name : Kernel, /* Kernel section */
size : 0x00100000, /* Size */
offset : MTDPART_OFS_APPEND, /* Append after bootloader section */
mask_flags : MTD_WRITEABLE /* This partition is not writable */
},
{ /* Third partition */
name : JFFS2, /* JFFS2 filesystem */
size : MTDPART_SIZ_FULL, /* Occupy rest of flash */
offset : MTDPART_OFS_APPEND /* Append after kernel section */
}
}

上面的分区表使用了 MTD_BLOCK 接口对闪存设备进行分区。这些分区的设备节点是:


简单闪存分区的设备节点
												 User      device node         Major number    Minor number

Bootloader /dev/mtdblock0 31 0
Kernel /dev/mtdblock1 31 1
Filesystem /dev/mtdblock2 31 2

在本例中,引导装载程序必须将有关 root 设备节点(/dev/mtdblock2)和可以在闪存中找到文件系统的地址(本例中是 FLASH_BASE_ADDRESS + 0x04000000 )的正确参数传递到内核。一旦完成分区,闪存设备就准备装入或挂装文件系统。

Linux 中 MTD 子系统的主要目标是在系统的硬件驱动程序和上层,或用户模块之间提供通用接口。硬件驱动程序不需要知道象 JFFS2 和 FTL 那样的用户模块使用的方法。所有它们真正需要提供的就是一组对底层闪存系统进行 readwriteerase 操作的简单例程。





回页首


嵌入式设备的文件系统

系统需要一种以结构化格式存储和检索信息的方法;这就需要文件系统的参与。Ramdisk(请参阅 参考资料)是通过将计算机的 RAM 用作设备来创建和挂装文件系统的一种机制,它通常用于无盘系统(当然包括微型嵌入式设备,它只包含作为永久存储媒质的闪存芯片)。

用户可以根据可靠性、健壮性和/或增强的功能的需求来选择文件系统的类型。下一节将讨论几个可用选项及其优缺点。

第二版扩展文件系统(Ext2fs)

Ext2fs 是 Linux 事实上的标准文件系统,它已经取代了它的前任 ― 扩展文件系统(或 Extfs)。Extfs 支持的文件大小最大为 2 GB,支持的最大文件名称大小为 255 个字符 ― 而且它不支持索引节点(包括数据修改时间标记)。Ext2fs 做得更好;它的 优点是:

  • Ext2fs 支持达 4 TB 的内存。
  • Ext2fs 文件名称最长可以到 1012 个字符。
  • 当创建文件系统时,管理员可以选择逻辑块的大小(通常大小可选择 1024、2048 和 4096 字节)。
  • Ext2fs 了实现快速符号链接:不需要为此目的而分配数据块,并且将目标名称直接存储在索引节点(inode)表中。这使性能有所提高,特别是在速度上。

因为 Ext2 文件系统的稳定性、可靠性和健壮性,所以几乎在所有基于 Linux 的系统(包括台式机、服务器和工作站 ― 并且甚至一些嵌入式设备)上都使用 Ext2 文件系统。然而,当在嵌入式设备中使用 Ext2fs 时,它有一些 缺点

  • Ext2fs 是为象 IDE 设备那样的块设备设计的,这些设备的逻辑块大小是 512 字节,1 K 字节等这样的倍数。这不太适合于扇区大小因设备不同而不同的闪存设备。
  • Ext2 文件系统没有提供对基于扇区的擦除/写操作的良好管理。在 Ext2fs 中,为了在一个扇区中擦除单个字节,必须将整个扇区复制到 RAM,然后擦除,然后重写入。考虑到闪存设备具有有限的擦除寿命(大约能进行 100,000 次擦除),在此之后就不能使用它们,所以这不是一个特别好的方法。
  • 在出现电源故障时,Ext2fs 不是防崩溃的。
  • Ext2 文件系统不支持损耗平衡,因此缩短了扇区/闪存的寿命。(损耗平衡确保将地址范围的不同区域轮流用于写和/或擦除操作以延长闪存设备的寿命。)
  • Ext2fs 没有特别完美的扇区管理,这使设计块驱动程序十分困难。

由于这些原因,通常相对于 Ext2fs,在嵌入式环境中使用 MTD/JFFS2 组合是更好的选择。

用 Ramdisk 挂装 Ext2fs
通过使用 Ramdisk 的概念,可以在嵌入式设备中创建并挂装 Ext2 文件系统(以及用于这一目的的任何文件系统)。


清单 6. 创建一个简单的基于 Ext2fs 的 Ramdisk
												 mke2fs -vm0 /dev/ram 4096
mount -t ext2 /dev/ram /mnt
cd /mnt
cp /bin, /sbin, /etc, /dev ... files in mnt
cd ../
umount /mnt
dd if=/dev/ram bs=1k count=4096 of=ext2ramdisk

mke2fs 是用于在任何设备上创建 ext2 文件系统的实用程序 — 它创建超级块、索引节点以及索引节点表等等。

在上面的用法中, /dev/ram 是上面构建有 4096 个块的 ext2 文件系统的设备。然后,将这个设备( /dev/ram )挂装在名为 /mnt 的临时目录上并且复制所有必需的文件。一旦复制完这些文件,就卸装这个文件系统并且设备( /dev/ram )的内容被转储到一个文件(ext2ramdisk)中,它就是所需的 Ramdisk(Ext2 文件系统)。

上面的顺序创建了一个 4 MB 的 Ramdisk,并用必需的文件实用程序来填充它。

一些要包含在 Ramdisk 中的重要目录是:

  • /bin ― 保存大多数象 initbusyboxshell 、文件管理实用程序等二进制文件。
  • /dev― 包含用在设备中的所有设备节点
  • /etc― 包含系统的所有配置文件
  • /lib― 包含所有必需的库,如 libc、libdl 等

日志闪存文件系统,版本 2(JFFS2)

瑞典的 Axis Communications 开发了最初的 JFFS,Red Hat 的 David Woodhouse 对它进行了改进。 第二个版本,JFFS2,作为用于微型嵌入式设备的原始闪存芯片的实际文件系统而出现。JFFS2 文件系统是日志结构化的,这意味着它基本上是一长列节点。每个节点包含有关文件的部分信息 ― 可能是文件的名称、也许是一些数据。相对于 Ext2fs,JFFS2 因为有以下这些 优点而在无盘嵌入式设备中越来越受欢迎:

  • JFFS2 在扇区级别上执行闪存擦除/写/读操作要比 Ext2 文件系统好。
  • JFFS2 提供了比 Ext2fs 更好的崩溃/掉电安全保护。当需要更改少量数据时,Ext2 文件系统将整个扇区复制到内存(DRAM)中,在内存中合并新数据,并写回整个扇区。这意味着为了更改单个字,必须对整个扇区(64 KB)执行读/擦除/写例程 ― 这样做的效率非常低。要是运气差,当正在 DRAM 中合并数据时,发生了电源故障或其它事故,那么将丢失整个数据集合,因为在将数据读入 DRAM 后就擦除了闪存扇区。JFFS2 附加文件而不是重写整个扇区,并且具有崩溃/掉电安全保护这一功能。
  • 这可能是最重要的一点:JFFS2 是专门为象闪存芯片那样的嵌入式设备创建的,所以它的整个设计提供了更好的闪存管理。

因为本文主要是写关于闪存设备的使用,所以在嵌入式环境中使用 JFFS2 的 缺点很少:

  • 当文件系统已满或接近满时,JFFS2 会大大放慢运行速度。这是因为垃圾收集的问题(更多信息,请参阅 参考资料)。

创建 JFFS2 文件系统
在 Linux 下,用 mkfs.jffs2 命令创建 JFFS2 文件系统(基本上是使用 JFFS2 的 Ramdisk)。


清单 7. 创建 JFFS2 文件系统
												mkdir jffsfile 
cd jffsfile

/* copy all the /bin, /etc, /usr/bin, /sbin/ binaries and /dev entries
that are needed for the filesystem here */

/* Type the following command under jffsfile directory to create the JFFS2 Image */

./mkfs.jffs2 -e 0x40000 -p -o ../jffs.image

上面显示了 mkfs.jffs2 的典型用法。 -e 选项确定闪存的擦除扇区大小(通常是 64 千字节)。 -p 选项用来在映像的剩余空间用零填充。 -o 选项用于输出文件,通常是 JFFS2 文件系统映像 ― 在本例中是 jffs.image。一旦创建了 JFFS2 文件系统,它就被装入闪存中适当的位置(引导装载程序告知内核查找文件系统的地址)以便内核能挂装它。

tmpfs
当 Linux 运行于嵌入式设备上时,该设备就成为功能齐全的单元,许多守护进程会在后台运行并生成许多日志消息。另外,所有内核日志记录机制,象 syslogd、dmesg 和 klogd,会在 /var 和 /tmp 目录下生成许多消息。由于这些进程产生了大量数据,所以允许将所有这些写操作都发生在闪存是不可取的。由于在重新引导时这些消息不需要持久存储,所以这个 问题的解决方案是使用 tmpfs。

tmpfs 是基于内存的文件系统,它主要用于减少对系统的不必要的闪存写操作这一唯一目的。因为 tmpfs 驻留在 RAM 中,所以写/读/擦除的操作发生在 RAM 中而不是在闪存中。因此,日志消息写入 RAM 而不是闪存中,在重新引导时不会保留它们。tmpfs 还使用磁盘交换空间来存储,并且当为存储文件而请求页面时,使用虚拟内存(VM)子系统。

tmpfs 的 优点包括:

  • 动态文件系统大小 ― 文件系统大小可以根据被复制、创建或删除的文件或目录的数量来缩放。使得能够最理想地使用内存。
  • 速度 ― 因为 tmpfs 驻留在 RAM,所以读和写几乎都是瞬时的。即使以交换的形式存储文件,I/O 操作的速度仍非常快。

tmpfs 的一个 缺点是当系统重新引导时会丢失所有数据。因此,重要的数据不能存储在 tmpfs 上。

挂装 tmpfs
诸如 Ext2fs 和 JFFS2 等大多数其它文件系统都驻留在底层块设备之上,而 tmpfs 与它们不同,它直接位于 VM 上。因而,挂装 tmpfs 文件系统是很简单的事:


清单 8. 挂装 tmpfs
												/* Entries in /etc/rc.d/rc.sysinit for creating/using tmpfs */ 

# mount -t tmpfs tmpfs /var -o size=512k
# mkdir -p /var/tmp
# mkdir -p /var/log
# ln -s /var/tmp /tmp

上面的命令将在 /var 上创建 tmpfs 并将 tmpfs 的最大大小限制为 512 K。同时,tmp/ 和 log/ 目录成为 tmpfs 的一部分以便在 RAM 中存储日志消息。

如果您想将 tmpfs 的一个项添加到 /etc/fstab,那么它可能看起来象这样:

												tmpfs /var tmpfs size=32m 0 0
										

这将在 /var 上挂装一个新的 tmpfs 文件系统。





回页首


图形用户界面(GUI)选项

从 用户的观点来看,图形用户界面(GUI)是系统的一个最至关重要的方面:用户通过 GUI 与系统进行交互。所以 GUI 应该易于使用并且非常可靠。但它还需要是有内存意识的,以便在内存受限的、微型嵌入式设备上可以无缝执行。所以,它应该是轻量级的,并且能够快速装入。

另一个要考虑的重要方面涉及许可证问题。一些 GUI 分发版具有允许免费使用的许可证,甚至在一些商业产品中也是如此。另一些许可证要求如果想将 GUI 合并入项目中则要支付版税。

最 后,大多数开发人员可能会选择 XFree86,因为 XFree86 为他们提供了一个能使用他们喜欢的工具的熟悉环境。但是市场上较新的 GUI,象 Century Software 的 Microwindows(Nano-X)和 Trolltech 的 QT/Embedded,与 X 在嵌入式 Linux 的竞技舞台中展开了激烈竞争,这主要是因为它们占用很少的资源、执行的速度很快并且具有定制窗口构件的支持。

让我们看一看这些选项中的每一个。

Xfree86 4.X(带帧缓冲区支持的 X11R6.4)

XFree86 Project, Inc. 是一家生产 XFree86 的公司,该产品是一个可以免费重复分发、开放源码的 X Window 系统。X Window 系统(X11)为应用程序以图形方式进行显示提供了资源,并且它是 UNIX 和类 UNIX 的机器上最常用的窗口系统。它很小但很有效,它运行在为数众多的硬件上,它对网络透明并且有良好的文档说明。X11 为窗口管理、事件处理、同步和客户机间通信提供强大的功能 ― 并且大多数开发人员已经熟悉了它的 API。它具有对内核帧缓冲区的内置支持,并占用非常少的资源 ― 这非常有助于内存相对较少的设备。X 服务器支持 VGA 和非 VGA 图形卡,它对颜色深度 1、2、4、8、16 和 32 提供支持,并对渲染提供内置支持。最新的发行版是 XFree86 4.1.0。

它的 优点包括:

  • 帧缓冲区体系结构的使用提高了性能。
  • 占用的资源相对很小 ― 大小在 600 K 到 700 K 字节的范围内,这使它很容易在小型设备上运行。
  • 非常好的支持:在线有许多文档可用,还有许多专用于 XFree86 开发的邮递列表。
  • X API 非常适合扩展。

它的 缺点包括:

  • 比最近出现的嵌入式 GUI 工具性能差。
  • 此外,当与 GUI 中最新的开发 ― 象专门为嵌入式环境设计的 Nano-X 或 QT/Embedded ― 相比时,XFree86 似乎需要更多的内存。

Microwindows

Microwindows 是 Century Software 的开放源代码项目,设计用于带小型显示单元的微型设备。它有许多针对现代图形视窗环境的功能部件。象 X 一样,有多种平台支持 Microwindows。

Microwindows 体系结构是基于客户机/服务器的并且具有分层设计。最底层是屏幕和输入设备驱动程序(关于键盘或鼠标)来与实际硬件交互。在中间层,可移植的图形引擎提供对线的绘制、区域的填充、多边形、裁剪以及颜色模型的支持。

在 最上层,Microwindows 支持两种 API:Win32/WinCE API 实现,称为 Microwindows;另一种 API 与 GDK 非常相似,它称为 Nano-X。Nano-X 用在 Linux 上。它是象 X 的 API,用于占用资源少的应用程序。

Microwindows 支持 1、2、4 和 8 bpp(每像素的位数)的 palletized 显示,以及 8、16、24 和 32 bpp 的真彩色显示。Microwindows 还支持使它速度更快的帧缓冲区。Nano-X 服务器占用的资源大约在 100 K 到 150 K 字节。

原始 Nano-X 应用程序的平均大小在 30 K 到 60 K。由于 Nano-X 是为有内存限制的低端设备设计的,所以它不象 X 那样支持很多函数,因此它实际上不能作为微型 X(Xfree86 4.1)的替代品。

可以在 Microwindows 上运行 FLNX,它是针对 Nano-X 而不是 X 进行修改的 FLTK(快速轻巧工具箱(Fast Light Toolkit))应用程序开发环境的一个版本。本文中描述 FLTK。

Nano-X 的 优点包括:

  • 与 Xlib 实现不同,Nano-X 仍在每个客户机上同步运行,这意味着一旦发送了客户机请求包,服务器在为另一个客户机提供服务之前一直等待,直到整个包都到达为止。这使服务器代码非常简单,而运行的速度仍非常快。
  • 占用很小的资源

Nano-X 的 缺点包括:

  • 联网功能部件至今没有经过适当地调整(特别是网络透明性)。
  • 还没有太多现成的应用程序可用。
  • 与 X 相比,Nano-X 虽然近来正在加速开发,但仍没有那么多文档说明而且没有很好的支持,但这种情形会有所改变。

Microwindows 上的 FLTK API

FLTK 是一个简单但灵活的 GUI 工具箱,它在 Linux 世界中赢得越来越多的关注,它特别适用于占用资源很少的环境。它提供了您期望从 GUI 工具箱中获得的大多数窗口构件,如按钮、对话框、文本框以及出色的“赋值器”选择(用于输入数值的窗口构件)。还包括滑动器、滚动条、刻度盘和其它一些构 件。

针对 Microwindows GUI 引擎的 FLTK 的 Linux 版本被称为 FLNX。FLNX 由两个组件构成:Fl_Widget 和 FLUID。Fl_Widget 由所有基本窗口构件 API 组成。FLUID(快速轻巧的用户界面设计器(Fast Light User Interface Designer, FLUID))是用来产生 FLTK 源代码的图形编辑器。总的来说,FLNX 是能用来为嵌入式环境创建应用程序的一个出色的 UI 构建器。

Fl_Widget 占用的资源大约是 40 K 到 48 K,而 FLUID(包括了每个窗口构件)大约占用 380 K。这些非常小的资源占用率使 Fl_Widget 和 FLUID 在嵌入式开发世界中非常受欢迎。

优点包括:

  • 习惯于在象 Windows 这样已建立得较好的环境中开发基于 GUI 的应用程序的任何人都会非常容易地适应 FLTK 环境。
  • 它的文档包括一本十分完整且编写良好的手册。
  • 它使用 LGPL 进行分发,所以开发人员可以灵活地发放他们应用程序的许可证。
  • FLTK 是一个 C++ 库(Perl 和 Python 绑定也可用)。面向对象模型的选择是一个好的选择,因为大多数现代 GUI 环境都是面向对象的;这也使将编写的应用程序移植到类似的 API 中变得更容易。
  • Century Software 的环境提供了几个有用的工具,诸如 ScreenToP 和 ViewML 浏览器。

它的 缺点是:

  • 普通的 FLTK 可以与 X 和 Windows API 一同工作,而 FLNX 不能。它与 X 的不兼容性阻碍了它在许多项目中的使用。

Qt/Embedded

Qt/Embedded 是 Trolltech 新开发的用于嵌入式 Linux 的图形用户界面系统。Trolltech 最初创建 Qt 作为跨平台的开发工具用于 Linux 台式机。它支持各种有 UNIX 特点的系统以及 Microsoft Windows。KDE ― 最流行的 Linux 桌面环境之一,就是用 Qt 编写的。

Qt/Embedded 以原始 Qt 为基础,并做了许多出色的调整以适用于嵌入式环境。Qt Embedded 通过 Qt API 与 Linux I/O 设施直接交互。那些熟悉并已适应了面向对象编程的人员将发现它是一个理想环境。而且,面向对象的体系结构使代码结构化、可重用并且运行快速。与其它 GUI 相比,Qt GUI 非常快,并且它没有分层,这使得 Qt/Embedded 成为用于运行基于 Qt 的程序的最紧凑环境。

Trolltech 还推出了 Qt 掌上机环境(Qt Palmtop Environment,俗称 Qpe)。Qpe 提供了一个基本桌面窗口,并且该环境为开发提供了一个易于使用的界面。Qpe 包含全套的个人信息管理(Personal Information Management (PIM))应用程序、因特网客户机、实用程序等等。然而,为了将 Qt/Embedded 或 Qpe 集成到一个产品中,需要从 Trolltech 获得商业许可证。(原始 Qt 自版本 2.2 以后就可以根据 GPL 获得 。)

它的 优点包括:

  • 面向对象的体系结构有助于更快地执行
  • 占用很少的资源,大约 800 K
  • 抗锯齿文本和混合视频的象素映射

它的 缺点是:

  • Qt/Embedded 和 Qpe 只能在获得商业许可证的情况下才能使用。




回页首


结束语

嵌 入式 Linux 开发正迅速地发展着。您必须学习并从引导装载程序和分发版到文件系统和 GUI 中的每一个事物的各种选项中作出选择。但是要感谢有这种选择自由度以及非常活跃的 Linux 社区,Linux 上的嵌入式开发已经达到了新的境界,并且调整模块以适合您的规范从未比现在更简单。这已经导致出现了许多时新的手持和微型设备作为开放盒,这是件好事 ― 因为事实是您不必成为一个专家从这些模块中进行选择来调整您的设备以满足您自己的要求和需要。

我们希望这篇对嵌入式 Linux 领域的介绍性概述能激起您进行试验的欲望,并且希望您将体会摆弄微型设备的乐趣以满足您的爱好。为进一步有助于您的项目,请参阅下面的“参考资料”,链接到有关我们这里已经概述的技术的更深入的信息。





回页首


参考资料

引导

小型分发版

工具链

  • Wiki 工具链页面包含到本文提到的所有三个工具链的链接,还有对它们的评论。

设备驱动程序

有用的工具

  • 请查看 LART 上的 Jflash-linux


  • BinutilsGCCGlibc都可从 Free Software Foundation 下载获得。


  • 许多有用的下载都可从 Netwinder.org获得,这是一个致力于 NetWinder 平台上开发工作的志愿者站点。


  • 请在 Mark Nielsen 写得非常棒的 How to use a Ramdisk for Linux一文中阅读有关 Ramdisk 的所有信息。


  • FLNX 是以 FLTK(快速轻巧的工具箱)为基础的。

文件系统

GUI

一般参考资料

  • General Public License 或 GPL确保用户复制、分发和修改软件的权利。


  • ARM Linux是您了解有关 Linux 用于 ARM 处理器的信息的一个非常好的站点。它由 ARM 的创建者 Russell King 来维护。


  • Penguinppc.org是关于 Linux 用于 PowerPC 系列处理器的的主页。该站点上有一个关于为基于 PPC 的体系结构建立工具链的资料丰富的教程。


  • Linux Devices是一个非常全面的站点,它包含有关 Linux 和嵌入式开发的出版发行、快速参考、新闻和特色报告等各种信息。


  • Silicon Penguin列表站点上拥有嵌入式 Linux 参考资料的详尽集合。


  • ARMLinux - the book可从 Aleph One 上获得。您可以定购一本,也可以 在线阅读


  • 嵌入式 Linux 协会(Embedded Linux Consortium)是一个非赢利的互助协会,它欢迎致力于嵌入式 Linux 领域的开发人员成为会员。


  • IBM 的 Linux wristwatch是运行 Linux 的微型嵌入式设备的示例;本文的作者之一,Vishal Kulkarni 也参与了它的研发。请在 本文FreeOS.com,2001 年 3 月)中阅读有关它的信息。


  • developerWorks上浏览 更多 Linux 参考资料


  • developerWorks上浏览 更多无线领域的参考资料




回页首


作者简介


Anand K Santhanam 在印度 Madras 大学获得计算机科学工学学士学位。自 1999 年 7 月以来他一直在印度为 IBM Global Services(软件实验室)工作。他是 IBM Linux 小组的成员,这个小组主要致力于嵌入式系统中的 ARM-Linux、设备驱动程序和电源管理的研究和开发。他感兴趣的其它领域是 O/S 本质和联网。可以通过 asanthan@in.ibm.com 与他联系。



Vishal Kulkarni 从印度 Maharashtra 的 Shivaji 大学获得电子工程的学士学位。自 1999 年 3 月以来他一直在印度为 IBM Global Services(软件实验室)工作。在此之前,他曾在美国 IBM Austin 工作了一年半多。他是 IBM Linux 小组的成员,这个小组主要致力于嵌入式设备上的 ARM-Linux、设备驱动程序和 GUI。他感兴趣的其它领域是 O/S 本质和联网。可以通过 kvishal@in.ibm.com与他联系。

posted @ 2006-05-30 18:41 易道 阅读(378) | 评论 (0)编辑 收藏

[OpenSource] 用红帽子的chkconfig管理Init脚本
作者:Jimmy Ball
译者:Fenng
日期:11-Dec-2001
出处:http://www.dbanotes.net
版本:0.99

你的管理工具中多了个简单但非常有用的东东。

我喜欢发现新的 UNIX 命令,尤其是那些关于系统管理的。当我得知红帽子发布 chkconfig 这个工具,我想起了在 IRIX --一个从 SGI而来的 UNIX 变种--下的 chkconfig。IRIX 的 chkconfig 用来激活/禁止系统初始化的时候的服务,无需编辑,重命名或是移动 /etc 中的 init 脚本。

类似,Red Hat 设计 chkconfig 的目的就是用来管理系统初始化的时候启动的服务。不过,在我仔细阅读 手册并作了些测试后,我很快发现 Red Hat 扩展了chkconfig,通过管理 init 脚本的符号连接得以最终控制启动关闭时的系统任务, 真是节省时间!

关于启动的基础知识

当你的 linux 启动时, 它显示的第一个进程是 init 。如果你以前没看到过显示 init 进程, 输入:

# ps -ef | grep init 
就会看到 init 的 PID。 简而言之,init 运行 /etc/inittab 中描述的任务。

/etc/inittab中说明的任务在init之后就会启动, 不过其它的任务启动很简单。例如,默认情况下 Red Hat 的 /etc/inittab 对 Ctrl-Alt-Delete 键序设置了一个 trap,当这些键在控制台模式下(不是 xdm)同 时按下 ,就会运行 shutdown 命令。 在启动的时候,init 基于 /etc/inittab 的设置选项设定这个特性,不过在这个键序发生的时候才会执行。

inittab 的格式允许以"#"开始注释行,正常的条目用 ":" 界定。遵从如下的格式:
id:runlevel:action:process 
id 代表用户定义的唯一的标志
runlevel 可以使0-6的组合 或者为空
action 来自一个关键词keyword 描述init如何对待process
process 是要执行的命令

描述 action 字段的各种关键字可以在 inittab 的手册中找到。常用的关键字,不是全部,UNIX 平台包括这些:

initdefault定义一个系统启动后进入的运行级
wait会被执行一次的进程 (当进入运行级的时候)。init 进程将等待这个进程被终止
boot定义一个启动的时候执行的进程
bootwait与 boot 类似 ,不过 init在继续运行前等待进程的终止
sysinit定义一个进程在 boot的时候执行,在任何 boot或者bootwait inittab 条目的前面执行。

runlevel 字段指明系统状态。例如,运行级 0 代表系统关机,运行级 6 代表系统重启 。不幸的是,不是所有的 Linux 发布都遵循同样的运行级定义 。在 Red Hat 中,默认情况下支持下面这些

0. 系统挂起
1. 但用户Single-user mode
2. 多用户,没有NFS
3. 完整的多用户Complete multiuser mode
4. 用户自定义
5. X11 (XDM 登录)
6. 重新启动

每一个运行级在 /etc/rc.d 下都有个相应的目录。如运行级 5,目录就是 /etc/rc.d/rc5.d 。包含启动这个运行级的时候运行的相关任务的相关文件。在 Red Hat 中, 这些文件一般都是 shell 脚本的符号连接,可以在 /etc/rc.d/init.d 中找到。

让我们用一个简单的例子看一下这些东西, 下面这两个例子行来自我们的 inittab 文件:

id:3:initdefault: 
l3:3:wait:/etc/rc.d/rc 3

在 Red Hat 系统中这很典型。一旦 init 被启动 ,读取 /etc/inittab 。 从第一行,我们知道 init 将在系统启动后从运行运行级3。一旦我们到了那个运行级 ,第二行告诉 init 去运行脚本 /etc/rc.d/rc 3 并且在执行前等待终止。

在 /etc/rc.d 目录的 rc 脚本收到 3 作为一个参数。 这个 3 相当于运行级 3。结果 rc 脚本执行 /etc/rc.d/rc3.d 目录中的所有脚本。它首先用参数 "stop" 执行所有 K(代表 "kill"杀掉进程或者服务)打头的脚本,接下来,它运行所有以字母 S 打头的脚本,带有参数 "start" 启动进程或者服务。 最后要指明,K 和 S 脚本的执行顺序是基于排序的;名为 S90mysql 的脚本将在 S95httpd 之前执行。

/etc/rc.d/rc3.d 中的脚本实际是对 /etc/rc.d/init.d 中文件的符号连接。UNIX管理员可以在rc3.d中放制文 件, 实际情况下 Red Hat 的 init.d 目录是所有脚本的第一位置,然后生成逻辑连接到 rc*.d 目录。手工进行这些文件的管理很烦人、琐碎。 chkconfig 现在接手这件事情! Red Hat 的这个 chkconfig 工具就是专为管理 /etc/rc.d/rc[0-6].d 中的符号连接而设计。

察看 chkconfig 的项(Entry)

chkconfig 的二进制软件在 /sbin 下,默认权限允许任何用户执行。不过没有 root 权限的用户只能察看当前的 chkconfig 配置。输入:

[root]# chkconfig --list | grep on 

输出的部分内容大致如下:

gpm             0:off   1:off   2:on    3:on    4:on    5:on    6:off
syslog 0:off 1:off 2:on 3:on 4:on 5:on 6:off
network 0:off 1:off 2:on 3:on 4:on 5:on 6:off
random 0:off 1:off 2:on 3:on 4:on 5:on 6:off
rawdevices 0:off 1:off 2:off 3:on 4:on 5:on 6:off
atd 0:off 1:off 2:off 3:on 4:on 5:on 6:off
apmd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
sendmail 0:off 1:off 2:on 3:on 4:on 5:on 6:off
sshd 0:off 1:off 2:on 3:off 4:on 5:on 6:off
portmap 0:off 1:off 2:off 3:off 4:on 5:on 6:off
crond 0:off 1:off 2:on 3:on 4:on 5:on 6:off
xinetd 0:off 1:off 2:off 3:on 4:on 5:on 6:off
xfs 0:off 1:off 2:on 3:off 4:on 5:on 6:off
httpd 0:off 1:off 2:off 3:on 4:off 5:off 6:off

在输出的每一行,最开始的段代表在 /etc/rc.d/init.d 中的 init 脚本名。 其余的区段表示脚本进入各个运行级时的各运行级 0-6 的状态。 例如,crond 应当在进入运行级 2、3、4、5 的时候启动,当进入 0、1、 6的时候停止。我们可以通过 find 命令查找在 /etc/rc.d 中所有 crond 结尾的文件确信我们设 置的正确性:

[root]# find /etc/rc.d -name '*crond' -print 
/etc/rc.d/init.d/crond
/etc/rc.d/rc0.d/K60crond
/etc/rc.d/rc1.d/K60crond
/etc/rc.d/rc2.d/S40crond
/etc/rc.d/rc3.d/S40crond
/etc/rc.d/rc4.d/S40crond
/etc/rc.d/rc5.d/S40crond
/etc/rc.d/rc6.d/K60crond
注意 chkconfig 报告的每个 "off" 节 (0, 1, 6), 一个 kill 脚本存在 script is in place 每一个 "on" 节 (2, 3, 4, 5),有一个 start 脚本。接下来,执行一个不同的 find 命令以确信每个发现的文件的类型:
[root]# find /etc/rc.d -name '*crond' -exec file {} ; 
/etc/rc.d/init.d/crond: Bourne shell script text
/etc/rc.d/rc0.d/K60crond: symbolic link to
../init.d/crond
/etc/rc.d/rc1.d/K60crond: symbolic link to
../init.d/crond
/etc/rc.d/rc2.d/S40crond: symbolic link to
../init.d/crond
/etc/rc.d/rc3.d/S40crond: symbolic link to
../init.d/crond
/etc/rc.d/rc4.d/S40crond: symbolic link to
../init.d/crond
/etc/rc.d/rc5.d/S40crond: symbolic link to
../init.d/crond
/etc/rc.d/rc6.d/K60crond: symbolic link to
../init.d/crond

这表明在 init.d 中找到的 crond 是一个 shell 脚本,找到的所有其他的文件都是对 crond 脚本的符号连接。

调整 chkconfig 项

调整 chkconfig 的项几乎和列出现在的设置一样容易。格式:

chkconfig [--level <运行级>] <名字> 

例如,如果我们决定在运行级 2 禁止crond,

# chkconfig --level 2 crond off 
(root执行)会在运行级 2 关掉 crond。运行 chkconfig --list会确信crond的配置已经被调整。更进一步,下面的 find 命令显示一个 kill 脚本已经在目录 rc2.d 中代替了 start 脚本:
[root]# find /etc/rc.d -name '*crond' -print 
/etc/rc.d/init.d/crond
/etc/rc.d/rc0.d/K60crond
/etc/rc.d/rc1.d/K60crond
/etc/rc.d/rc2.d/K60crond
/etc/rc.d/rc3.d/S40crond
/etc/rc.d/rc4.d/S40crond
/etc/rc.d/rc5.d/S40crond
/etc/rc.d/rc6.d/K60crond

切记: chkconfig 不是立即自动禁止或激活一个服务的,它只是简单的改变了符号连接,超级用户 可以用这个命令 /etc/rc.d/init.d/crond stop 立刻禁止 crond 服务。最后,你可以用一个命令行激活/禁止多个运行级的某个命令 。 例如输入:

chkconfig --levels 2345 crond on 

会设定 crond 在运行级2, 3, 4 和 5启动。

删掉一项

有的时候, 删掉一个服务也很恰当。例如,针对 sendmail,在客户机上导入本地账号的邮件没有必要。 运行 sendmail 作为守护进程就不是必要的了。这种情况,我发现禁止 sendmail 服务很有必要 ,减少了潜在的安全问题,从 chkconfig 中删掉 sendmail,输入:

chkconfig --del sendmail 

在下面,我们的 find 命令显示该处没有符号连接了,不过 sendmail 的 init 脚本仍然有:

[root]# find /etc/rc.d -name '*sendmail' -print 
/etc/rc.d/init.d/sendmail

在我看来这很完美。脚本保留了,万一 sendmail 需要作为一个服务实现呢?不过所有的符号连接去掉了。我们能在每一个运行级禁 止 sendmail 服务,这将在每一个 rc*.d 子目录中放置一个 kill 脚本,虽然 sendmail 从不在初始化阶段启 动,是个不必要的任务 ,可是,我曾看到一些系统管理员需要在特定的场合手工启动服务。把 kill 脚本留在那里确保可以干净的杀掉服务。

添加chkconfig 项

到目前为止,一切顺利。我们已经知道使用 chkconfig 如何查看、调整、删掉服务。现在添 加一个新的服务。看下面的脚本 oracle:

Listing 1. Oracle Script
#!/bin/sh

#chkconfig: 2345 80 05
#description: Oracle 8 Server

ORA_HOME=/usr/home/oracle/product/8.0.5
ORA_OWNER=oracle

if [ ! -f $ORA_HOME/bin/dbstart ]
then
echo "Oracle startup: cannot start"
exit
fi

case "$1" in
"start")
su - $ORA_OWNER -c $ORA_HOME/bin/dbstart
su - $ORA_OWNER -c "$ORA_HOME/bin/lsnrctl start"
;;
"stop")
su - $ORA_OWNER -c $ORA_HOME/bin/dbshut
su - $ORA_OWNER -c "$ORA_HOME/bin/lsnrctl stop"
;;
esac
-----------------------------------------------------

使用这个脚本,Oracle 8 可以以参数 "start" 启动,以 "stop" 参数停止。它符合 init 脚本的最小要求可以和 /etc/rc.d/rc 脚本联合使用。

把脚本放到 /etc/rc.d/init.d 中并运行(以 root) :

chmod +x /etc/rc.d/init.d/oracle 

使你的脚本可执行。如果你担心普通用户察看这个脚本,你可以设定更严格的文件权限 。只要这个脚本可以被 root 作为单独的脚本运行就可以。

注意脚本中的两行注释:

#chkconfig: 2345 80 05 
#description: Oracle 8 Server

chkconfig 需要这些行来决定如何实现初始运行级添加服务,如何设定启动和停止顺序的优 先级。这些行指明脚本将为运行级 2、3、4、5 启动 Oracle 8 服务。另外, 启动优先权将被设定为 80 而停止优先权设定为 05。

现在脚本在合适的位置,并且有合适的执行权限,以及恰当的 chkconfig 注释, 我们可以添加 init 脚本,以 root 用户执行,

# chkconfig --add oracle. 

用 chkconfig 的查询,我们能核实我们所作的添加:

[root]# chkconfig --list | grep oracle 
oracle 0:off 1:off 2:on 3:on 4:on 5:on 6:off

而且,我们可以用标准的 find 命令察看 chkconfig 如何设定符号连接:

[root]# find /etc/rc.d -name '*oracle' -print 

/etc/rc.d/init.d/oracle
/etc/rc.d/rc0.d/K05oracle
/etc/rc.d/rc1.d/K05oracle
/etc/rc.d/rc2.d/S80oracle
/etc/rc.d/rc3.d/S80oracle
/etc/rc.d/rc4.d/S80oracle
/etc/rc.d/rc5.d/S80oracle
/etc/rc.d/rc6.d/K05oracle

正如需要的那样,kill 连接的名字包含优先权 05 而 start 连接包含 80。如果你需要调整优先 权,(如:我们停止的优先权需要设为 03),简单的调整 oracle init 脚本的chkconfig 注释行并运行 reset命令 command,如下所示。符号连接会被改名:

[root]# chkconfig oracle reset 
[root]# find /etc/rc.d -name '*oracle' -print
/etc/rc.d/init.d/oracle
/etc/rc.d/rc0.d/K03oracle
/etc/rc.d/rc1.d/K03oracle
/etc/rc.d/rc2.d/S80oracle
/etc/rc.d/rc3.d/S80oracle
/etc/rc.d/rc4.d/S80oracle
/etc/rc.d/rc5.d/S80oracle
/etc/rc.d/rc6.d/K03oracle

Red Hat 7中的改进

大家可能都知道了,inetd在 Red Hat 7中已经被xinetd 所取代(参考本站 "使用xinetd" 一文)。而且,chkconfig 的功能已经被扩展,可以管理一些 xinetd 的 Internet 服务。例子如下:

[root]# chkconfig --list 
...
xinetd based services:
finger: on
linuxconf-web: off
rexec: off
rlogin: off
rsh: off
ntalk: off
talk: off
telnet: on
tftp: off
wu-ftpd: on

禁掉一个 xinetd 服务,可能是 finger,你应该输入:

[root]# chkconfig finger off. 

很简捷啊,呵呵。可是,这里有个问题。当配置已经改变,命令 /etc/init.d/xinetd reload 指明 xinetd 自动重载入新的配置,被 chkconfig 执行。这个脚本运行一个带有 SIGUSR2 信号的 kill 指示 xinetd 进行一个“硬“重配置。

那意味着什么?哦,当我测试的时候,通过 xinetd 提供的活动服务(如 Telnet, FTP 等)立刻被中止。如果你能计划在最合适的时间启动/禁止你的系统上的服务,可能不是个 问题。作为一种替代方式,你可以调整你的 /etc/init.d/xinetd 脚本 ,这样 reload 选项发送一个 SIGUSR1 信号。 这是个“软“重配置。这将重启动你的服务而不中断你现存的连接 。

chkconfig 管理下,添加xinetd服务只要简单的添加xinetd服务文件到 /etc/xinetd.d目录中。chkconfig会自动的“捡起“它并使其可用,通过chkconfig 工具进行管理。简洁阿!

结论

现在你已经应该认识到红帽子的 chkconfig 工具管理 init 脚本的好处了,虽然它的功能似乎 简单了些,但是它节省时间,这使其成为一个系统管理员适用的命令,值得记牢。


参考信息(译者提供)


chkconfig -http://www.fastcoder.net/~thumper/software/sysadmin/chkconfig/

使用 xinetd - http://www.dbanotes.net/OpenSource/Using_xinetd.html




本文译者

Fenng,某美资公司DBA,业余时间混迹于各数据库相关的技术论坛且乐此不疲。 目前关注如何利用ORACLE数据库有效地构建企业应用。对Oracle tuning、troubleshooting有一点研究。
个人技术站点:http://www.dbanotes.net/ 。 可以通过电子邮件 dbanotes@gmail.com 联系到他。

原文出处

http://www.dbanotes.net/OpenSource/Managing_Initscripts_with_RedHat's_chkconfig_CN.htm

回上页<-|->回首页

All Articles (by Fenng) are licensed under a Creative Commons License.
I would welcome any feedback. Please send questions, comments or corrections to dbanotes@gmail.com
Valid XHTML 4.01 / Valid CSS
posted @ 2006-05-29 14:41 易道 阅读(209) | 评论 (0)编辑 收藏

对于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合,所以内核代码无法轻易地放在调 试器中执行,而且也很难跟踪。同样,要想复现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样也就破坏了可以用来跟踪它们的现 场。

本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。

4.1  通过打印调试
最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用printf 显示监视信息。调试内核代码的时候,则可以用 printk 来完成相同的工作。

4.1.1  printk
在前面的章节中,我们只是简单假设 printk 工作起来和 printf 很类似。现在则是介绍它们之间一些不同点的时候了。

其 中一个差别就是,通过附加不同日志级别(loglevel),或者说消息优先级,可让 printk根据这些级别所标示的严重程度,对消息进行分类。一般采用宏来指示日志级别,例如,KERN_INFO,我们在前面已经看到它被添加在一些打 印语句的前面,它就是一个可以使用的消息日志级别。日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接在一起;这也就是为什么下面的例子 中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子,一个是调试信息,一个是临界信息:

[quote]
printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
[/quote]

在头文件 <linux/kernel.h> 中定义了 8 种可用的日志级别字符串。

KERN_EMERG
用于紧急事件消息,它们一般是系统崩溃之前提示的消息。

KERN_ALERT
用于需要立即采取动作的情况。

KERN_CRIT
临界状态,通常涉及严重的硬件或软件操作失败。

KERN_ERR
用于报告错误状态;设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。

KERN_WARNING
对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重问题。

KERN_NOTICE
有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。

KERN_INFO
提示性信息。很多驱动程序在启动的时候,以这个级别打印出它们找到的硬件信息。

KERN_DEBUG
用于调试信息。

每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从0到7,数值越小,优先级就越高。

没 有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在文件 kernel/printk.c 中指定为一个整数值。在 Linux 的开发过程中,这个默认的级别值已经有过好几次变化,所以我们建议读者始终指定一个明确的级别。

根据日志级别,内核可能会把消息打印到当前 控制台上,这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。如果优先级小于 console_loglevel 这个整数值的话,消息才能显示出来。如果系统同时运行了 klogd  和 syslogd,则无论 console_loglevel 为何值,内核消息都将追加到 /var/log/messages 中(否则的话,除此之外的处理方式就依赖于对 syslogd 的设置)。如果 klogd 没有运行,这些消息就不会传递到用户空间,这种情况下,就只好查看 /proc/kmsg 了。

变量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL,而且还可以通过sys_syslog 系统调用进行修改。调用 klogd 时可以指定 -c 开关选项来修改这个变量, klogd 的 man 手册页对此有详细说明。注意,要修改它的当前值,必须先杀掉 klogd,再加 -c选项重新启动它。此外,还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的源文件 miscprogs/setlevel.c 里找到这样的一段程序。新优先级被指定为一个 1 到 8 之间的整数值。如果值被设为 1,则只有级别为 0(KERN_EMERG) 的消息才能到达控制台;如果设为 8,则包括调试信息在内的所有消息都能显示出来。

如果在控制台上工作,而且 常常遇到内核错误(参见本章后面的“调试系统故障”一节)的话,就有必要降低日志级别,因为出错处理代码会把 console_loglevel 增为它的最大数值,导致随后的所有消息都显示在控制台上。如果需要查看调试信息,就有必要提高日志级别;这在远程调试内核,并且在交互会话未使用文本控制 台的情况下,是很有帮助的。

从2.1.31这个版本起,可以通过文本文件 /proc/sys/kernel/printk 来读取和修改控制台的日志级别。这个文件容纳了 4 个整数值。读者可能会对前面两个感兴趣:控制台的当前日志级别和默认日志级别。例如,在最近的这些内核版本中,可以通过简单地输入下面的命令使所有的内核 消息得到显示:

[quote]
# echo 8 > /proc/sys/kernel/printk
[/quote]

不过,如果仍在 2.0 版本下的话,就需要使用 setlevel 这样的工具了。

现在大家应该清楚为什么在 hello.c范例中使用 <1> 这些标记了,它们用来确保这些消息能在控制台上显示出来。

对 于控制台日志策略,Linux考虑到了某些灵活性,也就是说,可以发送消息到一个指定的虚拟控制台(假如控制台是文本屏幕的话)。默认情况下,“控制台” 就是当前地虚拟终端。可以在任何一个控制台设备上调用 ioctl(TIOCLINUX),来指定接收消息的虚拟终端。下面的 setconsole  程序,可选择专门用来接收内核消息的控制台;这个程序必须由超级用户运行,在 misc-progs 目录里可以找到它。下面是程序的代码:

[quote]
int main(int argc, char **argv)
{
   char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */

   if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
   else {
       fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1);
   }
   if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) {    /* use stdin */
       fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
               argv[0], strerror(errno));
       exit(1);
   }
   exit(0);
}
[/quote]

setconsole 使用了特殊的ioctl命令:TIOCLINUX ,这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX 时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字,接下去的字节所具有的功能则由这个子命令决定。在 setconsole 中,使用的子命令是 11,后面那个字节(存于bytes[1]中)标识虚拟控制台。关于 TIOCLINUX 的详尽描述可以在内核源码中的 drivers/char/tty_io.c 文件得到。

4.1.2  消息如何被记录
printk 函数将消息写到一个长度为 LOG_BUF_LEN(定义在 kernel/printk.c 中)字节的循环缓冲区中,然后唤醒任何正在等待消息的进程,即那些睡眠在 syslog 系统调用上的进程,或者读取 /proc/kmesg 的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对 /proc/kmesg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog 系统调用却能随意地返回日志数据,并保留这些数据以便其它进程也能使用。一般而言,读 /proc 文件要容易些,这使它成为 klogd 的默认方法。

手工读取内核消息时,在停止klogd之后,可以发现 /proc 文件很象一个FIFO,读进程会阻塞在里面以等待更多的数据。显然,如果已经有 klogd 或其它的进程正在读取相同的数据,就不能采用这种方法进行消息读取,因为会与这些进程发生竞争。

如 果循环缓冲区填满了,printk就绕回缓冲区的开始处填写新数据,覆盖最陈旧的数据,于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处 相比,这个问题可以忽略不计。例如,循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时覆盖那些不再会有人去读的旧数据,从而使内存的浪费减到最 少。Linux消息处理方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。而这个方法 的唯一缺点就是可能丢失某些数据。

klogd 运行时,会读取内核消息并将它们分发到 syslogd,syslogd 随后查看 /etc/syslog.conf ,找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分;这两者的允许值均定义在 <sys/syslog.h> 中。内核消息由 LOG_KERN 设施记录,并以 printk 中使用的优先级记录(例如,printk 中使用的 KERN_ERR对应于syslogd 中的 LOG_ERR)。如果没有运行 klogd,数据将保留在循环缓冲区中,直到某个进程读取或缓冲区溢出为止。

如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则 可以为 klogd 指定 -f (file) 选项,指示 klogd 将消息保存到某个特定的文件,或者修改 /etc/syslog.conf 来适应自己的需求。另一种可能的办法是采取强硬措施:杀掉klogd,而将消息详细地打印到空闲的虚拟终端上。*

[quote]
注: 例如,使用下面的命令可设置 10 号终端用于消息的显示:
setlevel 8
setconsole 10
[/quote]

或者在一个未使用的 xterm 上执行cat /proc/kmesg来显示消息。

4.1.3  开启及关闭消息
在 驱动程序开发的初期阶段,printk 对于调试和测试新代码是相当有帮助的。不过,当正式发布驱动程序时,就得删除这些打印语句,或至少让它们失效。不幸的是,你可能会发现这样的情况,在删除 了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或是有人发现了一个 bug),这时,又希望至少把一部分消息重新开启。这两个问题可以通过几个办法解决,以便全局地开启或禁止消息,并能对个别消息进行开关控制。

我们在这里给出了一个编写 printk 调用的方法,可个别或全局地对它们进行开关;这个技巧是定义一个宏,在需要时,这个宏展开为一个printk(或printf)调用。

可以通过在宏名字中删减或增加一个字母,打开或关闭每一条打印语句。

编译前修改 CFLAGS 变量,则可以一次关闭所有消息。

同样的打印语句既可以用在内核态也可以用在用户态,因此,关于这些额外的信息,驱动和测试程序可以用同样的方法来进行管理。

下面这些来自 scull.h 的代码,就实现了这些功能。

[quote]
#undef PDEBUG             /* undef it, just in case */
#ifdef SCULL_DEBUG
#  ifdef _ _KERNEL_ _
    /* This one if debugging is on, and kernel space */
#    define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt,
                                        ## args)
#  else
    /* This one for user space */
#    define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
#  endif
#else
#  define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */
[/quote]

符 号 PDEBUG 依赖于是否定义了SCULL_DEBUG,它能根据代码所运行的环境选择合适的方式显示信息:内核态运行时使用printk系统调用;用户态下则使用 libc调用fprintf,向标准错误设备进行输出。符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除。

为了进一步简化这个过程,可以在 Makefile加上下面几行:

[quote]
# Comment/uncomment the following line to disable/enable debugging
DEBUG = y

# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
 DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
 DEBFLAGS = -O2
endif

CFLAGS += $(DEBFLAGS)
[/quote]

本节所给出的宏依赖于gcc 对ANSI C预编译器的扩展,这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖并不是什么问题,因为内核对 gcc 特性的依赖更强。此外,Makefile依赖于 GNU 的make 版本;基于同样的道理,这也不是什么问题。

如果读者熟悉 C 预编译器,可以将上面的定义进行扩展,实现“调试级别”的概念,这需要定义一组不同的级别,并为每个级别赋一个整数(或位掩码),用以决定各个级别消息的详细程度。

但 是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点,对读者来说,我们无法预知最合适的点在哪里。记住,预处 理程序的条件语句(以及代码中的常量表达式)只在编译时执行,要再次打开或关闭消息必须重新编译。另一种方法就是使用C条件语句,它在运行时执行,因此可 以在程序运行期间打开或关闭消息。这是个很好的功能,但每次代码执行时系统都要进行额外的处理,甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法 接受的。

在很多情况下,本节提到的这些宏都已被证实是很有用的,仅有的缺点是每次开启和关闭消息显示时都要重新编译模块。

4.2  通过查询调试
上一节讲述了 printk 是如何工作的以及如何使用它,但还没谈到它的缺点。

由 于 syslogd 会一直保持对其输出文件的同步刷新,每打印一行都会引起一次磁盘操作,因此大量使用 printk 会严重降低系统性能。从 syslogd 的角度来看,这样的处理是正确的。它试图把每件事情都记录到磁盘上,以防系统万一崩溃时,最后的记录信息能反应崩溃前的状况;然而,因处理调试信息而使系 统性能减慢,是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日志文件的名字前面,前缀一个减号符解决。*

[quote]
注: 这个减号是个“特殊”标记,避免 syslogd 在每次出现新信息时都去刷新磁盘文件,这些内容记述在 syslog.conf(5) 中,这个手册页很值得一读。
[/quote]


修 改配置文件带来的问题在于,在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希望尽快把信息刷新到磁盘时,也是如此。如果不愿作这种持久性修改 的话,另一个选择是运行一个非 klogd 程序(如前面介绍的cat /proc/kmesg),但这样并不能为通常的系统操作提供一个合适的环境。

多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生数据。实际上,每个 Unix 系统都提供了很多工具,用于获取系统信息,如:ps、netstat、vmstat等等。

驱动程序开发人员对系统进行查询时,可以采用两种主要的技术:在 /proc 文件系统中创建文件,或者使用驱动程序的 ioctl 方法。/proc 方式的另一个选择是使用 devfs,不过用于信息查找时,/proc 更为简单一些。

4.2.1  使用 /proc 文件系统
/proc 文件系统是一种特殊的、由程序创建的文件系统,内核使用它向外界输出信息。/proc 下面的每个文件都绑定于一个内核函数,这个函数在文件被读取时,动态地生成文件的“内容”。我们已经见到过这类文件的一些输出情况,例如, /proc/modules 列出的是当前载入模块的列表。

Linux系统对/proc的使用很频繁。现代Linux系统中的很多工具都是 通过 /proc 来获取它们的信息,例如 ps、top 和 uptime。有些设备驱动程序也通过 /proc 输出信息,你的驱动程序当然也可以这么做。因为 /proc 文件系统是动态的,所以驱动程序模块可以在任何时候添加或删除其中的文件项。

特 征完全的 /proc 文件项相当复杂;在所有的这些特征当中,有一点要指出的是,这些 /proc 文件不仅可以用于读出数据,也可以用于写入数据。不过,大多数时候,/proc 文件项是只读文件。本节将只涉及简单的只读情形。如果有兴趣实现更为复杂的事情,读者可以先在这里了解基础知识,然后参考内核源码来建立完整的认识。

所有使用 /proc 的模块必须包含 <linux/proc_fs.h>,通过这个头文件定义正确的函数。

为 创建一个只读 /proc 文件,驱动程序必须实现一个函数,用于在文件读取时生成数据。当某个进程读这个文件时(使用 read 系统调用),请求会通过两个不同接口的其中之一发送到驱动程序模块,使用哪个接口取决于注册情况。我们先把注册放到本节后面,先直接讲述读接口。

无论采用哪个接口,在这两种情况下,内核都会分配一页内存(也就是 PAGE_SIZE 个字节),驱动程序向这片内存写入将返回给用户空间的数据。

推荐的接口是 read_proc,不过还有一个名为 get_info 的老一点的接口。

[quote]
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
[/quote]

参 数表中的 page 指针指向将写入数据的缓冲区;start 被函数用来说明有意义的数据写在页面的什么位置(对此后面还将进一步谈到);offset 和 count 这两个参数与在 read 实现中的用法相同。eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data 参数是一个驱动程序特有的数据指针,可用于内部记录。*

[quote]
注: 纵览全书,我们还会发现这样的一些指针;它们表示了这类处理中有关的“对象”,与C++ 中的同类处理有些相似。
[/quote]

这个函数可以在2.4内核中使用,如果使用我们的 sysdep.h 头文件,那么在2.2内核中也可以用这个函数。

[quote]
int (*get_info)(char *page, char **start, off_t offset, int count);  
[/quote]

get_info 是一个用来读取 /proc 文件的较老接口。所有的参数与 read_proc 中的对应参数用法相同。缺少的是报告到达文件尾的指针和由data 指针带来的面向对象风格。这个函数可以用在所有我们感兴趣的内核版本中(尽管在它 2.0 版本的实现中有一个额外未用的参数)。

这两个函数的返回值都是实际放入页面缓冲区的数据的字节数,这一点与 read 函数对其它类型文件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标记,而 start 的用法就有点复杂了。

对于 /proc 文件系统的用户扩展,其最初实现中的主要问题在于,数据传输只使用单个内存页面。这样就把用户文件的总体尺寸限制在了 4KB 以内(或者是适合于主机平台的其它值)。start 参数在这里就是用来实现大数据文件的,不过该参数可以被忽略。

如 果 proc_read 函数不对 *start 指针进行设置(它最初为 NULL),内核就会假定 offset 参数被忽略,并且数据页包含了返回给用户空间的整个文件。反之,如果需要通过多个片段创建一个更大的文件,则可以把 *start 赋值为页面指针,因此调用者也就知道了新数据放在缓冲区的开始位置。当然,应该跳过前 offset 个字节的数据,因为这些数据已经在前面的调用中返回。

长久以来,关于 /proc 文件还有另一个主要问题,这也是 start 意图解决的一个问题。有时,在连续的 read 调用之间,内核数据结构的 ASCII 表述会发生变化,以至于读进程发现前后两次调用所获得的数据不一致。如果把 *start 设为一个小的整数值,调用程序可以利用它来增加 filp->f_pos 的值,而不依赖于返回的数据量,因此也就使 f_pos 成为read_proc 或 get_info 程序中的一个内部记录值。例如,如果 read_proc 函数从一个大的结构数组返回数据,并且这些结构的前 5 个已经在第一次调用中返回,那么可将 *start 设置为 5。下次调用中这个值将被作为偏移量;驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作“hack”,可以在 /fs/proc/generic.c 中看到。

现在我们来看个例子。下面是scull 设备 read_proc 函数的简单实现:

[quote]
int scull_read_procmem(char *buf, char **start, off_t offset,
                  int count, int *eof, void *data)
{
   int i, j, len = 0;
   int limit = count - 80; /* Don't print more than this */

   for (i = 0; i < scull_nr_devs && len <= limit; i++) {
       Scull_Dev *d = &scull_devices[ i];
       if (down_interruptible(&d->sem))
               return -ERESTARTSYS;
       len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",
                      i, d->qset, d->quantum, d->size);
       for (; d && len <= limit; d = d->next) { /* scan the list */
           len += sprintf(buf+len, "  item at %p, qset at %p\n", d,
                                   d->data);
           if (d->data && !d->next) /* dump only the last item
                                                   - save space */
               for (j = 0; j < d->qset; j++) {
                   if (d->data[j])
                       len += sprintf(buf+len,"    % 4i: %8p\n",
                                                   j,d->data[j]);
               }
       }
       up(&scull_devices[ i].sem);
   }
   *eof = 1;
   return len;
}
[/quote]

这是一个相当典型的 read_proc 实现。它假定决不会有这样的需求,即生成多于一页的数据,因此忽略了 start 和 offset 值。但是,小心不要超出缓冲区,以防万一。

使用 get_info 接口的 /proc 函数与上面说明的 read_proc 非常相似,除了没有最后的那两个参数。既然这样,则通过返回少于调用者预期的数据(也就是少于 count 参数),来提示已到达文件尾。

一 旦定义好了一个 read_proc 函数,就需要把它与一个 /proc 文件项连接起来。依赖于将要支持的内核版本,有两种方法可以建立这样的连接。最容易的方法是简单地调用 create_proc_read_entry,但这只能用于2.4内核(如果使用我们的 sysdep.h 头文件,则也可用于 2.2 内核)。下面就是 scull 使用的调用,以 /proc/scullmem 的形式来提供 /proc 功能。

[quote]
create_proc_read_entry("scullmem",
                      0    /* default mode */,
                      NULL /* parent dir */,
                      scull_read_procmem,
                      NULL /* client data */);
[/quote]

这 个函数的参数表包括:/proc 文件项的名称、应用于该文件项的文件许可权限(0是个特殊值,会被转换为一个默认的、完全可读模式的掩码)、文件父目录的 proc_dir_entry 指针(我们使用 NULL 值使该文件项直接定位在 /proc 下)、指向 read_proc 的函数指针,以及将传递给 read_proc 函数的数据指针。

目录项指针(proc_dir_entry)可用来在 /proc 下创建完整的目录层次结构。不过请注意,将文件项置于 /proc 的子目录中有更为简单的方法,即把目录名称作为文件项名称的一部分――只要目录本身已经存在。例如,有个新的约定,要求设备驱动程序对应的 /proc 文件项应转移到子目录 driver/ 中;scull 可以简单地指定它的文件项名称为 driver/scullmem,从而把它的 /proc 文件放到这个子目录中。

当然,在模块卸载时,/proc 中的文件项也应被删除。 remove_proc_entry 就是用来撤消 create_proc_read_entry 所做工作的函数。

[quote]
remove_proc_entry("scullmem", NULL /* parent dir */);
[/quote]

另 一个创建 /proc 文件项的方法是,创建并初始化一个 proc_dir_entry 结构,并将该结构传递给函数 proc_register_dynamic (2.0 版本)或 proc_register(2.2 版本,如果结构中的索引节点号为0,该函数即认为是动态文件)。作为一个例子,当在2.0内核的头文件下进行编译时,考虑下面 scull 所使用的这些代码:

[quote]
static int scull_get_info(char *buf, char **start, off_t offset,
               int len, int unused)
{
   int eof = 0;
   return scull_read_procmem (buf, start, offset, len, &eof, NULL);
}

struct proc_dir_entry scull_proc_entry = {
       namelen:    8,
       name:       "scullmem",
       mode:       S_IFREG | S_IRUGO,
       nlink:      1,
       get_info:   scull_get_info,
};

static void scull_create_proc()
{
   proc_register_dynamic(&proc_root, &scull_proc_entry);
}

static void scull_remove_proc()
{
   proc_unregister(&proc_root, scull_proc_entry.low_ino);
}
[/quote]

代码声明了一个使用 get_info 接口的函数,并填写了一个 proc_dir_entry 结构,用于对文件系统进行注册。

这 段代码借助sysdep.h 中宏定义的支持,提供了 2.0 和 2.4 内核之间的兼容性。因为 2.0 内核不支持 read_proc,它使用了 get_info 接口。如果对 #ifdef 作一些更多的处理,可以使这段代码在 2.2 内核中使用 read_proc,不过这样收益并不大。

4.2.2  ioctl 方法
ioctl是作用于文件描述符之上的一个系统调用,我们会在下一章介绍它的用法;它接收一个“命令”号,用以标识将执行的命令;以及另一个(可选的)参数,通常是个指针。

做为替代 /proc文件系统的方法,可以为调试设计若干ioctl命令。这些命令从驱动程序复制相关数据到用户空间,在用户空间中可以查看这些数据。

使用ioctl 获取信息比起 /proc 来要困难一些,因为需要另一个程序调用 ioctl 并显示结果。这个程序是必须编写并编译的,而且要和测试模块配合一致。从另一方面来说,相对实现 /proc 文件所需的工作,驱动程序的编码则更为容易些。

有时 ioctl 是获取信息的最好方法,因为它比起读 /proc 要快得多。如果在数据写到屏幕之前要完成某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外,ioctl 并不要求把数据分割成不超过一个内存页面的片断。

ioctl 方法的一个优点是,在结束调试之后,用来取得信息的这些命令仍可以保留在驱动程序中。/proc文件对任何查看这个目录的人都是可见的(很多人可能会纳闷 “这些奇怪的文件是用来做什么的”),然而与 /proc文件不同,未公开的 ioctl 命令通常都不会被注意到。此外,万一驱动程序有什么异常,这些命令仍然可以用来调试。唯一的缺点就是模块会稍微大一些。

4.3  通过监视调试
有时,通过监视用户空间中应用程序的运行情况,可以捕捉到一些小问题。监视程序同样也有助于确认驱动程序工作是否正常。例如,看到 scull 的 read 实现如何响应不同数据量的 read 请求后,我们就可以判断它是否工作正常。

有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数,插入打印语句,或者在 strace 状态下运行程序。在检查内核代码时,最后一项技术最值得关注,我们将在此对它进行讨论。

strace 命令是一个功能非常强大的工具,它可以显示程序所调用的所有系统调用。它不仅可以显示调用,而且还能显示调用参数,以及用符号方式表示的返回值。当系统调 用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。strace 有许多命令行选项;最为有用的是 -t,用来显示调用发生的时间;-T,显示调用所花费的时间; -e,限定被跟踪的调用类型;-o,将输出重定向到一个文件中。默认情况下,strace将跟踪信息打印到 stderr 上。

strace从内核中接收信息。这意味着一个程序无论是否按调试方式编译(用 gcc 的 -g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样,strace 也可以跟踪一个正在运行的进程。

跟踪信息通常用于生成错误报告,然后发给应用开发人员,但是它对内核编程人员来说也同样非常有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace 允许我们检查每次调用中输入和输出数据的一致性。

例如,下面的屏幕信息显示了 strace ls /dev > /dev/scull0 命令的最后几行:

[quote]
[...]
open("/dev", O_RDONLY|O_NONBLOCK)     = 4
fcntl(4, F_SETFD, FD_CLOEXEC)         = 0
brk(0x8055000)                        = 0x8055000
lseek(4, 0, SEEK_CUR)                 = 0
getdents(4, /* 70 entries */, 3933)   = 1260
[...]
getdents(4, /* 0 entries */, 3933)    = 0
close(4)                              = 0
fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
ioctl(1, TCGETS, 0xbffffa5c)          = -1 ENOTTY (Inappropriate ioctl
                                                    for device)
write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000
write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96
write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325
close(1)                              = 0
_exit(0)                              = ?
[/quote]

很 明显,ls 完成对目标目录的检索后,在首次对 write 的调用中,它试图写入 4KB 数据。很奇怪(对于 ls 来说),实际只写了4000个字节,接着它重试这一操作。然而,我们知道scull的 write 实现每次最多只写一个量子(scull 中设置的量子大小为4000个字节),所以我们所预期的就是这样的部分写入。经过几个步骤之后,每件工作都顺利通过,程序正常退出。

另一个例子,让我们来对 scull 设备进行读操作(使用 wc 命令):

[quote]
[...]
open("/dev/scull0", O_RDONLY)           = 4
fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000
read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421
read(4, "", 16384)                      = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "   7421 /dev/scull0\n", 20)   = 20
close(4)                                = 0
_exit(0)                                = ?
[/quote]

正 如所料,read 每次只能读取4000个字节,但数据总量与前面例子中写入的数量是相同的。与上面的写跟踪相对比,请读者注意本例中重试工作是如何组织的。为了快速读取数 据,wc 已被优化了,因而它绕过了标准库,试图通过一次系统调用读取更多的数据。可以从跟踪的 read 行中看到 wc 每次均试图读取 16KB 数据。

Linux行家可以在 strace 的输出中发现很多有用信息。如果觉得这些符号过于拖累的话,则可以仅限于监视文件方法(open,read 等)是如何工作的。

就个人观点而言,我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的 perror 调用在用于调试时信息还不够详细,而 strace 能够确切查明系统调用的哪个参数引发了错误,这一点对调试是大有帮助的。

4.4  调试系统故障
即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就会产生系统故障。在出现这种情况时,获取尽可能多的信息对解决问题是至关重要的。

注 意,“故障”不意味着“panic”。Linux 代码非常健壮(用术语讲即为鲁棒,robust),可以很好地响应大部分错误:故障通常会导致当前进程崩溃,而系统仍会继续运行。如果在进程上下文之外发 生故障,或是系统的重要组成被损害时,系统才有可能 panic。但如果问题出在驱动程序中时,通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时,为进程上下文分配的一 些内存可能会丢失;例如,由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而,由于内核在进程中止时会对已打开的设备调用 close 操作,驱动程序仍可以释放由 open 方法分配的资源。

我们已经说过,当内核行为异常时,会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息。尽管它们对于初学者来说相当晦涩,不过处理器在出错时转储出的这些数据包含了许多值得关注的信息,通常足以查明程序错误,而无需额外的测试。

4.4.1  oops消息
大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。

由 处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失 效”的信号。如果地址非法,内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。

值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的 printk 语句产生,就象前面“printk”一节所介绍的那样分发出来。

让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。

[quote]
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:

c48370c3
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[<c48370c3>]
EFLAGS: 00010286
eax: ffffffea   ebx: c2281a20   ecx: c48370c0   edx: c2281a40
esi: 4000c000   edi: 4000c000   ebp: c38adf8c   esp: c38adf8c
ds: 0018   es: 0018   ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \
           0000010e
      4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \
           4000c000
      0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \
           00000004
Call Trace: [<c01356e6>] [<c010b860>]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
[/quote]

这个消息是通过对 faulty  模块的一个设备进行写操作而产生的,faulty 这个模块专为演示出错而编写。faulty.c 中 write 方法的实现很简单:

[quote]
ssize_t faulty_write (struct file *filp, const char *buf, size_t count,
loff_t *pos)
{
   /* make a simple fault by dereferencing a NULL pointer */
   *(int *)0 = 0;
   return 0;
}
[/quote]

正如读者所见,我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值,所以错误发生,内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中,faulty 模块还有更多有意思的错误状态。

[quote]
char faulty_buf[1024];

ssize_t faulty_read (struct file *filp, char *buf, size_t count,
                    loff_t *pos)
{
   int ret, ret2;
   char stack_buf[4];

   printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count);
   /* the next line oopses with 2.0, but not with 2.2 and later */
   ret = copy_to_user(buf, faulty_buf, count);
   if (!ret) return count; /* we survived */

   printk(KERN_DEBUG "didn't fail: retry\n");
   /* For 2.2 and 2.4, let's try a buffer overflow  */
   sprintf(stack_buf, "1234567\n");
   if (count > 8) count = 8; /* copy 8 bytes to the user */
   ret2 = copy_to_user(buf, stack_buf, count);
   if (!ret2) return count;
   return ret2;
}
[/quote]

这 段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:

[quote]
EIP:    0010:[<00000000>]
[...]
Call Trace: [<c010b860>]
Code:  Bad EIP value.
[/quote]

用 户处理 oops 消息的主要问题在于,我们很难从十六进制数值中看出什么内在的意义;为了使这些数据对程序员更有意义,需要把它们解析为符号。有两个工具可用来为开发人员 完成这样的解析:klogd 和 ksymoops。前者只要运行就会自行进行符号解码;后者则需要用户有目的地调用。下面的讨论,使用了在我们第一个 oops 例子中通过使用NULL 指针而产生的出错信息。

使用 klogd
klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下,klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助。

当 faulty 的一个oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号):

[quote]
Unable to handle kernel NULL pointer dereference at virtual address \
    00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[faulty:faulty_write+3/576]
EFLAGS: 00010286
eax: ffffffea   ebx: c2c55ae0   ecx: c48370c0   edx: c2c55b00
esi: 0804d038   edi: 0804d038   ebp: c2337f8c   esp: c2337f8c
ds: 0018   es: 0018   ss: 0018
Process cat (pid: 23413, stackpage=c2337000)
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
           00000001
      0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \
           0804d038
      00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \
           00000004
Call Trace: [sys_write+214/256] [system_call+52/56]  
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00  
[/quote]

klogd 提供了大多数必要信息用于发现问题。在这个例子中,我们看到指令指针(EIP)正执行于函数 faulty_write 中,因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数的第3个字节上,而函数整体长度为 576 个字节。注意这些数值都是十进制的,而非十六进制。

然而,当错误发生在可装载模块中时,为了获取错误相关的有用信息,开发者还必须注意一些 情况。klogd 在开始运行时装入所有可用符号,并随后使用这些符号。如果在 klogd 已经对自身初始化之后(一般在系统启动时),装载某个模块,那 klogd 将不会有这个模块的符号信息。强制 klogd取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd 进程,这种操作在时间顺序上,必须是在模块已经装入(或重新装载)之后,而在进行任何可能引起 oops 的处理之前。

还可以在运行 klogd 时加上 -p 选项,这会使它在任何发现 oops 消息的时刻重新读入符号信息。不过,klogd 的man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息。而发生错误之后,所获得的信息可能是完全错误的了。

为 了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本。通常这个文件在 /boot 中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot,或告知 klogd 到什么位置查看。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。

使用 ksymoops
有 些时候,klogd 对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址,又要获得对应的符号,而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外,往 往还需要更多的信息。对 klogd 来说,在出错期间被杀掉,也是常用的事情。在这些情况下,可以调用一个更为强大的 oops 分析器,ksymoops 就是这样的一个工具。

在 2.3 开发系列之前,ksymoops 是随内核源码一起发布的,位于 scripts 目录之下。它现在则在自己的FTP 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许还可以从 ftp://ftp.ocs.com.au/pub/ksymoops 站点上获取这个工具的升级版本。

为了取得最佳的工作状态,除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项:

System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。
模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块,以便获得它们的符号信息。如果未提供这个列表,ksymoops 会查看 /proc/modules。
在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。
当 前正运行的内核映像的复本注意,ksymoops 需要的是一个直接的内核映像,而不是象 vmlinuz、zImage 或 bzImage 这样被大多数系统所使用的压缩版本。默认是不使用内核映像,因为大多数人都不会保存这样的一个内核。如果手边就有这样一个符合要求的内核的话,就应该采用 -v 选项告知 ksymoops 它的位置。
已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块,不过在开发中,几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。

虽 然 ksymoops 会访问 /proc 中的文件来取得它所需的信息,但这样获得的结果是不可靠的。在 oops 发生和 ksymoops 运行的时间间隙中,系统几乎一定会重新启动,这样取自 /proc 的信息就可能与故障发生时的实际状态不符合。只要有可能,最好在引起 oops 发生之前,保存 /proc/modules 和 /proc/ksyms 的复本。

我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页,这是一个很好的资料文档。

这 个工具命令行中的最后一个参数是 oops 消息的位置;如果缺少这个参数,ksymoops 会按Unix 的惯例去读取标准输入设备。运气好的话,消息可以从系统日志中重新恢复;在发生很严重的崩溃情况时,我们可能不得不将这些消息从屏幕上抄下来,然后再敲进 去(除非用的是串口控制台,这对内核开发人员来说,是非常棒的工具)。

注意,当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱。如果 klogd 已经运行,而且 oops 发生后系统仍在运行,那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。

如果没有明确地提供全部的上述信息,ksymoops 会发出警告。对于载入模块未作符号定义这类的情况,它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。

ksymoops 的输出类似如下:

[quote]
>>EIP; c48370c3 <[faulty]faulty_write+3/20>   <=====
Trace; c01356e6 <sys_write+d6/100>
Trace; c010b860 <system_call+34/38>
Code;  c48370c3 <[faulty]faulty_write+3/20>
00000000 <_EIP>:
Code;  c48370c3 <[faulty]faulty_write+3/20>   <=====
  0:   c7 05 00 00 00    movl   $0x0,0x0   <=====
Code;  c48370c8 <[faulty]faulty_write+8/20>
  5:   00 00 00 00 00
Code;  c48370cd <[faulty]faulty_write+d/20>
  a:   31 c0             xorl   %eax,%eax
Code;  c48370cf <[faulty]faulty_write+f/20>
  c:   89 ec             movl   %ebp,%esp
Code;  c48370d1 <[faulty]faulty_write+11/20>
  e:   5d                popl   %ebp
Code;  c48370d2 <[faulty]faulty_write+12/20>
  f:   c3                ret    
Code;  c48370d3 <[faulty]faulty_write+13/20>
 10:   8d b6 00 00 00    leal   0x0(%esi),%esi
Code;  c48370d8 <[faulty]faulty_write+18/20>
 15:   00
[/quote]

正 如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd 所做的很相似,不过要更为准确,而且是十六进制形式的。可以注意到,faulty_write 函数的长度被正确地报告为 0x20个字节。这是因为 ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息。

而且在这个例子中,还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确切地判断发生了些什么事情;这里很明显,错误在于一个向 0 地址写入数据 0 的指令。

ksymoops 的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上,而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出 PC 的世界,我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了):

[quote]
Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
             \/ ____ \/
             "@'/ .. \`@"
            /_| \_ _/ |_\
               \_ _ _/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 <c0202000> \
30680005 01000000 01000000 01000000 01000000
[/quote]

请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。

下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息:

[quote]
>>TPC; 0000000001000128 <[faulty].text.start+88/a0>   <=====
>>O7;  0000000000457fb4 <sys_write+114/160>
>>I7;  0000000000410114 <linux_sparc_syscall+34/40>
Trace; 0000000000410114 <linux_sparc_syscall+34/40>
Trace; 000000007007cba4 <END_OF_CODE+6f07c40d/????>
Code;  000000000100011c <[faulty].text.start+7c/a0>
0000000000000000 <_TPC>:
Code;  000000000100011c <[faulty].text.start+7c/a0>
  0:   01 00 00 00       nop
Code;  0000000001000120 <[faulty].text.start+80/a0>
  4:   90 10 20 00       clr  %o0     ! 0 <_TPC>
Code;  0000000001000124 <[faulty].text.start+84/a0>
  8:   81 c3 e0 08       retl
Code;  0000000001000128 <[faulty].text.start+88/a0>   <=====
  c:   c0 20 20 00       clr  [ %g0 ]   <=====
Code;  000000000100012c <[faulty].text.start+8c/a0>
 10:   30 68 00 05       b,a   %xcc, 24 <_TPC+0x24> \
                       0000000001000140 <[faulty]faulty_write+0/20>
Code;  0000000001000130 <[faulty].text.start+90/a0>
 14:   01 00 00 00       nop
Code;  0000000001000134 <[faulty].text.start+94/a0>
 18:   01 00 00 00       nop
Code;  0000000001000138 <[faulty].text.start+98/a0>
 1c:   01 00 00 00       nop
Code;  000000000100013c <[faulty].text.start+9c/a0>
 20:   01 00 00 00       nop
[/quote]

要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要这些信息,是因为 SPARC64 用户空间的本地结构是32位的)。本例中,使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。

读 者可能会抱怨对调用的跟踪并没带回什么值得注意的信息;然而,SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中:07 和 I7 寄存器保存了最后调用的两个函数的指令指针,这就是它们出现在调用跟踪记录边上的原因。在这个例子中,我们可以看到,故障指令位于一个由 sys_write 调用的函数中。

要注意的是,无论平台/结构是怎样的一种配合情况,用来显示反汇编代码的格式与 objdump 程序所使用的格式是一样的。objdump 是个很强大的工具;如果想查看发生故障的完整函数,可以调用命令: objdump -d faulty.o(再次重申,对于 SPARC64 平台,需要使用特殊选项:--target elf64-sparc-architecture sparc:v9)。

关于 objdump 和它的命令行选项的更多信息,可以参阅这个命令的手册页帮助。

学习对 oops 消息进行解码,需要一定的实践经验,并且了解所使用的目标处理器,以及汇编语言的表达习惯等。这样的准备是值得的,因为花费在学习上的时间很快会得到回 报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识,仍有必要花些时间对此进行学习,因为Unix 的语法与 Intel 的语法并不一样。(在 as 命令 infor 页的“i386-specific”一章中,对这种差异进行了很好的描述。)

4.4.2  系统挂起
尽 管内核代码中的大多数错误仅会导致一个oops 消息,但有时它们则会将系统完全挂起。如果系统挂起了,任何消息都无法打印。例如,如果代码进入一个死循环,内核就会停止进行调度,系统不会再响应任何动 作,包括 Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然;要么就是亡羊补牢,在发生挂起后调试代码。

通 过在一些关键点上插入 schedule 调用可以防止死循环。schedule 函数(正如读者猜到的)会调用调度器,并因此允许其他进程“偷取”当然进程的CPU时间。如果该进程因驱动程序的错误而在内核空间陷入死循环,则可以在跟 踪到这种情况之后,借助 schedule 调用杀掉这个进程。

当然,应该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule 允许其他进程开始运行。假设驱动程序进行了合适的锁定,这种重入通常还并不致于带来问题。不过,一定不要在驱动程序持有spinlock 的任何时候调用 schedule。

如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule 调用时,最好的方法是加入一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)。

有 时系统看起来象挂起了,但其实并没有。例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况。运行专为探明此种情况而设计的程序,通过查看它的输出情 况,可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新,就说明 scheduler 正在工作。如果没有使用图形显示,则可以运行一个程序让键盘LED闪烁,或不时地开关软驱马达,或不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免; 可改为寻求 ioctl 命令 KDMKTONE ),来检查 scheduler 是否工作正常。O’Reilly FTP站点上可以找到一个例子(misc-progs/heartbeat.c),它会使键盘LED不断闪烁。

如果键盘不接收输入,最佳的 处理方法是从网络登录到系统中,杀掉任何违例的进程,或是重新设置键盘(用 kdb_mode -a)。然而,如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起是由键盘死锁造成的也没有用了。如果是这样的情况,就应该配置一种可替代的输入设 备,以便至少可以正常地重启系统。比起去按所谓的“大红钮”,在你的计算机上,通过替代的输入设备来关机或重启系统要更为容易些,而且它可以免去fsck 对磁盘的长时间扫描。

例如,这种替代输入设备可以是鼠标。1.10或更新版本的 gpm 鼠标服务器可以通过命令行选项支持类似的功能,不过仅限于文本模式。如果没有网络连接,并且以图形方式运行,则建议采用某些自定义的解决方案,比如,设置 一个与串口线 DCD 针脚相连的开关,并编写一个查询 DCD 信号状态变化的脚本,用于从外界干预键盘已被死锁的系统。

对于上述情形, 一个不可缺少的工具是“magic SysRq key”,2.2 和后期版本内核中,在其它体系结构上也可利用得到它。SysRq 魔法键是通过PC键盘上的 ALT 和 SysRq 组合键来激活的,在 SPARC 键盘上则是 ALT 和 Stop 组合键。连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种,这些动作如下:

r
在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式。

k
激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程,留下一个干净的终端。

s
对所有磁盘进行紧急同步。

u
尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着 s 动作之后被调用,它可以在系统处于严重故障状态时节省很多检查文件系统的时间。

b
立即重启系统。注意先要同步并重新挂装磁盘。

p
打印当前的寄存器信息。

t
打印当前的任务列表。

m
打印内存信息。

还 有其它的一些 SysRq 功能;要获得完整列表,可参阅内核源码 Documentation 目录下的sysrq.txt 文件。注意,SysRq 功能必须明确地在内核配置中被开启,出于安全原因,大多数发行系统并未开启它。不过,对于一个用于驱动程序开发的系统来说,为开启 SysRq 功能而带来的重新编译新内核的麻烦是值得的。SysRq 必须在运行时通过下面的命令启动:

[quote]
echo 1 > /proc/sys/kernel/sysrq
[/quote]

在 复现系统的挂起故障时,另一个要采取的预防措施是,把所有的磁盘都以只读的方式挂装在系统上(或干脆就卸装它们)。如果磁盘是只读的或者并未挂装,就不会 发生破坏文件系统或致使文件系统处于不一致状态的危险。另一个可行方法是,使用通过 NFS (网络文件系统)将其所有文件系统挂装入系统的计算机。这个方法要求内核具有“NFS-Root”的能力,而且在引导时还需传入一些特定参数。如果采用这 种方法,即使我们不借助于 SysRq,也能避免任何文件系统的崩溃,因为NFS 服务器管理文件系统的一致性,而它并不受驱动程序的影响。

4.5  调试器和相关工具
最后一种调试模块的方法就是使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。这种方法非常耗时,应该尽量避免。不过,某些情况下通过调试器对代码进行细粒度的分析是很有价值的。

在内核中使用交互式调试器是一个很复杂的问题。出于对系统所有进程的整体利益考虑,内核在它自己的地址空间中运行。其结果是,许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。本节着眼于调试内核的几种方法;它们中的每一种都各有利弊。

4.5.1  使用 gdb
gdb在探究系统内部行为时非常有用。在我们这个层次上,熟练使用调试器,需要掌握 gdb 命令、了解目标平台的汇编代码,还要具备对源代码和优化后的汇编码进行匹配的能力。

启动调试器时必须把内核看作是一个应用程序。除了指定未压缩的内核映像文件名外,还应该在命令行中提供“core 文件”的名称。对于正运行的内核,所谓 core 文件就是这个内核在内存中的核心映像,/proc/kcore。典型的 gdb 调用如下所示:

gdb /usr/src/linux/vmlinux /proc/kcore

第一个参数是未经压缩的内核可执行文件的名字,而不是 zImage 或 bzImage 以及其他任何压缩过的内核。

gdb 命令行的第二个参数是是 core 文件的名字。与其它 /proc中的文件类似,/proc/kcore也是在被读取时产生的。当在 /proc文件系统中执行 read 系统调用时,它会映射到一个用于数据生成而不是数据读取的函数上;我们已在“使用 /proc文件系统”一节中介绍了这个特性。kcore 用来按照 core 文件的格式表示内核“可执行文件”;由于它要表示对应于所有物理内存的整个内核地址空间,所以是一个非常巨大的文件。在 gdb 的使用中,可以通过标准gdb命令查看内核变量。例如,p jiffies可以打印从系统启动到当前时刻的时钟滴答数。

从gdb 打印数据时,内核仍在运行,不同数据项的值会在不同时刻有所变化;然而,gdb为了优化对 core 文件的访问,会将已经读到的数据缓存起来。如果再次查看jiffies变量,仍会得到和上次一样的值。对通常的 core 文件来说,对变量值进行缓存是正确的,这样可避免额外的磁盘访问。但对“动态的”core 文件来说就不方便了。解决方法是在需要刷新gdb 缓冲区的时候,执行命令core-file /proc/kcore;调试器将使用新的 core 文件并丢弃所有的旧信息。不过,读新数据时并不总是需要执行core-file 命令;gdb 以几KB大小的小数据块形式读取 core 文件,缓存的仅是已经引用的若干小块。

对内核进行调试时,gdb 通常能提供的许多功能都不可用。例如,gdb 不能修改内核数据;因为在处理其内存映像之前,gdb 期望把待调试程序运行在自己的控制之下。同样,也不能设置断点或观察点,或者单步跟踪内核函数。

如果用调试选项(-g)编译了内核,产生的 vmlinux 会比没有使用 -g选项的更适合于gdb。不过要注意,用 -g选项编译内核需要大量的磁盘空间(每个目标文件和内核自身都会比通常的大三倍甚至更多)。

在 非PC类计算机上,情况则不尽相同。在 Alpha 上,make boot会在生成可启动映像前将调试信息去掉,所以最终会获得 vmlinux 和 vmlinux.gz 两个文件。gdb 可以使用前者,后者用来启动。在SPARC上,默认情况则是不把内核(至少是2.0内核)调试信息去掉。

当 用 -g选项编译内核并且和 /proc/kcore一起使用 vmlinux 运行调试器时,gdb 可以返回很多内核内部信息。例如,可以使用下面的命令来转储结构数据,如p *module_list、p *module_list->next 和 p *chrdevs[4]->fops 等。为了在使用 p 命令时取得最好效果,有必要保留一份内核映射表和随手可及的源码。

利用 gdb 可在当前内核上执行的另一个有用任务是,通过disassemble命令(可缩写为 disass )或是“检查指令”(x/i)命令对函数进行反汇编。disassemble 命令的参数可以是函数名或是内存范围;而 x/i 则使用一个内存地址做为参数,也可以是符号名称的形式。例如,可以用 x/20i 反汇编 20 条指令。注意,不能反汇编一个模块的函数,因为调试器作用的是 vmlinux,它并不知道模块的情况。如果试图通过地址反汇编模块代码,gdb 很有可能会返回“Cannot access memory at xxxx(不能访问 xxxx 处的内存)”这样的信息。基于同样的原因,也不能查看属于模块的数据项。如果已知道变量的地址,可以从 /dev/mem 中读出它们的值,但要弄明白从系统内存中分解出的原始数据的含义,难度是相当大的。

如果需要反汇编模块函数,最好对模块的目标文件用 objdump 工具进行处理。很不幸,该工具只能对磁盘上的文件复本进行处理,而不能对运行中的模块进行处理;因此,由objdump给出的地址都是未经重定位的地址, 与模块的运行环境无关。对未经链接的目标文件进行反汇编的另一个不利因素在于,其中的函数调用仍是未作解析的,所以就无法轻松地区分是对 printk 的调用呢,还是对 kmalloc 的调用。

正如上面看到的,当目的在于查看内核的运行情况时,gdb是一个有用的工具,但对于设备驱动程序的调试,它还缺少一些至关重要的功能。

4.5.2  kdb 内核调试器
很 多读者可能会奇怪这一点,即为什么不把一些更高级的调试功能直接编译进内核呢。答案很简单,因为 Linus 不信任交互式的调试器。他担心这些调试器会导致一些不良的修改,也就是说,修补的仅是一些表面现象,而没有发现问题的真正原因所在。因此,没有在内核中内 建调试器。

然而,其他的内核开发人员偶尔也会用到一些交互式的调试工具。kdb 就是其中一种内建的内核调试器,它在 oss.sgi.com 上以非正式的补丁形式提供。要使用 kdb,必须首先获得这个补丁(取得的版本一定要和内核版本相匹配),然后对当前内核源码进行 patch 操作,再重新编译并安装这个内核。注意,kdb 仅可用于 IA-32(x86) 系统(虽然用于 IA-64 的一个版本在主流内核源码中短暂地出现过,但很快就被删去了)。

一旦运行的是支持 kdb 的内核,有几个方法可以进入 kdb 的调试状态。在控制台上按下 Pause(或 Break)键将启动调试。当内核发生 oops,或到达某个断点时,也会启动 kdb。无论是哪一种情况,都看到下面这样的消息:

[quote]
Entering kdb (0xc1278000) on processor 1 due to Keyboard Entry [1]kdb>  
[/quote]

注意,当 kdb 运行时,内核所做的每一件事情都会停下来。当激活 kdb 调试时,系统不应运行其他的任何东西;尤其是,不要开启网络――当然,除非是在调试网络驱动程序。一般来说,如果要使用 kdb 的话,最好在启动时进入单用户模式。

作为一个例子,考虑下面这个快速的 scull 调试过程。假定驱动程序已被载入,可以象下面这样指示 kdb 在 scull_read 函数中设置一个断点:

[quote]
[1]kdb> bp scull_read
Instruction(i) BP #0 at 0xc8833514 (scull_read)     is enabled on cpu 1

[1]kdb> go
[/quote]

bp 命令指示 kdb 在内核下一次进入 scull_read 时停止运行。随后我们输入 go 继续执行。在把一些东西放入 scull 的某个设备之后,我们可以在另一个终端的 shell 中运行 cat 命令尝试读取这个设备,这样一来就会产生如下的状态:

[quote]
Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515
Instruction(i) breakpoint #0 at 0xc8833514
scull_read+0x1:   movl   %esp,%ebp
[0]kdb>
[/quote]

我们现在正处于 scull_read 的开头位置。为了查明是怎样到达这个位置的,我们可以看看堆栈跟踪记录:

[quote]
[0]kdb> bt
   EBP       EIP         Function(args)
0xc3109c5c 0xc8833515  scull_read+0x1
0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000,
0x1000, 0x804ad78)
0xbffffc88 0xc010bec0  system_call
[0]kdb>
[/quote]

kdb 试图打印出调用跟踪所记录的每个函数的参数列表。然而,它往往会被编译器所使用的优化技巧弄糊涂。所以在这个例子中,虽然 scull_read 实际只有四个参数,kdb 却打印出了五个。

下面我们来看看如何查询数据。mds 命令是用来对数据进行处理的;我们可以用下面的命令查询 scull_devices 指针的值:

[quote]
[0]kdb> mds scull_devices 1
c8836104: c4c125c0 ....
[/quote]

在这里,我们请求查看的是从 scull_devices 指针位置开始的一个字大小(4个字节)的数据;应答告诉我们设备数据数组的起始地址位于 c4c125c0。要查看设备结构自身的数据值,我们需要用到这个地址:

[quote]
[0]kdb> mds c4c125c0
c4c125c0: c3785000  ....
c4c125c4: 00000000  ....
c4c125c8: 00000fa0  ....
c4c125cc: 000003e8  ....
c4c125d0: 0000009a  ....
c4c125d4: 00000000  ....
c4c125d8: 00000000  ....
c4c125dc: 00000001  ....
[/quote]

上 面的8行分别对应于 Scull_Dev 结构中的8个成员。因此,通过显示的这些数据,我们可以知道,第一个设备的内存是从 0xc3785000 开始分配的,链表中没有下一个数据项,量子大小为 4000(十六进制形式为 fa0)字节,量子集大小为 1000(十六进制形式为 3e8),这个设备中有 154 个字节(十六进制形式为 9a)的数据,等等。

kdb 还可以修改数据。假设我们要从设备中削减一些数据:

[quote]
[0]kdb> mm c4c125d0 0x50
0xc4c125d0 = 0x50
[/quote]

接下来对设备的 cat 操作所返回的数据就会少于上次。

kdb 还有许多其他的功能,包括单步调试(根据指令,而不是C源代码行),在数据访问中设置断点,反汇编代码,跟踪链表,访问寄存器数据等等。加上 kdb 补丁之后,在内核源码树的 Documentation/kdb 目录可以找到完整的手册页。

4.5.3  集成的内核调试器补丁
有 很多内核开发人员为一个名为“集成的内核调试器”的非正式补丁作出过贡献,我们可将其简称为 IKD(integrated kernel debugger)。IKD 提供了很多值得关注的内核调试工具。x86 是这个补丁的主要平台,不过它也可以用于其它的结构体系之上。IKD 补丁可以从 ftp://ftp.kernel.org/pub/linux/kernel/people/andrea/ikd 下载。它是一个必须应用于内核源码的patch 补丁;因为这个 patch 是与版本相关的,所以要确保下载的补丁与正使用的内核版本相一致。

IKD 补丁的功能之一是内核堆栈调试。如果开启这个功能,内核就会在每个函数调用时检查内核堆栈的空闲空间的大小,如果过小的话就会强制产生一个 oops。如果内核中的某些事情引起堆栈崩溃,这个工具就能用来帮助查找问题。这其实也就是一种“堆栈计量表”的功能,可以在任何特定的时刻查看堆栈的填 充程度。

IKD 补丁还包含了一些用于发现内核死锁的工具。如果某个内核过程持续时间过久而没有得到调度的话,“软件死锁”探测器就会强制产生一个 oops。这是简单地通过对函数调用进行计数来实现的,如果计数值超过了一个预定义的阈值,探测器就会动作,并中止一些工作。IKD 的另一个功能是可以连续地把程序计数器打印到虚拟控制台上,这可以作为跟踪死锁的最后手段。“信号量死锁”探测器则是在某个进程的 down 调用持续时间过久时强制产生 oops。

IKD 中的其它调试功能包括内核的跟踪功能,它可以记录内核代码的执行路径。还有一些内存调试工具,包括一个内存泄漏探测器和一些称为“poisoner”的工具,它们在跟踪内存崩溃问题时非常有用。

最后,IKD 也包含前一节讨论过的 kdb 调试器。不过,IKD 补丁中的 kdb 版本有些老。如果需要 kdb 的话,我们推荐直接从 oss.sgi.com 获取当前的版本。

4.5.4  kgdb 补丁
kgdb 是一个在Linux 内核上提供完整的 gdb 调试器功能的补丁,不过仅限于 x86 系统。它通过串口连线以钩子的形式挂入目标调试系统进行工作,而在远端运行 gdb。使用 kgdb 时需要两个系统――一个用于运行调试器,另一个用于运行待调试的内核。和 kdb 一样,kgdb 目前可从 oss.sgi.com 获得。

设 置 kgdb包括安装内核补丁并引导打过补丁之后的内核两个步骤。两个系统之间需要通过串口电缆(或空调制解调器电缆)进行连接,在 gdb 这一侧,需要安装一些支持文件。kgdb 补丁把详细的用法说明放在了文件 Documentation/i386/gdb-serial.txt 中;我们在这里就不再赘述。建议读者阅读关于“调试模块”的说明:接近末尾的地方,有一些出于这个目的而编写的很好的 gdb 宏。

4.5.5  内核崩溃转储分析器
崩 溃转储分析器使系统能把发生 oops 时的系统状态记录下来,以便在随后空闲的时候查看这些信息。如果是对于一个异地用户的驱动程序进行支持,这些工具就会特别有用。用户可能不太愿意把 oops 复制下来,因此安装崩溃转储系统可以使技术支持人员不必依赖于用户的工作,也能获得用于跟踪用户问题的必要信息。也正是出于这样的原因,可供利用的崩溃转 储分析器都是由那些对用户系统进行商业支持的公司开发的,这也就不足为奇了。

目前有两个崩溃转储分析器的补丁可以用于 Linux。在编写本节的时候,这两个工具都比较新,而且都处在不断的变化之中。与其提供可能已经过时的详细信息,我们倒不如只是给出一个概观,并指点读者在哪里可以找到更多的信息。

第 一个分析器是 LKCD(Linux Kernel Crash Dumps,“Linux内核崩溃转储”)。这个工具仍可以从 oss.sgi.com 上获得。当内核发生 oops 时,LKCD 会把当前系统状态(主要指内存)写入事先指定好的转储设备中。这个转储设备必须是一个系统交换区。下次重启中(在存储交换功能开启之前)系统会运行一个称 为 LCRASH 的工具,来生成崩溃的概要记录,并可选择地把转储的复本保存在一个普通文件中。LCRASH 可以交互方式地运行,提供了很多调试器风格的命令,用以查询系统状态。

LKCD 目前只支持 Intel 32位体系结构,并只能用在 SCSI 磁盘的交换分区上。

另 一个崩溃转储设施可以从 www.missioncriticallinux.com 获得。这个崩溃转储子系统直接在目录 /var/dumps 中创建崩溃转储文件,而且并不使用交换区。这样就使某些事情变得更为容易,但也意味着在知道问题已经出现在哪里的时候,文件系统已被系统修改。生成的崩溃 转储的格式是标准的 core 文件格式,所以可以利用 gdb 这类工具进行事后的分析。这个工具包也提供了另外的分析器,可以从崩溃转储文件中解析出比 gdb 更丰富的信息。

4.5.6  用户模式的 Linux 虚拟机
用 户模式 Linux 是一个很有意思的概念。它作为一个独立的可移植的 Linux 内核而构建,包含在子目录 arch/um 中。然而,它并不是运行在某种新的硬件上,而是运行在基于 Linux 系统调用接口所实现的虚拟机之上。因此,用户模式 Linux 可以使 Linux 内核成为一个运行在 Linux 系统之上单独的、用户模式的进程。

把一个内核的复本当作用户模式下的进程来运行可以带来很多 好处。因为它运行在一个受约束的虚拟处理器之上,所以有错误的内核不会破坏“真正的”系统。对软/硬件的不同配置可以在相同的框架中轻易地进行尝试。并 且,对于内核开发人员来说最值得注目的特点在于,可以很容易地利用 gdb 或其它调试器对用户模式 Linux 进行处理。归根结底,它只是一个进程。很明显,用户模式 Linux 有潜力加快内核的开发过程。

迄今为止,用户模式 Linux 虚拟机还未在主流内核中发布;要下载它,必须访问它的 web 站点(http://user-mode-linux.sourceforge.net)。需要提醒的是,它仅可以集成到 2.4.0 之后的早期 2.4 内核版本中;当然等到本书出版的时候,版本支持方面可能会做得更好。

目前,用户模式 Linux 虚拟机也存在一些重大的限制,不过大部分可能很快就会得到解决。虚拟处理器当前只能工作于单处理器模式;虽然虚拟机可以毫无问题地运行在 SMP 系统上,但它仍是把主机模拟成单 CPU 模式。不过,对于驱动编写者来说,最大的麻烦在于,用户模式内核不能访问主机系统上的硬件设备。因此,尽管用户模式 Linux虚拟机对于本书中的大多数样例驱动程序的调试非常有用,却无法用于调试那些处理实际硬件的驱动程序。最后一点,用户模式 Linux虚拟机仅能运行在 IA-32 体系结构之上。

因为对所有这些问题的修补工作正在进行之中,所以在不远的将来,对于 Linux 设备驱动程序的开发人员,用户模式 Linux虚拟机可能会成为一个不可或缺的工具。

4.5.7  Linux 跟踪工具包
Linux 跟踪工具包(LTT)是一个内核补丁,包含了一组可以用于内核事件跟踪的相关工具集。跟踪内容包括时间信息,而且还能合理地建立在一段指定时间内所发生事件的完整图形化描述。因此,LTT不仅能用于调试,还能用来捕捉性能方面的问题。

在 Web 站点 www.opersys.com/LTT 上,可以找到 LTT 以及大量的资料。

4.5.8  Dynamic Probes
Dynamic Probes (或 DProbes )是 IBM 为基于 IA-32 结构的Linux 发布的一种调试工具(遵循 GPL 协议)。它可以在系统的几乎任何一个地方放置一个“探针”,既可以是用户空间也可以是内核空间。这个探针由一些当控制到达指定地点即开始执行的代码(用一 种特别设计的,面向堆栈的语言编写)组成。这种代码能把信息传送回用户空间,修改寄存器,或者完成许多其它的工作。DProbes 很有用的特点是,一旦内核编译进了这个功能,探针就可以插到一个运行系统的任一个位置,而无需重建内核或重新启动。DProbes 也可以协同 LTT 工具在任意位置插入新的跟踪事件。

DProbes 工具可以从 IBM 的开放源码站点,即 [url]http://oss.software.ibm.com[/url] 上下载。
posted @ 2006-05-26 11:03 易道 阅读(3830) | 评论 (0)编辑 收藏

    打开一个DOS命令窗口,在MySQL中建立一个数据库,并添加一个用户:
   C:\mysql\bin> mysql -uroot -p      // 登陆   mysql [-u username] [-h host] [-p[password]] [dbname]
   ******** (输入root密码,如果还没有设置,直接输入回车即可)
   mysql> GRANT ALL ON db1.* TO user1@localhost IDENTIFIED BY '123';   //user@"%"  可以在其它的机子登陆 本例是本机
   mysql> CREATE DATABASE db1;
   mysql> quit
  
  
  
   导入数据库的结构
   C:\mysql\bin> mysql -uuser1 -Ddb1 -p < [myicqd目录]\ttt.sql
   password: 123
  
  
   修改密码   
   格式:mysqladmin -u用户名 -p旧密码 password 新密码

   启动
   MySQL安装完成后启动文件mysql在/etc/init.d目录下,在需要启动时运行下面命令即可。
   [root@test1 init.d]# /etc/init.d/mysql start
  停止
   /usr/bin/mysqladmin -u root -p shutdown
    [root@test1 init.d]# /etc/init.d/mysql stop
    重新启动MySQL服务
   [root@test1 init.d]# /etc/init.d/mysql restart

   数据库的备份
     mysqldump --add-drop-table --add-locks --lock-tables  $MYSQL_DBNAME -uroot -pcanada> $BACKUP_FILE
   数据库还原
    mysql $MYSQL_DBNAME -uroot -pcanada< $RESTORE_FILE
   
   
     MySQL的常用操作
   注意:MySQL中每个命令后都要以分号;结尾。
   1、显示数据库
   mysql> show databases;
   +----------+
   | Database |
   +----------+
   | mysql  |
   | test   |
   +----------+
   2 rows in set (0.04 sec)
   Mysql刚安装完有两个数据库:mysql和test。mysql库非常重要,它里面有MySQL的系统信息,我们改密码和新增用户,实际上就是用这个库中的相关表进行操作。
   2、显示数据库中的表
   mysql> use mysql; (打开库,对每个库进行操作就要打开此库,类似于foxpro )
   Database changed
   mysql> show tables;
   +-----------------+
   | Tables_in_mysql |
   +-----------------+
   | columns_priv  |
   | db       |
   | func      |
   | host      |
   | tables_priv   |
   | user      |
   +-----------------+
   6 rows in set (0.01 sec)
   3、显示数据表的结构:
   describe 表名;
   4、显示表中的记录:
   select * from 表名;
   例如:显示mysql库中user表中的纪录。所有能对MySQL用户操作的用户都在此表中。
   Select * from user;
   5、建库:
   create database 库名;
   例如:创建一个名字位aaa的库
   mysql> create databases aaa;
6、建表:
   use 库名;
   create table 表名 (字段设定列表);
   例如:在刚创建的aaa库中建立表name,表中有id(序号,自动增长),xm(姓名),xb(性别),csny(出身年月)四个字段
   use aaa;
   mysql> create table name (id int(3) auto_increment not null primary key, xm char(8),xb char(2),csny date);
   可以用describe命令察看刚建立的表结构。
   mysql> describe name;
   +-------+---------+------+-----+---------+----------------+
   | Field | Type  | Null | Key | Default | Extra     |
   +-------+---------+------+-----+---------+----------------+
   | id  | int(3) |   | PRI | NULL  | auto_increment |
   | xm  | char(8) | YES |   | NULL  |        |
   | xb  | char(2) | YES |   | NULL  |        |
   | csny | date  | YES |   | NULL  |        |
   +-------+---------+------+-----+---------+----------------+
   7、增加记录
   例如:增加几条相关纪录。
   mysql> insert into name values(\'\',\'张三\',\'男\',\'1971-10-01\');
   mysql> insert into name values(\'\',\'白云\',\'女\',\'1972-05-20\');
   可用select命令来验证结果。
   mysql> select * from name;
   +----+------+------+------------+
   | id | xm  | xb  | csny    |
   +----+------+------+------------+
   | 1 | 张三 | 男  | 1971-10-01 |
   | 2 | 白云 | 女  | 1972-05-20 |
   +----+------+------+------------+
   8、修改纪录
   例如:将张三的出生年月改为1971-01-10
   mysql> update name set csny=\'1971-01-10\' where xm=\'张三\';
   9、删除纪录
   例如:删除张三的纪录。
   mysql> delete from name where xm=\'张三\';
   10、删库和删表
   drop database 库名;
   drop table 表名;
posted @ 2006-05-24 11:11 易道 阅读(222) | 评论 (0)编辑 收藏

摘要
CVS是一个C/S系统,多个开发人员通过一个中心版本控制系统来记录文件版本,从而达到保证文件同步的目的。(2004-07-05 20:19:18)

By lanf, 出处:http://www.chedong.com/tech/cvs_card.html

作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com

内容摘要:

CVS是一个C/S系统,多个开发人员通过一个中心版本控制系统来记录文件版本,从而达到保证文件同步的目的。工作模式如下:

       CVS服务器(文件版本库)
/ | \
(版 本 同 步)
/ | \
开发者1 开发者2 开发者3

作为一般开发人员挑选2,6看就可以了,CVS的管理员则更需要懂的更多一些,最后还简单介绍了一些Windows下的cvs客户端使用,CVS远程用户认证的选择及与BUG跟踪系统等开发环境的集成问题。

  1. CVS环境初始化:CVS环境的搭建 管理员
  2. CVS的日常使用:日常开发中最常用的CVS命令, 开发人员 管理员
  3. CVS的分支开发:项目按照不同进度和目标并发进行 管理员
  4. CVS的用户认证:通过SSH的远程用户认证,安全,简单 管理员
  5. CVSWEB:CVS的WEB访问界面大大提高代码版本比较的效率 管理员
  6. CVS TAG:将$Id$ 加入代码注释中,方便开发过程的跟踪开发人员
  7. CVS vs VSS: CVS和Virsual SourceSafe的比较 开发人员 管理员
  8. WinCVS: 通过SSH认证的WinCVS认证设置
  9. 基于CVSTrac的小组开发环境搭建:通过CVSTrac实现web界面的CVS用户管理,集成的BUG跟踪和WIKI交流
  10. CVS中的用户权限管理:基于系统用户的CVS权限管理和基于CVSROOT/passwd的虚拟用户管理

一个系统20%的功能往往能够满足80%的需求,CVS也不例外,以下是CVS最常用的功能,可能还不到它全部命令选项的20%,作为一般开发人员平时会用cvs update和cvs commit就够了,更多的需求在实际应用过程中自然会出现,不时回头看看相关文档经常有意外的收获。


CVS环境初始化 环境设置:指定CVS库的路径CVSROOT

tcsh
setenv CVSROOT /path/to/cvsroot
bash
CVSROOT=/path/to/cvsroot ; export CVSROOT

后面还提到远程CVS服务器的设置:
CVSROOT=:ext:$USER@test.server.address#port:/path/to/cvsroot CVS_RSH=ssh; export CVSROOT CVS_RSH

初始化:CVS版本库的初始化。
cvs init

一个项目的首次导入
cvs import -m "write some comments here" project_name vendor_tag release_tag
执行后:会将所有源文件及目录导入到/path/to/cvsroot/project_name目录下
vender_tag: 开发商标记
release_tag: 版本发布标记

项目导出:将代码从CVS库里导出
cvs checkout project_name
cvs 将创建project_name目录,并将最新版本的源代码导出到相应目录中。这个checkout和Virvual SourceSafe中的check out不是一个概念,相对于Virvual SourceSafe的check out是cvs update, check in是cvs commit。

CVS的日常使用

注意:第一次导出以后,就不是通过cvs checkout来同步文件了,而是要进入刚才cvs checkout project_name导出的project_name目录下进行具体文件的版本同步(添加,修改,删除)操作。

将文件同步到最新的版本
cvs update
不制定文件名,cvs将同步所有子目录下的文件,也可以制定某个文件名/目录进行同步
cvs update file_name
最好每天开始工作前或将自己的工作导入到CVS库里前都要做一次,并养成“先同步 后修改”的习惯,和Virvual SourceSafe不同,CVS里没有文件锁定的概念,所有的冲突是在commit之前解决,如果你修改过程中,有其他人修改并commit到了CVS 库中,CVS会通知你文件冲突,并自动将冲突部分用
>>>>>>
content on cvs server
<<<<<<
content in your file
>>>>>>
标记出来,由你确认冲突内容的取舍。
版本冲突一般是在多个人修改一个文件造成的,但这种项目管理上的问题不应该指望由CVS来解决。

确认修改写入到CVS库里
cvs commit -m "write some comments here" file_name

注意:CVS的很多动作都是通过cvs commit进行最后确认并修改的,最好每次只修改一个文件。在确认的前,还需要用户填写修改注释,以帮助其他开发人员了解修改的原因。如果不用写-m "comments"而直接确认`cvs commit file_name` 的话,cvs会自动调用系统缺省的文字编辑器(一般是vi)要求你写入注释。
注释的质量很重要:所以不仅必须要写,而且必须写一些比较有意义的内容:以方便其他开发人员能够很好的理解
不好的注释,很难让其他的开发人员快速的理解:比如: -m "bug fixed" 甚至 -m ""
好的注释,甚至可以用中文: -m "在用户注册过程中加入了Email地址校验"


修改某个版本注释:每次只确认一个文件到CVS库里是一个很好的习惯,但难免有时候忘了指定文件名,把多个文件以同样注释commit到CVS库里了,以下命令可以允许你修改某个文件某个版本的注释:
cvs admin -m 1.3:"write some comments here" file_name

添加文件
创建好新文件后,比如:touch new_file
cvs add new_file
注意:对于图片,Word文档等非纯文本的项目,需要使用cvs add -kb选项按2进制文件方式导入(k表示扩展选项,b表示binary),否则有可能出现文件被破坏的情况
比如:
cvs add -kb new_file.gif
cvs add -kb readme.doc

如果关键词替换属性在首次导入时设置错了怎么办?
cvs admin -kkv new_file.css

然后确认修改并注释
cvs ci -m "write some comments here"

删除文件
将某个源文件物理删除后,比如:rm file_name
cvs rm file_name
然后确认修改并注释
cvs ci -m "write some comments here"
以上面前2步合并的方法为:
cvs rm -f file_name
cvs ci -m "why delete file"
注意:很多cvs命令都有缩写形式:commit=>ci; update=>up; checkout=>co/get; remove=>rm;

添加目录
cvs add dir_name

查看修改历史
cvs log file_name
cvs history file_name

查看当前文件不同版本的区别
cvs diff -r1.3 -r1.5 file_name
查看当前文件(可能已经修改了)和库中相应文件的区别
cvs diff file_name
cvs的web界面提供了更方便的定位文件修改和比较版本区别的方法,具体安装设置请看后面的cvsweb使用

正确的通过CVS恢复旧版本的方法
如果用cvs update -r1.2 file.name
这个命令是给file.name加一个STICK TAG: "1.2" ,虽然你的本意只是想将它恢复到1.2版本
正确的恢复版本的方法是:cvs update -p -r1.2 file_name >file_name
如果不小心已经加成STICK TAG的话:用cvs update -A 解决

移动文件/文件重命名
cvs里没有cvs move或cvs rename,因为这两个操作是可以由先cvs remove old_file_name,然后cvs add new_file_name实现的。

删除/移动目录
最方便的方法是让管理员直接移动,删除CVSROOT里相应目录(因为CVS一个项目下的子目录都是独立的,移动到$CVSROOT目录下都可以作为新的独立项目:好比一颗树,其实砍下任意一枝都能独立存活),对目录进行了修改后,要求其开发人员重新导出项目cvs checkout project_name 或者用cvs update -dP同步。

项目发布导出不带CVS目录的源文件
做开发的时候你可能注意到了,每个开发目录下,CVS都创建了一个CVS/目录。里面有文件用于记录当前目录和CVS库之间的对应信息。但项目发布的时候你一般不希望把文件目录还带着含有CVS信息的CVS目录吧,这个一次性的导出过程使用cvs export命令,不过export只能针对一个TAG或者日期导出,比如:
cvs export -r release1 project_name
cvs export -D 20021023 project_name
cvs export -D now project_name

CVS Branch:项目多分支同步开发 确认版本里程碑:多个文件各自版本号不一样,项目到一定阶段,可以给所有文件统一指定一个阶段里程碑版本号,方便以后按照这个阶段里程碑版本号导出项目,同时也是项目的多个分支开发的基础。

cvs tag release_1_0

开始一个新的里程碑
cvs commit -r 2 标记所有文件开始进入2.x的开发

注意:CVS里的revsion和软件包的发布版本可以没有直接的关系。但所有文件使用和发布版本一致的版本号比较有助于维护。

版本分支的建立
在开发项目的2.x版本的时候发现1.x有问题,但2.x又不敢用,则从先前标记的里程碑:release_1_0导出一个分支 release_1_0_patch
cvs rtag -b -r release_1_0 release_1_0_patch proj_dir

一些人先在另外一个目录下导出release_1_0_patch这个分支:解决1.0中的紧急问题,
cvs checkout -r release_1_0_patch
而其他人员仍旧在项目的主干分支2.x上开发

在release_1_0_patch上修正错误后,标记一个1.0的错误修正版本号
cvs tag release_1_0_patch_1

如果2.0认为这些错误修改在2.0里也需要,也可以在2.0的开发目录下合并release_1_0_patch_1中的修改到当前代码中:
cvs update -j release_1_0_patch_1

CVS的远程认证通过SSH远程访问CVS 使用cvs本身基于pserver的远程认证很麻烦,需要定义服务器和用户组,用户名,设置密码等,

常见的登陆格式如下:
cvs -d :pserver:cvs_user_name@cvs.server.address:/path/to/cvsroot login
例子:
cvs -d :pserver:cvs@samba.org:/cvsroot login

不是很安全,因此一般是作为匿名只读CVS访问的方式。从安全考虑,通过系统本地帐号认证并通过SSH传输是比较好的办法,通过在客户机的 /etc/profile里设置一下内容:
CVSROOT=:ext:$USER@cvs.server.address#port:/path/to/cvsroot CVS_RSH=ssh; export CVSROOT CVS_RSH
所有客户机所有本地用户都可以映射到CVS服务器相应同名帐号了。

比如:

CVS服务器是192.168.0.3,上面CVSROOT路径是/home/cvsroot,另外一台开发客户机是192.168.0.4,如果 tom在2台机器上都有同名的帐号,那么从192.168.0.4上设置了:
export CVSROOT=:ext:tom@192.168.0.3:/home/cvsroot
export CVS_RSH=ssh
tom就可以直接在192.168.0.4上对192.168.0.3的cvsroot进行访问了(如果有权限的话)
cvs checkout project_name
cd project_name
cvs update
...
cvs commit

如果CVS所在服务器的SSH端口不在缺省的22,或者和客户端与CVS服务器端SSH缺省端口不一致,有时候设置了:
:ext:$USER@test.server.address#port:/path/to/cvsroot

仍然不行,比如有以下错误信息:
ssh: test.server.address#port: Name or service not known
cvs [checkout aborted]: end of file from server (consult above messages if any)

解决的方法是做一个脚本指定端口转向(不能使用alias,会出找不到文件错误):
创建一个/usr/bin/ssh_cvs文件,假设远程服务器的SSH端口是非缺省端口:34567
#!/bin/sh
/usr/bin/ssh -p 34567 "$@"
然后:chmod +x /usr/bin/ssh_cvs
并CVS_RSH=ssh_cvs; export CVS_RSH

注意:port是指相应服务器SSH的端口,不是指cvs专用的pserver的端口

CVSWEB:提高文件浏览效率 CVSWEB就是CVS的WEB界面,可以大大提高程序员定位修改的效率:

使用的样例可以看:http://www.freebsd.org/cgi/cvsweb.cgi

CVSWEB的下载:CVSWEB从最初的版本已经演化出很多功能界面更丰富的版本,这个是我个人感觉安装设置比较方便的:
原先在:http://www.spaghetti-code.de/software/linux/cvsweb/,但目前已经删除,目前仍可以在本站下载CVSWEB,其实最近2年FreeBSD的CVSWeb项目已经有了更好的发展吧,而当初没有用FreeBSD那个版本主要就是因为没有彩色的文件Diff功能。
下载解包:
tar zxf cvsweb.tgz
把配置文件cvsweb.conf放到安全的地方(比如和apache的配置放在同一个目录下),
修改:cvsweb.cgi让CGI找到配置文件:
$config = $ENV{'CVSWEB_CONFIG'} || '/path/to/apache/conf/cvsweb.conf';

转到/path/to/apache/conf下并修改cvsweb.conf:

  1. 修改CVSROOT路径设置:
    %CVSROOT = (
    'Development' => '/path/to/cvsroot', #<==修改指向本地的CVSROOT
    );
  2. 缺省不显示已经删除的文档:
    "hideattic" => "1",#<==缺省不显示已经删除的文档
  3. 在配置文件cvsweb.conf中还可以定制页头的描述信息,你可以修改$long_intro成你需要的文字

CVSWEB可不能随便开放给所有用户,因此需要使用WEB用户认证:
先生成 passwd:
/path/to/apache/bin/htpasswd -c cvsweb.passwd user

修改httpd.conf: 增加
<Directory "/path/to/apache/cgi-bin/cvsweb/">
AuthName "CVS Authorization"
AuthType Basic
AuthUserFile /path/to/cvsweb.passwd
require valid-user
</Directory>

CVS TAGS: $Id: cvs_card.html,v 1.5 2003/03/09 08:41:46 chedong Exp $ 将$Id: cvs_card.html,v 1.9 2003/11/09 07:57:11 chedong Exp $ 加在程序文件开头的注释里是一个很好的习惯,cvs能够自动解释更新其中的内容成:file_name version time user_name 的格式,比如:cvs_card.txt,v 1.1 2002/04/05 04:24:12 chedong Exp,可以这些信息了解文件的最后修改人和修改时间


几个常用的缺省文件:
default.php
<?php
/*
* Copyright (c) 2002 Company Name.
* $Header: /home/cvsroot/tech/cvs_card.html,
* v 1.9 2003/11/09 07:57:11 chedong Exp $
*/

?>
====================================
Default.java: 注意文件头一般注释用 /* 开始 JAVADOC注释用 /** 开始的区别
/*
* Copyright (c) 2002 MyCompany Name.
* $Header: /home/cvsroot/tech/cvs_card.html,
* v 1.9 2003/11/09 07:57:11 chedong Exp $
*/

package com.mycompany;

import java.;

/**
* comments here
*/
public class Default {
/**
* Comments here
* @param
* @return
*/
public toString() {

}
}
====================================
default.pl:
#!/usr/bin/perl -w
# Copyright (c) 2002 Company Name.
# $Header: /home/cvsroot/tech/cvs_card.html,
* v 1.9 2003/11/09 07:57:11 chedong Exp $

# file comments here

use strict;

CVS vs VSS

CVS没有文件锁定模式,VSS在check out同时,同时记录了文件被导出者锁定。

CVS的update和commit, VSS是get_lastest_version和check in

对应VSS的check out/undo check out的CVS里是edit和unedit

在CVS中,标记自动更新功能缺省是打开的,这样也带来一个潜在的问题,就是不用-kb方式添加binary文件的话在cvs自动更新时可能会导致文件失效。

$Header: /home/cvsroot/tech/cvs_card.html,v 1.5 2003/03/09 08:41:46 chedong Exp $ $Date: 2003/11/09 07:57:11 $这样的标记在Virsual SourceSafe中称之为Keyword Explaination,缺省是关闭的,需要通过OPITION打开,并指定需要进行源文件关键词扫描的文件类型:*.txt,*.java, *.html...

对于Virsual SourceSafe和CVS都通用的TAG有:
$Header: /home/cvsroot/tech/cvs_card.html,v 1.5 2003/03/09 08:41:46 chedong Exp $
$Author: chedong $
$Date: 2003/11/09 07:57:11 $
$Revision: 1.9 $

我建议尽量使用通用的关键词保证代码在CVS和VSS都能方便的跟踪。

WinCVS 下载:

cvs Windows客户端:目前稳定版本为1.2
http://cvsgui.sourceforge.net
ssh Windows客户端
http://www.networksimplicity.com/openssh/

安装好以上2个软件以后:
WinCVS客户端的admin==>preference设置
1 在general选单里
设置CVSROOT: username@192.168.0.123:/home/cvsroot
设置Authorization: 选择SSH server

2 Port选单里
钩上:check for alternate rsh name
并设置ssh.exe的路径,缺省是装在 C:\Program Files\NetworkSimplicity\ssh\ssh.exe

然后就可以使用WinCVS进行cvs操作了,所有操作都会跳出命令行窗口要求你输入服务器端的认证密码。

当然,如果你觉得这样很烦的话,还有一个办法就是生成一个没有密码的公钥/私钥对,并设置CVS使用基于公钥/私钥的SSH认证(在general 选单里)。

可以选择的diff工具:examdiff
下载:
http://www.prestosoft.com/examdiff/examdiff.htm
还是在WinCVS菜单admin==>preference的WinCVS选单里
选上:Externel diff program
并设置diff工具的路径,比如:C:\Program Files\ed16i\ExamDiff.exe
在对文件进行版本diff时,第一次需要将窗口右下角的use externel diff选上。

基于CVSTrac的小组开发环境搭建 作为一个小组级的开发环境,版本控制系统和BUG跟踪系统等都涉及到用户认证部分。如何方便的将这些系统集成起来是一个非常困难的事情,毕竟我们不能指望 Linux下有像Source Offsite那样集成度很高的版本控制/BUG跟踪集成系统。

我个人是很反对使用pserver模式的远程用户认证的,但如果大部分组员使用WINDOWS客户端进行开发的话,总体来说使用 CVSROOT/passwd认证还是很难避免的,但CVS本身用户的管理比较麻烦。本来我打算自己用perl写一个管理界面的,直到我发现了 CVSTrac:一个基于WEB界面的BUG跟踪系统,它外挂在CVS系统上的BUG跟踪系统,其中就包括了WEB界面的CVSROOT/passwd文件的管理,甚至还集成了WIKIWIKI讨论组功能。

这里首先说一下CVS的pserver模式下的用户认证,CVS的用户认证服务是基于inetd中的:
cvspserver stream tcp nowait apache /usr/bin/cvs cvs --allow-root=/home/cvsroot pserver
一般在2401端口(这个端口号很好记:49的平方)

CVS用户数据库是基于CVSROOT/passwd文件,文件格式:
[username]:[crypt_password]:[mapping_system_user]
由于密码都用的是UNIX标准的CRYPT加密,这个passwd文件的格式基本上是apache的htpasswd格式的扩展(比APACHE的 PASSWD文件多一个系统用户映射字段),所以这个文件最简单的方法可以用
apache/bin/htpasswd -b myname mypassword
创建。注意:通过htpasswd创建出来的文件会没有映射系统用户的字段
例如:
new:geBvosup/zKl2
setup:aISQuNAAoY3qw
test:hwEpz/BX.rEDU

映射系统用户的目的在于:你可以创建一个专门的CVS服务帐号,比如用apache的运行用户apache,并将/home/cvsroot目录下的所有权限赋予这个用户,然后在passwd文件里创建不同的开发用户帐号,但开发用户帐号最后的文件读写权限都映射为apache用户,在SSH模式下多个系统开发用户需要在同一个组中才可以相互读写CVS库中的文件。

进一步的,你可以将用户分别映射到apache这个系统用户上。
new:geBvosup/zKl2:apache
setup:aISQuNAAoY3qw:apache
test:hwEpz/BX.rEDU:apache

CVSTrac很好的解决了CVSROOT/passwd的管理问题,而且包含了BUG跟踪报告系统和集成WIKIWIKI交流功能等,使用的 CGI方式的安装,并且基于GNU Public License

在inetd里加入cvspserver服务:
cvspserver stream tcp nowait apache /usr/bin/cvs cvs --allow-root=/home/cvsroot pserver

xietd的配置文件:%cat cvspserver
service cvspserver
{
disable = no
socket_type = stream
wait = no
user = apache
server = /usr/bin/cvs
server_args = -f --allow-root=/home/cvsroot pserver
log_on_failure += USERID
}

注意:这里的用户设置成apache目的是和/home/cvsroot的所有用户一致,并且必须让这个这个用户对/home/cvsroot/下的 CVSROOT/passwd和cvstrac初始化生成的myproj.db有读取权限。

安装过程

  1. 下载:可以从http://www.cvstrac.org 下载
    我用的是已经在Linux上编译好的应用程序包:cvstrac-1.1.2.bin.gz,
    %gzip -d cvstrac-1.1.2.bin.gz
    %chmod +x cvstrac-1.1.2.bin
    #mv cvstarc-1.1.1.bin /usr/bin/cvstrac
    如果是从源代码编译:
    从 http://www.sqlite.org/download.html 下载SQLITE的rpm包:
    rpm -i sqlite-devel-2.8.6-1.i386.rpm
    从 ftp://ftp.cvstrac.org/cvstrac/ 下载软件包
    解包,假设解包到/home/chedong/cvstrac-1.1.2下,并规划将cvstrac安装到/usr/local/bin目录下, cd /home/chedong/cvstrac-1.1.2 编辑linux-gcc.mk:
    修改:
    SRCDIR = /home/chedong/cvstrac-1.1.2
    INSTALLDIR = /usr/local/bin
    然后
    mv linux-gcc.mk Makefile
    make
    #make install

  2. 初始化cvstrac数据库:假设数据库名是 myproj
    在已经装好的CVS服务器上(CVS库这时候应该已经是初始化好了,比如:cvs init初始化在/home/cvsroot里),运行一下
    %cvstrac init /home/cvsroot myproj
    运行后,/home/cvsroot里会有一个的myproj.db库,使用CVSTRAC服务,/home/cvsroot/myproj.db /home/cvsroot/CVSROOT/readers /home/cvsroot/CVSROOT/writers /home/cvsroot/CVSROOT/passwd这几个文件对于web服务的运行用户应该是可写的,在RedHat8上,缺省就有一个叫 apache用户和一个apache组,所以在httpd.conf文件中设置了用apache用户运行web服务:
    User apache
    Group apache,
    然后设置属于apache用户和apache组
    #chown -R apache:apache /home/cvsroot
    -rw-r--r-- 1 apache apache 55296 Jan 5 19:40 myproj.db
    drwxrwxr-x 3 apache apache 4096 Oct 24 13:04 CVSROOT/
    drwxrwxr-x 2 apache apache 4096 Aug 30 19:47 some_proj/
    此外还在/home/cvsroot/CVSROOT中设置了:
    chmod 664 readers writers passwd
  3. 在apche/cgi-bin目录中创建脚本cvstrac:
    #!/bin/sh
    /usr/bin/cvstrac cgi /home/cvsroot
    设置脚本可执行:
    chmod +x /home/apache/cgi-bin/cvstrac
  4. 从 http://cvs.server.address/cgi-bin/cvstrac/myproj 进入管理界面
    缺省登录名:setup 密码 setup
    对于一般用户可以从:
    http://cvs.server.address/cgi-bin/cvstrac/myproj
  5. 在setup中重新设置了CVSROOT的路径后,/home/cvsroot
    如果是初次使用需要在/home/cvsroot/CVSROOT下创建passwd, readers, writers文件
    touch passwd readers writers
    然后设置属于apache用户,
    chown apache.apache passwd readers writers
    这样使用setup用户创建新用户后会同步更新CVSROOT/passwd下的帐号

修改登录密码,进行BUG报告等,
更多使用细节可以在使用中慢慢了解。

对于前面提到的WinCVS在perference里设置:
CVSROOT栏输入:username@ip.address.of.cvs:/home/cvsroot
Authenitication选择:use passwd file on server side
就可以了从服务器上进行CVS操作了。

CVS的用户权限管理

CVS的权限管理分2种策略:

  • 基于系统文件权限的系统用户管理:适合多个在Linux上使用系统帐号的开发人员进行开发。
  • 基于CVSROOT/passwd的虚拟用户管理:适合多个在Windows平台上的开发人员将帐号映射成系统帐号使用。
为什么使用apache/apache用户?首先RedHat8中缺省就有了,而且使用这个用户可以方便通过cvstrac进行WEB管理。
chown -R apache.apache /home/cvsroot
chmod 775 /home/cvsroot

Linux上通过ssh连接CVS服务器的多个开发人员:通过都属于apache组实现文件的共享读写
开发人员有开发服务器上的系统帐号:sysuser1 sysuser2,设置让他们都属于apache组,因为通过cvs新导入的项目都是对组开放的:664权限的,这样无论那个系统用户导入的项目文件,只要文件的组宿主是apache,所有其他同组系统开发用户就都可以读写;基于ssh远程认证的也是一样。

   apache(system group)
/            |           \
sysuser1   sysuser2     sysuser3

Windows上通过cvspserver连接CVS服务器的多个开发人员:通过在passwd文件种映射成 apache用户实现文件的共享读写
他们的帐号通过CVSROOT/passwd和readers writers这几个文件管理;通过cvstrac设置所有虚拟用户都映射到apache用户上即可。

   apache(system user)
/            |            \
windev1     windev2      windev3             

利用cvs + (WinCVS/cvsweb/cvstrac),构成了一个相对完善的跨平台工作组开发版本控制环境。

相关资源:

CVS HOME:
http://www.cvshome.org

CVS FAQ:
http://www.loria.fr/~molli/cvs-index.html

相关网站:
http://directory.google.com/Top/Computers/Software/ Configuration_Management/Tools/Concurrent_Versions_System/

CVS--并行版本系统
http://www.soforge.com/cvsdoc/zh_CN/book1.html

CVS 免费书:
http://cvsbook.red-bean.com/

CVS 命令的速查卡片:
http://www.refcards.com/about/cvs.html

WinCVS:
http://cvsgui.sourceforge.net/

CVSTrac: A Web-Based Bug And Patch-Set Tracking System For CVS
http://www.cvstrac.org

StatCVS:基于CVS的代码统计工具:按代码量,按开发者的统计表等
http://sourceforge.net/projects/statcvs

如何在WEB开发中规划CVS上:在Google上查 "cvs web development"
http://ccm.redhat.com/bboard-archive/cvs_for_web_development/index.html

一些集成了CVS的IDE环境:
Eclipse
Magic C++
posted @ 2006-05-24 10:56 易道 阅读(290) | 评论 (1)编辑 收藏

整理:Jims of 肥肥世家

第一次发布时间:2004年7月16日


1. grep简介

grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的re元字符, fgrep就是fixed grep或fast grep,它们把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。linux使用GNU版本的grep。它功能更强,可以通过-G、-E、-F命令行选项来使用egrep和fgrep的功能。

grep的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到屏幕,不影响原文件内容。

grep可用于shell脚本,因为grep通过返回一个状态值来说明搜索的状态,如果模板搜索成功,则返回0,如果搜索不成功,则返回1,如果搜索的文件不存在,则返回2。我们利用这些返回值就可进行一些自动化的文本处理工作。

2. grep正则表达式元字符集(基本集)

^

锚定行的开始 如:'^grep'匹配所有以grep开头的行。

$

锚定行的结束 如:'grep$'匹配所有以grep结尾的行。

.

匹配一个非换行符的字符 如:'gr.p'匹配gr后接一个任意字符,然后是p。

*

匹配零个或多个先前字符 如:'*grep'匹配所有一个或多个空格后紧跟grep的行。 .*一起用代表任意字符。

[]

匹配一个指定范围内的字符,如'[Gg]rep'匹配Grep和grep。

[^]

匹配一个不在指定范围内的字符,如:'[^A-FH-Z]rep'匹配不包含A-R和T-Z的一个字母开头,紧跟rep的行。

\(..\)

标记匹配字符,如'\(love\)',love被标记为1。

\<

锚定单词的开始,如:'\<grep'匹配包含以grep开头的单词的行。

\>

锚定单词的结束,如'grep\>'匹配包含以grep结尾的单词的行。

x\{m\}

重复字符x,m次,如:'0\{5\}'匹配包含5个o的行。

x\{m,\}

重复字符x,至少m次,如:'o\{5,\}'匹配至少有5个o的行。

x\{m,n\}

重复字符x,至少m次,不多于n次,如:'o\{5,10\}'匹配5--10个o的行。

\w

匹配文字和数字字符,也就是[A-Za-z0-9],如:'G\w*p'匹配以G后跟零个或多个文字或数字字符,然后是p。

\W

\w的反置形式,匹配一个或多个非单词字符,如点号句号等。

\b

单词锁定符,如: '\bgrepb\'只匹配grep。

3. 用于egrep和 grep -E的元字符扩展集

+

匹配一个或多个先前的字符。如:'[a-z]+able',匹配一个或多个小写字母后跟able的串,如loveable,enable,disable等。

?

匹配零个或多个先前的字符。如:'gr?p'匹配gr后跟一个或没有字符,然后是p的行。

a|b|c

匹配a或b或c。如:grep|sed匹配grep或sed

()

分组符号,如:love(able|rs)ov+匹配loveable或lovers,匹配一个或多个ov。

x{m},x{m,},x{m,n}

作用同x\{m\},x\{m,\},x\{m,n\}

4. POSIX字符类

为了在不同国家的字符编码中保持一至,POSIX(The Portable Operating System Interface)增加了特殊的字符类,如[:alnum:]是A-Za-z0-9的另一个写法。要把它们放到[]号内才能成为正则表达式,如[A- Za-z0-9]或[[:alnum:]]。在linux下的grep除fgrep外,都支持POSIX的字符类。

[:alnum:]

文字数字字符

[:alpha:]

文字字符

[:digit:]

数字字符

[:graph:]

非空字符(非空格、控制字符)

[:lower:]

小写字符

[:cntrl:]

控制字符

[:print:]

非空字符(包括空格)

[:punct:]

标点符号

[:space:]

所有空白字符(新行,空格,制表符)

[:upper:]

大写字符

[:xdigit:]

十六进制数字(0-9,a-f,A-F)

5. Grep命令选项

-?

同时显示匹配行上下的?行,如:grep -2 pattern filename同时显示匹配行的上下2行。

-b,--byte-offset

打印匹配行前面打印该行所在的块号码。

-c,--count

只打印匹配的行数,不显示匹配的内容。

-f File,--file=File

从文件中提取模板。空文件中包含0个模板,所以什么都不匹配。

-h,--no-filename

当搜索多个文件时,不显示匹配文件名前缀。

-i,--ignore-case

忽略大小写差别。

-q,--quiet

取消显示,只返回退出状态。0则表示找到了匹配的行。

-l,--files-with-matches

打印匹配模板的文件清单。

-L,--files-without-match

打印不匹配模板的文件清单。

-n,--line-number

在匹配的行前面打印行号。

-s,--silent

不显示关于不存在或者无法读取文件的错误信息。

-v,--revert-match

反检索,只显示不匹配的行。

-w,--word-regexp

如果被\<和\>引用,就把表达式做为一个单词搜索。

-V,--version

显示软件版本信息。

6. 实例

要用好grep这个工具,其实就是要写好正则表达式,所以这里不对grep的所有功能进行实例讲解,只列几个例子,讲解一个正则表达式的写法。

$ ls -l | grep '^a'

通过管道过滤ls -l输出的内容,只显示以a开头的行。

$ grep 'test' d*

显示所有以d开头的文件中包含test的行。

$ grep 'test' aa bb cc

显示在aa,bb,cc文件中匹配test的行。

$ grep '[a-z]\{5\}' aa

显示所有包含每个字符串至少有5个连续小写字符的字符串的行。

$ grep 'w\(es\)t.*\1' aa

如果west被匹配,则es就被存储到内存中,并标记为1,然后搜索任意个字符(.*),这些字符后面紧跟着另外一个es(\1),找到就显示该行。如果用egrep或grep -E,就不用"\"号进行转义,直接写成'w(es)t.*\1'就可以了。

posted @ 2006-05-23 17:31 易道 阅读(197) | 评论 (0)编辑 收藏

     摘要: 整理:Jims of 肥肥世家 <jims.yang@gmail.com> Copyrigh...  阅读全文
posted @ 2006-05-23 17:29 易道 阅读(305) | 评论 (0)编辑 收藏

作者:Jims of 肥肥世家

发布时间:2004年09月20日

最近更新:2005年12月22日,增加小技巧章节。


1. Sed简介

sed 是一种在线编辑器,它一次处理一行内容。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有改变,除非你使用重定向存储输出。Sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。以下介绍的是Gnu版本的Sed 3.02。

2. 定址

可以通过定址来定位你所希望编辑的行,该地址用数字构成,用逗号分隔的两个行数表示以这两行为起止的行的范围(包括行数表示的那两行)。如1,3表示1,2,3行,美元符号($)表示最后一行。范围可以通过数据,正则表达式或者二者结合的方式确定 。

3. Sed命令

调用sed命令有两种形式:

  • sed [options] 'command' file(s)

  • sed [options] -f scriptfile file(s)

a\

在当前行后面加入一行文本。

b lable

分支到脚本中带有标记的地方,如果分支不存在则分支到脚本的末尾。

c\

用新的文本改变本行的文本。

d

从模板块(Pattern space)位置删除行。

D

删除模板块的第一行。

i\

在当前行上面插入文本。

h

拷贝模板块的内容到内存中的缓冲区。

H

追加模板块的内容到内存中的缓冲区

g

获得内存缓冲区的内容,并替代当前模板块中的文本。

G

获得内存缓冲区的内容,并追加到当前模板块文本的后面。

l

列表不能打印字符的清单。

n

读取下一个输入行,用下一个命令处理新的行而不是用第一个命令。

N

追加下一个输入行到模板块后面并在二者间嵌入一个新行,改变当前行号码。

p

打印模板块的行。

P(大写)

打印模板块的第一行。

q

退出Sed。

r file

从file中读行。

t label

if分支,从最后一行开始,条件一旦满足或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。

T label

错误分支,从最后一行开始,一旦发生错误或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。

w file

写并追加模板块到file末尾。

W file

写并追加模板块的第一行到file末尾。

!

表示后面的命令对所有没有被选定的行发生作用。

s/re/string

用string替换正则表达式re。

=

打印当前行号码。

#

把注释扩展到下一个换行符以前。

以下的是替换标记
  • g表示行内全面替换。

  • p表示打印行。

  • w表示把行写入一个文件。

  • x表示互换模板块中的文本和缓冲区中的文本。

  • y表示把一个字符翻译为另外的字符(但是不用于正则表达式)

4. 选项

-e command, --expression=command

允许多台编辑。

-h, --help

打印帮助,并显示bug列表的地址。

-n, --quiet, --silent

取消默认输出。

-f, --filer=script-file

引导sed脚本文件名。

-V, --version

打印版本和版权信息。

5. 元字符集

^

锚定行的开始 如:/^sed/匹配所有以sed开头的行。

$

锚定行的结束 如:/sed$/匹配所有以sed结尾的行。

.

匹配一个非换行符的字符 如:/s.d/匹配s后接一个任意字符,然后是d。

*

匹配零或多个字符 如:/*sed/匹配所有模板是一个或多个空格后紧跟sed的行。

[]

匹配一个指定范围内的字符,如/[Ss]ed/匹配sed和Sed。

[^]

匹配一个不在指定范围内的字符,如:/[^A-RT-Z]ed/匹配不包含A-R和T-Z的一个字母开头,紧跟ed的行。

\(..\)

保存匹配的字符,如s/\(love\)able/\1rs,loveable被替换成lovers。

&

保存搜索字符用来替换其他字符,如s/love/**&**/,love这成**love**。

\<

锚定单词的开始,如:/\<love/匹配包含以love开头的单词的行。

\>

锚定单词的结束,如/love\>/匹配包含以love结尾的单词的行。

x\{m\}

重复字符x,m次,如:/0\{5\}/匹配包含5个o的行。

x\{m,\}

重复字符x,至少m次,如:/o\{5,\}/匹配至少有5个o的行。

x\{m,n\}

重复字符x,至少m次,不多于n次,如:/o\{5,10\}/匹配5--10个o的行。

6. 实例

删除:d命令
  • $ sed '2d' example-----删除example文件的第二行。

  • $ sed '2,$d' example-----删除example文件的第二行到末尾所有行。

  • $ sed '$d' example-----删除example文件的最后一行。

  • $ sed '/test/'d example-----删除example文件所有包含test的行。

替换:s命令
  • $ sed 's/test/mytest/g' example-----在整行范围内把test替换为mytest。如果没有g标记,则只有每行第一个匹配的test被替换成mytest。

  • $ sed -n 's/^test/mytest/p' example-----(-n)选项和p标志一起使用表示只打印那些发生替换的行。也就是说,如果某一行开头的test被替换成mytest,就打印它。

  • $ sed 's/^192.168.0.1/&localhost/' example-----&符号表示替换换字符串中被找到的部份。所有以192.168.0.1开头的行都会被替换成它自已加 localhost,变成192.168.0.1localhost。

  • $ sed -n 's/\(love\)able/\1rs/p' example-----love被标记为1,所有loveable会被替换成lovers,而且替换的行会被打印出来。

  • $ sed 's#10#100#g' example-----不论什么字符,紧跟着s命令的都被认为是新的分隔符,所以,“#”在这里是分隔符,代替了默认的“/”分隔符。表示把所有10替换成100。

选定行的范围:逗号
  • $ sed -n '/test/,/check/p' example-----所有在模板test和check所确定的范围内的行都被打印。

  • $ sed -n '5,/^test/p' example-----打印从第五行开始到第一个包含以test开始的行之间的所有行。

  • $ sed '/test/,/check/s/$/sed test/' example-----对于模板test和west之间的行,每行的末尾用字符串sed test替换。

多点编辑:e命令
  • $ sed -e '1,5d' -e 's/test/check/' example-----(-e)选项允许在同一行里执行多条命令。如例子所示,第一条命令删除1至5行,第二条命令用check替换test。命令的执行顺序对结果有影响。如果两个命令都是替换命令,那么第一个替换命令将影响第二个替换命令的结果。

  • $ sed --expression='s/test/check/' --expression='/love/d' example-----一个比-e更好的命令是--expression。它能给sed表达式赋值。

从文件读入:r命令
  • $ sed '/test/r file' example-----file里的内容被读进来,显示在与test匹配的行后面,如果匹配多行,则file的内容将显示在所有匹配行的下面。

写入文件:w命令
  • $ sed -n '/test/w file' example-----在example中所有包含test的行都被写入file里。

追加命令:a命令
  • $ sed '/^test/a\\--->this is a example' example<-----'this is a example'被追加到以test开头的行后面,sed要求命令a后面有一个反斜杠。

插入:i命令

$ sed '/test/i\\

new line

-------------------------' example

如果test被匹配,则把反斜杠后面的文本插入到匹配行的前面。

下一个:n命令
  • $ sed '/test/{ n; s/aa/bb/; }' example-----如果test被匹配,则移动到匹配行的下一行,替换这一行的aa,变为bb,并打印该行,然后继续。

变形:y命令
  • $ sed '1,10y/abcde/ABCDE/' example-----把1--10行内所有abcde转变为大写,注意,正则表达式元字符不能使用这个命令。

退出:q命令
  • $ sed '10q' example-----打印完第10行后,退出sed。

保持和获取:h命令和G命令
  • $ sed -e '/test/h' -e '$G example-----在sed处理文件的时候,每一行都被保存在一个叫模式空间的临时缓冲区中,除非行被删除或者输出被取消,否则所有被处理的行都将打印在屏幕上。接着模式空间被清空,并存入新的一行等待处理。在这个例子里,匹配test的行被找到后,将存入模式空间,h命令将其复制并存入一个称为保持缓存区的特殊缓冲区内。第二条语句的意思是,当到达最后一行后,G命令取出保持缓冲区的行,然后把它放回模式空间中,且追加到现在已经存在于模式空间中的行的末尾。在这个例子中就是追加到最后一行。简单来说,任何包含test的行都被复制并追加到该文件的末尾。

保持和互换:h命令和x命令
  • $ sed -e '/test/h' -e '/check/x' example -----互换模式空间和保持缓冲区的内容。也就是把包含test与check的行互换。

7. 脚本

Sed脚本是一个sed的命令清单,启动Sed时以-f选项引导脚本文件名。Sed对于脚本中输入的命令非常挑剔,在命令的末尾不能有任何空白或文本,如果在一行中有多个命令,要用分号分隔。以#开头的行为注释行,且不能跨行。

8. 小技巧

  • 在sed的命令行中引用shell变量时要使用双引号,而不是通常所用的单引号。下面是一个根据name变量的内容来删除named.conf文件中zone段的脚本:

    name='zone\ "localhost"'
    sed "/$name/,/};/d" named.conf 
    
posted @ 2006-05-23 17:28 易道 阅读(246) | 评论 (0)编辑 收藏

仅列出标题
共6页: 1 2 3 4 5 6