星期四, 四月 26, 2007

C语言 网络地址

在网络编程中,或多或少都会与网络地址打交道,我们从易到难,再来一个三层境界,菜鸟级,入门级,进阶级。下面一一说明。

菜鸟级:
菜鸟都知道,网络编程中要用到IP地址,IP地址是一个32为的整数,是一个unsigned int或unsigned long, 我编程中见到的int与long都是4个自己的!
将一个doted字符串IP地址转化为整型IP地址: unsigned long inet_addr( const char* cp ); 失败的化返回INADDR_NONE(0xffffffff) 竟然是一个广播地址,所以这个操作完成后,一定要检查返回值!
整形IP地址其实就是下面的结构(这个结构被称为最恶心的结构),你可以直接转过去:
typedef struct in_addr {
union {
struct {
u_char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
u_short s_w1,s_w2;
} S_un_w;
u_long S_addr;
} S_un;
} in_addr;
将一个整型IP地址转华为doted字符串IP地址: char* FAR inet_ntoa( struct in_addr in ); 你完全可以直接参数整数!这里要注意的仍然是返回值,这个值保存在被Socket实现内部的内存中,MSDN中说明Windows下,同一线程中下一次调用inet_ntoa将会破坏这个值,所以你可能需要保存这个返回值。这个内存也不需要释放。
一种可能的实现如下:
char FAR * inet_ntoa (struct in_addr in)
{
static char strRet[16];
// ...
return strRet;
}

以后请不要对这个指针的使用有太多的疑惑!
黄金规则: 所有被返回的地址都是网络地址(字节序从左到右)
Linux下的表中C库中,IP地址的操作与转化可以使用下面的函数,不多说了,请大家man!
     #include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int
inet_aton(const char *cp, struct in_addr *pin);

in_addr_t
inet_addr(const char *cp);

in_addr_t
inet_network(const char *cp);

char *
inet_ntoa(struct in_addr in);

const char *
inet_ntop(int af, const void * restrict src, char * restrict dst,
socklen_t size);

int
inet_pton(int af, const char * restrict src, void * restrict dst);

struct in_addr
inet_makeaddr(in_addr_t net, in_addr_t lna);

in_addr_t
inet_lnaof(struct in_addr in);

in_addr_t
inet_netof(struct in_addr in);

char *
inet_net_ntop(int af, const void *src, int bits, char *dst, size_t size);

int
inet_net_pton(int af, const char *src, void *dst, size_t size);
除了上面的两个函数外,Windows下不再有其他的inet_开头的兼容函数!

诸位可能要嚷嚷了,我老早以前就不是菜鸟了,好的!让我们马上进入下一个阶段!

入门级:
入门级的两个代表是函数是 struct HOSTENT* FAR gethostbyaddr( const char* addr, int len, int type ); 与 struct hostent* FAR gethostbyname( const char* name );
我们再来看另外一个重要的结构:
typedef struct hostent {
char FAR* h_name;
char FAR FAR** h_aliases;
short h_addrtype;
short h_length;
char FAR FAR** h_addr_list;
} hostent;

h_name
Official name of the host (PC). If using the DNS or similar resolution system, it is the Fully Qualified Domain Name (FQDN) that caused the server to return a reply. If using a local hosts file, it is the first entry after the IP address.
h_aliases
Null-terminated array of alternate names.
h_addrtype
Type of address being returned.
h_length
Length of each address, in bytes.
h_addr_list
Null-terminated list of addresses for the host. Addresses are returned in network byte order. The macro h_addr is defined to be h_addr_list[0] for compatibility with older software.
struct hostent* FAR gethostbyname( const char* name ) 这个函数的恶心之处在于在Windows下,输入的参数不能是doted字符串IP地址,只能时域名,要不然工作不正常.Linux没有这样的限制。
struct HOSTENT* FAR gethostbyaddr( const char* addr, int len, int type ) 的恶心之处在于参数过于复杂且相互影响,各参数描述如下
addr
[in] Pointer to an address in network byte order. (一个描述地址的结构转化为字符串指针)
len
[in] Length of the address, in bytes. (地址结构的长度,不同的长度代表不同的地址类型)
type
[in] Type of the address, such as the AF_INET address family type (defined as TCP, UDP, and other associated Internet protocols). Address family types and their corresponding values are defined in the Winsock2.h header file. (地址组类型,到现在位置,我页不知道到底有多少地址类型)
这三个恶心的参数导致了这个函数强大的扩展能力。
整形IP地址 unsigned long addr = inet_addr("192.168.1.39"); struct hostend* remoteHost = gethostbyaddr((char *) &addr, sizeof(unsigned long)(4), AF_INET);
sockaddr型的地址: struct sockaddr addr; struct hostend* remoteHost = gethostbyaddr((char *) &addr, sizeof(struct sockaddr)(16), addr.sa_family);

我们说道sockaddr结构了,他的定义如下:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
这个结构也不太稳定,容易变异. 如果是internet地址组,也就是sa_family==AF_INET的情况下,这个结构变成
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
上面的结构里出现了结构in_addr, 怎么,忘了,到菜鸟级再去看看!另外sockaddr类的结构中包含了应用成信息,有个端口号! hostent 结构里的 short h_addrtype; short h_length; char FAR FAR** h_addr_list; 直接受到gethostbyaddr的三个参数的控制!
下面还有几个我没有用过的,也不知道Linux下是否对应!
struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
u_long sin6_scope_id;
};
struct sockaddr_in6_old {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
};

struct in6_addr {
union {
u_char Byte[16];
u_short Word[8];
} u;
};
typedef struct _SOCKADDR_IRDA {
u_short irdaAddressFamily;
u_char irdaDeviceID[4];
char irdaServiceName[25];
} SOCKADDR_IRDA,
typedef struct sockaddr_storage {
short ss_family;
char __ss_pad1[_SS_PAD1SIZE];
__int64 __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
} SOCKADDR_STORAGE,
gethostbyname 和 gethostbyaddr在Windows下已经被deprecated,用getaddrinfo 代替!我们进入入第三个阶段来看看这个函数!

进阶级:
先来看看下面的函数与结构
int getaddrinfo( const TCHAR* nodename, const TCHAR* servname, const struct addrinfo* hints, struct addrinfo** res );
nodename
[in] Pointer to a null-terminated string containing a host (node) name or a numeric host address string. The numeric host address string is a dotted-decimal IPv4 address or an IPv6 hex address.
servname
[in] Pointer to a null-terminated string containing either a service name or port number.
hints
[in] Pointer to an addrinfo structure that provides hints about the type of socket the caller supports. See Remarks.
res
[out] Pointer to a linked list of one or more addrinfo structures containing response information about the host.
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
TCHAR* ai_canonname;
struct sockaddr* ai_addr;
struct addrinfo* ai_next;
} addrinfo;

ai_flags
Flags that indicate options used in the getaddrinfo function. See AI_PASSIVE, AI_CANONNAME, and AI_NUMERICHOST.
ai_family
Protocol family, such as PF_INET.
ai_socktype
Socket type, such as SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai_protocol
Protocol, such as IPPROTO_TCP or IPPROTO_UDP. For protocols other than IPv4 and IPv6, set ai_protocol to zero.
ai_addrlen
Length of the ai_addr member, in bytes.
ai_canonname
Canonical name for the host.
ai_addr
Pointer to a sockaddr structure.
ai_next
Pointer to the next structure in a linked list. This parameter is set to NULL in the last addrinfo structure of a linked list.

int getnameinfo( const struct sockaddr* sa, socklen_t salen, TCHAR* host, DWORD hostlen, TCHAR* serv, DWORD servlen, int flags );
sa
[in] Pointer to a socket address structure containing the address and port number of the socket. For IPv4, the sa parameter points to a sockaddr_in structure; for IPv6, the sa parameter points to a sockaddr_in6 structure.
salen
[in] Length of the structure pointed to in the sa parameter, in bytes.
host
[out] Pointer to the host name. The host name is returned as a Fully Qualified Domain Name (FQDN) by default.
hostlen
[in] Length of the buffer pointed to by the host parameter, in bytes. The caller must provide a buffer large enough to hold the host name, including terminating NULL characters. A value of zero indicates the caller does not want to receive the string provided in host.
serv
[out] Pointer to the service name associated with the port number.
servlen
[in] Length of the buffer pointed to by the serv parameter, in bytes. The caller must provide a buffer large enough to hold the service name, including terminating null characters. A value of zero indicates the caller does not want to receive the string provided in serv.
flags
[in] Flags used to customize processing of the getnameinfo function. See Remarks.



void freeaddrinfo( struct addrinfo* ai );
ai
[in] Pointer to the addrinfo structure or linked list of addrinfo structures to be freed. All dynamic storage pointed to within the addrinfo structure(s) is also freed.
----------------------------------------------------------
...

//--------------------------------
// Declare and initialize variables.
char* ip = "127.0.0.1";
char* port = "27015";
struct addrinfo aiHints;
struct addrinfo *aiList = NULL;
int retVal;

//--------------------------------
// Setup the hints address info structure
// which is passed to the getaddrinfo() function
memset(&aiHints, 0, sizeof(aiHints));
aiHints.ai_family = AF_INET;
aiHints.ai_socktype = SOCK_STREAM;
aiHints.ai_protocol = IPPROTO_TCP;

//--------------------------------
// Call getaddrinfo(). If the call succeeds,
// the aiList variable will hold a linked list
// of addrinfo structures containing response
// information about the host
if ((retVal = getaddrinfo(ip, port, &aiHints, &aiList)) != 0) {
printf("getaddrinfo() failed.n");
}
------------------------------------------------------------
...
//-----------------------------------------
// Declare and initialize variables
struct sockaddr_in saGNI;
char hostName[256];
char servInfo[256];
u_short port;
port = 27015;

//-----------------------------------------
// Set up sockaddr_in structure which is passed
// to the getnameinfo function
saGNI.sin_family = AF_INET;
saGNI.sin_addr.s_addr = inet_addr(ip);
saGNI.sin_port = htons(port);

//-----------------------------------------
// Call getnameinfo
if ((retVal = getnameinfo((SOCKADDR *)&saGNI,
sizeof(sockaddr),
hostName,
256,
servInfo,
256,
NI_NUMERICSERV)) != 0) {
printf("getnameinfo() failed.n");
printf("Error #: %ldn", WSAGetLastError());
}

这几个函数就比较高级,与前面两级的的函数相比,已经上升到应用而不是停留在网络,而且用户自主管理内存,这样的好处是支持可重入的并发使用,到了这个阶段,也不需要多说了,手册就在那里,自己看了!
下面在在第一二阶段的函数基础上对getaddrinfo的简单实现!作为对此文的总结!
int
fake_getaddrinfo(const char *hostname, const char* strport, const struct addrinfo* hints,struct addrinfo **ai)
{
struct hostent *he;
int addr = inet_addr(hostname);
struct sockaddr_in *saddr;
*ai = (struct addrinfo *)malloc(sizeof(struct addrinfo));
/*
The gethostbyname function cannot resolve IP address strings passed to it.
Such a request is treated exactly as if an unknown host name were passed.
Use inet_addr to convert an IP address string the string to an actual IP address,
then use another function, gethostbyaddr, to obtain the contents of the hostent structure.
*/
if(addr==INADDR_NONE)
{
he = gethostbyname(hostname);
if (!he)
return (-1);
ai->ai_family = hints->ai_family;
ai->ai_socktype = hints->ai_socktype; //SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai->ai_protocol = hints->ai_proocol; //IPPROTO_TCP or IPPROTO_UDP or 0
ai->ai_canonname = hostname;
ai->ai_addrlen = sizeof(struct sockaddr);
if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen)))
return (-1);
saddr = (struct sockaddr_in*)ai->ai_addr;
memset(saddr,0,sizeof(struct sockaddr_in));
saddr->sin_family = AF_INET;
saddr->sin_port = atoi(strport);
memcpy(&saddr->sin_addr, he->h_addr, sizeof(struct in_addr));
ai->ai_next = NULL;
}else
{
he = gethostbyaddr(&addr, sizeof(int), AF_INET );
if (!he)
return (-1);
ai->ai_family = he->h_addrtype;
ai->ai_socktype = hints->ai_socktype; //SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai->ai_protocol = hints->ai_proocol; //IPPROTO_TCP or IPPROTO_UDP or 0
ai->ai_canonname = he->h_name;
ai->ai_addrlen = sizeof(struct sockaddr);
if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen)))
return (-1);
saddr = (struct sockaddr_in*)ai->ai_addr;
memset(saddr,0,sizeof(struct sockaddr_in));
saddr->sin_family = AF_INET;
saddr->sin_port = atoi(strport);
memcpy(&saddr->sin_addr, he->h_addr, sizeof(struct in_addr));
ai->ai_next = NULL;
}
return (0);
}

void fake_freeaddrinfo(struct addrinfo *ai)
{
free(ai->ai_addr);
free(ai);
}

LSP and Winsock

Komodia.com is the leading market expert in the field of LSP (Layered Service Provider) and winsock programming.

星期四, 四月 19, 2007

Linux 系统裁减指南(LiPS)

Linux 系统裁减指南(LiPS)


[本篇全文] [回复本文] [本篇作者: dot ]
发信人: dot (爱吃萝卜和青菜), 信区: Embedded
标 题: Linux 系统裁减指南(LiPS)
发信站: 武汉白云黄鹤站 (2004年10月11日15:04:22 星期一), 站内信件

重要文档,吐血推荐!

《Linux 系统裁减指南 (LiPS)》 【原创】
(LiPS: Linux Powered System)

华中科技大学信息存储系统实验室
产品版本
密级
项目名称
项目ID
共 46 页
文档编号

NAS 项目
LiPS 详细设计说明书
————————————
(Linux 系统裁减指南)
一种创建小型化Linux系统的方法

拟制:
董晓明
日期:
2003-6-17
评审:
日期:
批准:
日期:
yyyy/mm/dd
签发:
日期:
yyyy/mm/dd
华中科技大学信息存储系统国家专业实验室
版权所有 不得复制
修 订 记 录
日期
修订版本
描述
作者
2003-6-17
0.1
初稿,文档结构,USB引导盘制作步骤。
董晓明
2003-8-19
0.9
完成了文档的主要内容。
董晓明
2003-10-15
参考[9]对文档中进行了完善和补充,修正了一些不准确的描述。本文档的内容组织
跟该书的结构不谋而合啊。
董晓明
2003-10-24
补充了init,PAM和再论initrd节。
董晓明
2003-12-9
增加有关远程串口控制台的内容。
董晓明



目 录
1 引言 6
1.1 编写目的 6
1.2 背景 6
1.3 定义 6
1.4 参考资料 6
2 总体描述 8
2.1 硬件环境 8
2.2 软件环境 8
2.3 需求规定和约束 8
3 总体设计 9
3.1 基本设计概念 9
3.2 软件结构 9
4 内核 10
4.1 创建内核的方法 10
4.2 内核配置 10
4.2.1 概述 10
4.2.2 Loadable module support 11
4.2.3 Processor type and features 11
4.2.4 General setup 12
4.2.5 Plug and Play configuration 12
4.2.6 Block devices 12
4.2.7 Multi-device support (RAID and LVM) 13
4.2.8 Networking options 13
4.2.9 ATA/IDE/MFM/RLL support 14
4.2.10 SCSI support 15
4.2.11 Network device support 15
4.2.12 Character devices 16
4.2.13 File systems 17
4.2.14 Console drivers 18
4.2.15 USB support 18
4.3 提示 19
4.3.1 EXTRAVERSION 19
4.3.2 .config 19
5 根文件系统 21
5.1 根文件系统 21
5.2 文件系统的内容 21
5.2.1 应用程序(applications) 21
5.2.2 设备文件(device files) 21
5.2.3 脚本和配置文件(scripts and configuration files) 22
5.2.4 库文件(libraries) 23
5.2.5 必要的目录 24
5.3 初始化RAM盘(initrd) 24
5.3.1 操作步骤 24
5.3.2 引导选项 24
5.3.3 安装 25
5.3.4 改变根目录设备 26
5.3.5 使用场合 27
5.3.6 淘汰的根转换机制(change_root) 27
5.4 系统初始化(init) 27
5.4.1 init的任务 28
5.4.2 SysVinit 28
5.4.3 两种风格:Slackware vs. Debian 28
5.5 PAM 29
5.5.1 什么是PAM 29
5.5.2 PAM的结构 29
5.5.3 配置文件 29
5.5.4 other 29
5.6 再论initrd 30
5.6.1 根到底在哪里 31
5.6.2 linuxrc 31
5.6.3 根转换机制:新的?旧的? 32
5.7 提示 34
5.7.1 文件系统目录层次结构 34
5.7.2 mklips.sh(制作LiPS的脚本) 34
5.7.3 lips.conf(LiPS配置文件) 34
5.7.4 cp –dpR 35
5.7.5 如何判断一个程序依赖哪些文件 35
5.7.6 带库文件复制——lcp() 35
5.7.7 库文件的命名 36
6 引导 38
6.1 引导过程概述 38
6.2 几种引导方式 38
6.3 grub引导器 38
6.4 USB引导 38
6.4.1 制作步骤 39
6.5 提示 40
6.5.1 版本问题? 40
7 远程串口控制台 41
7.1 概述 41
7.2 配置方法 41
7.3 提示 41
7.4 Linux 远程串口控制台指南 41
8 系统安装光盘 44
8.1 概述 44
8.2 boot.iso光盘的文件结构分析 44
8.3 制作步骤 45
9 源代码(配置文件和脚本) 46
9.1 lips.conf 46
9.2 mklips.sh 46



1 引言

1.1 编写目的
说明Linux系统裁减的过程及解决的主要技术问题。本文档的编写是为了说明L
iPS的详细设计过程,也可以作为构造一个小型化Linux系统的指南。
本文档内容的组织如下所述:这一部分是引言,第2部分是对项目软、硬件环境
和需求的总体描述,第3部分说明了裁减LINUX的相关基本概念。后面几个部分是具
体实现方法的详细说明,4~6分别详细描述了创建内核、创建根文件系统和引导系
统的方法。最后的内容可以看作附录,其中7说明了一般Linux系统安装光盘的创建
方法,8列出了相关的配置文件和源代码。
1.2 背景
华中科技大学计算机存储系统国家专业实验室,NAS项目(2003.3——2003.8)

1.3 定义
O boot loader: 引导器
O kernel: 内核
O root filesystem: 根文件系统
O USB: 通用串行总线
O
1.4 参考资料
[1] DIY:自己动手做一个迷你 Linux 系统. IBM developerWorks 中国网站. htt
p://www-900.ibm.com/developerWorks/cn/linux/embed/diy/index.shtml
[2] Linux From Scratch. http://clfsbook.mylab.idv.tw/index.html
[3] Linux启动过程综述. http://www.linuxaid.com.cn/forum/showdoc.jsp?l=1&
i=15750
[4] The Linux Bootdisk HOWTO (中译版). http://www.linux.org.tw/CLDP/gb/B
ootdisk-HOWTO.html
[5] Using the initial RAM disk (initrd). /usr/src/linux-2.4/Documentatio
n/initrd.txt. (译文:http://zope.slat.org/Project/3ybaby/discuss-area/c
d-boot/cdboot_refer/initrd.htm)
[6] Quickmouse于2003.1发表于白云黄鹤BBS (http://bbs.whnet.edu.cn Linux讨
论区的帖子“裁减Linux”。
[7] Filesystem Hierarchy Standard. http://www.pathname.com/fhs/pub/fhs-2
.2.pdf
[8] LINUX ALLOCATED DEVICES. /usr/src/linux-2.4/Documentation/device.txt
. 或者最新的列表在http://www.kernel.org/pub/linux/docs/device-list/
[9] Karim Yaghmour. Building Embedded Linux Systems. O'Reilly, April 200
3.
[10] Alessandro Rubini. Take Command: Init. http://www.linux.it/kerneldo
cs/init/ , November 1998.
[11] 深入 Linux PAM 体系结构. IBM developerWorks 中国网站. http://www-90
0.ibm.com/developerWorks/cn/linux/l-pam/index.shtml . 2003.7.
[12]




2 总体描述

2.1 硬件环境
开发主机:
Intel 845芯片组主板,P4赛扬1.8GHz CPU,256M内存,60G IDE硬盘,RTL813
9网卡。
目标机:
与主机同构。
2.2 软件环境
开发主机:
Redhat Linux 7.3 (kernel 2.4.18-3),或者Redhat Linux 9 (kernel 2.4.2
0-8)。可以根据需要使用其他版本的内核,比如Redhat 7.3+kernel 2.4.20。
目标机:
LiPS,以及应用程序。
2.3 需求规定和约束
我们目标系统LiPS,即Linux Powered Storage是一个嵌入式Linux操作系统,
主要着眼于提供各种文件服务,不包括X GUI;并且足够小,可以装入32M USB盘或
者DOC/DOM等常用设备引导。




3 总体设计

3.1 基本设计概念
提起“裁减Linux”,最容易想到的方法是以一个已经安装好的系统为基础,删
除掉不需要的文件,以减小整个系统的尺寸。而事实上这个方法几乎是行不通的,
因为通过光盘安装的一个Redhat 9 Linux系统,即便是只选择了比较少的软件包,
其所占空间也可以轻松达到300~500MB,想在这个基础上裁减到30MB左右是比较困
难的。更困难的是确定哪些东西是可以删除的?如果删除了系统必须的文件,可能
导致系统不能引导。
另一个途径是从零开始根据需要构建整个系统。“Linux From Scratch”采用
的就是这种方式,一个个安装需要的软件包。类似的,更简单的方法是,安装一个
完整的Linux系统,然后将必要的软件(可执行程序)、配置文件、库文件、内核等
复制出来构建一个小系统。我们采取后一种方式。

3.2 软件结构
LiPS包括3部分:Linux内核,根文件系统和引导器。
内核(Kernel)提供了一个操作系统的基本功能,如内存管理、进程调度、文
件系统、网络等,以及设备驱动程序。
根文件系统(Root filesystem)是存放运行、维护系统所必须的各种工具软件
、库文件、脚本、配置文件和其他特殊文件(比如设备节点)的地方,也可以安装
各种软件包。通常根文件系统位于某个磁盘分区,而在LiPS中应用了initrd(初始
RAM盘)机制,将根文件系统放在RAM Disk中。
引导器(Boot Loader)的任务是从引导设备装载内核,引导系统运行。Linux
系统常见的引导器包括早期的LILO和近期的GRUB。LiPS用的是GRUB。




4 内核

4.1 创建内核的方法
Linux提供了方便灵活的方法来定制内核,主要步骤包括:下载安装内核源代码
树,配置内核,编译内核。具体方法请参考相关文档。关键指令如下:
cd /usr/src/linux-2.4
make menuconfig
make dep; make clean; make bzImage
编译成功的内核文件为arch/i386/boot/bzImage.
4.2 内核配置
4.2.1 概述
make menuconfig之后可以看到类似如下的内容(编号是为了描述方便而加上的
)。根据内核版本的不同,列出的项目会有差异。而变化主要发生在发行版为标准
内核打的各种补丁上面,其实标准内核是比较稳定的。
如下所示,我们配置的内核版本为2.4.20-8,这个“-8”就是Redhat所打的补
丁版本号。补丁一般是对标准内核错误的修正和新增的功能。与2.4.20内核配置选
项比较会发现,[10,25,30]都属于Redhat的补丁。
一般地,这些补丁选项都不是必须的,因此我们的LiPS内核中都没有包括这些
选项。其实,完全可以通过直接配置一个标准的内核版本(比如2.4.20)来产生我
们所需要的内核。
Linux Kernel v2.4.20-8-R2Lips Configuration
[1] Code maturity level options --->
[2] Loadable module support --->
[3] Processor type and features --->
[4] General setup --->
[5] Memory Technology Devices (MTD) --->
[6] Parallel port support --->
[7] Plug and Play configuration --->
[8] Block devices --->
[9] Multi-device support (RAID and LVM) --->
[10] Cryptography support (CryptoAPI) --->
[11] Networking options --->
[12] Telephony Support --->
[13] ATA/IDE/MFM/RLL support --->
[14] SCSI support --->
[15] Fusion MPT device support --->
[16] I2O device support --->
[17] Network device support --->
[18] Amateur Radio support --->
[19] IrDA (infrared) support --->
[20] ISDN subsystem --->
[21] Old CD-ROM drivers (not SCSI, not IDE) --->
[22] Input core support --->
[23] Character devices --->
[24] Multimedia devices --->
[25] Crypto Hardware support --->
[26] File systems --->
[27] Console drivers --->
[28] Sound --->
[29] USB support --->
[30] Additional device driver support --->
[31] Bluetooth support --->
[32] Kernel hacking --->
[33] Library routines --->
Load an Alternate Configuration File
Save Configuration to an Alternate File
<Select> < Exit > < Help >
下面我们以2.4.20内核为例,来详细说明内核的配置,也就是选哪些东西、不
选哪些东西。没有说明的选项,比如“Code maturity level options”,表示不选

4.2.2 Loadable module support
Enable loadable module support
[ ] Set version information on all module symbols
Kernel module loader
可加载模块支持。虽然目前所选项目全部编译进内核,但是内核是支持模块的
。可以考虑将一些硬件设备的驱动程序编译为模块,从而减小内核尺寸,同时增加
适应不同硬件平台的灵活性。
4.2.3 Processor type and features
(Pentium-III/Celeron(Coppermine)) Processor family
Machine Check Exception
< > Toshiba Laptop support
< > Dell laptop support
< > /dev/cpu/microcode - Intel IA32 CPU microcode support
< > /dev/cpu/*/msr - Model-specific register support
< > /dev/cpu/*/cpuid - CPU information support
(off) High Memory Support
[ ] Math emulation
[ ] MTRR (Memory Type Range Register) support
[ ] Symmetric multi-processing support
[ ] Local APIC support on uniprocessors
[ ] Unsynced TSC support
处理器特性。选择一下Processor family,其余基本上都没有选。High Memor
y Support也可以打开,不清楚有什么用。
4.2.4 General setup
Networking support
PCI support
(Any) PCI access mode
ISA bus support
PCI device name database
[ ] EISA support
[ ] MCA support
[ ] Support for hot-pluggable devices
System V IPC
BSD Process Accounting
Sysctl support
(ELF) Kernel core (/proc/kcore) format
< > Kernel support for a.out binaries
<*> Kernel support for ELF binaries
< > Kernel support for MISC binaries
Power Management support
< > Kernel support for MISC binaries
Power Management support
<*> Advanced Power Management BIOS support
Ignore USER SUSPEND
Enable PM at boot time
Make CPU Idle calls when idle
Enable console blanking using APM
RTC stores time in GMT
Allow interrupts during APM BIOS calls
Use real mode APM BIOS call to power off
总体设置。这一部分选项比较重要,包括网络、总线、进程、二进制文件格式
、电源管理等。其中APM相关的选项比较搞不清楚,有时候halt –p命令不能关闭系
统电源跟这里有关系。
4.2.5 Plug and Play configuration
<*> Plug and Play support
< > ISA Plug and Play support
即插即用设置。
4.2.6 Block devices
<*> Normal floppy disk support
< > XT hard disk support
< > Compaq SMART2 support
< > Compaq Smart Array 5xxx support
< > Mylex DAC960/DAC1100 PCI RAID Controller support
<*> Loopback device support
< > Network block device support
<*> RAM disk support
(4096) Default RAM disk size
Initial RAM disk (initrd) support
Per partition statistics in /proc/partitions
块设备。软驱也可以不选。这里的关键选项是Loopback device,RAM disk和i
nitrd,一定要选中。Default RAM disk size多少都没有关系。
4.2.7 Multi-device support (RAID and LVM)
Multiple devices driver support (RAID and LVM)
<*> RAID support
<*> Linear (append) mode
<*> RAID-0 (striping) mode
<*> RAID-1 (mirroring) mode
<*> RAID-4/RAID-5 mode
<*> Multipath I/O support
<*> Logical volume manager (LVM) support
MD支持。软RAID和LVM的支持在这里选择,如果不使用,也可以不选。
4.2.8 Networking options
<*> Packet socket
Packet socket: mmapped IO
< > Netlink device emulation
Network packet filtering (replaces ipchains)
[ ] Network packet filtering debugging
Socket Filtering
<*> Unix domain sockets
TCP/IP networking
IP: multicasting
IP: advanced router
[ ] IP: policy routing
[ ] IP: equal cost multipath
[ ] IP: use TOS value as routing key
[ ] IP: verbose route monitoring
[ ] IP: large routing tables
IP: kernel level autoconfiguration
IP: DHCP support
IP: BOOTP support
IP: RARP support
<*> IP: tunneling
< > IP: GRE tunnels over IP
[ ] IP: multicast routing
[ ] IP: TCP Explicit Congestion Notification support
[ ] IP: TCP syncookie support (disabled per default)
IP: Netfilter Configuration --->
< > 802.1Q VLAN Support
---
< > The IPX protocol
< > Appletalk protocol support
Appletalk devices --->
< > DECnet Support
< > 802.1d Ethernet Bridging
QoS and/or fair queueing --->
Network testing --->
网络选项。这里选项也比较多,如果没有什么特殊要求,又不是十分理解这些
选项的意义,基本上可以用缺省设置。注意DHCP选项,如果要用到的话要记得选中
这个。
IP: Netfilter Configuration(跟iptables有关)等几个子选项都没有选。
4.2.9 ATA/IDE/MFM/RLL support
<*> ATA/IDE/MFM/RLL support
IDE, ATA and ATAPI Block devices --->
<*> Enhanced IDE/MFM/RLL disk/cdrom/tape/floppy support
--- Please see Documentation/ide.txt for help/info on IDE drives
[ ] Use old disk-only driver on primary interface
<*> Include IDE/ATA-2 DISK support
Use multi-mode by default
Auto-Geometry Resizing support
<*> Include IDE/ATAPI CDROM support
< > Include IDE/ATAPI TAPE support
< > Include IDE/ATAPI FLOPPY support
<*> SCSI emulation support
[ ] IDE Taskfile Access
--- IDE chipset support/bugfixes
[ ] CMD640 chipset bugfix/support
[ ] RZ1000 chipset bugfix/support
Generic PCI IDE chipset support
Sharing PCI IDE interrupts support
Generic PCI bus-master DMA support
[ ] Boot off-board chipsets first support
[ ] Force enable legacy 2.0.X HOSTS to use DMA
Use PCI DMA by default when available
[ ] Enable DMA only for disks
[ ] AEC62XX chipset support
[ ] ALI M15x3 chipset support
[ ] AMD Viper support
[ ] CMD64X and CMD680 chipset support
[ ] CY82C693 chipset support
[ ] Cyrix CS5530 MediaGX chipset support
[ ] HPT34X chipset support
[ ] HPT366/368/370 chipset support
Intel PIIXn chipsets support
PIIXn Tuning support
[ ] PROMISE PDC202{46|62|65|67|68|69|70} support
[ ] ServerWorks OSB4/CSB5 chipsets support
[ ] SiS5513 chipset support
[ ] SLC90E66 chipset support
[ ] VIA82CXXX chipset support
[ ] Other IDE chipset support
[ ] IGNORE word93 Validation BITS
IDE设备支持。要用IDE硬盘,当然要选中。CDROM选项看情况也可以不要。SCS
I emulation似乎对使用USB盘有影响。剩下的很多都是跟IDE chipset有关的。
最好是用dmesg命令找出目标系统主板芯片组,然后选中对应的芯片组支持,这
样相当于安装了IDE芯片组的驱动程序。如果没有指定应该也可以,只是性能可能会
受到一些影响。
4.2.10 SCSI support
<*> SCSI support
--- SCSI support type (disk, tape, CD-ROM)
<*> SCSI disk support
(40) Maximum number of SCSI disks that can be loaded as modules
< > SCSI tape support
< > SCSI OnStream SC-x0 tape support
< > SCSI CD-ROM support
<*> SCSI generic support
--- Some SCSI devices (e.g. CD jukebox) support multiple LUNs
[ ] Enable extra checks in new queueing code
[ ] Probe all LUNs on each SCSI device
[ ] Verbose SCSI error reporting (kernel size +=12K)
[ ] SCSI logging facility
SCSI low-level drivers --->
SCSI支持。如果要用到SCSI硬盘等SCSI设备,这里当然要选。目前我们的目标
系统其实没有SCSI硬盘,但是为了使用USB盘,也要选中SCSI支持。
SCSI low-level drivers子选项中是一些SCSI卡,都不用选。
4.2.11 Network device support
Network device support
ARCnet devices --->
< > Dummy net driver support
< > Bonding driver support
< > EQL (serial line load balancing) support
< > Universal TUN/TAP device driver support
Ethernet (10 or 100Mbit) --->
Ethernet (10 or 100Mbit)
EISA, VLB, PCI and on board controllers
<*> EtherExpressPro/100 support (eepro100, original Becker driv
er
<*> RealTek RTL-8139 PCI Fast Ethernet Adapter support
Ethernet (1000 Mbit) --->
[ ] FDDI driver support
< > PPP (point-to-point protocol) support
< > SLIP (serial line) support
Wireless LAN (non-hamradio) --->
Token Ring devices --->
[ ] Fibre Channel driver support
Wan interfaces --->
网络设备支持。主要选择网卡类型,Ethernet (10 or 100Mbit)子选项中列出
了内核可以支持的10/100M以太网卡,这里仅列出了我们选中的2种。其他子选项都
没有选。
4.2.12 Character devices
Virtual terminal
Support for console on virtual terminal
<*> Standard/generic (8250/16550 and compatible UARTs) serial suppo
rt
Support for console on serial port
[ ] Extended dumb serial driver options
[ ] Non-standard serial port support
[ ] Unix98 PTY support
I2C support --->
Mice --->
Joysticks --->
< > QIC-02 tape support
Watchdog Cards --->
< > AMD 768 Random Number Generator support
< > Intel i8x0 Random Number Generator support
< > AMD 76x native power management (Experimental)
< > /dev/nvram support
< > Enhanced Real Time Clock Support
< > Double Talk PC internal speech card support
< > Siemens R3964 line discipline
< > Applicom intelligent fieldbus card support
Ftape, the floppy tape device driver --->
< > /dev/agpgart (AGP Support)
[ ] Direct Rendering Manager (XFree86 DRI support)
< > ACP Modem (Mwave) support
字符设备。这里主要就是选择了最前面4个跟终端和串口有关的选项,以支持虚
拟终端(远程登录要用到),支持标准串口,支持串口控制台。
4.2.13 File systems
Quota support
< > Kernel automounter support
<*> Kernel automounter version 4 support (also supports v3)
<*> Reiserfs support
[ ] Enable reiserfs debug mode
Stats in /proc/fs/reiserfs
<*> Ext3 journalling file system support
[ ] JBD (ext3) debugging support
<*> DOS FAT fs support
<*> MSDOS fs support
< > UMSDOS: Unix-like file system on top of standard MSDOS fs
<*> VFAT (Windows-95) fs support
< > Compressed ROM file system support
Virtual memory file system support (former shm fs)
<*> ISO 9660 CDROM file system support
Microsoft Joliet CDROM extensions
[ ] Transparent decompression extension
< > JFS filesystem support
< > Minix fs support
< > FreeVxFS file system support (VERITAS VxFS(TM) compatible)
< > NTFS file system support (read only)
< > OS/2 HPFS file system support
/proc file system support
< > QNX4 file system support (read only)
< > ROM file system support
<*> Second extended fs support
< > System V/Xenix/V7/Coherent file system support
< > UDF file system support (read only)
< > UFS file system support (read only)
Network File Systems --->
< > Coda file system support (advanced network fs)
<*> NFS file system support
Provide NFSv3 client support
[ ] Root file system on NFS
<*> NFS server support
Provide NFSv3 server support
<*> SMB file system support (to mount Windows shares etc.)
[ ] Use a default NLS
< > NCP file system support (to mount NetWare volumes)
Partition Types --->
Native Language Support --->
Default NLS Option: "iso8859-1"
<*> Codepage 437 (United States, Canada)
<*> Simplified Chinese charset (CP936, GB2312)
<*> Traditional Chinese charset (Big5)
文件系统。这部分内容比较多,也很重要。首先是Quota,如果目标系统支持这
个功能,这里就要选中。然后是选择要支持的文件系统,常用的有Reiserfs,Ext3
,FAT,ISO9660,JFS等等,可以根据情况选择。/proc和ext2一定要选中。
还有Network File Systems子选项,可以选择是否支持NFS和SMB。Native Lan
guage Support子选项跟所支持的语言有关,这里没有全部列出来,我们选中了英语
和简体、繁体中文字符集。
4.2.14 Console drivers
VGA text console
[ ] Video mode selection support
控制台驱动。调试的时候可以给目标系统接显示器看一下,实际系统应该可以
不选。
4.2.15 USB support
<*> Support for USB
[ ] USB verbose debug messages
--- Miscellaneous USB options
Preliminary USB device filesystem
[ ] Long timeout for slow-responding devices (some MGE Ellipse UP
Se
--- USB Host Controller Drivers
<*> UHCI Alternate Driver (JE) support
<*> OHCI (Compaq, iMacs, OPTi, SiS, ALi, ...) support
--- USB Device Class drivers
< > USB MIDI support
<*> USB Mass Storage support
USB Mass Storage verbose debug
[ ] Freecom USB/ATAPI Bridge support
[ ] ISD-200 USB/ATA Bridge support
[ ] Microtech CompactFlash/SmartMedia support
< > USB Modem (CDC ACM) support
< > USB Printer support
--- USB Human Interface Devices (HID)
<*> USB Human Interface Device (full HID) support
--- Input core support is needed for USB HID input layer or HID
BP
[ ] /dev/hiddev raw HID device support
--- USB Imaging devices
< > USB Kodak DC-2xx Camera support
< > USB Scanner support
< > Microtek X6USB scanner support
--- USB Multimedia devices
--- Video4Linux support is needed for USB Multimedia device suppo
rt
--- USB Network adaptors
--- USB port drivers
USB Serial Converter support --->
--- USB Miscellaneous drivers
< > Texas Instruments Graph Link USB (aka SilverLink) cable suppo
rt
< > USB LCD device support
USB支持。如果没有使用USB设备,这部分可以不选。要使用USB盘,一定要选中
USB Mass Storage support。其余很多选项我也不清楚是干什么用的。
4.3 提示
4.3.1 EXTRAVERSION
在系统中使用uname可以获得系统版本信息。比如Redhat 9:
[root@lips root]# uname -r
2.4.20-8
[root@lips root]#
返回结果表示了Redhat修订后的内核版本号。前面我们提到过,这个“-8”就
是Redhat附加在标准内核版本号后面的一个信息。
在一个运行LiPS的系统中,我们也需要确定当前使用的内核版本,通过版本信
息来知道这个内核的配置,也可以通过在标准内核版本后面附加特定的编码来实现

/usr/src/linux-2.4/Makefile中的变量EXTRAVERSION可以帮助我们达到这个目
标。下面是Makefile文件的最前面几行内容:
VERSION = 2
PATCHLEVEL = 4
SUBLEVEL = 20
EXTRAVERSION = -8
KERNELRELEASE=$(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)
现在KERNELRELEASE的值就是2.4.20-8。这个信息会被编译到内核中,uname工
具会从内核文件中提取出版本号。如果将Makefile做一些修改,比如,
EXTRAVERSION = -8-R2Lips
那么我们可以得到内核版本号2.4.20-8-R2Lips,表示为了创建LiPS而编译的一
个特定配置的内核,这个内核是以2.4.20-8为基础,R2Lips(Release 2 Lips)即
第二个版本。
对Makefile的修改要在编译内核之前完成。
4.3.2 .config
内核配置(make menuconfig)完成之后,会询问:
Do you wish to save your new kernel configuration?
< Yes > < No >
回答Yes,配置信息会保存在/usr/src/linux-2.4/.config文件中。注意以“.
”开头的文件是个隐藏文件,要用ls –a才看的到。.config是个文本文件,内容类
似下面:
#
# Loadable module support
#
CONFIG_MODULES=y
# CONFIG_MODVERSIONS is not set
CONFIG_KMOD=y
所以可以把这个文件保存起来,并且给文件名带上版本号,方便管理、使用。

cp .config ~/config-2.4.20-8-R2Lips




5 根文件系统

创建根文件系统比编译内核要复杂的多,也更难理解。这里的关键是掌握init
rd(初始化RAM盘)的使用方法。
5.1 根文件系统
这里我们将要创建的根文件系统与通常Linux主机的根文件系统类似,只是它应
该仅仅包括系统运行所必须的应用程序、库和相关文件的最小集合。根文件系统的
尺寸大小是一个重要的指标。
5.2 文件系统的内容
5.2.1 应用程序(applications)
/bin,/sbin,/usr/bin,/usr/sbin
应用程序大致可以分为3部分,第一是操作系统正常运行所需的基本工具软件,
比如bash,cp,rm等;第二是提供某项服务的服务器软件,比如httpd,telnetd,
proftpd等;第三是我们所开发的应用程序。其实后面讲的配置文件等也可以粗略按
照这个原则来分类。
到底需要复制哪些软件,弹性是比较大的,很多软件都是可要可不要,可以实
际情况灵活选择。这个列表可能很长,并且变化也比较大,因此不在这里列出。
为了进一步减小所创建的根文件系统的尺寸,可以考虑使用下列工具包软件来
替代某些标准的工具:
O BusyBox(http://www.busybox.net)
O TinyLogin(http://tinylogin.busybox.net)
O Embutils(http://www.fefe.de/embutils/)
其详细使用方法请参考相关资料,此处不再赘述。LIPS的实现目前没有使用这
些软件包。
5.2.2 设备文件(device files)
/dev
设备文件也可以称作设备节点(device node)。设备文件非常重要,缺少某些
有些设备文件可能导致系统不能正常运行甚至不能引导。有些设备文件是必须的,
而更多的是根据具体目标系统的硬件配置来进行取舍。
比如硬盘的设备文件,在完整的系统中一般有hda,hdb,……,hdt,即最多支
持20个IDE硬盘,每个硬盘有hdX1,hdX2,……,hdX32,(其中X表示a-t),即支
持32个分区,另外还有表示SCSI硬盘的节点。根据实际情况,如果只需要支持少量
的硬盘、少量的分区,这些节点可以被大大简化。如果目标系统中没有的设备,其
对应的设备文件也可以省掉。
设备文件
描述
/dev/console
系统控制台设备,非常重要。
/dev/fd0
第一个软驱
/dev/hda
/dev/hda[1-8]
IDE硬盘及分区
/dev/initctl
实际上是一个FIFO设备,跟init有关(切换运行级别时用于新init与原init通信)

/dev/initrd
Initial RAM disk
/dev/input
(目录)Input core(包括游戏杆、鼠标等)
/dev/kmem
内核虚拟内存
/dev/loop[0-7]
Loopback设备
/dev/mem
访问物理内存
/dev/null
NULL设备
/dev/psaux
PS/2鼠标
/dev/ptmx
UNIX98 PTY master
/dev/pts
(目录)UNIX98 PTY slaves
/dev/ptyp[0-7]
伪终端主设备(远程登录使用)
/dev/ram[0-7]
/dev/ramdisk
/dev/ram
RAM Disk设备。至少/dev/ram0是应用initrd机制所必须的。
/dev/ramdisk链接到/dev/ram0,是为了兼容老版本内核而保留的。
/dev/ram链接到/dev/ram1。
/dev/random
随机数发生器
/dev/sda
/dev/sda[1-8]
SCSI磁盘及分区设备
/dev/shm
共享内存设备
/dev/systty
指向系统tty设备的符号链接,一般是tty0。
/dev/tty
当前TTY设备
/dev/tty[0-7]
虚控制台(Virtual console)
/dev/ttyp[0-7]
伪终端从设备
/dev/ttyS0
/dev/ttyS1
串口(COM1和COM2)
/dev/urandom
速度更快、安全性较差的随机数发生器
/dev/zero
零设备,只能读0出来
设备节点的主设备号(Major)、次设备号(Minor)的文档是内核源代码中的
/Documentation/device.txt,如果有疑问可以查看这个文件[8]。
5.2.3 脚本和配置文件(scripts and configuration files)
/etc
/etc/rc.d目录下的启动脚本是系统的重要部分。必须对启动脚本做相应的修改
以简化系统的启动过程。
系统和各种应用程序用到的几乎所有的配置文件都位于/etc目录,是裁减Linu
x最麻烦的部分,最容易出问题。配置文件的选择需要综合很多方面的信息,需要对
系统有比较全面、深入的了解,并结合经验才能做出正确的判断。
配置文件
描述
/etc/default
(目录)某个命令(比如useradd)的缺省设置(man useradd(8))
/etc/ld.so.cache
由ldconfig命令根据/etc/ld.so.conf文件产生
/etc/ld.so.conf
库文件路径配置文件,ldconfig命令根据该配置文件生成/etc/ld.so.cache
/etc/localtime
本地时间、时区设置
/etc/login.defs
全局缺省设置
/etc/fstab
文件系统列表(man fstab(5))
/etc/group
组文件(man group(5))
/etc/hosts
列出主机名和IP地址(man hosts(5))
/etc/init.d
符号链接到/etc/rc.d/init.d
/etc/initlog.conf
Initlog日志配置文件(man initlog(8))
/etc/inittab
Init配置文件(man inittab(5))
/etc/ioctl.save
该文件包含了用于单用户模式的串口和终端参数,因为这些参数是由getty设置的,
而在单用户模式时没有运行getty,所以用该文件保存参数。单用户模式对系统安全
是个威胁,我们应该禁止使用单用户模式,因此这个文件实际上并没有必要复制过
来。
/etc/issue
登录信息和标识文件(man issue(5))
/etc/modules.conf
模块的配置文件(man modules.conf(5))
/etc/mtab
已经挂载的文件系统列表(man mount(8))
/etc/nsswitch.conf
Name Service Switch的配置文件(配置名称服务数据源和查询的顺序)(man nss
witch.conf(5))
/etc/pam.d
放置PAM配置文件的目录(有关PAM请参考5.5节)
/etc/passwd
用户口令文件(man passwd(5))
/etc/profile
系统环境变量和登录配置文件
/etc/rc.d
放置启动脚本的目录
/etc/services
列出可用的网络服务及其端口(man services(5))
/etc/termcap
终端(terminal)功能数据库(man termcap(5))
还有那些跟特定应用程序相关的配置文件,比如apache服务器需要的/etc/htt
pd/conf/httpd.conf 等,此处不再一一列出。
5.2.4 库文件(libraries)
/lib,/usr/lib,/usr/share
库文件也是系统运行所必需的。到底需要哪些库文件,是根据所复制的可执行
程序用 ldd 工具来确定的。比如,要知道/bin/bash需要哪些库文件,使用如下命
令:
[root@lips xmdong]# ldd /bin/bash
libtermcap.so.2 => /lib/libtermcap.so.2 (0x40020000)
libdl.so.2 => /lib/libdl.so.2 (0x40024000)
libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
对复制到新的根文件系统的每个可执行程序,都要使用这种方法来确定其所需
要的库,然后把这些库文件也复制过来。
需要注意的是,有些库文件使用上述方法是找不出来的,但是却是系统必须的
。比如:
库文件
描述
/lib/libnss_files*
这个库是跟/etc/nsswitch.conf有关的,必不可少,否则系统不能正常使用。(ma
n nsswitch.conf(5))
/lib/security/pam_unix.so
这个库跟PAM有关,是安全、认证方面的,必不可少,否则系统无法登录。


5.2.5 必要的目录
/home,/mnt,/proc,/root,
/var,/var/log,/var/run,/var/lock/subsys


5.3 初始化RAM盘(initrd)
(参考资料[5])
初始化RAM盘(initrd)提供了引导器加载RAM盘的能力。这个RAM盘可以被挂载
(mount)成根文件系统,并执行其上的程序。然后,可以从另一个设备挂载一个新
的根文件系统,而原来的根文件系统(也就是initrd)会被移到一个目录里并卸载

initrd技术主要设计用来让系统启动过程可以分两个阶段进行,首先让内核以
一组最小的、被编译进内核里的驱动程序来启动,然后从initrd中加载其他的模块

5.3.1 操作步骤
使用initrd时,系统典型的引导步骤如下:
1) 引导器加载内核和初始化RAM盘;
2) 内核把initrd转到一个一般的RAM盘中,并且释放原来被initrd占用的RAM;
3) initrd以可读写模式被挂载到根目录;
4) 执行/linuxrc(linuxrc可以是任何可执行文件,包括shell脚本在内;它具有u
id 0即超级用户的权限,基本上可以做init程序中能够做的任何事情);
5) 在linuxrc中挂载真正的根文件系统;
6) linuxrc使用pivot_root系统调用,把真正使用的根文件系统挂载到根目录;
7) 在根文件系统上执行通常的引导过程(比如执行/sbin/init);
8) initrd文件系统被删除。
注意,改变根目录并不包括卸载旧的根文件系统,因此有可能在转变过程中仍
有进程在initrd上运行。另外,挂载在initrd目录下的根文件系统仍然是可用的。

5.3.2 引导选项
initrd技术增加了下列引导选项:
initrd=<path>
装入指定的文件作为初始化RAM盘。当使用 LILO 当引导器时,你可以用/etc/
lilo.conf 文件中 INITRD 这个配置参数,来指定初始化RAM盘文件。
noinitrd
initrd的数据仍会保留,但不会装入到一个RAM盘里,真正使用的根文件系统将
会被挂载。initrd的数据能夠从 /dev/initrd 这个设备中被读出来。注意,initr
d的数据可以是任何结构,并不一定必须是一个文件系统的映像,这个选项主要用来
进行debug。
注意:/dev/initrd 是一个只读并且只能使用一次的设备,最后一个程序一旦
关闭它, 所有数据将被释放,而且设备也不能再被打开。
root=/dev/ram0
initrd 先被挂载成根目录,接着进行正常的启动过程(这时RAM盘仍被挂载成
根)。
5.3.3 安装
首先:我们要在正常的根文件系统中创建一个容纳initrd文件系统的目录,例
如:
# mkdir /initrd
对目录名称并没有特别的限制,在pivot_root(2)的man手册页中有更详细的说
明。
如果根文件系统是在引导程序时被建立的(例如,你在制作安装软盘),在建
立根文件系统的同时应该创建 /initrd 目录。
在某些情况下initrd虽然未被挂载,只要有下列设备存在,它的內容仍是可被
访问的。(注意:这个设备无法在 devfs 下使用)
# mknod /dev/initrd b 1 250
# chmod 400 /dev/initrd
第二:支持初始化RAM盘的能力,及所有执行格式和文件系统模块,都必须直接
编译进内核,不能采用可加载模块的方式。
第三:必须制作一个RAM盘映像文件。大概的步骤是,在一个块设备上创建一个
文件系统,把需要的文件复制进去,然后把这个块设备的内容输出成一个initrd文
件。目前至少有3种设备适合作为这种块设备:
O 软盘(能拿到任何地方试验,但速度太慢);
O RAM盘(最快,但浪费内存);
O Loopback设备(比较合适的解决方案)。

下面,我们将讨论使用loopback设备创建initrd文件的方法。
1)
确认lookback设备已经配置到内核里。
2)
创建一个适当大小的空白文件系统,例如:
# dd if=/dev/zero of=initrd bs=300k count=1
# mke2fs -F -m0 initrd
3)
把这个文件系统挂载进来,例如:
# mount -t ext2 -o loop initrd /mnt
4)
创建控制台设备。
# mkdir /mnt/dev
# mknod /mnt/dev/console c 5 1
5)
复制所有可能在initrd环境中用到的文件到/mnt目录中。(别忘了/linuxrc文
件)
6)
initrd执行环境的测试需要反复进行修正,为了避免不断的重新启动,可以使
用下列命令:
# chroot /mnt /linuxrc
当然这样启用initrd还是有限制的,就是不能干扰正常系统的执行状态(比如
重新配置网络接口等等。如果在一个用pivot_root指令转换根目录的执行环境中,
就可以做这些事情了)。
7)
卸载这个文件系统:
# umount /mnt
8)
这时,初始化RAM盘系统就在“initrd”这个文件中了。可以把它压缩小一点:

# gzip -9 initrd
最后,必须启动内核并载入initrd系统。需要设置下列引导参数:
root=/dev/ram0 init=/linuxrc rw
(只有需要写入initrd文件系统时才需要附加rw参数)。
使用 LOADLIN 当引导器时,可以执行:
LOADLIN C:LINUXBZIMAGE initrd=C:LINUXINITRD.GZ root=/dev/ram0 i
nit=/linuxrc rw
使用 LILO当引导器时,可以在 /etc/lilo.conf 中如下设置:
image = /boot/bzImage
initrd = /boot/initrd.gz
append = "root=/dev/ram0 init=/linuxrc rw"
如果使用其他引导器,请参考相关文档。
5.3.4 改变根目录设备
在执行完重要任务的尾声,linuxrc一般会执行改变根目录设备的动作,并让所
有程序在真实的根文件系统中展开。这个过程包括下列步骤:
O 挂载新的根文件系统
O 把initrd自己转到这个根文件系统中
O 结束所有旧的对initrd根文件系统的存取动作
O 卸载initrd文件系统并释放RAM盘内存
挂载新的根文件系统很简单:只需要把它挂载到当前根文件系统中的一个目录
下即可。例如:
# mkdir /new-root
# mount -o ro /dev/hda1 /new-root
最终改变根文件系统由pivot_root()系统调用或者pivot_root工具来完成(参
考手册页pivot_root(8))。pivot_root可以把当前根文件系统转移到新根的一个目
录中,然后把指定的目录当作新的根。在调用pivot_root之前,必须先为旧根准备
目录,例如:
# cd /new-root
# mkdir initrd
# pivot_root . initrd
现在,linuxrc仍然可以访问旧的根文件系统。执行下列指令将完全结束与旧根
的联系:
# exec chroot . what-follows <dev/console >dev/console 2>&1
其中what-follows是新根下面的一个程序,比如/sbin/init。(注意,这时如
果没有/dev/console,系统不能启动)。同时,为了保持不同版本之间的兼容性,
需要特别注意下列事项:
O 在调用pivot_root之前,当前目录应该是新的根目录;
O 使用“.”(当前目录)作为pivot_root的第一个参数(新根),为旧根指定目录
的第二个参数也要用相对路径;
O chroot这个命令应该在新、旧根系统当中都能使用;
O 然后调用chroot转移根文件系统;
O 在exec命令中使用象 dev/console 这样的相对路径。
特别提示:让initrd文件系统的目录结构与新根文件系统的结构一致,有利于
转换过程的顺利。
这时,initrd可以卸载,RAM盘也可以释放:
# umount /initrd
# blockdev --flushbufs /dev/ram0
注意:如果linuxrc或者它调用的其他程序的执行因为某种原因中断,则会启用
旧的change_root机制。
5.3.5 使用场合
实现initrd的主要动机是允许系统安装时模块化的内核配置。这个过程大致如
下:
1) 系统可以用一个最小配置的内核从软盘或者其他存储媒体启动,然后载入initr
d系统;
2) /linuxrc来决定需要什么来进一步挂载真实的根文件系统(如设备类型、驱动程
序等)或者支持发行版媒体(如CD-ROM,网络,磁带等);
3) /linuxrc载入必须的内核模块;
4) /linuxrc建立并安装根文件系统;
5) /linuxrc调用pivot_root改变根文件系统,并通过chroot一个程序(继续安装过
程);
6) 安装引导器;
7) 引导器被配置成装入initrd以及相关的模块,建立起系统环境;
8) 现在,系统可以引导,并执行其他的安装任务。
Initrd的关键作用是能够多重配置一个正常操作的系统,而不需要用一个庞大
的内核,或者重新编译、连接内核。在制作Linux发行版(光盘等)、系统恢复盘等
方面有广泛的应用。
5.3.6 淘汰的根转换机制(change_root)
可以通过改写/proc/sys/kernel/real-root-dev这个文件的数字值来改变真实
的根设备。例如:
# echo 0x301 >/proc/sys/kernel/real-root-dev
但是这个机制已经被淘汰,虽然目前的内核仍然支持,不能保证以后的内核会
支持。所以不要使用。

5.4 系统初始化(init)
(参考资料[10])
5.4.1 init的任务
UNIX的init指的不是一个程序、而是一类程序。Init一般是指系统引导时执行
的第一个进程,也是唯一的进程。当内核完成计算机硬件的设置之后,就把控制权
交给init。内核只产生init这一个进程,而系统中其他所有进程都是由init负责产
生(spawn),主要包括各种系统服务进程,比如控制台的登录会话(getty)。主
要任务包括:
O 产生其他进程;
O 重新启动已经退出的进程;
O 负责清理系统中的“僵尸”进程(init是所有其他进程的祖先);
O 处理系统关机(stop所有进程,unmount文件系统);
内核并不关心拿什么来作为系统的init,可以是下列几种选择之一:
O SysVinit (作者:Miquel van Smoorenburg),或者
O simpleinit (作者:Peter Orbaek),或者
O 一个shell脚本,或者
O 嵌入式系统中你的应用程序。
不过路径名字必须是/sbin/init,/etc/init,或/bin/init(因为已经编译到
内核里面了)。如果这几个路径都找不到,系统就完蛋了。为了增加灵活性,内核
提供了命令行选项可以指定init路径:“init=”。
5.4.2 SysVinit
/etc/inittab
/etc/rc.d
大多数Linux发行版使用的init是SysVinit,也就是System V UNIX的实现。其
主要思想是规定了不同的“运行级别(runlevel)”。通过配置文件/etc/inittab
,定义了系统引导时做什么,进入或者切换到一个运行级别时做什么。配置文件每
一行的语法为:
id:runlevel:action:command
细节请参考手册页inittab(5)。
整个过程中用到的脚本都放在/etc/rc.d目录。
5.4.3 两种风格:Slackware vs. Debian
关于配置文件/etc/inittab和脚本/etc/rc.d的实现和组织主要有两种不同的风
格,其有代表性的发行版分别为Slackware和Debian(Redhat同Debian)。这两种风
格之间有几个明显的区别,可以比较容易的识别。
例如在/etc/inittab中,定义进入运行级别0时运行的脚本分别为/etc/rc.d/r
c.0和/etc/init.d/rc 0(在Redhat中/etc/init.d是指向/etc/rc.d/init.d的一个
符号链接,注意这里0是脚本rc的命令行参数)。因此,Slackware风格的/etc/rc.
d中应该是一系列相对独立的脚本,对应于配置文件中每个动作的定义(我没有见过
Slackware哦)。
在我们所熟悉的Redhat中,/etc/rc.d的组织要复杂的多,每个运行级别对应一
个子目录/etc/rc.d/rcX.d(X表示运行级别0~6),下面放的是一系列形如SXXfoo
和KXXbar(S表示Start某个服务,K表示Kill某个服务,XX是两位数字,决定了该脚
本执行的顺序)的符号链接,指向/etc/rc.d/init.d中的脚本,每个脚本对应一项
服务程序。
另外,还有两个重要脚本值得一提:/etc/rc.d/rc.sysinit是系统引导时首先
要执行的(完成系统初始化的各项工作),而/etc/rc.d/rc.local在最后执行(类
似DOS的autoexec.bat)。
读一下/etc/rc.d/rc.sysinit,/etc/rc.d/rc和/etc/rc.d/init.d/中的某个脚
本,就会对Redhat的启动过程和风格有比较清晰的了解。
据说Slackware风格比Debian的速度要快一些。可能是后者的组织结构比较复杂
的缘故吧。

5.5 PAM
(参考资料[11],http://www.kernel.org/pub/linux/libs/pam/,手册页pam
(8))
5.5.1 什么是PAM
PAM(Pluggable Authentication Modules)是为了解决计算机系统中用户认证
的问题而引入的一种实现方案。PAM的目标为:
O 将认证功能从应用中独立出来,单独进行模块化设计、实现和维护(而不是象以
前那样,将认证功能的代码跟应用程序编译在一块);
O 为这些认证模块建立标准API,以便各应用程序能方便的使用它们提供的各种功能

O 认证机制对其上层用户(包括应用程序和最终用户)是透明的。
PAM机制由SUN设计并首先在Solaris 2.3上部分实现,后来逐渐在其他UNIX平台
上实现,包括Linux(其实现称为Linux-PAM)。
5.5.2 PAM的结构
PAM采用分层的体系结构。最下面是模块层,负责实现具体的认证功能,包括帐
户管理(account)、口令鉴别(auth)、口令管理(password)和会话管理(ses
sion)4个模块。
应用接口层位于模块层之上,调用下层提供的服务,并向上(应用程序)隐藏
PAM实现的细节。
5.5.3 配置文件
/etc/pam.conf
/etc/pam.d (如果存在这个目录,则忽略/etc/pam.conf)
配置文件是应用接口层的另一个重要组件。其作用主要是为应用选定具体的鉴
别模块,模块间的组合以及规定模块的行为。
/etc/pam.conf的语法为每行5项(在Linux-PAM的手册页pam(8)中有更详细的描
述):
service type control module-path module-arguments
Service是服务的名字,比如login,su等。注意,/etc/pam.d目录下面有许多
配置文件,分别对应某项系统服务,文件名即等于service,因此每行只有4项。
Type为account,auth,password,session之一,即要使用的认证模块。每个
应用可以使用多个认证模块,或者说可以将模块“堆叠”使用,但是每一行只能定
义一个,因此一项服务可以有多行配置。
Control规定了如何处理模块认证失败或成功,可以是requisite、required、
sufficient或optional。
Module-path是PAM库文件的文件名。缺省路径是/lib/security/。
Module-arguments是可以传给模块的参数。
5.5.4 other
特别需要指出的是,有一个特殊的服务名字——other。如果没有明确指明应用
于某项服务的规则,就用other的定义来处理。因此,我们可以想到,系统中PAM的
最简配置为,只有/etc/pam.d/other这一个配置文件,即可处理所有的认证请求。
下面是我们用过的一个other配置文件的例子:
[xmdong@lips pam.d]$ cat other
#%PAM-1.0
auth required /lib/security/pam_unix.so
account required /lib/security/pam_unix.so
password required /lib/security/pam_unix.so
session required /lib/security/pam_unix.so
当然,这样处理对系统的安全性来说是不利的,最好还是给每项服务定义单独
的配置文件。这不过是一种简化问题的临时方案。

5.6 再论initrd
在前面的5.3节中,对initrd机制已经有了比较详细的讨论。准确的说,该部分
内容来自linux-2.4/Documentation/initrd.txt 文件。但是在实践中,我们发现其
中描述的有些方法并不适用。是新版本内核的变化造成的(内核实现与文档不同步
),还是2.4版内核的bug,不得而知。在我查证的过程中,看到网上有不少相关的
问题和讨论。遗憾的是至今我还不能确认到底是什么原因。
后来我又专门读了2.2版内核中所附带的 initrd.txt 文件。我感觉实际的情况
似乎更接近于这个老文档的描述。我还看到一个有关的patch文件,从该文件中可以
明确看到新的initrd.txt文档的改变。下面是该文件的开始部分(行首的-表示老文
档有而新文档中已经删除,+表示新文档中增加的内容)。
Using the initial RAM disk (initrd)
===================================

-Written 1996 by Werner Almesberger <almesber@lrc.epfl.ch> and
- Hans Lermen <lermen@elserv.ffm.fgan.de>
+Written 1996,2000 by Werner Almesberger <werner.almesberger@epfl.ch
> and
+ Hans Lermen <lermen@fgan.de>


-initrd adds the capability to load a RAM disk by the boot loader. T
his
-RAM disk can then be mounted as the root file system and programs c
an be
-run from it. Afterwards, a new root file system can be mounted from
a
-different device. The previous root (from initrd) is then either mo
ved
-to the directory /initrd or it is unmounted.
+initrd provides the capability to load a RAM disk by the boot loade
r.
+This RAM disk can then be mounted as the root file system and progr
ams
+can be run from it. Afterwards, a new root file system can be mount
ed
+from a different device. The previous root (from initrd) is then mo
ved
+to a directory and can be subsequently unmounted.
下面我试图将我遇到的问题和能够确认的东西记录一下。(后面我们分别称新
、老文档为initrd.txt-2000和initrd.txt-1996)
5.6.1 根到底在哪里
在配置使用initrd之前,首先你要清楚,系统正常运行时,根文件系统挂载在
什么设备上(也就是说,root device是什么——root在哪里?)。
我们知道,使用initrd机制的话,系统启动时先以RAM Disk(/dev/ram0)作为
根,然后通过某种方法将根转换到“真实的根(real-root-dev)”(之所以说rea
l,是与前面的RAM里临时的根比较而言)。一种常见的情形是启动时用initrd,然
后切换到一个硬盘分区,比如/dev/hda1。
事实上,这个根转换不是必须的。我们完全可以就把/dev/ram0作为我们真实的
根设备。这时,系统启动之后所有的操作都在RAM Disk中进行。当然这样做也有一
个明显的缺点,即无法保存数据。比如对系统配置进行的修改(象IP地址),重新
启动之后就丢掉了。如果要保存数据,就要想别的办法。
LiPS目前的做法就是这一种(没有根转换,initrd就是真实的根)。为了在重
新启动之后保持配置文件的修改,我们将目录/etc链接到了其他非易失存储器(例
如,一个硬盘分区,或者USB盘的分区)。
指定根设备是通过内核参数“root=”来完成的。以GRUB引导器的配置为例:
kernel /boot/bzImage ro ramdisk_size=65536 root=/dev/ram0
initrd /boot/initrd.img
其中 ramdisk_size=65536 说明初始RAM盘的大小为64MB,root=/dev/ram0 说
明根的位置。下面第二个例子来自一台在hda1安装了Redhat 9的机子:
kernel /boot/vmlinuz-2.4.20-8 ro root=/dev/hda1
initrd /boot/initrd-2.4.20-8.img
很明显,这里 root=/dev/hda1 指定了真实的根,其启动过程包含根的转换。
但是这里没有指定 ramdisk_size,因为initrd的缺省大小是4MB,而initrd-2.4.2
0-8.img比较小,所以用缺省值即可。
(我们多次提到root, root filesystem, root device, 自己体会吧。)
5.6.2 linuxrc
在initrd机制的设计中,初始RAM盘只是作为一个过渡性质的根设备,由/linu
xrc来完成操作环境准备(比如加载一些不常用的、没有编译进内核的驱动程序模块
)和根转换工作。
Linuxrc的执行是自动的,即内核挂载initrd盘作为根,如果根下面有这个文件
(也就是/linuxrc),就执行它。(如果没有呢?当然什么也不做。所以linuxrc也
并不是必不可少的。)
接下来说说linuxrc的内容。下面的例子是从initrd-2.4.20-8.img中释放出来
的:
[root@lips initrd]# cat linuxrc
#!/bin/nash

echo "Loading jbd.o module"
insmod /lib/jbd.o
echo "Loading ext3.o module"
insmod /lib/ext3.o
echo Mounting /proc filesystem
mount -t proc /proc /proc
echo Creating block devices
mkdevices /dev
echo Creating root device
mkrootdev /dev/root
echo 0x0100 > /proc/sys/kernel/real-root-dev
echo Mounting root filesystem
mount -o defaults --ro -t ext3 /dev/root /sysroot
pivot_root /sysroot /sysroot/initrd
umount /initrd/proc
该脚本中首先加载了支持ext3文件系统所需的模块,然后挂载真实的根文件系
统,并进行转换。特别需要注意的是,linuxrc脚本一般是用nash解释执行的,而不
是通常的bash。有关nash的详情请看手册页nash(8)。简单说,nash内置实现了多数
在执行linuxrc这个阶段可能用到的命令,比如mount, pivot_root, umount;而如
果使用bash,象mount等都是“外部命令”,就需要复制/bin/mount到initrd映像文
件中。实际上,在initrd-2.4.20-8.img的/bin中只有insmod和nash两个程序(mod
probe符号链接到insmod):
[root@lips initrd]# ls bin/
insmod modprobe nash
当我按照5.3.4节描述的方法测试linuxrc的时候发现,我的脚本头两句执行就
报错。
mount –o ro /dev/hda11 /sysroot
cd /sysroot
这里我的真实的根设备是/dev/hda11,/sysroot是initrd映像中存在的一个目
录。这两句完全是按照该文档的要求写的。后来经过多次试验才发现问题之所在。
第一,nash实现的mount命令语法与我们常用的/bin/mount并不完全兼容;第二,n
ash根本没有实现cd命令。
因此,可以推断initrd.txt-2000中讲linuxrc时所举例子不是用的nash。实际
上在该文档中根本没有指明用什么shell。再回头仔细看才发现,所有例句都是在r
oot的shell命令提示符“#”后面的,那么它用的是bash之类了。但是令人迷惑的是
,同样在该小节中稍后出现如下例句: # exec chroot . what-follows <dev
/console >dev/console 2>&1
为什么用exec执行chroot呢?chroot可以在命令行直接用啊。文档中讲到,wh
at-follows可以是/sbin/init。我用nash试了下面这个句子(nash中用exec执行外
部命令):
exec chroot . /sbin/init <dev/console >dev/console 2>&1
还是不行。错误信息:
Usage: init 0123456SsQqAaBbCcUu
开始我还以为后面那一串是个“magic number”,后来想以Usage开头应该是讲
使用这个命令的语法。看init(8)手册页,果然要求给一个参数,0~6当然是运行级
别啦,试了一个init 3也还是不行。尤其搞不明白后面那些个console什么意思。
5.6.3 根转换机制:新的?旧的?
前面我们多次提到了根的转换。在initrd.txt的两个版本中,分别描述了两种
根转换机制,其中2000版中称1996版的内容已经“过时”,不推荐使用。然而事实
上要麻烦的多,事情并不总是按照预期(如文档所述)运行。
为了弄清楚这个问题,我进行了一系列测试。环境是这样的:
O 使用initrd机制,已经准备好了根文件系统映像文件initrd.img,准备好了内核
bzImage;
O initrd.img的所有内容释放到一个硬盘分区/dev/hda11,ext2文件系统;
O 在/dev/hda11上面增加一个/boot目录,存放bzImage和initrd.img文件;
O 以/dev/ram0作为根启动,然后以某种机制转换到“真实的根”/dev/hda11。
系统的引导器是GRUB,安装在硬盘MBR,grub.conf位于/dev/hda1(也就是做测
试的实际的根文件系统)。按照上述环境配置,在grub.conf中增加如下内容:
title lips (testing ......)
root (hd0,10)
kernel /boot/bzImage ro root=/dev/hda11 ramdisk_size=65536
initrd /boot/initrd.img
下面我们分别使用不同的/linuxrc进行了测试,以确认到底哪种机制是可行的

第一,如果没有/linuxrc会怎么样呢?
结果是系统能够成功启动到以/dev/hda11作为根,并且把initrd(也就是旧根
文件系统)的内容移到了/initrd目录。这是符合initrd.txt-1996文档中第一段的
描述的。所以只要你使用了initrd,指定了新的根,并且准备好了/initrd目录(已
经在/dev/hda11创建了/initrd),内核可以自动完成根的转换工作。终端上面有如
下输出信息:
Trying to move old root to /initrd ... okay
这里有个小问题,即最后/initrd没有卸载,这意味着initrd所分配内存也没有
释放。因此umount /initrd不是内核自动完成的,而应该在/linuxrc中的某个位置
进行。如果linuxrc仅包含卸载/initrd的命令,如下:
#!/sbin/nash
umount /initrd
则执行情况是这样的:umount命令报错(返回错误码22),然后出现前面所示
Trying…的信息。结果跟前面是一样的。因为linuxrc执行在先,当时旧根还没有被
转移到/initrd中来。
如果没有/initrd目录会怎样?我们把/dev/hda11上面的/initrd目录删除,重
新启动(没有/linuxrc),结果是可以启动到以/dev/hda11为根,且看不到原来的
initrd。该过程中出现如下信息:
Trying to move old root to /initrd ... failed
Unmouting old root
Trying to free ramdisk memory ... okey

第二,pivot_root可以用,但是chroot不能用。
pivot_root是在initrd.txt-2000文档中引入的新机制。在测试pivot_root的过
程中,除了在nash中不能用cd命令造成的困惑(这个问题前面已经说明)之外,使
用pivot_root基本上是成功的。/linuxrc脚本的主要内容如下:
#!/sbin/nash
mount -o defaults --ro -t ext2 /dev/hda11 /sysroot
pivot_root /sysroot /sysroot/initrd
但是,用这个脚本不能启动,造成系统挂起:
VFS: Cannot open root device “hda11” or 03:0b
Please append a correct “root=” boot option
Kernel panic: VFS: Unable to mount root fs on 03:0b
如果按照initrd.txt-2000的指导,pivot_root之后应该执行chroot,但是我始
终没有办法使chroot成功执行(有关情况前面已经说明)。不过可以肯定的是,pi
vot_root执行成功之后,当前目录就是新的根目录。
第三,/proc/sys/kernel/real-root-dev
initrd.txt的新版本中最后一节专门指出,向/proc/sys/kernel/real-root-d
ev文件中echo数字的方法(称为change_root机制)是过时的。然而令人迷惑的是,
Redhat 9的缺省安装产生的initrd-2.4.20-8.img中(前面已经列出了其/linuxrc文
件内容),同时使用了pivot_root和change_root机制。
测试下列/linuxrc脚本,系统可以启动到/dev/hda11,但是/initrd没有卸载。
如果在脚本末尾umount /initrd,会返回错误码16。
#!/sbin/nash
mount -t proc /proc /proc
mkrootdev /dev/root
echo 0x0100 > /proc/sys/kernel/real-root-dev
#echo 0x030b > /proc/sys/kernel/real-root-dev
mount -o defaults --ro -t ext2 /dev/hda11 /sysroot
pivot_root /sysroot /sysroot/initrd
umount /initrd/proc
虽然这样可以启动,但是有关real-root-dev还有几个没有弄清楚的问题:
real-root-dev的值到底是什么?
目前我还没有看到解释real-root-dev的值是什么东西的正式文档,根据情况判
断的话,我认为应该是该设备的“major-minor”(主、次设备号)。
为什么用0100而不是030b?
如果上述判断准确的话,那么写入0100就表示/dev/ram0。这就奇怪了,“真实
的根设备”应该是/dev/hda11呀,那么是不是应该写030b呢?但是测试发现用030b
同样会导致不能启动。
Kernel panic: VFS: Unable to mount root fs on 03:0b

5.7 提示
5.7.1 文件系统目录层次结构
根文件系统的目录结构最好符合“Filesystem Hierarchy Standard(文件系统
层次标准)”[7],而不能自己随意创建。
顺便说一下,象这种情况:/bin, /sbin, /usr/bin, /usr/sbin 这4个目录都
可以放应用程序,具体放在哪个目录,则要根据该应用程序在系统中的作用来确定
。一般地,系统的关键应用程序放在/bin,只能给超级用户root访问的关键应用程
序放在/sbin,相比之下比较次要的应用程序和root应用程序分别放在/usr/bin和/
usr/sbin。
5.7.2 mklips.sh(制作LiPS的脚本)
复制文件的工作全部可以手工进行,但是做一个shell脚本来完成所有工作显然
是一个更好的办法。
在我们的项目中,制作LiPS(也就是完成裁减Linux)的工作,除了编译内核之
外,都可以通过mklips.sh脚本完成。
5.7.3 lips.conf(LiPS配置文件)
lips.conf是LiPS的配置文件,由mklips.sh脚本使用,主要用来控制所制作的
LiPS系统包括哪些可选的模块,例如:
INCLUDE_APACHE=no
INCLUDE_IPTABLES=no
INCLUDE_PROFTPD=yes
INCLUDE_NFSD=no
INCLUDE_SAMBA=yes
通过一系列的INCLUDE_?=yes或者no,可以实现LIPS的模块化。在mklips.sh脚
本中,判断每个INCLUDE的值,来确定是否复制某个模块相关的文件。
配置文件中还可以包括其他可配置的信息。
5.7.4 cp –dpR
复制文件请使用“cp –dpR”命令,可以保留文件的属性(特别是对于设备节
点和符号连接等特殊文件),还可以复制整个目录。
5.7.5 如何判断一个程序依赖哪些文件
除了库文件外,关键是找出相关的配置文件。man手册页是判断应用程序配置文
件的重要依据。我们以安装Apache Server为例来说明判断复制哪些文件的过程。
首先确定其可执行文件是/usr/sbin/httpd。然后看httpd的手册页,即
[root@lips lips]# man httpd
httpd(8)
httpd(8)
NAME
httpd - Apache hypertext transfer protocol server
…………(略)
FILES
/etc/httpd/conf/httpd.conf
/etc/mime.types
/etc/httpd/conf/magic
/var/log/httpd/error_log
/var/log/httpd/access_log
/var/run/httpd.pid
February 1997
httpd(8)
(END)
这时关键看最后一段FILES,列出了有关的配置文件、日志文件和进程文件,那
么我们就知道了httpd要在目标系统中运行,这几个配置文件是必不可少的。
把httpd和用ldd找出的库文件以及上述配置文件复制到目标系统,运行httpd试
一下,看看是否正常。一般情况下应该没有问题。但是httpd比较特殊,除了ldd找
出的库文件外,它还需要很多可加载的模块(库)。怎么发现呢?重要的一点是看
出错信息(有时候还要查看日志中的信息,/var/log/messages或者应用程序日志)
,看httpd报告缺少什么;还可以看看相关的配置文件httpd.conf,也能找到有用的
信息。然后我们发现需要/usr/lib/httpd这个目录下的库,把它也复制过去即可。

5.7.6 带库文件复制——lcp()
在复制可执行文件(软件)的过程中,如果没有正确复制相关的库文件,该软
件肯定不能在目标系统中正常运行。而通过ldd命令手工确定每个软件的库文件,是
一个繁琐且费时的任务。另一个隐含的缺陷是,当你不再需要某个软件时,删掉可
执行文件之后,相应的库文件是否需要删除呢?如果删除了,而该库文件是其他软
件也需要的,会导致其他软件不能用;如果不删除,就有可能留一些垃圾库文件在
目标根文件系统中。
因此,最好是能够保证每次复制可执行文件的同时自动复制相关联的库文件。
我写了一个脚本函数 lcp() 来解决这个问题。语法和主要代码如下(以#开头的行
为注释):
# lcp BIN BINPATH LIBPATH
# BIN:可执行文件路径
# BINPATH:可执行文件的目标路径(复制到哪里)
# LIBPATH:相关库文件的目标路径
lcp()
# run()是另一个函数,执行它后面的shell命令,并在遇到错误时报错、写错
误日志
# 首先复制BIN到BINPATH
run "$CP $BIN $BINPATH"
# 使用ldd找出与BIN相关的库文件,把ldd的输出送给LDDSTR
LDDSTR=`ldd $BIN`
# 循环检查LDDSTR字符串的每一项
for STR in $LDDSTR; do
# 判断出库文件路径
LIB=`echo $STR | grep "/lib/" `
if [ -z "$LIB" ]; then
continue
fi
# 复制LIB到LIBPATH,除非要复制的库文件在目标路径中已经存在
LIBNAME=`basename $LIB`
if [ ! -f "$LIBPATH/$LIBNAME" ]; then
run "$CP $LIB $LIBPATH"
run "cp $LIB $LIBPATH"
else
log "Warning: File "$LIBPATH/$LIBNAME" exis
t."
fi
done
}
5.7.7 库文件的命名
(参考资料[9]之6.2节)
我们这里所说的“库”都是指的GNU C Library,也就是glibc。
在/lib下面可以看到很多库文件,主要分为4类:
O 实际的共享库(Actual shared libraries)
文件名格式为 libLIBRARY_NAME-GLIBC_VERSION.so,其中LIBRARY_NAME是库的真正
名称,GLIBC_VERSION是glibc包的版本号。比如glibc 2.2.3版math(数学)库的文
件名为libm-2.2.3.so。
O 主修订版本符号链接(Major revision version symbolic links)
库的主修订版本号与glibc版本号不同。比如,glibc 2.2.3的C共享库libc-2.2.3的
主修订版本号为6,libdl-2.2.3为2。主修订版本符号链接的命名格式为 :libLIB
RARY_NAME.so.MAJOR_REVISION_VERSION,那么C共享库的符号链接即 libc.so.6。
一个程序连接到一个库之后,它在运行时访问的就是这个符号链接。
O 版本无关的符号链接(Version-independent symbolic links to the major re
vision version symbolic links)
这些符号链接的作用是为用到某个库的所有程序提供一个统一的入口,而不管实际
上glibc的版本或者该库的主修订版本。其命名格式为 libLIBRARY_NAME.so。比如
,libm.so链接到libm.so.6,libm.so.6又链接到libm-2.2.3.so。唯一的例外是li
bc.so,是个脚本。连接一个程序时用的是这个符号链接。
O 静态库文件(Static library archives)
这些库是应用程序静态连接用的。格式为 libLIBRARY_NAME.a.




6 引导

6.1 引导过程概述
6.2 几种引导方式
几种引导方式的粗略比较如下表所示。
优点
缺点
DOC
DOM
FLOPPY
HARDDISK
NETWORK
USB-HDD
容量大,读写可靠

6.3 grub引导器
有了前面生成的Linux内核和根文件系统映像文件后,制作引导盘非常简单。在
动手制作引导盘之前,最好先了解一下grub引导器。
Grub是一个通用的多引导加载软件,与较早期多数Linux发行版本提供的另一个
引导器lilo相比,grub的功能更强大、设置更方便、更灵活。因此本项目选用grub
作为引导器软件。
本文档不是grub的教学文档,所以有关grub的详细用法,请参考其他有关文档
。这里仅强调几点:
O 引导盘的文件系统可以是ext2,ext3,msdos (FAT),vfat (FAT32),等等;
O grub表示硬盘、分区的方式与Linux的方式不同。Linux系统的IDE硬盘表示为/de
v/hd{a,b,c,d},SCSI设备表示为/dev/sd{a,b,c,d,…},分区从1开始编号,比如P
rimary Master IDE硬盘的第二个分区是/dev/hda2;而grub的硬盘(包括IDE和SCS
I)统一从0开始编号,分区也是从0开始编号,比如系统中的第一个硬盘是(hd0),
其第一个分区是(hd0,0)。
O 在grub的提示符下面输入命令和参数时,按TAB键可以自动补全。
6.4 USB引导
使用USB引导盘有很多好处,比如说,容量大、读写可靠,不占用IDE接口,携
带方便等等。
6.4.1 制作步骤
6.4.1.1 创建文件系统
首先,必须在USB盘上面创建文件系统(也就是格式化)。你几乎可以使用gru
b支持的所有文件系统,包括常见的各种文件系统,比如ext2,ext3,FAT,FAT32等
。但是根据我的经验,需要注意的是:
O 最好不要使用FAT,即msdos,因为FAT的文件名有“8.3”的限制,所以象“grub
.conf”这种文件名就被截断成了“GRUB.CON”。
O FAT和FAT32都不能表示符号链接文件,虽然有变通的方法,即将文件再复制一份

顺便说一下在Linux下面怎么访问USB盘。
如果系统中没有其他的SCSI设备,那么插在USB 1接口上面的USB盘的设备名为
/dev/sda,其分区为/dev/sda1。依此类推。一般插入USB盘后,窗口中会出现驱动
程序输出的一些信息,其中有sd?的字眼。接下来就跟访问普通的硬盘分区没有什么
两样了。
#mke2fs /dev/sda1
#mount –t ext2 /dev/sda1 /mnt
注意:Redhat 7.3不支持Intel 845系列芯片组的主板。
6.4.1.2 复制文件
按照下面的列表,复制文件(包括目录结构)到USB引导盘:
备注
/boot/
bzImage
或者vmlinuz
initrd.img
/grub/
grub.conf
menu.lst
符号链接
stage1
stage2
6.4.1.3 安装grub
安装grub到USB盘的引导扇区。假设系统有一块IDE硬盘,这时USB盘是grub看到
的第二块“硬盘”,所以表示为(hd1)。
#grub
grub> root (hd1,0)
grub> setup (hd1)
root命令告诉grub,内核文件所在的位置。Setup命令安装grub到hd1的引导扇
区,如果是setup (hd1,0),则安装到第一个分区(即/dev/sda1)。
上述命令序列也可以通过执行脚本完成。
6.4.1.4 用USB盘引导系统
在用新制作的USB引导盘引导之前,还需要确认BIOS的一个设置,即第一个启动
设备应该设置为“USB-HDD”。
OK,插上USB盘,试一试。
6.5 提示
6.5.1 版本问题?
我们的项目开始的时候用Redhat 7.3,但是后来做USB引导盘时发现,我们的环
境不支持USB盘。我想Redhat 7.3应该是支持USB盘的,因为我以前在7.1下面就用过
。那么有可能是主板的问题,i845的芯片组比Redhat 7.3要新,也许是没有该芯片
组的正确驱动。
后来在Redhat 7.3下更新了内核,使用2.4.20版本的Linux内核即可正常支持U
SB盘。
然而还有一个问题,Redhat 7.3的GRUB版本是0.91,在使用中发现,制作USB引
导盘没有问题,但是一旦你改变USB盘的文件(比如复制一个新的文件上去),本来
可以引导的USB盘就不行了。
而Redhat 9.0使用的是GRUB 0.93,就没有上述问题,并且使用USB盘也没有问
题。也就是说,使用Redhat 9就可以了。不过Redhat 9集成的其他很多软件也有变
化,比如Apache升级到了2.0版本,以前的httpd.conf配置文件要修改过之后才能正
常使用;另外quota接口也有变化。
因此改变版本需要一定的代价,究竟使用什么版本应该综合考虑,最好在项目
开始时就做出正确的选择,并保持稳定。




7 远程串口控制台

7.1 概述

7.2 配置方法
7.3 提示
7.4 Linux 远程串口控制台指南
(这是我以前写的一份简单的文档,曾经贴到网上,附在这里供参考)
Linux 远程串口控制台指南

dot <anonymou@163.com>
Dec 27, 2002

2002年圣诞节期间,我在 Redhat Linux 7.0 (kernel 2.2.16-22) 平台上配置
了远程串口终端,整个过程基本顺利,但是也经历了一些意想不到的波折,幸好最
终是成功了的。乘着记忆还比较清晰的时候,写下这篇文档,以供自己和他人参考


首先参考 "Linux-Remote-Serial-Console-HOWTO",该文档已经足够详尽,按
要求一步步地做即可。该文档描述的是 Redhat 7.2 平台,个别地方跟 7.0 有些差
异,不过问题不大。

要注意的问题主要是:

* 串口直连电缆

开始我并没有在意这个问题,我手头上有一根以前做的直连电缆,是只用了3根
线的那种(2、3交叉,5对接),因此 HOWTO 文档中讲电缆制作的章节我就跳过去
了,没有仔细看,以为我这根电缆就可以了。事实也给人以这种假相:当我按照文
档的说明完成了配置文件的修改,重新引导 Linux 机子,这时在 Windows 的超级
终端中确实显示了所有该显示的信息,看起来没问题。然而接下来配置 getty 时就
不行了,应该出现在超级终端中的 login: 提示符死活不肯出现。以为是 getty 软
件的问题,先后测试了 getty 和 mgetty,都不行。后来在 quickmouce 的提醒下
,又仔细看文档,按照 11.3 节的图 11-1 重新制作了串口直连电缆,才解决问题


* getty 软件

HOWTO 文档中已经指出,mingetty 不支持串口终端,所以这个软件要首先排除
(mingetty 的手册页中也有相关说明)。而 Redhat 缺省安装的就是这个软件,所
以要重新下载安装其他的 getty 类软件。文档中也已经列出了几种常用的 getty
的变种。

来说说 mgetty 先。为什么我先使用了这个软件呢,一是因为 Redhat 7.0 的
安装盘上面有它的 RPM 包,直接拿来装很方便,另外是因为 mingetty 的手册页中
推荐使用 mgetty 。结果是 mgetty 运行后在日志文件中报告大量错误,莫名其妙
。我想可能是这个光盘上的版本老了,于是上网下载了比较新的版本 mgetty-1.1.
25-5.i386.rpm ,运行之后情况好多了,似乎要成功的样子。在没有使用新的串口
线之前,login: 出不来,但是在超级终端中敲的字符 mgetty 可以收到(从日志中
看的出来);换了新制作的串口线之后,第一次!成功了,一切都象预期的那样,
我可以从超级终端登录。可是当我退出想再登录一次,就不行了,甚至我把两台机
子全部重启也不行啦。我又试用 Linux 的 minicom 终端来连接,也是只成功了一
次,就再也不行了。真是怪哉,脑袋都快想破了,也不知道是为什么。后来终于怀
疑 mgetty 有问题,换了其他的,天哪,终于解脱了。不知道后面更新的版本是否
可以,我是没有劲去试了。

再来说说 agetty 。其实没有什么好说的,因为我根本没有用过这个。我本来
想试一下的,但是从网上下载安装太麻烦了。下载了 agetty-2.11w-2.i386.rpm ,
安装时 RPM 说要先安装 login 包,然后又要求什么 pam 包,然后又需要 awk ,
真是 faint 了,太麻烦了,所以最终我也没有安装它。

最后说 getty 啦。也就是 HOWTO 文档中所谓的 “传统” getty 。我下载的
RPM 包的名字叫 getty_ps-2.0.7j-9.i386.rpm 。当我正在为 mgetty 的问题头疼
的时候,换成它之后就一切 OK 了。

* 终端软件

Windows 下面可以用超级终端、NetTerm,Linux 下面有 Minicom 。都比较好
用,注意参数配置与目标机的串口一致。

* 修改过的配置文件列表

/etc/lilo.conf
/etc/inittab
/etc/gettydefs
/etc/securetty
/etc/makedev.d/linux-2.2.x
/etc/security/console.perms
/etc/sysconfig/init
/etc/sysconfig/kudzu

* 再谈串口直连电缆

这次调试在串口直连电缆上面花费了不少力气,对它们的了解也增加了不少。
以前以为直连电缆就是接 3 根线,其实不然。只用了 2、3、5 引脚的直连电缆不
能提供握手信号,因此如果软件需要使用 CTS/RTS 或者 DTR/DSR 流控,就需要连
接更多的引脚。

PC DB9 DB9
-------------------- -----------------------
RxD Receive Data 2 <----- 3 TxD Transmit Data
TxD Transmit Data 3 -----> 2 RxD Receive Data
|--> 6 DSR Data Set Ready
DTR Data Terminal Ready 4 --|--> 1 DCD Carrier Detect
GND Signal Ground 5 ------ 5 GND Signal Ground
DCD Carrier Detect 1 <--|
DSR Data Set Ready 6 <--|-- 4 DTR Data Terminal Ready
RTS Request To Send 7 -----> 8 CTS Clear To Send
CTS Clear To Send 8 <----- 7 RTS Request To Send
RI Ring Indicator 9 (not used)

上面是两端都是 DB9 接头的标准 NULL MODEM 电缆的接线图,可以提供全部握
手信号。这也是 Remote-Serial-Console-HOWTO 中要求的直连电缆做法。我做通串
口控制台使用的就是这种电缆。

如果你打算自己制作电缆,就会发现一些问题。我们一般利用网线(双绞线)
来做,这种双绞线是 8 芯的,而按照图示的连接方法则需要 9 根线。因此最简单
的方法是用那种很多芯的排线来做,需要多少根割多少根(最好是彩色的,不容易
弄混)。后来我想,我们用到的其实就是 CTS/RTS 流控信号,到 2000 年为止 Li
nux 还不支持 DTR/DSR 流控,应该用不了这么多线。于是我就用网线做了根不完整
的直连电缆:2、3 交叉,5 对接,7、8 交叉,还空 3 根线,就把 1、4 也交叉连
上了,结果是用了 7 根线,还空 1 根,与上面比只是少了 4、6 交叉,也就是没
有 DTR/DSR 信号。事实证明这样也是通的。我猜想 1、4 交叉不要应该也是通的,
只是没有试过,已经焊上去懒得再弄了。

关于串口方面的更多内容可以参考 "Linux-Text-Terminal-HOWTO" 和 "Linux
-Serial-HOWTO" 文档。

* 为什么要用串口终端(控制台)?

为什么费劲做这个 Serial Console 呢?最容易想到的理由是省钱,呵呵,省
掉了显卡、显示器、键盘,只剩下一个 "Box",多好玩。

说正经的,这种做法在嵌入式系统的开发中也是很有价值的。现在很多人都在
利用 X86 平台做嵌入式系统开发,甚至做产品。我曾经测试过清华某某做的 foob
ar 服务器,镜像光盘用的,卖给你的就是一个主机,我自己给接上显示器、键盘,
开机就是 Linux ,还可以进入单用户模式,两分钟就把 root 口令改了,你说这样
的服务器有什么安全性可言?如果能够屏蔽本地控制台,配置串口终端,不是更“
象”一个产品吗?
(完)




8 系统安装光盘

8.1 概述
本节介绍一种系统安装光盘的制作方法。安装光盘用于产品阶段的生产安装。
因此该安装光盘必须是可引导的、简单好用,并且一般不包含源代码。
制作可引导光盘的方法有很多种,我们用了一种最简单的方法,就是拿来一个
可引导光盘的ISO映像,然后做必要的修改以满足我们的要求。
请准备Redhat 9的第一张安装光盘,我们将要用到的文件是 /images/boot.is
o。(其他发行版本应该也可以,只要原理是一样的。)
另外如果你的工作平台是Windows,请准备WinISO 5.3软件,用来编辑光盘ISO
映像文件。
当然如果你要烧盘测试的话,还要准备刻录机和CD-RW或者CD-R光盘。
8.2 boot.iso光盘的文件结构分析
用WinISO打开boot.iso即可看到该光盘的文件(其实Redhat 9第一张安装光盘
的/isolinux目录就是boot.iso解开后的内容)。下面是文件列表:
[xmdong@lips isolinux]$ ls
boot.cat initrd.img options.msg snake.msg vmlinuz
boot.msg isolinux.bin param.msg splash.lss
general.msg isolinux.cfg rescue.msg TRANS.TBL
其中关键的文件是boot.cat,initrd.img,isolinux.bin,isolinux.cfg和vm
linuz;isolinux.cfg是如何引导的配置文件,我们主要修改这个文件即可;initr
d.img和vmlinuz分别是初始化RAM盘和内核,其作用在前面章节中已经详细描述过;
另外两个文件是光盘引导需要的,不动它。
其余的*.msg文件是一些引导过程中可能会显示的文本信息,可以不管;splas
h.lss是引导时显示的一个图片,也可以不管。
下面来看看isolinux.cfg的内容。
default linux
prompt 1
timeout 600
display boot.msg
F1 boot.msg
F2 options.msg
F3 general.msg
F4 param.msg
F5 rescue.msg
F7 snake.msg
label linux
kernel vmlinuz
append initrd=initrd.img
label text
kernel vmlinuz
append initrd=initrd.img text
label expert
kernel vmlinuz
append expert initrd=initrd.img
label ks
kernel vmlinuz
append ks initrd=initrd.img
label lowres
kernel vmlinuz
append initrd=initrd.img lowres
这个配置文件的语法结构跟其他引导器的配置文件,比如grub.conf或者lilo.
conf,非常类似,比较好懂。如果再对照光盘引导安装Redhat的实际过程看一下,
基本上不需要什么解释就可以理解。其中以label开头的每个小节是一种引导配置,
缺省(或者timeout后)以label linux引导。每个配置的第一句,kernel指定引导
所用的内核,第二句append指定内核引导参数(主要说明initrd文件的位置,以及
其他参数)。
8.3 制作步骤
1) 复制boot.iso,用WinISO打开;
2) 解放出isolinux.cfg文件,并从光盘映像中删除之;
3) 修改配置文件isolinux.cfg;
4) 制作bzImage和initrd.img,这两个文件用于光盘引导至一个比较正常的Linux系
统下,包括常用的工具软件和服务,我们要在这个环境下进行产品的安装;
5) 将修改后的isolinux.cfg以及initrd.img,bzImage加入光盘;
6) 准备一个setup目录,将产品安装需要的文件和脚本放在这个位置,然后将setu
p目录加入光盘;
7) 删除没有用到的文件,也可以不管,除非加入文件时遇到冲突,必须先把原来光
盘上的文件删除或者改名;
8) 存盘退出,刻录光盘。
下面是一个修改后的isolinux.cfg配置文件的例子,很简单:
default linux
prompt 1
timeout 600
label linux
kernel bzImage
append initrd=initrd.img ramdisk_size=65536 root=/dev/ram0


9 源代码(配置文件和脚本)
9.1 lips.conf
9.2 mklips.sh

??
??
??
??
National Storage System Lab.
46 · 46

--
.-.
/v L I N U X
// \
/( ) >The World is Linux Powered<
^^-^^

※ 修改:·dot 於 10月11日22:46:55 修改本文·[FROM: 202.114.61.57]
※ 来源:·武汉白云黄鹤
人类的智慧来至与工具的使用。

大概在5个月前,拿到了一个居牛的产品来分析,虽然时一个独立的产品,但是功能与配置都相当的复杂,着手之后,发现用Word,Excel,Visio等微软的工具都没有办法做比较完毕的记录。郁闷之后转到了MindManager,用这个思维管理工具才算搞定,理清楚了其中各个节点的关系。如果用其他的工具,恐怕写的人的累死,看的人也会累死。

最近,做一个产品设计,仍然时结构复杂,用线性思维来进行描述话恐怕也会一团遭,拿出MindManager,把设计方案揉碎了重新整理,终于得到了我想要的东西。不仅自己明白,还做到了方便交流。好东西!

C语言 结构对齐

在上次的一次结构设计中招到PK后,回家恶补了结构对齐,参考了多人的Blog和一些专业论坛,由于原始内容比较乱且我经过重新整理,就不给出下面内容的原始出处。

什么是内存对齐?

考虑下面的结构:
struct foo
{
char c1;
short s;
char c2;
int i;
};

假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
c1 00000000, s 00000001, c2 00000003, i 00000004。

可是,我们在Visual c/c++ 6中写一个简单的程序:

struct foo a;
printf("c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&amp;a);
运行,输出:
c1 00000000, s 00000002, c2 00000004, i 00000008。

为什么会这样?这就是内存对齐而导致的问题。

在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间;各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。

例如,下面的结构各成员空间分配情况
struct test
{
char x1; //0
short x2; //2
float x3; //4
char x4; //8
};
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

为什么会有内存对齐?


以下内容节选自《Intel Architecture 32 Manual》。
字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。

编译器对内存对齐的处理


缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,
struct foo
{
char c1;
short s;
char c2;
int i;
};
用上面的程序输出就变成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。

如何避免内存对齐的影响


那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:

struct bar
{
char c1;
char c2;
short s;
int i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。

这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。

比如,foo结构,我们的DLL使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。
而第三方将对齐选项关闭,导致
c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。

如何使用c/c++中的对齐选项


编译器默认都是8字节对齐,更改C编译器的缺省分配策略,一般地,可以通过下面的两种方法改变缺省的对界条件:
  · 使用伪指令#pragma pack ([n])
  · 在编译时使用命令行参数
#pragma pack ([n])伪指令允许你选择编译器为数据分配空间所采取的对界策略:
  
例如,在使用了#pragma pack (1)伪指令后,test结构各成员的空间分配情况就是按照一个字节对齐了
#pragma pack(push) //保存对齐状态
#pragma pack(1)
#pragma pack(pop)
vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min ( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。

要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意义和/Zpn选项相同。比如:

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()

栈内存对齐


我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。验证代码如下:

#include <stdio.h>

struct foo
{
char c1;
short s;
char c2;
int i;
};

struct bar
{
char c1;
char c2;
short s;
int i;
};

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()


int main(int argc, char* argv[])
{
char c1;
short s;
char c2;
int i;

struct foo a;
struct bar b;
struct foo_pack p;

printf("stack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned int)(void*)&s - (unsigned int)(void*)&i,
(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned int)(void*)&i - (unsigned int)(void*)&amp;i);

printf("struct foo c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&amp;a);

printf("struct bar c1 %p, c2 %p, s %p, i %pn",
(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.i - (unsigned int)(void*)&amp;b);

printf("struct foo_pack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.i - (unsigned int)(void*)&amp;p);

printf("sizeof foo is %dn", sizeof(foo));
printf("sizeof bar is %dn", sizeof(bar));
printf("sizeof foo_pack is %dn", sizeof(foo_pack));

return 0;
}


内存对齐与ANSI C中struct型数据的内存布局

  当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密。
  
  首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址。比如有这样一个结构体:
  
  struct vector{int x,y,z;} s;
  int *p,*q,*r;
  struct vector *ps;
  
  p = &s.x;
  q = &s.y;
  r = &s.z;
  ps = &s;
  
  assert(p < q);
  assert(p < r);
  assert(q < r);
  assert((int*)ps == p);
  // 上述断言一定不会失败
  
  这时,有朋友可能会问:"标准是否规定相邻字段在内存中也相邻?"。 唔,对不起,ANSI C没有做出保证,你的程序在任何时候都不应该依赖这个假设。那这是否意味着我们永远无法勾勒出一幅更清晰更精确的结构体内存布局图?哦,当然不是。不过先让我们从这个问题中暂时抽身,关注一下另一个重要问题————内存对齐。
  
  许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。
  
  现在回到我们关心的struct上来。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。嗯?填充区?对,这就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身有什么对齐要求吗?有的,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,内存对齐编译选项是"默认",即不指定/Zp与/pack选项):
  
  typedef struct ms1
  {
  char a;
  int b;
  } MS1;
  
  假设MS1按如下方式内存布局(本文所有示意图中的内存地址从左至右递增):

  _____________________________

  |    |          |

  |  a  |    b     |

  |    |          |

  +---------------------------+

  Bytes:  1       4

  
  因为MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSI C标准,MS1对象的首地址一定是4(int类型的对齐模数)的倍数。那么上述内存布局中的b字段能满足int类型的对齐要求吗?嗯,当然不能。如果你是编译器,你会如何巧妙安排来满足CPU的癖好呢?呵呵,经过1毫秒的艰苦思考,你一定得出了如下的方案:
  
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+
  Bytes:  1     3       4
  
  这个方案在a与b之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:
  
typedef struct ms2
{
  int a;
  char b;
} MS2;
  
  或许你认为MS2比MS1的情况要简单,它的布局应该就是
  
  _______________________   |       |    |   |   a    |  b  |   |       |    |   +---------------------+   Bytes:   4      1
  
  因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。嗯,分析得有道理,可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:
  
  |<-  array[1]   ->|<-  array[2]   ->|<- array[3] .....      __________________________________________________________   |       |    |       |   |   |   a    |  b  |   a    |  b |.............   |       |    |       |   |   +----------------------------------------------------------   Bytes: 4     1     4      1
  
  当数组首地址是4字节对齐时,array[1].a也是4字节对齐,可是array[2].a呢?array[3].a ....呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:
  
  ___________________________________   |       |    |\\\\\|   |   a    |  b  |\padding\|   |       |    |\\\\\|   +---------------------------------+   Bytes:   4      1     3
  
  现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0,b的偏移是4。
  
  好的,现在你已经掌握了结构体内存布局的基本准则,尝试分析一个稍微复杂点的类型吧。
  
typedef struct ms3
{
  char a;
  short b;
  double c;
} MS3;
  
  我想你一定能得出如下正确的布局图:
  
  padding   |   _____v_________________________________   |  ||   |\\\\|        |   | a || b |padding|    c    |   |  ||   |\\\\|        |   +-------------------------------------+   Bytes: 1 1  2    4      8
  
  sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接着看看结构体中字段还是结构类型的情况:
  
typedef struct ms4 {   char a;   MS3 b; } MS4;
  
  MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是:
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+   Bytes:  1     7       16
  
  显然,sizeof(MS4)等于24,b的偏移等于8。
  
  在实际开发中,我们可以通过指定/Zp编译选项来更改编译器的对齐规则。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告诉编译器最大对齐模数是n。在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。事实上,VC7.1的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPS和Alpha平台上用/Zp1和/Zp2选项,也不要在16位平台上指定/Zp4和/Zp8(想想为什么?)。改变编译器的对齐选项,对照程序运行结果重新分析上面4种结构体的内存布局将是一个很好的复习。

星期四, 四月 12, 2007

LEX 从易到难

Lex 入门

First!
lex程序的结构是这样的!
定义
%%
规则
%%
用户代码

一个 Lex 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。 这些段以%%来分界。 下面是一个行数与字数的统计工具。

int num_lines = 0, num_chars = 0;
%%
n ++num_lines; ++num_chars;
. ++num_chars;

%%
main()
{
yylex();
printf( "# of lines = %d, # of chars = %dn",
num_lines, num_chars );
}

Second!
对First内容的回顾
C 和 Lex 的全局声明
这一段中我们可以增加 C 变量声明。这里我们将为字数统计程序声明一个整型变量,来保存程序统计出来的字数。我们还将进行 Lex 的标记声明。

字数统计程序的声明
%{
int wordCount = 0;
%}
chars [A-za-z_'."]
numbers ([0-9])+
delim [" "nt]
whitespace {delim}+
words {chars}+
%%

两个百分号标记指出了 Lex 程序中这一段的结束和三段中第二段的开始。

Lex 的模式匹配规则
让我们看一下 Lex 描述我们所要匹配的标记的规则。(我们将使用 C 来定义标记匹配后的动作。)继续看我们的字数统计程序,下面是标记匹配的规则。
字数统计程序中的 Lex 规则
{words} { wordCount++; /*
increase the word count by one*/ }
{whitespace} { /* do
nothing*/ }
{numbers} { /* one may
want to add some processing here*/ }
%%
C 代码
Lex 编程的第三段,也就是最后一段覆盖了 C 的函数声明(有时是主函数)。注意这一段必须包括 yywrap() 函数。 Lex 有一套可供使用的函数和变量。 其中之一就是 yywrap。一般来说,yywrap() 的定义如下例。我们将在 高级 Lex 中探讨这一问题。
字数统计程序的 C 代码段
void main()
{
yylex(); /* start the
analysis*/
printf(" No of words:
%dn", wordCount);
}
int yywrap()
{
return 1;
}

Lex 编程的基本元素就这样搞定了,它将帮助你编写简单的词法分析程序。
Third
高级Lex
Lex 有几个函数和变量提供了不同的信息,可以用来编译实现复杂函数的程序。下表中列出了一些变量和函数,以及它们的使用。 详尽的列表请参考 Lex 手册。
Lex 变量
yyin FILE* 类型。 它指向 lexer 正在解析的当前文件。
yyout FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。
yytext 匹配模式的文本存储在这一变量中(char*)。
yyleng 给出匹配模式的长度。
yylineno 提供当前的行数信息。(lexer不一定支持。)

Lex 函数
yylex() 这一函数开始分析。 它由 Lex 自动生成。
yywrap() 这一函数在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。最后,yywrap() 可以返回 1 来表示解析的结束。
yyless(int n) 这一函数可以用来送回除了前�n? 个字符外的所有读出标记。
yymore() 这一函数告诉 Lexer 将下一个标记附加到当前标记后。
到此为止,可能你看到lex程序还会范晕,没关系,下面我们接着来,分析一个类pascal语法的极简析器!
/* 这个就是注释了*/
/* scanner for a toy Pascal-like language */
申明部分开始
%{ 内的东西会原封不动地出现在输出文件中 }%
%{
/* need this for the call to atof() below */
#include <math.h>
%}
DIGIT [0-9]
ID [a-z][a-z0-9]*
%%
模式部分开始
{DIGIT}+ {
printf( "An integer: %s (%d)n", yytext,
atoi( yytext ) );
}
{DIGIT}+"."{DIGIT}* {
printf( "A float: %s (%g)n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function {
printf( "A keyword: %sn", yytext );
}
{ID} printf( "An identifier: %sn", yytext );
"+"|"-"|"*"|"/" printf( "An operator: %sn", yytext );
"{"[^}n]*"}" /* eat up one-line comments */
[ tn]+ /* eat up whitespace */
. printf( "Unrecognized character: %sn", yytext );
%%
补充部分开始
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* skip over program name */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
想要真正了解lex, [[正则表达式]] 是关键!
Four
yytext 匹配模式的文本存储变量, 可以通过在申明阶段使用%pointer或%array来控制是一个字符指针还是一个字符数组。指针模式与数组模式各有特点,导致在yytex申明上也不一样,具体请参考lex手册!
在模式阶段中
模式 动作
[ t]+ putchar( ' ' );
[ t]+$ /* ignore this token */
模式部分是正则表达式,动作部分是处理方法,动作部分如果时{开头,那么,动作将会持续到},如果动作中出现了括号{},开始采用 %{ %}来表示动作去区段。动作部分如果时 |,就表示与下一条规则执行相同的动作。
好的,我们来看一个更为实用一点的lex程序。
我们先定义三个动作:
ECHO 将yytext输出
BEGIN 开始一个条件处理块
REJECT 指示简析器对当前规则不做处理,而是采用第二匹配规则。
int word_count = 0;
%%
frob special(); REJECT;
[^ tn]+ ++word_count;
如果frob没有REJECT动作,frob将不会被计数,因为解析器在通常情况下,每个被匹配的对象只会对一个动作生效,多个REJECT也是允许的,会寻找下一个最配的规则来做处理。所以,下面的规则会把输入的"abcd"处理后输出"abcdabcaba".
%%
a |
ab |
abc |
abcd ECHO; REJECT;
.|n /* eat up any unmatched character */

`yymore()' 告诉解析器下一次匹配的规则,满足的部分将会添加到当前yytext值得后面而不是替换它。 例如,指定的输入"mega-kludge"经过下面的程序处理后将会输出"mega-mega-kludge"。
%%
mega- ECHO; yymore();
kludge ECHO;
第一个 "mega-" 被满足并且输出. 然后 "kludge" 满足, 但是并没有替换之前的"mega-"而是"kludge"附加到他的后面,然后输出的其实是"mega-kludge".
yymore()需要两件事情需要注意。第一,yymnore()依赖于表现当前匹配项的长度yyleng的值,所以使用yymore不允许改变yyleng的值。第二,yymore()的使用会使解析器付出一点点性能的代价。
有yymore()就有yyless()
yyless(n) 返回当前匹配项除了开始的n个字符内的所有的内容到输入缓存区,解析器处理下一个匹配时,它们将会被重新解析。yyless将会导致yytext与yyleng的调整。(yyleng将会等于=n) 如输入"foobar"被下面的程序处理后,将会输出"boobarbar". 因为前n=3个字符foo外的字符bar被重新返回到输入缓存区了。
%%
foobar ECHO; yyless(3);
[a-z]+ ECHO;
参数0对于yyless将会导致整个当前匹配将会被重新解析。除非你改变了解析器本来的处理流程(如使用begin),这将会导致循环结束。需要注意的是,yyless是一个宏,并且在flex输入文件中使用,不能在其他源文件中使用。
unput(c) 将字符c放回到输入流中,该字符可以重新被解析。下面的动作将当前的匹配值附上括号后重新进行匹配。
{
int i;
/* Copy yytext because unput() trashes yytext */
char *yycopy = strdup( yytext );
unput( ')' );
for ( i = yyleng - 1; i >= 0; --i )
unput( yycopy[i] );
unput( '(' );
free( yycopy );
}
注意: 由于每次unput()将指定的字符添加到输入源的开头,所以将字符串添加到输入源开头必须从后道前处理。一个比较重要的潜在问题是使用unput()的时候,如果采用了%pointer指针模式保存yytext,unput会破坏yytext的内容,从最右边的字符开始将会破坏左边的一个字符。如果在unput()后要用到yytext,你首先必须复制一份yytext,或者用%array模式来保存yytext. 最后你不能放一个EOF去试图标志输入流的结束。
input 从输入源中读取下一个字符。例如,下面有的例子将会吃掉C语言注释
%%
"/*" {
register int c;
for ( ; ; )
{
while ( (c = input()) != '*' &&
c != EOF )
; /* eat up text of comment */
if ( c == '*' )
{
while ( (c = input()) == '*' )
;
if ( c == '/' )
break; /* found the end */
}
if ( c == EOF )
{
error( "EOF in comment" );
break;
}
}
}
注意: 如果简析器采用用C++编译,input()被yyinput()的替代,因为input()与C++中的流名称input冲突。
YY_FLUSH_BUFFER 刷新解析器内部缓存以便于下一次的匹配工作,首先它会使用YY_INPUT填充缓存区。这是通用yy_flush_buffer()的一个特例,将会在多输入缓存中描述。
yyterminate()可以在动作内部返回描述区域中使用,它将终止解析器并返回0给解析器调用者,表示操作完成。缺省情况下,到达文件结束位置也会被调用,它是一个宏,并且可能重定义。

Lex进阶

模式
模式在第一阶段或第二个阶段使用,也就是在申明或规则阶段中出现,模式定义了匹配的目标,目标被匹配后将会执行动作。
对于模式不想做太多说明,使用正则表达式定义,可以参看 regex 或 pcre.

开始条件
lex提供了根据条件激活规则的机制。在<sc>前缀的规则将会在解析器在"sc"的开始条件下被匹配。
<STRING>[^"]*        { /* eat up the string body ... */
...
}
将会在启动条件"STRING"的情况下被激活。
<INITIAL,STRING,QUOTE>.        { /* handle an escape ... */
...
}
将会在 "INITIAL", "STRING", "QUOTE"三者之一的条件下被激活。

开始条件在输入源的定义(第一)部分被申明,在‘%s' 或 ’%x'后跟随着名字列表。 %s申明了包含的开始条件,%x申明了排他的开始条件。开始条件被BEGIN动作激活。直到下一个BEGIN动作,满足开始条件名称的规则将会被规则,不满足启动条件的规则将不会被执行。

如果是包含条件,没有开始条件的规则也会被激活执行,如果时排他条件,只有满足开始条件的规则才会被执行。
具有相同排他条件的规则的集合可以使解析器独立于其他的规则。 因此,排他条件可以容易地创建微型解析器处理输入源中的独立与其他部分的一部分(如,注释)。如果对于包含与排他条件还有混淆,可以看下面的例子。
%s example
%%

<example>foo do_something();

bar something_else();

等同于

%x example
%%

<example>foo do_something();

<INITIAL,example>bar something_else();
上面的程序中如果没有<INITIAL,example>,在example条件下bar规则将永远不会被激活。如果使用<example>,将会导致只能在exmaple开始条件下激活,而INITIAL条件下不会被激活。而第一个程序中在任何条件下bar都被会激活。因为第一个程序用example时%s,时包含条件。页可以通过特殊开始条件<*>来配置任何开始条件,上面的程序还可以写为:
%x example
%%

<example>foo do_something();

<*>bar something_else();
缺省规则(显示任何未被匹配的字符)在开始条件下仍然生效。等同于:
<*>.|\n     ECHO;
‘BEGIN(0)’在无开始条件的规则激活条件下返回原始状态,这个状态同于开始条件下的'INITIAL',所以‘BEGIN(INITIAL)'等同于’BEGIN(0)'。
BEGIN行为在规则部分的开头是默认的代码(BEGIN actions can also be given as indented code at the beginning of the rules section.请翻译) 例如,下面的代码将会仅需SPECIAL开始条件,不管合适yylex()被调用并且全局变量enter_special是true。
        int enter_special;

%x SPECIAL
%%
if ( enter_special )
BEGIN(SPECIAL);

<SPECIAL>blahblahblah
...more rules follow...
为了说明开始条件,我们用两种方法处理"123.456".缺省将会被解析为 '123','.','456'三个标记,如果expect-floats后面将会被解析为浮点数 123.456
%{
#include <math.h>
%}
%s expect

%%
expect-floats BEGIN(expect);

<expect>[0-9]+"."[0-9]+ {
printf( "found a float, = %fn",
atof( yytext ) );
}
<expect>n {
/* that's the end of the line, so
* we need another "expect-number"
* before we'll recognize any more
* numbers
*/
BEGIN(INITIAL);
}

[0-9]+ {

printf( "found an integer, = %dn",
atoi( yytext ) );
}

"." printf( "found a dotn" );
下面的代码能够是被C语言注释并且统计行数。
%x comment
%%
int line_num = 1;

"/*" BEGIN(comment);

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(INITIAL);
实际上,编写高速解析程序的办法时在每个规则中做尽可能多的匹配。

This scanner goes to a bit of trouble to match as much text as possible with each rule. In general, when attempting to write a high-speed scanner try to match as much possible in each rule, as it's a big win.

注意: 开始条件的名字实际上时一个整形值并且能够被保存,所以,上面的代码可以扩展为:
%x comment foo
%%
int line_num = 1;
int comment_caller;

"/*" {
comment_caller = INITIAL;
BEGIN(comment);
}

...

<foo>"/*" {
comment_caller = foo;
BEGIN(comment);
}

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(comment_caller);
而且,可能易使用YY_START宏来访问当前的开始条件。如上面的赋值条件可以改写为
comment_caller = YY_START
YYSTATE是YY_START的别名(因为AT&T lex使用了YYSTATE)。
注意 开始条件没有他们的名字空间; %s 与 %x 申明与 #define形式一样。

到这里,时一个使用排他开始条件如何匹配C风格的引用字符串的处理。包含的扩展的转义,但不包括检查,因为代码太长。
%x str

%%
char string_buf[MAX_STR_CONST];
char *string_buf_ptr;

" string_buf_ptr = string_buf; BEGIN(str);

<str>" { /* saw closing quote - all done */
BEGIN(INITIAL);
*string_buf_ptr = '0';
/* return string constant token type and
* value to parser
*/
}

<str>n {
/* error - unterminated string constant */
/* generate error message */
}

<str>\[0-7]{1,3} {
/* octal escape sequence */
int result;

(void) sscanf( yytext + 1, "%o", &result );

if ( result > 0xff )
/* error, constant is out-of-bounds */

*string_buf_ptr++ = result;
}

<str>\[0-9]+ {
/* generate error - bad escape sequence; something
* like '48' or '0777777'
*/
}

<str>\n *string_buf_ptr++ = 'n';
<str>\t *string_buf_ptr++ = 't';
<str>\r *string_buf_ptr++ = 'r';
<str>\b *string_buf_ptr++ = 'b';
<str>\f *string_buf_ptr++ = 'f';

<str>\(.|n) *string_buf_ptr++ = yytext[1];

<str>[^\n"]+ {
char *yptr = yytext;

while ( *yptr )
*string_buf_ptr++ = *yptr++;
}
通常,如上面的例子中所看到你,会有许多相同开始条件的处理。开始条件范围可以简化重复操作。

<SCs>{
}

SCs 是一个或开始条件的列表。在这个开始条件范围内,每个规则将会自动具有前缀 `<SCs>' 直到 `}' 与开始的 `{' 匹配. 例如

<ESC>{
"\n" return 'n';
"\r" return 'r';
"\f" return 'f';
"\0" return '0';
}

等价于

<ESC>"\n"  return 'n';
<ESC>"\r" return 'r';
<ESC>"\f" return 'f';
<ESC>"\0" return '0';

开始条件页可以嵌套,下面时三个管理开始条件堆栈的参数。

`void yy_push_state(int new_state)'
将当前的开始条件压栈,切换到 new_state 与使用 `BEGIN new_state'类似。
`void yy_pop_state()'
从栈顶弹出,类似于 BEGIN.
`int yy_top_state()'
返回栈顶值,不改变栈内容。

开始条件栈动态增长,没有固定限制,如果内容用尽,程序竟会终止。

为了使用开始条件栈,需要使用 `%option stack' 指令。



多输入缓存区


一些允许include文件解析器的解析器要求从几个输入流中读取内容。YY_INPUT只在结束缓存时被调用,碰到 include 后需要切换输入源,而解析一个描述也许需要很长时间。为了解决此类问题,解析器提供了创建并在多个输入缓存中创建的机制。输入缓存可以通过下面的方式创建:

YY_BUFFER_STATE yy_create_buffer( FILE *file, int size )

参数为与缓存关联的输入文件指针,以及足够的可维持size字符(如果不确定,size可以使用YY_BUF_SIZE)。返回一个YY_BUFFER_STATE,可以传递到其他的处理过程。YY_BUFFER_STATE是一个不可见结构yy_buffer_state的指针,所以可以安全地使用`((YY_BUFFER_STATE) 0)'来初始化YY_BUFFER_STATE,如果你愿意,你可以在解析器之外的源程序中引用这个不透明结构来正确的申明输入缓存。可以通过下面的参数来选择一个缓存区。

void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer )

切换解析器的输入缓存将会导致记接下来的匹配项来自于新的缓存中。yy_switch_to_buffer可能出现在yywrap中为继续解析做准备,替换打开一个新的文件并执行yyin. 通过yy_switch_to_buffer 或 yywrap切换输入源不改变开始条件。


void yy_delete_buffer( YY_BUFFER_STATE buffer )

用于收回与缓存关联的空间。你可以使用下面的函数清空当前内容:
void yy_flush_buffer( YY_BUFFER_STATE buffer )

此函数废弃缓存内容,下一个解析器试图匹配一个内容时将会使用YY_INPUT来更新缓存区。

`yy_new_buffer()' 是 `yy_create_buffer()' 的一个别名,用于提供C++使用new 与 delete操作创建与销毁动态对象的兼容性。

最后, YY_CURRENT_BUFFER 宏返回 YY_BUFFER_STATE 指针,表示当前的缓存。

这里是一个扩展include使用的一个解析器 (`<<EOF>>' 特性将会在以后讨论):

/* "incl" 状态用于获取include的文件名 */
%x incl

%{
#define MAX_INCLUDE_DEPTH 10
YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH];
int include_stack_ptr = 0;
%}

%%
include BEGIN(incl);

[a-z]+ ECHO;
[^a-zn]*n? ECHO;

<incl>[ t]* /* eat the whitespace */
<incl>[^ tn]+ { /* got the include file name */
if ( include_stack_ptr >= MAX_INCLUDE_DEPTH )
{
fprintf( stderr, "Includes nested too deeply" );
exit( 1 );
}

include_stack[include_stack_ptr++] =
YY_CURRENT_BUFFER;

yyin = fopen( yytext, "r" );

if ( ! yyin )
error( ... );

yy_switch_to_buffer(
yy_create_buffer( yyin, YY_BUF_SIZE ) );

BEGIN(INITIAL);
}

<<EOF>> {
if ( --include_stack_ptr < 0 )
{
yyterminate();
}

else
{
yy_delete_buffer( YY_CURRENT_BUFFER );
yy_switch_to_buffer(
include_stack[include_stack_ptr] );
}
}

提供三个过程来实现内存字符串而不是文件输入缓存的解析。它们都要创建一个输入缓存来解析字符串,并且返回YY_BUFFER_STATE (可以在完成解析后用 `yy_delete_buffer()' 删除).,也可以通过`yy_switch_to_buffer()'来切换, 下一次调用`yylex()' 将会解析字符串。
`yy_scan_string(const char *str)' 解析0结尾字符串。
`yy_scan_bytes(const char *bytes, int len)' 解析bytes开始的len个字符(可能包含 0 字符)

注意,上面的两个函数会创建字符串或字节串的副本。(这也许时期望的,因为`yylex()' 会修改被解析缓存的内容) 可以使用下面的方式来拒绝使用副本:
`yy_scan_buffer(char *base, yy_size_t size)'
将会从base开始解析,包含size个字节, 最后的两个字节必须是 YY_END_OF_BUFFER_CHAR (ASCII NUL)。他们不会被解析, 解析范围从 `base[0]' 到 `base[size-2]'(包含)。如果你没能按照这种规定使用base(如,忘记了最后的两个YY_END_OF_BUFFER_CHAR字节), `yy_scan_buffer()' 将会返回空指针而不创建YY_BUFFER_STATE。yy_size_t类型是个整型,可以转化为整数来反映buffer的长度。


文件结束规则

特殊规则 "<<EOF>>" 只是规则在文件结束位置发生且yywrap()返回非0值。(如,没有更多的文件要处理). 这个动作必须完成下面四件事情之一:
赋值给yyin一个新的文件 (早期版本的flex, 此操作后必须调用特殊动作 YY_NEW_FILE; 这个操作已经不需要了);
执行一个返回申明;
执行一个特殊的`yyterminate()' 动作;
或者使用`yy_switch_to_buffer()' 切换到一个新的输入缓存区.

<<EOF>> 不能与其他模式一起使用;它也许仅在开始条件列表申明。如果指定了不合法 <<EOF>> 规则, 它将会应用到所有的开始条件而不仅是 <<EOF>> 动作. 指定 <<EOF>> 规则仅在 initial 开始条件下匹配,就是用:
<INITIAL><<EOF>>

下面的规则可以发现象不关闭的注释类的问题。
%x quote
%%

...other rules for dealing with quotes...

<quote><<EOF>> {
error( "unterminated quote" );
yyterminate();
}
<<EOF>> {
if ( *++filelist )
yyin = fopen( *filelist, "r" );
else
yyterminate();
}