经验 |
针对好多Linux 爱好者对内核很有兴趣却无从下口,本文旨在介绍一种解读linux内核源码的入门方法,而不是解说linux复杂的内核机制;
一.核心源程序的文件组织:
1.Linux核心源程序通常都安装在/usr/src/linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如2.0.30)都是一个稳定地发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心。
本文基于稳定的2.2.5源代码,第二部分的实现平台为 Redhat Linux 6.0。
2.核心源程序的文件按树形结构进行组织,在源程序树的最上层你会看到这样一些目录:
●Arch :arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录;
●Include: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
●Init: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的一个非常好的起点。
●Mm :这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/*/mm/,例如arch/i386/mm/Fault.c
●Kernel:主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/*/kernel中;
●Drivers: 放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化网络,因为安装nfs文件系统的时候需要网络其他: 如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码; Fs ,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2;
●Scripts, 此目录包含用于配置核心的脚本文件等。
一般,在每个目录下,都有一个 .depend 文件和一个 Makefile 文件,这两个文件都是编译时使用的辅助文件,仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助;而且,在有的目录下还有Readme 文件,它是对该目录下的文件的一些说明,同样有利于我们对内核源码的理解;
二.解读实战:为你的内核增加一个系统调用
虽然,Linux 的内核源码用树形结构组织得非常合理、科学,把功能相关联的文件都放在同一个子目录下,这样使得程序更具可读性。然而,Linux 的内核源码实在是太大而且非常复杂,即便采用了很合理的文件组织方法,在不同目录下的文件之间还是有很多的关联,分析核心的一部分代码通常会要查看其它的几个相关的文件,而且可能这些文件还不在同一个子目录下。
体系的庞大复杂和文件之间关联的错综复杂,可能就是很多人对其望而生畏的主要原因。当然,这种令人生畏的劳动所带来的回报也是非常令人着迷的:你不仅可以从中学到很多的计算机的底层的知识(如下面将讲到的系统的引导),体会到整个操作系统体系结构的精妙和在解决某个具体细节问题时,算法的巧妙;而且更重要的是:在源码的分析过程中,你就会被一点一点地、潜移默化地专业化;甚至,只要分析十分之一的代码后,你就会深刻地体会到,什么样的代码才是一个专业的程序员写的,什么样的代码是一个业余爱好者写的。
为了使读者能更好的体会到这一特点,下面举了一个具体的内核分析实例,希望能通过这个实例,使读者对 Linux的内核的组织有些具体的认识,从中读者也可以学到一些对内核的分析方法。
以下即为分析实例:
【一】操作平台:
硬件:cpu intel Pentium II ;
软件:Redhat Linux 6.0; 内核版本2.2.5【二】相关内核源代码分析:
1.系统的引导和初始化:Linux 系统的引导有好几种方式:常见的有 Lilo, Loadin引导和Linux的自举引导
(bootsect-loader),而后者所对应源程序为arch/i386/boot/bootsect.S,它为实模式的汇编程序,限于篇幅在此不做分析;无论是哪种引导方式,最后都要跳转到 arch/i386/Kernel/setup.S, setup.S主要是进行时模式下的初始化,为系统进入保护模式做准备;此后,系统执行 arch/i386/kernel/head.S (对经压缩后存放的内核要先执行 arch/i386/boot/compressed/head.S); head.S 中定义的一段汇编程序setup_idt ,它负责建立一张256项的 idt 表(Interrupt Descriptor Table),此表保存着所有自陷和中断的入口地址;其中包括系统调用总控程序 system_call 的入口地址;当然,除此之外,head.S还要做一些其他的初始化工作;
2.系统初始化后运行的第一个内核程序asmlinkage void __init start_kernel(void) 定义在/usr/src/linux/init/main.c中,它通过调用usr/src/linux/arch/i386/kernel/traps.c 中的一个函数
void __init trap_init(void) 把各自陷和中断服务程序的入口地址设置到 idt 表中,其中系统调用总控程序system_cal就是中断服务程序之一;void __init trap_init(void) 函数则通过调用一个宏
set_system_gate(SYSCALL_VECTOR,&system_call); 把系统调用总控程序的入口挂在中断0x80上;
其中SYSCALL_VECTOR是定义在 /usr/src/linux/arch/i386/kernel/irq.h中的一个常量0x80; 而 system_call 即为中断总控程序的入口地址;中断总控程序用汇编语言定义在/usr/src/linux/arch/i386/kernel/entry.S中;
3.中断总控程序主要负责保存处理机执行系统调用前的状态,检验当前调用是否合法, 并根据系统调用向量,使处理机跳转到保存在 sys_call_table 表中的相应系统服务例程的入口; 从系统服务例程返回后恢复处理机状态退回用户程序;
而系统调用向量则定义在/usr/src/linux/include/asm-386/unistd.h 中;sys_call_table 表定义在/usr/src/linux/arch/i386/kernel/entry.S 中; 同时在 /usr/src/linux/include/asm-386/unistd.h 中也定义了系统调用的用户编程接口;
4.由此可见 , linux 的系统调用也象 dos 系统的 int 21h 中断服务, 它把0x80 中断作为总的入口, 然后转到保存在 sys_call_table 表中的各种中断服务例程的入口地址 , 形成各种不同的中断服务;
由以上源代码分析可知, 要增加一个系统调用就必须在 sys_call_table 表中增加一项 , 并在其中保存好自己的系统服务例程的入口地址,然后重新编译内核,当然,系统服务例程是必不可少的。
由此可知在此版linux内核源程序中,与系统调用相关的源程序文件就包括以下这些:
1.arch/i386/boot/bootsect.S
2.arch/i386/Kernel/setup.S
3.arch/i386/boot/compressed/head.S
4.arch/i386/kernel/head.S
5.init/main.c
6.arch/i386/kernel/traps.c
7.arch/i386/kernel/entry.S
8.arch/i386/kernel/irq.h
9.include/asm-386/unistd.h
当然,这只是涉及到的几个主要文件。而事实上,增加系统调用真正要修改文件只有include/asm-386/unistd.h和arch/i386/kernel/entry.S两个;
【三】 对内核源码的修改:
1.在kernel/sys.c中增加系统服务例程如下:
asmlinkage int sys_addtotal(int numdata)
{
int i=0,enddata=0;
while(i<=numdata)
enddata+=i++;
return enddata;
}
该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中,只是要在相应文件中作必要的说明;
2.把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:
arch/i386/kernel/entry.S 中的最后几行源代码修改前为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
.rept NR_syscalls-190
.long SYMBOL_NAME(sys_ni_syscall)
.endr
修改后为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
/* add by I */
.long SYMBOL_NAME(sys_addtotal)
.rept NR_syscalls-191
.long SYMBOL_NAME(sys_ni_syscall)
.endr
3. 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用:
增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188
#define __NR_putpmsg 189
#define __NR_vfork 190
/* add by I */
#define __NR_addtotal 191
4.测试程序(test.c)如下:
#include
#include
_syscall1(int,addtotal,int, num)
main()
{
int i,j;
do
printf("Please input a number\n");
while(scanf("%d",&i)==EOF);
if((j=addtotal(i))==-1)
printf("Error occurred in syscall-addtotal();\n");
printf("Total from 0 to %d is %d \n",i,j);
}
对修改后的新的内核进行编译,并引导它作为新的操作系统,运行几个程序后可以发现一切正常;在新的系统下对测试程序进行编译(*注:由于原内核并未提供此系统调用,所以只有在编译后的新内核下,此测试程序才能可能被编译通过),运行情况如下:
$gcc -o test test.c
$./test
Please input a number
36
Total from 0 to 36 is 666
可见,修改成功;
而且,对相关源码的进一步分析可知,在此版本的内核中,从/usr/src/linux/arch/i386/kernel/entry.S
文件中对 sys_call_table 表的设置可以看出,有好几个系统调用的服务例程都是定义在/usr/src/linux/kernel/sys.c 中的同一个函数:
asmlinkage int sys_ni_syscall(void)
{
return -ENOSYS;
}
例如第188项和第189项就是如此:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
... ...
而这两项在文件 /usr/src/linux/include/asm-386/unistd.h 中却申明如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188 /* some people actually want streams */
#define __NR_putpmsg 189 /* some people actually want streams */
#define __NR_vfork 190
由此可见,在此版本的内核源代码中,由于asmlinkage int sys_ni_syscall(void) 函数并不进行任何操作,所以包括 getpmsg, putpmsg 在内的好几个系统调用都是不进行任何操作的,即有待扩充的空调用; 但它们却仍然占用着sys_call_table表项,估计这是设计者们为了方便扩充系统调用而安排的; 所以只需增加相应服务例程(如增加服务例程getmsg或putpmsg),就可以达到增加系统调用的作用。
Andries Brouwer, aeb@cwi.nl 2001-01-01
A program
---------------------------------------------------------------------------------------------------
#include <unistd.h>
#include <fcntl.h>
int main(){
int fd;
char buf[512];
fd = open("/dev/hda", O_RDONLY);
if (fd >= 0)
read(fd, buf, sizeof(buf));
return 0;
}
---------------------------------------------------------------------------------------------------
This little program opens the block special device referring to the first IDE disk, and if the open succeeded reads the first sector. What happens in the kernel? Let us read 2.4.0 source.
---------------------------------------------------------------------------------------------------
int sys_open(const char *filename, int flags, int mode) {
char *tmp = getname(filename);
int fd = get_unused_fd();
struct file *f = filp_open(tmp, flags, mode);
fd_install(fd, f);
putname(tmp);
return fd;
}
---------------------------------------------------------------------------------------------------
The routine getname() is found in fs/namei.c. It copies the file name from user space to kernel space:
---------------------------------------------------------------------------------------------------
#define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL)
#define putname(name) kmem_cache_free(names_cachep, (void *)(name))
char *getname(const char *filename) {
char *tmp = __getname(); /* allocate some memory */
strncpy_from_user(tmp, filename, PATH_MAX + 1);
return tmp;
}
---------------------------------------------------------------------------------------------------
The routine get_unused_fd() is found in fs/open.c again. It returns the first unused filedescriptor:
---------------------------------------------------------------------------------------------------
int get_unused_fd(void) {
struct files_struct *files = current->files;
int fd = find_next_zero_bit(files->open_fds,
files->max_fdset, files->next_fd);
FD_SET(fd, files->open_fds); /* in use now */
files->next_fd = fd + 1;
return fd;
}
---------------------------------------------------------------------------------------------------
Here current is the pointer to the user task struct for the currently executing task.
The routine fd_install() is found in include/linux/file.h. It just stores the information returned by filp_open()
---------------------------------------------------------------------------------------------------
void fd_install(unsigned int fd, struct file *file) {
struct files_struct *files = current->files;
files->fd[fd] = file;
}
---------------------------------------------------------------------------------------------------
So all the interesting work of sys_open() is done in filp_open(). This routine is found in fs/open.c:
---------------------------------------------------------------------------------------------------
struct file *filp_open(const char *filename, int flags, int mode) {
struct nameidata nd;
open_namei(filename, flags, mode, &nd);
return dentry_open(nd.dentry, nd.mnt, flags);
}
---------------------------------------------------------------------------------------------------
The struct nameidata is defined in include/linux/fs.h. It is used during lookups.
---------------------------------------------------------------------------------------------------
struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr last;
};
---------------------------------------------------------------------------------------------------
The routine open_namei() is found in fs/namei.c:
---------------------------------------------------------------------------------------------------
open_namei(const char *pathname, int flag, int mode, struct nameidata *nd) {
if (!(flag & O_CREAT)) {
/* The simplest case - just a plain lookup. */
if (*pathname == '/') {
nd->mnt = mntget(current->fs->rootmnt);
nd->dentry = dget(current->fs->root);
} else {
nd->mnt = mntget(current->fs->pwdmnt);
nd->dentry = dget(current->fs->pwd);
}
path_walk(pathname, nd);
/* Check permissions etc. */
...
return 0;
}
...
}
---------------------------------------------------------------------------------------------------
An inode (index node) describes a file. A file can have several names (or no name at all), but it has a unique inode. A dentry (directory entry)describes a name of a file: the inode plus the pathname used to find it. Avfsmount describes the filesystem we are in.
So, essentially, the lookup part op open_namei() is found in path_walk():
---------------------------------------------------------------------------------------------------
path_walk(const char *name, struct nameidata *nd) {
struct dentry *dentry;
for(;;) {
struct qstr this;
this.name = next_part_of(name);
this.len = length_of(this.name);
this.hash = hash_fn(this.name);
/* if . or .. then special, otherwise: */
dentry = cached_lookup(nd->dentry, &this);
if (!dentry)
dentry = real_lookup(nd->dentry, &this);
nd->dentry = dentry;
if (this_was_the_final_part)
return;
}
}
---------------------------------------------------------------------------------------------------
Here the cached_lookup() tries to find the given dentry in a cache of recently used dentries. If it is not found, the real_lookup() goes to the filesystem, which probably goes to disk, and actually finds the thing.After path_walk() is done, the nd argument contains the required dentry,which in turn has the inode information on the file. Finally we do dentry_open() that initializes a file struct:
---------------------------------------------------------------------------------------------------
struct file *
dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags) {
struct file *f = get_empty_filp();
f->f_dentry = dentry;
f->f_vfsmnt = mnt;
f->f_pos = 0;
f->f_op = dentry->d_inode->i_fop;
...
return f;
}
---------------------------------------------------------------------------------------------------
So far the open. In short: walk the tree, for each component hope the information is in cache, and if not ask the file system. How does this work? Each file system type provides structs super_operations,file_operations, inode_operations, address_space_operations that contain the addresses of the routines that can do stuff. And thus
---------------------------------------------------------------------------------------------------
struct dentry *real_lookup(struct dentry *parent, struct qstr *name, int flags) {
struct dentry *dentry = d_alloc(parent, name);
parent->d_inode->i_op->lookup(dir, dentry);
return dentry;
}
---------------------------------------------------------------------------------------------------
calls on the lookup routine for the specific fiilesystem, as found in the struct inode_operations in the inode of the dentry for the directory in which we do the lookup.
And this file system specific routine must read the disk data and search the directory for the file we are looking for. Good examples of file systems are minix and romfs because they are simple and small. For example,in fs/romfs/inode.c:
---------------------------------------------------------------------------------------------------
romfs_lookup(struct inode *dir, struct dentry *dentry) {
const char *name = dentry->d_name.name;
int len = dentry->d_name.len;
char fsname[ROMFS_MAXFN];
struct romfs_inode ri;
unsigned long offset = dir->i_ino & ROMFH_MASK;
for (;;) {
romfs_copyfrom(dir, &ri, offset, ROMFH_SIZE);
romfs_copyfrom(dir, fsname, offset+ROMFH_SIZE, len+1);
if (strncmp (name, fsname, len) == 0)
break;
/* next entry */
offset = ntohl(ri.next) & ROMFH_MASK;
}
inode = iget(dir->i_sb, offset);
d_add (dentry, inode);
return 0;
}
romfs_copyfrom(struct inode *i, void *dest,
unsigned long offset, unsigned long count) {
struct buffer_head *bh;
bh = bread(i->i_dev, offset>>ROMBSBITS, ROMBSIZE);
memcpy(dest, ((char *)bh->b_data) + (offset & ROMBMASK), count);
brelse(bh);
}
(All complications, all locking, and all error handling deleted.)
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
ssize_t sys_read(unsigned int fd, char *buf, size_t count) {
struct file *file = fget(fd);
return file->f_op->read(file, buf, count, &file->f_pos);
}
---------------------------------------------------------------------------------------------------
That is, the read system call asks the file system to do the reading,starting at the current file position. The f_op field was filled in the dentry_open() routine above with the i_fop field of an inode.
For romfs the struct file_operations is assigned in romfs_read_inode(). For a regular file (case 2) it assigns generic_ro_fops. For a block special file (case 4) it calls init_special_inode() (see devices.c) which assigns
def_blk_fops.
How come romfs_read_inode() was ever called? When the filesystem was mounted, the routine romfs_read_super() was called, and it assigned romfs_ops to the s_op field of the superblock struct.
---------------------------------------------------------------------------------------------------
struct super_operations romfs_ops = {
read_inode: romfs_read_inode,
statfs: romfs_statfs,
};
---------------------------------------------------------------------------------------------------
And the iget() that was skipped over in the discussion above (in romfs_lookup()) finds the inode with given number ino in a cache, and if it cannot be found there creates a new inode struct by calling get_new_inode()(see fs/inode.c):
---------------------------------------------------------------------------------------------------
struct inode * iget(struct super_block *sb, unsigned long ino) {
struct list_head * head = inode_hashtable + hash(sb,ino);
struct inode *inode = find_inode(sb, ino, head);
if (inode) {
wait_on_inode(inode);
return inode;
}
return get_new_inode(sb, ino, head);
}
struct inode *
get_new_inode(struct super_block *sb, unsigned long ino,
struct list_head *head) {
struct inode *inode = alloc_inode();
inode->i_sb = sb;
inode->i_dev = sb->s_dev;
inode->i_ino = ino;
...
sb->s_op->read_inode(inode);
}
---------------------------------------------------------------------------------------------------
So that is how the inode was filled, and we find that in our case (/dev/hda is a block special file) the routine that is called by sys_read is def_blk_fops.read, and inspection of block_dev.c shows that that is the routine block_read():
---------------------------------------------------------------------------------------------------
ssize_t block_read(struct file *filp, char *buf, size_t count, loff_t *ppos) {
struct inode *inode = filp->f_dentry->d_inode;
kdev_t dev = inode->i_rdev;
ssize_t blocksize = blksize_size[MAJOR(dev)][MINOR(dev)];
loff_t offset = *ppos;
ssize_t read = 0;
size_t left, block, blocks;
struct buffer_head *bhreq[NBUF];
struct buffer_head *buflist[NBUF];
struct buffer_head **bh;
left = count; /* bytes to read */
block = offset / blocksize; /* first block */
offset &= (blocksize-1); /* starting offset in block */
blocks = (left + offset + blocksize - 1) / blocksize;
bh = buflist;
do {
while (blocks) {
--blocks;
*bh = getblk(dev, block++, blocksize);
if (*bh && !buffer_uptodate(*bh))
bhreq[bhrequest++] = *bh;
}
if (bhrequest)
ll_rw_block(READ, bhrequest, bhreq);
/* wait for I/O to complete,
copy result to user space,
increment read and *ppos, decrement left */
} while (left > 0);
return read;
}
---------------------------------------------------------------------------------------------------
So the building blocks here are getblk(), ll_rw_block(), and wait_on_buffer().
The first of these lives in fs/buffer.c. It finds the buffer that already contains the required data if we are lucky, and otherwise a buffer that is going to be used.
---------------------------------------------------------------------------------------------------
struct buffer_head * getblk(kdev_t dev, int block, int size) {
struct buffer_head *bh;
int isize;
try_again:
bh = __get_hash_table(dev, block, size);
if (bh)
return bh;
isize = BUFSIZE_INDEX(size);
bh = free_list[isize].list;
if (bh) {
__remove_from_free_list(bh);
init_buffer(bh);
bh->b_dev = dev;
bh->b_blocknr = block;
...
return bh;
}
refill_freelist(size);
goto try_again;
}
---------------------------------------------------------------------------------------------------
The real I/O is started by ll_rw_block(). It lives in drivers/block/ll_rw_blk.c.
---------------------------------------------------------------------------------------------------
ll_rw_block(int rw, int nr, struct buffer_head * bhs[]) {
int i;
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
bh->b_end_io = end_buffer_io_sync;
submit_bh(rw, bh);
}
}
---------------------------------------------------------------------------------------------------
Here bh->b_end_io specifies what to do when I/O is finished. In this case:
---------------------------------------------------------------------------------------------------
end_buffer_io_sync(struct buffer_head *bh, int uptodate) {
mark_buffer_uptodate(bh, uptodate);
unlock_buffer(bh);
}
---------------------------------------------------------------------------------------------------
So, ll_rw_block() just feeds the requests it gets one by one to submit_bh():
---------------------------------------------------------------------------------------------------
submit_bh(int rw, struct buffer_head *bh) {
bh->b_rdev = bh->b_dev;
bh->b_rsector = bh->b_blocknr * (bh->b_size >> 9);
generic_make_request(rw, bh);
}
---------------------------------------------------------------------------------------------------
So, submit_bh() just passes things along to generic_make_request(), the routine to send I/O requests to block devices:
---------------------------------------------------------------------------------------------------
generic_make_request (int rw, struct buffer_head *bh) {
request_queue_t *q;
q = blk_get_queue(bh->b_rdev);
q->make_request_fn(q, rw, bh);
}
---------------------------------------------------------------------------------------------------
Thus, it finds the right queue and calls the request function for that queue.
---------------------------------------------------------------------------------------------------
struct blk_dev_struct {
request_queue_t request_queue;
queue_proc *queue;
void *data;
} blk_dev[MAX_BLKDEV];
request_queue_t *blk_get_queue(kdev_t dev)
{
return blk_dev[MAJOR(dev)].queue(dev);
}
---------------------------------------------------------------------------------------------------
In our case (/dev/hda), the blk_dev struct was filled by hwif_init (from drivers/ide/ide-probe.c):
and this ide_get_queue() is found in drivers/ide/ide.c:
---------------------------------------------------------------------------------------------------
blk_dev[hwif->major].data = hwif;
blk_dev[hwif->major].queue = ide_get_queue;
#define DEVICE_NR(dev) (MINOR(dev) >> PARTN_BITS)
request_queue_t *ide_get_queue (kdev_t dev) {
ide_hwif_t *hwif = (ide_hwif_t *) blk_dev[MAJOR(dev)].data;
return &hwif->drives[DEVICE_NR(dev) & 1].queue;
}
---------------------------------------------------------------------------------------------------
This .queue field was filled by ide_init_queue():
And blk_init_queue() (from ll_rw_blk.c again):
---------------------------------------------------------------------------------------------------
ide_init_queue(ide_drive_t *drive) {
request_queue_t *q = &drive->queue;
q->queuedata = HWGROUP(drive);
blk_init_queue(q, do_ide_request);
}
blk_init_queue(request_queue_t *q, request_fn_proc *rfn) {
...
q->request_fn = rfn;
q->make_request_fn = __make_request;
q->merge_requests_fn = ll_merge_requests_fn;
...
}
---------------------------------------------------------------------------------------------------
Aha, so we found the q->make_request_fn. Here it is:
---------------------------------------------------------------------------------------------------
__make_request(request_queue_t *q, int rw, struct buffer_head *bh) {
/* try to merge request with adjacent ones */
...
/* get a struct request and fill it with device, start,length, ... */
...
add_request(q, req, insert_here);
if (!q->plugged)
q->request_fn(q);
}
add_request(request_queue_t *q, struct request *req,
struct list_head *insert_here) {
list_add(&req->queue, insert_here);
}
---------------------------------------------------------------------------------------------------
When the request has been queued, q->request_fn is called. What is that? We can see it above - it is do_ide_request() and lives in ide.c.
---------------------------------------------------------------------------------------------------
do_ide_request(request_queue_t *q) {
ide_do_request(q->queuedata, 0);
}
ide_do_request(ide_hwgroup_t *hwgroup, int masked_irq) {
ide_startstop_t startstop;
while (!hwgroup->busy) {
hwgroup->busy = 1;
drive = choose_drive(hwgroup);
startstop = start_request(drive);
if (startstop == ide_stopped)
hwgroup->busy = 0;
}
}
ide_startstop_t
start_request (ide_drive_t *drive) {
unsigned long block, blockend;
struct request *rq;
rq = blkdev_entry_next_request(&drive->queue.queue_head);
block = rq->sector;
block += drive->part[minor & PARTN_MASK].start_sect;
SELECT_DRIVE(hwif, drive);
return (DRIVER(drive)->do_request(drive, rq, block));
}
---------------------------------------------------------------------------------------------------
So, in the case of a partitioned disk it is only at this very low level that we add in the starting sector of the partition in order to get an absolute sector.
The first actual port access happened already:
---------------------------------------------------------------------------------------------------
#define SELECT_DRIVE(hwif,drive) \
OUT_BYTE((drive)->select.all,
hwif->io_ports[IDE_SELECT_OFFSET]);
---------------------------------------------------------------------------------------------------
but this do_request function must do the rest. For a disk it is defined in ide-disk.c, in the ide_driver_t idedisk_driver, and the function turns out to be do_rw_disk().
---------------------------------------------------------------------------------------------------
ide_startstop_t
do_rw_disk (ide_drive_t *drive, struct request *rq, unsigned long
block) {
if (IDE_CONTROL_REG)
OUT_BYTE(drive->ctl,IDE_CONTROL_REG);
OUT_BYTE(rq->nr_sectors,IDE_NSECTOR_REG);
if (drive->select.b.lba) {
OUT_BYTE(block,IDE_SECTOR_REG);
OUT_BYTE(block>>=8,IDE_LCYL_REG);
OUT_BYTE(block>>=8,IDE_HCYL_REG);
OUT_BYTE(((block>>8)&0x0f)|drive->select.all,IDE_SELECT_REG);
} else {
unsigned int sect,head,cyl,track;
track = block / drive->sect;
sect = block % drive->sect + 1;
OUT_BYTE(sect,IDE_SECTOR_REG);
head = track % drive->head;
cyl = track / drive->head;
OUT_BYTE(cyl,IDE_LCYL_REG);
OUT_BYTE(cyl>>8,IDE_HCYL_REG);
OUT_BYTE(head|drive->select.all,IDE_SELECT_REG);
}
if (rq->cmd == READ) {
ide_set_handler(drive, &read_intr, WAIT_CMD, NULL);
OUT_BYTE(WIN_READ, IDE_COMMAND_REG);
return ide_started;
}
...
}
---------------------------------------------------------------------------------------------------
This fills the remaining control registers of the interface and starts the actual I/O. Now ide_set_handler() sets up read_intr() to be called when we get an interrupt. This calls ide_end_request() when a request is done, which calls
end_that_request_first() (which calls bh->b_end_io() as promised earlier) and end_that_request_last() which calls
blkdev_release_request() which wakes up whoever waited for the block.
编辑者: hyl (07/12/02 13:56)
====================================================================
《Unix Internals - The New Frontiers》 Author: Uresh Vahalia
中译本:《UNIX 高级教程:系统技术内幕》
---------------------
翻译者:聊鸿斌等
清华大学出版社出版,16开,桔皮,58¥
---------------------
一本新书,也是bible级的。其主要特点是80%的内容都是现代UNIX操作系统的新思想
和新概念。对各UNIX大家的精彩设计点都有很详尽的阐述。阅读者最好先看看贝奇那
本书。
====================================================================
====================================================================
《Linux Core Kernel Commentary》
---------------------
Author: Scott Maxwell
---------------------
有中译本,但具体信息不详 对照源码讲Linux内核,版本还比较高,2.2.X。有些新
东西可看看。可惜没讲网络部分的实现。
====================================================================
====================================================================
《Linux Kernel Internals: 2nd version》
---------------------
Author: M Beck ...
---------------------
目前无中译本 以数据结构为主体来介绍内核,内容很丰富,但版本太低(2.0.x),有
些陈旧的东西容易令人误入歧途。
====================================================================
====================================================================
《The Design and Implementation of the 4.4BSD Operating System》
---------------------
Author: McKusick, Bostic, Karels, Quarterman
---------------------
目前无中译本 讲述BSD操作系统最权威的书。书的作者亦即BSD最早的几名开发者。
====================================================================
====================================================================
《Linux 操作系统及实验教程》
---------------------
作者:李善平,郑扣根(浙大教师)
机工出版
---------------------
有些内容不错,讲得还比较深入。也缺少网络部分的描述。
====================================================================
====================================================================
《UNIX 系统下的80386》
---------------------
作者:周明德, 张淑玲
清华出版
---------------------
讲X86的体系结构。要想看懂/arch/i386下的辕马,最好先看看这本书。
====================================================================
====================================================================
《UNIX Systems for Modern Architectures》
---------------------
Author: Curt Schimmel
---------------------
目前无中译本 如果想了解辕马中SMP的实现。可参考看这本书。
====================================================================
====================================================================
《保护模式下的80386及其编程》
---------------------
出版社:清华大学出版社
---------------------
本书全面地介绍了80386的结构。首先是80386实模式和保护模式下的寄存器集和
指令集,接着从保护模式下的虚存管理、保护级、多任务支持、中断和异常等方
面深入地剖析386的强大功能,再接着提供几个典型的编程实例以及一个完整的从
386加电初始化开始到形成一个有基本的多任务和虚拟存储能力的系统的例子并作
了详细解释,最后还清楚地说明了80386与8086/80286的兼容性和差别。本书的特
点是严谨深入,对CPU各种条件下的动作和反应用形式化的微程序讲解得十分清楚,
尤其适合系统程序员阅读。总之,这实在是一本不可多得的好书.
====================================================================
====================================================================
《Linux 操作系统的内核分析》 作者:陈莉君编著
---------------------
价格:40 元
出版社:人民邮电出版社
---------------------
无
====================================================================
linux kernel internals 2.4(不是特别全)
http://www.moses.uklinux.net/patches/lki.html
http://tzhang.xiloo.com/unix/kernel/
UNIX高级教程 系统技术内幕
Uresh Vahalia 清华大学出版社
书。这是一本非常好的书,思想性很强,对学习Linux以及操作系统非常有帮助。
Understanding the Linux Kernel
Daniel P. Bovet Marco Cesati
O'Reilly出版
超级宝书!:-)2001年内会有中译本。
这里是第10章
Process Scheduling
Linux 操作系统及实验教程
李善平 郑扣根 编著
机械工业出版社
Linux 操作系统的内核分析
陈莉君编著
人民邮电出版社
Linux Device Driver
书。有中译本。帮助理解中断,任务队列等内核机制,帮助掌握驱动编程。
Operating Systems
resource center
jkl 推荐
关于操作系统的介绍,有许多专题文章
http://www.nondot.org/sabre//os/
关于Linux的重要链接
Pengcheng Zou 推荐
给出了许多重要的链接
http://www.linuxlinks.com/Kernel/
非常完备的CPU开发手册
Intel Architecture Software Developer’s Manual
Volume 3:
System Programming
" target="_new">http://developer.intel.com/design/pentiumii/manuals/24319202.pdf
对i386的机制讲得非常详细,可以帮助深入了解i386保护模式机制,对理解Linux的相关实现非常有帮助
~~~~~~~
关于内存
~~~~~~~
Outline of the Linux Memory Management System
黑饭 推荐
http://home.earthlink.net/~jknapka/linux-mm/vmoutline.html
已经是一个比较细致的介绍内存(包括内核的和用户空间的)了。
SGI公司的公开项目
http://oss.sgi.com/projects/
SGI公司的关于伸缩性的技术非常棒!Linux的NUMA就是由他们做的,这是他们的网页
Linux内存开发小组的网站
jkl 推荐
http://www.linux-mm.org
这是Linux负责开发内存管理的核心成员的网站
下面有他们关于zone设计思想的描述:
http://surriel.com/zone-alloc.html
~~~~~~~
关于中断
~~~~~~~
关于中断上锁的专题文档
http://netfilter.kernelnotes.org/unreliable-guides/kernel-locking/lklockingguide.html
我们知道在中断中写程序要小心,不小心会发生race condition.这篇文献对这方面做了介绍
~~~~~~~~~~
关于文件系统
~~~~~~~~~~
Linux Commentary
dream_seeker 推荐
http://www.cse.unsw.edu.au/~neilb/oss/linux-commentary/
主要介绍文件系统, 另外,下面的
linux-vm-readme
主要介绍VM和交换,也很值得一看
关于汇编
[http://www.linuxassembly.org http://www.linuxassembly.org ]
Linuxkernel推荐
关于AT
http://www-106.ibm.com/developerworks/linux/library/l-ia.html
lisoleg推荐
关于汇编:
http://www-aig.jpl.nasa.gov/public/home/decoste/HTMLS/GNU/binutils/as_toc.html
http://www.linuxassembly.org/resources.html#tutorials
lucian推荐
关于驱动,专门研究linux驱动设计的bbs
http://short.xilubbs.com
joyfire推荐
Edited by lucian_yao on 05/20/01 08:05 PM.
block_dev_struct
此结构用于向核心登记块设备,它还被buffer
cache实用。所有此类结构都位于blk_dev数组中。
struct blk_dev_struct {
void (*request_fn)(void);
struct request * current_request;
struct request plug;
struct tq_struct plug_tq;
};
buffer_head
此结构包含关于buffer cache中一块缓存的信息。
/* bh state bits */
#define BH_Uptodate 0 /* 1 if the buffer contains valid data */
#define BH_Dirty 1 /* 1 if the buffer is dirty */
#define BH_Lock 2 /* 1 if the buffer is locked */
#define BH_Req 3 /* 0 if the buffer has been invalidated */
#define BH_Touched 4 /* 1 if the buffer has been touched (aging) */
#define BH_Has_aged 5 /* 1 if the buffer has been aged (aging) */
#define BH_Protected 6 /* 1 if the buffer is protected */
#define BH_FreeOnIO 7 /* 1 to discard the buffer_head after IO */
struct buffer_head {
/* First cache line: */
unsigned long b_blocknr; /* block number */
kdev_t b_dev; /* device (B_FREE = free) */
kdev_t b_rdev; /* Real device */
unsigned long b_rsector; /* Real buffer location on disk */
struct buffer_head *b_next; /* Hash queue list */
struct buffer_head *b_this_page; /* circular list of buffers in one
page */
/* Second cache line: */
unsigned long b_state; /* buffer state bitmap (above) */
struct buffer_head *b_next_free;
unsigned int b_count; /* users using this block */
unsigned long b_size; /* block size */
/* Non-performance-critical data follows. */
char *b_data; /* pointer to data block */
unsigned int b_list; /* List that this buffer appears */
unsigned long b_flushtime; /* Time when this (dirty) buffer
* should be written */
unsigned long b_lru_time; /* Time when this buffer was
* last used. */
struct wait_queue *b_wait;
struct buffer_head *b_prev; /* doubly linked hash list */
struct buffer_head *b_prev_free; /* doubly linked list of buffers */
struct buffer_head *b_reqnext; /* request queue */
};
device
系统中每个网络设备都用一个设备数据结构来表示。
struct device
{
/*
* This is the first field of the "visible" part of this structure
* (i.e. as seen by users in the "Space.c" file). It is the name
* the interface.
*/
char *name;
/* I/O specific fields */
unsigned long rmem_end; /* shmem "recv" end */
unsigned long rmem_start; /* shmem "recv" start */
unsigned long mem_end; /* shared mem end */
unsigned long mem_start; /* shared mem start */
unsigned long base_addr; /* device I/O address */
unsigned char irq; /* device IRQ number */
/* Low-level status flags. */
volatile unsigned char start, /* start an operation */
interrupt; /* interrupt arrived */
unsigned long tbusy; /* transmitter busy */
struct device *next;
/* The device initialization function. Called only once. */
int (*init)(struct device *dev);
/* Some hardware also needs these fields, but they are not part of
the usual set specified in Space.c. */
unsigned char if_port; /* Selectable AUI,TP, */
unsigned char dma; /* DMA channel */
struct enet_statistics* (*get_stats)(struct device *dev);
/*
* This marks the end of the "visible" part of the structure. All
* fields hereafter are internal to the system, and may change at
* will (read: may be cleaned up at will).
*/
/* These may be needed for future network-power-down code. */
unsigned long trans_start; /* Time (jiffies) of
last transmit */
unsigned long last_rx; /* Time of last Rx */
unsigned short flags; /* interface flags (BSD)*/
unsigned short family; /* address family ID */
unsigned short metric; /* routing metric */
unsigned short mtu; /* MTU value */
unsigned short type; /* hardware type */
unsigned short hard_header_len; /* hardware hdr len */
void *priv; /* private data */
/* Interface address info. */
unsigned char broadcast[MAX_ADDR_LEN];
unsigned char pad;
unsigned char dev_addr[MAX_ADDR_LEN];
unsigned char addr_len; /* hardware addr len */
unsigned long pa_addr; /* protocol address */
unsigned long pa_brdaddr; /* protocol broadcast addr*/
unsigned long pa_dstaddr; /* protocol P-P other addr*/
unsigned long pa_mask; /* protocol netmask */
unsigned short pa_alen; /* protocol address len */
struct dev_mc_list *mc_list; /* M'cast mac addrs */
int mc_count; /* No installed mcasts */
struct ip_mc_list *ip_mc_list; /* IP m'cast filter chain */
__u32 tx_queue_len; /* Max frames per queue */
/* For load balancing driver pair support */
unsigned long pkt_queue; /* Packets queued */
struct device *slave; /* Slave device */
struct net_alias_info *alias_info; /* main dev alias info */
struct net_alias *my_alias; /* alias devs */
/* Pointer to the interface buffers. */
struct sk_buff_head buffs[DEV_NUMBUFFS];
/* Pointers to interface service routines. */
int (*open)(struct device *dev);
int (*stop)(struct device *dev);
int (*hard_start_xmit) (struct sk_buff *skb,
struct device *dev);
int (*hard_header) (struct sk_buff *skb,
struct device *dev,
unsigned short type,
void *daddr,
void *saddr,
unsigned len);
int (*rebuild_header)(void *eth,
struct device *dev,
unsigned long raddr,
struct sk_buff *skb);
void (*set_multicast_list)(struct device *dev);
int (*set_mac_address)(struct device *dev,
void *addr);
int (*do_ioctl)(struct device *dev,
struct ifreq *ifr,
int cmd);
int (*set_config)(struct device *dev,
struct ifmap *map);
void (*header_cache_bind)(struct hh_cache **hhp,
struct device *dev,
unsigned short htype,
__u32 daddr);
void (*header_cache_update)(struct hh_cache *hh,
struct device *dev,
unsigned char * haddr);
int (*change_mtu)(struct device *dev,
int new_mtu);
struct iw_statistics* (*get_wireless_stats)(struct device *dev);
};
device_struct
此结构被块设备和字符设备用来向核心登记(包含设备名称以及可对此设备进行的
文件操作)。chrdevs和blkdevs中的每个有效分别表示一个字符设备和块设备。
struct device_struct {
const char * name;
struct file_operations * fops;
};
file
每个打开的文件、套接口都用此结构表示。
struct file {
mode_t f_mode;
loff_t f_pos;
unsigned short f_flags;
unsigned short f_count;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct file *f_next, *f_prev;
int f_owner; /* pid or -pgrp where SIGIO should be sent */
struct inode * f_inode;
struct file_operations * f_op;
unsigned long f_version;
void *private_data; /* needed for tty driver, and maybe others */
};
files_struct
描叙被某进程打开的所有文件。
struct files_struct {
int count;
fd_set close_on_exec;
fd_set open_fds;
struct file * fd[NR_OPEN];
};
fs_struct
struct fs_struct {
int count;
unsigned short umask;
struct inode * root, * pwd;
};
gendisk
包含关于某个硬盘的信息。用于磁盘初始化与分区检查时。
struct hd_struct {
long start_sect;
long nr_sects;
};
struct gendisk {
int major; /* major number of driver */
const char *major_name; /* name of major driver */
int minor_shift; /* number of times minor is shifted to
get real minor */
int max_p; /* maximum partitions per device */
int max_nr; /* maximum number of real devices */
void (*init)(struct gendisk *);
/* Initialization called before we
do our thing */
struct hd_struct *part; /* partition table */
int *sizes; /* device size in blocks, copied to
blk_size[] */
int nr_real; /* number of real devices */
void *real_devices; /* internal use */
struct gendisk *next;
};
inode
此VFS inode结构描叙磁盘上一个文件或目录的信息。
struct inode {
kdev_t i_dev;
unsigned long i_ino;
umode_t i_mode;
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
kdev_t i_rdev;
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
unsigned long i_version;
unsigned long i_nrpages;
struct semaphore i_sem;
struct inode_operations *i_op;
struct super_block *i_sb;
struct wait_queue *i_wait;
struct file_lock *i_flock;
struct vm_area_struct *i_mmap;
struct page *i_pages;
struct dquot *i_dquot[MAXQUOTAS];
struct inode *i_next, *i_prev;
struct inode *i_hash_next, *i_hash_prev;
struct inode *i_bound_to, *i_bound_by;
struct inode *i_mount;
unsigned short i_count;
unsigned short i_flags;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_sock;
unsigned char i_seek;
unsigned char i_update;
unsigned short i_writecount;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext_inode_info ext_i;
struct ext2_inode_info ext2_i;
struct hpfs_inode_info hpfs_i;
struct msdos_inode_info msdos_i;
struct umsdos_inode_info umsdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
struct xiafs_inode_info xiafs_i;
struct sysv_inode_info sysv_i;
struct affs_inode_info affs_i;
struct ufs_inode_info ufs_i;
struct socket socket_i;
void *generic_ip;
} u;
};
ipc_perm
此结构描叙对一个系统V IPC对象的存取权限。
struct ipc_perm
{
key_t key;
ushort uid; /* owner euid and egid */
ushort gid;
ushort cuid; /* creator euid and egid */
ushort cgid;
ushort mode; /* access modes see mode flags below */
ushort seq; /* sequence number */
};
irqaction
用来描叙系统的中断处理过程。
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
linux_binfmt
用来表示可被Linux理解的二进制文件格式。
struct linux_binfmt {
struct linux_binfmt * next;
long *use_count;
int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
int (*load_shlib)(int fd);
int (*core_dump)(long signr, struct pt_regs * regs);
};
mem_map_t
用来保存每个物理页面的信息。
typedef struct page {
/* these must be first (free area handling) */
struct page *next;
struct page *prev;
struct inode *inode;
unsigned long offset;
struct page *next_hash;
atomic_t count;
unsigned flags; /* atomic flags, some possibly
updated asynchronously */
unsigned dirty:16,
age:8;
struct wait_queue *wait;
struct page *prev_hash;
struct buffer_head *buffers;
unsigned long swap_unlock_entry;
unsigned long map_nr; /* page->map_nr == page - mem_map */
} mem_map_t;
mm_struct
用来描叙某任务或进程的虚拟内存。
struct mm_struct {
int count;
pgd_t * pgd;
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack, start_mmap;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
struct vm_area_struct * mmap;
struct vm_area_struct * mmap_avl;
struct semaphore mmap_sem;
};
pci_bus
表示系统中的一个PCI总线。
struct pci_bus {
struct pci_bus *parent; /* parent bus this bridge is on */
struct pci_bus *children; /* chain of P2P bridges on this bus */
struct pci_bus *next; /* chain of all PCI buses */
struct pci_dev *self; /* bridge device as seen by parent */
struct pci_dev *devices; /* devices behind this bridge */
void *sysdata; /* hook for sys-specific extension */
unsigned char number; /* bus number */
unsigned char primary; /* number of primary bridge */
unsigned char secondary; /* number of secondary bridge */
unsigned char subordinate; /* max number of subordinate buses */
};
pci_dev
表示系统中的每个PCI设备,包括PCI-PCI和PCI-PCI桥接器。
/*
* There is one pci_dev structure for each slot-number/function-number
* combination:
*/
struct pci_dev {
struct pci_bus *bus; /* bus this device is on */
struct pci_dev *sibling; /* next device on this bus */
struct pci_dev *next; /* chain of all devices */
void *sysdata; /* hook for sys-specific extension */
unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
unsigned int master : 1; /* set if device is master capable */
/*
* In theory, the irq level can be read from configuration
* space and all would be fine. However, old PCI chips don't
* support these registers and return 0 instead. For example,
* the Vision864-P rev 0 chip can uses INTA, but returns 0 in
* the interrupt line and pin registers. pci_init()
* initializes this field with the value at PCI_INTERRUPT_LINE
* and it is the job of pcibios_fixup() to change it if
* necessary. The field must not be 0 unless the device
* cannot generate interrupts at all.
*/
unsigned char irq; /* irq generated by this device */
};
request
被用来向系统的块设备发送请求。它总是向buffer cache读出或写入数据块。
struct request {
volatile int rq_status;
#define RQ_INACTIVE (-1)
#define RQ_ACTIVE 1
#define RQ_SCSI_BUSY 0xffff
#define RQ_SCSI_DONE 0xfffe
#define RQ_SCSI_DISCONNECTING 0xffe0
kdev_t rq_dev;
int cmd; /* READ or WRITE */
int errors;
unsigned long sector;
unsigned long nr_sectors;
unsigned long current_nr_sectors;
char * buffer;
struct semaphore * sem;
struct buffer_head * bh;
struct buffer_head * bhtail;
struct request * next;
};
rtable
用来描叙向某个IP主机发送包的路由信息。此结构在IP路由cache内部实用。
struct rtable
{
struct rtable *rt_next;
__u32 rt_dst;
__u32 rt_src;
__u32 rt_gateway;
atomic_t rt_refcnt;
atomic_t rt_use;
unsigned long rt_window;
atomic_t rt_lastuse;
struct hh_cache *rt_hh;
struct device *rt_dev;
unsigned short rt_flags;
unsigned short rt_mtu;
unsigned short rt_irtt;
unsigned char rt_tos;
};
semaphore
保护临界区数据结构和代码信号灯。
struct semaphore {
int count;
int waking;
int lock ; /* to make waking testing atomic */
struct wait_queue *wait;
};
sk_buff
用来描叙在协议层之间交换的网络数据。
struct sk_buff
{
struct sk_buff *next; /* Next buffer in list
*/
struct sk_buff *prev; /* Previous buffer in list
*/
struct sk_buff_head *list; /* List we are on
*/
int magic_debug_cookie;
struct sk_buff *link3; /* Link for IP protocol level buffer chai
ns *
struct sock *sk; /* Socket we are owned by
*/
unsigned long when; /* used to compute rtt's
*/
struct timeval stamp; /* Time we arrived
*/
struct device *dev; /* Device we arrived on/are leaving by
*/
union
{
struct tcphdr *th;
struct ethhdr *eth;
struct iphdr *iph;
struct udphdr *uh;
unsigned char *raw;
/* for passing file handles in a unix domain socket */
void *filp;
} h;
union
{
/* As yet incomplete physical layer views */
unsigned char *raw;
struct ethhdr *ethernet;
} mac;
struct iphdr *ip_hdr; /* For IPPROTO_RAW
*/
unsigned long len; /* Length of actual data
*/
unsigned long csum; /* Checksum
*/
__u32 saddr; /* IP source address
*/
__u32 daddr; /* IP target address
*/
__u32 raddr; /* IP next hop address
*/
__u32 seq; /* TCP sequence number
*/
__u32 end_seq; /* seq [+ fin] [+ syn] + datalen
*/
__u32 ack_seq; /* TCP ack sequence number
*/
unsigned char proto_priv[16];
volatile char acked, /* Are we acked ?
*/
used, /* Are we in use ?
*/
free, /* How to free this buffer
*/
arp; /* Has IP/ARP resolution finished
*/
unsigned char tries, /* Times tried
*/
lock, /* Are we locked ?
*/
localroute, /* Local routing asserted for this frame
*/
pkt_type, /* Packet class
*/
pkt_bridged, /* Tracker for bridging
*/
ip_summed; /* Driver fed us an IP checksum
*/
#define PACKET_HOST 0 /* To us
*/
#define PACKET_BROADCAST 1 /* To all
*/
#define PACKET_MULTICAST 2 /* To group
*/
#define PACKET_OTHERHOST 3 /* To someone else
*/
unsigned short users; /* User count - see datagram.c,tcp.c
*/
unsigned short protocol; /* Packet protocol from driver.
*/
unsigned int truesize; /* Buffer size
*/
atomic_t count; /* reference count
*/
struct sk_buff *data_skb; /* Link to the actual data skb
*/
unsigned char *head; /* Head of buffer
*/
unsigned char *data; /* Data head pointer
*/
unsigned char *tail; /* Tail pointer
*/
unsigned char *end; /* End pointer
*/
void (*destructor)(struct sk_buff *); /* Destruct function
*/
__u16 redirport; /* Redirect port
*/
};
sock
包含BSD套接口的协议相关信息。例如对于一个INET(Internet AddressDomain)套接口
此数据结构 包含TCP/IP和UDP/IP信息。
struct sock
{
/* This must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;
struct options *opt;
atomic_t wmem_alloc;
atomic_t rmem_alloc;
unsigned long allocation; /* Allocation mode */
__u32 write_seq;
__u32 sent_seq;
__u32 acked_seq;
__u32 copied_seq;
__u32 rcv_ack_seq;
unsigned short rcv_ack_cnt; /* count of same ack */
__u32 window_seq;
__u32 fin_seq;
__u32 urg_seq;
__u32 urg_data;
__u32 syn_seq;
int users; /* user count */
/*
* Not all are volatile, but some are, so we
* might as well say they all are.
*/
volatile char dead,
urginline,
intr,
blog,
done,
reuse,
keepopen,
linger,
delay_acks,
destroy,
ack_timed,
no_check,
zapped,
broadcast,
nonagle,
bsdism;
unsigned long lingertime;
int proc;
struct sock *next;
struct sock **pprev;
struct sock *bind_next;
struct sock **bind_pprev;
struct sock *pair;
int hashent;
struct sock *prev;
struct sk_buff *volatile send_head;
struct sk_buff *volatile send_next;
struct sk_buff *volatile send_tail;
struct sk_buff_head back_log;
struct sk_buff *partial;
struct timer_list partial_timer;
long retransmits;
struct sk_buff_head write_queue,
receive_queue;
struct proto *prot;
struct wait_queue **sleep;
__u32 daddr;
__u32 saddr; /* Sending source */
__u32 rcv_saddr; /* Bound address */
unsigned short max_unacked;
unsigned short window;
__u32 lastwin_seq; /* sequence number when we las
t
updated the window we offer
*/
__u32 high_seq; /* sequence number when we did
current fast retransmit */
volatile unsigned long ato; /* ack timeout */
volatile unsigned long lrcvtime; /* jiffies at last data rcv */
2.准备编译:
现在要做一些准备工作. 对于新释放出来的核心源程序也没啥好做的, 就打一个:
cd /usr/src/linux
make menuconfig
然后就会看到一个很友好的界面(在LINUX下...已经是很友好的了), 大致上有点像
WIN 9X安装时的选择安装项目. 这就是在配置核心, 选择哪些内容要, 哪些不要.
然后选EXIT退出来, 问是否保存修改时答YES. 然后会有一些提示. 如果看到了有叫你
"make dep", 就要打"make dep"先. 完了后就打 make bzImage. 如果提示信息中没有
叫你"make dep", 只有叫你 "make zImage", "make zdisk" 或 "make zlilo" 的,
就直接打 make bzImage 就行了.
一点说明: make dep 是作一些准备工作, make bzImage 则是开始编译生成核心. 而
make bzImage与make zImage的区别在于, 作成bzImage的核心压缩率比zImage
高, 核心就更小一些. make zdisk 与 make zlilo 是做别的用处的核心的.
然后就等吧(有得你等的). 一般从5分钟到半个钟头不等, 看你的机器了. 第一次编
译会 比较慢. 以后再改了配置后make就会快很多了.
等这个完了后一定还要 make modules 和 make modules_install.
make bzImage 完后会显示核心放在什么地方, 一般是/usr/src/linux/arch/i386/boot/
下. 把bzImage拷到根下. 然后修改 /etc/lilo.conf, 照着原来的image = XXXXX来加上
image = /bzImage
root = /dev/hda1 (这里视你的LINUX安装而定, 照你原有的改)
label = linux
read-only
把原来的 label = linux 改一下, 如 label = oldlinux.
把image = /bzImage 这一节加在原来的前面, 这样会自动作为缺省的核心. 你也可以在
LILO时打linux或oldlinux来启动不同的核心. 关于这一段, 也可以参考俺前面的"ALS007
发声经过". 最后, 切记切记, 一定要打个lilo来重新生成LILO程序.
好了, 重启...
processor family (386,486/cx486,586/k5/5x86/6x86,pentinum/k6/tsc,ppro/6x86)
这应该没有太多可说的吧,选择你的CPU的种类,BIOS可以自检得
到,注意系统的启动信息。需要注意的是不能选择比你的CPU类型
还高级的CPU,否则可能不能正常工作。
math emulation
模拟数学协处理器,如果你的机器没有数学协处理器,那就选上
以提高性能,但486以后数学协处理器就集成到CPU内部了,应该是
用不上的,所以一般的选择是N。当然选上也不会有什么问题,除
了内 松陨 变大外。
mttr(memory type range register) support
这个选项是用来启动pentinum pro和pentinum II 的特殊功能,如果你用
的不是这类CPU就选N,否则也仅仅是使内核变大而已。
symmetric multi-processing support
同步处理器支持,如果你有多个CPU就选上吧。
enable loadable module support
这会启动动态载入额外模块的功能,所以一定选上。
set version information on all symbols for modules
这个选项可以为某个版本的内核而编译的模块在另一个版本的内
核下使用,但一般用不上。
kernel module loader
如果你启用这个选项,你可以通过kerneld程序的帮助在需要的时候
自动载入或卸载那些可载入式的模块。建议选上。
networking support
如果你用到任何网络就应该选上
pci bios support
这个一般是应该选上的,除非你用没有任何PCI设备的机器。PCI
BIOS是用来侦测并启用PCI设备的。
pci bridge optimization(v1.3)
当这个选项被启动时,操作系统会对从CPU和系统内存在PCI总线
来往的数据做最佳化,这个功能已经完成实验阶段,使用起来应
该很安全,而且还可增进系统的效率。
system v ipc
起用这个选项可以使内核支持System V 的进程间通信的功能
(IPC),有些从System V转移过来的程序会需要这个功能,建议启
用该功能。
sysctl support
除非你的内存少的可怜,否则你应该启动这个功能,启用该选项
后内核会大8K,但能让你直接改变内核的参数而不必重新开机。
kernel support for elf binaries
该选项让你的系统得以执行用ELF格式存储的可执行文件,而ELF
是现代LINUX的可执行文件、目标文件和系统函数库的标准格式。
当操作系统要和编译器以及连接器合作时会需要这些标准,所以
应该回答Y。
compile kernel as elf
这选项让你的内核本身以ELF的格式编译,如果你的系统上的过程
gcc默认产生ELF格式的可执行文件,那么你就应该启动这个选项。
先看看你的编译器的版本再决定。
parallel port support
如果你有任何并行口的设备并且想让LINUX使用,那么就可以启用
这个选项。LINUX不仅可以使用并口的打印机,还可以支持PLIP
(一种为并行口而设计的网络通讯协定),ZIP磁盘驱动器、扫描
仪等。在大多情况下,你需要额外的驱动程序才能使用外接的并
口设备。
plug and play support
支持PNP设备并非Microsoft的专利,如果你要让LINUX也支持PNP设
备,只要启用该选项就可以,但有些情况下会和其他设备产生冲
突(I/O,DMA,IRQ等)。这个选项对PCI设备没有影响,因为他们
天生就是PNP设备。
normal floppy disk support
除非你不想在LINUX下使用软盘,否则就应该回答Y。但对于一些
不需要支持软盘驱动器的系统而言,这个选项可以节省一些内
存。
enhanced ide/mfm/dll disk support
除非你不需要MFM/DLL/IDE硬盘的的支持,否则就应该回答Y,但如
果你只有SCSI的硬盘,关掉这个选项会比较安全。
enhanced ide/mfm/dll cdrom support
和上面的类似,只不过是对CDROM的支持而已。
enhanced ide/mfm/dll tape support
一般没有多少人在用磁带机吧,所以回答N是比较好的答案。
enhanced ide/mfm/dll floppy support
这个设备好象一般也没有人用,所以也可以回答N。
xt harddisk support
如果你有这种石器时代的XT硬盘,那么恭喜你你可以用上他了。
parallel port ide device support
LINUX是支持这种很新的并口的IDE设备的,如果你有的话就用上
吧。
networking options
如果你在前面选了支持网络的话,在这里会回答很多问题。除非
你有特别的需求,否则使用默认的选项应该就足够了。
scsi support
如果你有任何一种SCSI控制卡,这个选项就应该回答Y。事先搞清
楚你的硬件的类型,因为这些问题都是针对特定的SCSI控制芯片和
控制卡的,如果你不确定自己使用的是哪一种,查看你的硬件的
说明文件或者LINUX的HOWTO文档。同样也会让你回答很多SCSI设
备的支持(硬盘、CDROM、Tape、floppy等),依据你的情况选择。
如果你没有SCSI设备的话,建议不要支持,因为这会节约很多内核
空间。
network device support
这里面有很多关于网络控制卡的问题,如果你无法确定如何正确
选择,查看硬件文档或LINUX HOWTO文档。
amateur radio support
这个选项可以用来启动无线网络的基本支持,目前的无线网络可
以通过公众频率传输数据,如果你有此类设备就可以启用,具体
请参考AX25和HAM HOWTO 文档。
isdn subsystem
如果你有ISDN硬件就应该启用该选项并安装合适的硬件驱动程
序,你应该还会需要启用Support synchronous PPP选项(参考PPP over
ISDN)。
old cd-rom drivers
这是针对一些特殊光盘驱动器程序的问题,如果你有IDE或SCSI的
CDROM控制卡,那么就不用启用该选项了。
character devices
LINUX支持很多特殊的字符设备,例如并口、串口控制卡、QIC02磁
带驱动器以及特定界面的鼠标,此外对于游戏杆和影象摄取和麦
克等也在这里面,依据你自己的情况选者吧。
filesystems
这是一系列内核所支持的各文件系统的问题,对ext2 /proc文件系统
是一定应该支持的,有光驱还应该支持ISO9660(或模块支持),
有WINDOWS或DOS分区并且想在LINUX下访问他们也可以进行支
持。
console drivers
你至少应该支持VGA text console,否则你无法从控制台使用LINUX。
sound card support
在这里回答Y会出现很多关于声卡的问题,根据你自己的情况来配
置。
kernel profiling support(v1.3)
这个选项可以开启内核做效率统计的功能,并且会提供其他在为
系统侦错时有用的信息。这些功能会需要付出一些代价并造成系
统执行得较为缓慢,除非你正在研究内核的某个问题,否则你应
该回答N。
kernel hacking
如果你正打算深入研究自己系统上运行的LINUX如何运作,这里有
很多选项,但一般没有必要的话可以全部关掉。
用打印信息调试
最一般的调试技术就是监视,就是在应用内部合适的点加上printf调用。当你调试内核代码的时候,你可以用printk完成这个任务。
Printk
一般简单介绍的时候,都假设printk工作起来和printf很类似。现在是的它们不同。
其中一个不同点就是,printk允许你根据它们的严重程度,通过附加不同的“记录级”来对消息分类,或赋予消息优先级。你可以用宏来指示记录级。例如,KERN_INFO,我们前面已经看到它被加在打印语句的前面,它就是一种可能的消息记录级。记录级宏展开为一个字串,在编译时和消息文本拼接在一起。
在<linux/kernel.h>中定义了8种记录级别串。没有指定优先级的printk语句默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,它是一个在kernel/printk.c中定义的整数。默认记录级的具体数值在Linux的开发期间曾变化过若干次,所以我建议你最好总是指定一个合适的记录级。
根据记录级,内核将消息打印到当前文本控制台上:如果优先级低于console_loglevel这个数值的话,该消息就显示在控制台上。如果系统同时运行了klogd和syslogd,无论console_loglevel为何值,内核都将消息追加到/var/log/messages中。
变量console_loglevel最初初始化为DEFAULT_CONSOLE_LOGLEVEL,但可以通过sys_syslog系统调用修改。如klogd的手册所示,可以在启动klogd时指定-c开关来修改这个变量。此外,你还可以写个程序来改变控制台记录级。你可以在O’Reilly站点上的源文件中找到我写的一个这种功能的程序,miscprogs/setlevel.c。新优先级是通过一个1到8之间的整数值指定的。
你也许需要在内核失效后降低记录级(见“调试系统故障”),这是因为失效处理代码会将console_loglevel提升到15,之后所有的消息都会出现在控制台上。为看到你的调试信息,如果你运行的是内核2.0.x话,你需要提升记录级。内核2.0发行降低了MINIMUM_CONSOLE_LOGLEVEL,而旧版本的klogd默认情况下要打印很多控制消息。如果你碰巧使用了这个旧版本的守护进程,除非你提升记录级,内核2.0会比你预期的打印出更少的消息。这就是为什么hello.c中使用了<1>标记,这样可以保证消息显示在控制台上。
从1.3.43后的内核版本通过允许你向指定虚控制台发送消息,藉此提供一个灵活的记录策略。默认情况下,“控制台”是当前虚终端。也可以选择不同的虚终端接收消息,你只需向所选的虚终端调用ioctl(TIOCLINUX)。setconsole可以用来选择哪个虚终端接收内核消息;它必须以超级用户身份运行。如果你对ioctl还不有把握,你可以跳过这至下一节,等到读完第5章“字符设备驱动程序的扩展操作”的“ioctl”一节后,再回到这里。
setconsole使用了用于Linux专用功能的特殊的ioctl命令TIOCLINUX。为了使用TIOCLINUX,你要传递给它一个指向字节数组的指针。数组的第一个字节是所请求的子命令的编码,随后的字节依命令而不同。在setconsole中使用了子命令11,后一个字节(存放在bytes[1]中)标别虚拟控制台。TIOCLINUX的完成介绍可以在内核源码drivers/char/tty_io.c中找到。
消息是如何记录的
printk函数将消息写到一个长度为LOG_BUF_LEN个字节的循环缓冲区中。然后唤醒任何等待消息的进程,即那些在调用syslog系统调用或读取/proc/kmesg过程中睡眠的进程。这两个访问记录引擎的接口是等价的。不过/proc/kmesg文件更象一个FIFO文件,从中读取数据更容易些。一条简单的cat命令就可以读取消息。
如果循环缓冲区填满了,printk就绕到缓冲区的开始处填写新数据,覆盖旧数据。于是记录进程就丢失了最旧的数据。这个问题与利用循环缓冲区所获得的好处相比可以忽略不计。例如,循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时又不浪费内存。Linux处理消息的方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。这个方法的唯一缺点就是可能丢失某些数据。
如果klogd正在运行,它读取内核消息并将它们分派到syslogd,它随后检查/etc/syslog.conf找到处理这些数据的方式。syslogd根据一个“设施”和“优先级”切分消息;可以使用的值定义在<sys/syslog.h>中。内核消息根据相应printk中指定的优先级记录到LOG_KERN设施中。如果klogd没有运行,数据将保存在循环缓冲区中直到有进程来读取数据或数据溢出。
如果你不希望因监视你的驱动程序的消息而把你的系统记录搞乱,你给klogd指定-f(文件)选项或修改/etc/syslog.conf将记录写到另一个文件中。另一种方法是一种强硬方法:杀掉klogd,将消息打印到不用的虚终端上*,或者在一个不用的xterm上执行cat /proc/kmesg显示消息。
使用预处理方便监视处理
在驱动程序开发早期,printk可以对调试和测试新代码都非常有帮助。然而当你正式发行驱动程序时,你应该去掉,或者至少关闭,这些打印语句。很不幸,你可能很快就发现,随着你想不再需要那些消息并去掉它们时,你可能又要加新功能,你又需要这些消息了。解决这些问题有几种方法――如何从全局打开和关闭消息以及如何打开和关闭个别消息。
通过查询调试
上一节谈到了printk是如何工作的以及如何使用它。但没有谈及它的缺点。
由于syslogd会一直保持刷新它的输出文件,每打印一行都会引起一次磁盘操作,因此过量使用printk会严重降低系统性能。至少从syslogd的角度看是这样的。它会将所有的数据都一股脑地写到磁盘上,以防在打印消息后系统崩溃;然而,你不想因为调试信息的缘故而降低系统性能。这个问题可以通过在/etc/syslogd.conf中记录文件的名字前加一个波折号解决,但有时你不想修改你的配置文件。如果不这样,你还可以运行一个非klogd的程序(如前面介绍的cat /proc/kmesg),但这样并不能为正常操作提供一个合适的环境。
与这相比,最好的方法就是在你需要信息的时候,通过查询系统获得相关信息,而不是持续不断地产生数据。事实上,每一个Unix系统都提供了很多工具用来获得系统信息:ps,netstat,vmstat等等。
有许多技术适合与驱动程序开发人员查询系统,简而言之就是,在/proc下创建文件和使用ioctl驱动程序方法。
使用/proc文件系统
Linux中的/proc文件系统与任何设备都没有关系――/proc中的文件都在被读取时有核心创建的。这些文件都是普通的文本文件,它们基本上可由普通人理解,也可被工具程序理解。例如,对于大多数Linux的ps实现而言,它都通过读取/proc文件系统获得进程表信息的。/proc虚拟文件的创意已由若干现代操作系统使用,且非常成功。
/proc的当前实现可以动态创建inode节点,允许用户模块为方便信息检索创建如何入口点。
为了在/proc中创建一个健全的文件节点(可以read,write,seek等等),你需要定义file_operations结构和inode_operations结构,后者与前者有类似的作用和尺寸。创建这样一个i节点比起创建整个字符设备并没有什么不同。我们这里不讨论这个问题,如果你感兴趣,你可以在源码树fs/proc中获得进一步细节。
与大多数/proc文件一样,如果文件节点仅仅用来读,创建它们是比较容易的,我将这里介绍这一技术。这一技术只能在2.0及其后续版本中使用。
填写/proc文件非常容易。你的函数获取一个空闲页面填写数据;它将数据写进缓冲区并返回所写数据的长度。其他事情都由/proc文件系统处理。唯一的限制就是所写的数据不能超过PAGE_SIZE个字节(宏PAGE_SIZE定义在头文件<asm/page.h>中;它是与体系结构相关的,但你至少可以它有4KB大小)。
如果你需要写多于一个页面的数据,你必须实现功能健全的文件。
注意,如果一个正在读你的/proc文件的进程发出了若干read调用,每一个都获取新数据,尽管只有少量数据被读取,你的驱动程序每次都要重写整个缓冲区。这些额外的工作会使系统性能下降,而且如果文件产生的数据与下一次的不同,以后的read调用要重新装配不相关的部分,这一会造成数据错位。事实上,由于每个使用C库的应用程序都大块地读取数据,性能并不是什么问题。然而,由于错位时有发生,它倒是一个值得考虑的问题。在获取数据后,库调用至少要调用1次read――只有当read返回0时才报告文件尾。如果驱动程序碰巧比前面产生了更多的数据,系统就返回到用户空间额外的字节并且与前面的数据块是错位的。我们将在第6章“时间流”的“任务队列”一节中涉及/proc/jiq*,那时我们还会遇到错位问题。
cleanup_module中应该注销/proc节点
传递给函数的参数是包含要撤销文件的目录名和文件的i节点号。由于i节点号是自动分配的,在编译时是无法知道的,必须从数据结构中读取。
ioctl方法
ioctl是一个系统调用,它可以操做在文件描述符上;它接收一个“命令”号和(可选的)一个参数,通常这是一个指针。
做为替代/proc文件系统的方法,你可以为调试实现若干ioctl命令。这些命令从驱动程序空间复制相关数据到进程空间,在进程空间里检查这些数据。
只有使用ioctl获取信息比起/proc来要困难一些,因为你一个程序调用ioctl并显示结果。必须编写这样的程序,还要编译,保持与你测试的模块间的一致性等。
不过有时候这是最好的获取信息的方法,因为它比起读/proc来要快得多。如果在数据写到屏幕前必须完成某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外,ioctl不限制返回数据的大小。
ioctl方法的一个优点是,当调试关闭后调试命令仍然可以保留在驱动程序中。/proc文件对任何查看这个目录的人都是可见的,然而与/proc文件不同,未公开的ioctl命令通常都不会被注意到。此外,如果驱动程序有什么异常,它们仍然可以用来调试。唯一的缺点就是模块会稍微大一些。
通过监视调试
有时你遇到的问题并不特别糟,通过在用户空间运行应用程序来查看驱动程序与系统之间的交互过程可以帮助你捕捉到一些小问题,并可以验证驱动程序确实工作正常。例如,看到scull的read实现如何处理不同数据量的read请求后,我对scull更有信心。
有许多方法监视一个用户态程序的工作情况。你可以用调试器一步步跟踪它的函数,插入打印语句,或者用strace运行程序。在实际目的是查看内核代码时,最后一项技术非常有用。
strace命令是一个功能非常强大的工具,它可以现实程序所调用的所有系统调用。它不仅可以显示调用,而且还能显示调用的参数,以符号方式显示返回值。当系统调用失败时,错误的符号值(如,ENOMEM)和对应的字串(Out of memory)同时显示。strace还有许多命令行选项;最常用的是-t,它用来显示调用发生的时间,-T,显示调用所花费的时间,以及-o,将输出重定向到一个文件中。默认情况下,strace将所有跟踪信息打印到stderr上。
strace从内核接收信息。这意味着一个程序无论是否按调试方式编译(用gcc的-g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它类似,你还可以跟踪一个已经运行的进程。
跟踪信息通常用来生成错误报告报告给应用开发人员,但是对内核编程人员来说也一样非常有用。我们可以看到系统调用是如何执行驱动程序代码的;strace允许我们检查每一次调用输入输出的一致性。
很明显,在ls完成目标目录的检索后首次对write的调用中,它试图写4KB。很奇怪,只写了4000个字节,接着重试这一操作。然而,我们知道scull的write实现每次只写一个量子,我在这里看到了部分写。经过若干步骤之后,所有的东西都清空了,程序正常退出。
正如所料,read每次只能读到4000个字节,但是数据总量是不变的。注意本例中重试工作是如何组织的,注意它与上面写跟踪的对比。wc专门为快速读数据进行了优化,它绕过了标准库,以便每次用一个系统调用读取更多的数据。你可以从跟踪的read行中看到wc每次要读16KB。
Unix专家可以在strace的输出中找到很多有用信息。如果你被这些符号搞得满头雾水,我可以只看文件方法(open,read等等)是如何工作的。
个人认为,跟踪工具在查明系统调用的运行时错误过程中最有用。通常应用或演示程序中的perror调用不足以用来调试,而且对于查明到底是什么样的参数触发了系统调用的错误也很有帮助。
调试系统故障
即便你用了所有监视和调试技术,有时候驱动程序中依然有错误,当这样的驱动程序执行会造成系统故障。当这种情况发生时,获取足够多的信息来解决问题是至关重要的。
注意,“故障”不意味着“panic”。Linux代码非常鲁棒,可以很好地响应大部分错误:故障通常会导致当前进程的终止,但系统继续运行。如果在进程上下文之外发生故障,或是组成系统的重要部件发生故障时,系统可能panic。但问题出在驱动程序时,通常只会导致产生故障的进程终止――即那个使用驱动程序的进程。唯一不可恢复的损失就是当进程被终止时,进程上下文分配的内存丢失了;例如,由驱动程序通过kmalloc分配的动态链表可能丢失。然而,由于内核会对尚是打开的设备调用close,你的驱动程序可以释放任何有open方法分配的资源。
我们已经说过,当内核行为异常时会在控制台上显示一些有用的信息。下一节将解释如何解码和使用这些消息。尽管它们对于初学者来说相当晦涩,处理器的给出数据都是些很有意思的信息,通常无需额外测试就可以查明程序错误。
Oops消息
大部分错误都是NULL指针引用或使用其他不正确的指针数值。这些错误通常会导致一个oops消息。
由处理器使用的地址都是“虚”地址,而且通过一个复杂的称为页表(见第13章“Mmap和DMA”中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,并且处理器向操作系统发出一个“页面失效”。如果地址确实是非法的,内核就无法从失效地址上“换页”;如果此时处理在超级用户太,系统于是就产生一个“oops”。值得注意的是,在版本2.1中内核处理失效的方式有所变化,它可以处理在超级用户态的非法地址引用了。新实现将在第17章“最近发展”的“处理内核空间失效”中介绍。
oops显示故障时的处理器状态,模块CPU寄存器内容,页描述符表的位置,以及其他似乎不能理解的信息。这些是由失效处理函数(arch/*/kernel/traps.c)中的printk语句产生的,而且象前面“Printk”一节介绍的那样进行分派。
让我们看看这样一个消息。这里给出的是传统个人电脑(x86平台),运行Linux 2.0或更新版本的oops――版本1.2的输出稍有不同。
(代码)
上面的消息是在一个有意加入错误的失效模块上运行cat所至。fault.c崩溃如下代码:
(代码)
由于read从它的小缓冲区(faulty_buf)复制数据到用户空间,我们希望读一小块文件能够工作。然而,每次读出多于1KB的数据会跨越页面边界,如果访问了非法页面read就会失败。事实上,前面给出的oops是在请求一个4KB大小的read时发生的,这条消息在/var/log/messages(syslogd默认存放内核消息的文件)的oops消息前给出了:
(代码)
同样的cat命令却不能在Alpha上产生oops,这是因为从faulty_buf读取4KB字节没有超出页边界(Alpha上的页面大小是8KB,缓冲区正好在页面的起始位置附近)。如果在你的系统上读取faulty没有产生oops,试试wc,或者给dd显式地指定块大小。
使用ksymoops
oops消息的最大问题就是十六进制数值对于程序员来说没什么意义;需要将它们解析为符号。
内核源码通过其所包含的ksymoops工具帮助开发人员――但是注意,版本1.2的源码中没有这个程序。该工具将oops消息中的数值地址解析为内核符号,但只限于PC机产生的oops消息。由于消息本身就是处理器相关的,每一体系结构都有其自身的消息格式。
ksymoops从标准输入获得oops消息,并从命令行内核符号表的名字。符号表通常就是/usr/src/linux/System.map。程序以更可读的方式打印调用轨迹和程序代码,而不是最原始的oops消息。下面的片断就是用上一节的oops消息得出的结果:
(代码)
由ksymoops反汇编出的代码给出了失效的指令和其后的指令。很明显――对于那些知道一点汇编的人――repz movsl指令(REPeat till cx is Zero, MOVe a String of Longs)用源索引(esi,是0x202e000)访问了一个未映射页面。用来获得模块信息的ksymoops -m命令给出,模块映射到一个在0x0202dxxx的页面上,这也确认乐esi确实超出了范围。
由于faulty模块所占用的内存不在系统表中,被解码的调用轨迹还给出了两个数值地址。这些值可以手动补充,或是通过ksyms命令的输出,或是在/proc/ksyms中查询模块的名字。
然而对于这个失效,这两个地址并不对应与代码地址。如果你看了arch/i386/kernel/traps.c,你就发现,调用轨迹是从整个堆栈并利用一些启发式方法区分数据值(本地变量和函数参数)和返回地址获得的。调用轨迹中只给出了引用内核代码的地址和引用模块的地址。由于模块所占页面既有代码也有数据,错综复杂的栈可能会漏掉启发式信息,这就是上面两个0x202xxxx地址的情况。
如果你不愿手动查看模块地址,下面这组管道可以用来创建一个既有内核又有模块符号的符号表。无论何时你加载模块,你都必须重新创建这个符号表。
(代码)
这个管道将完整的系统表与/proc/ksyms中的公开内核符号混合在一起,后者除了内核符号外,还包括了当前内核里的模块符号。这些地址在insmod重定位代码后就出现在/proc/ksyms中。由于这两个文件的格式不同,使用了sed和awk将所有的文本行转换为一种合适的格式。然后对这张表排序,去除重复部分,这样ksymoops就可以用了。
如果我们重新运行ksymoops,它从新的符号表中截取出如下信息:
(代码)
正如你所见到的,当跟踪与模块有关的oops消息时,创建一个修订的系统表是很有助益的:现在ksymoops能够对指令指针解码并完成整个调用轨迹了。还要注意,显式反汇编码的格式和objdump所使用的格式一样。objdump也是一个功能强大的工具;如果你需要查看失败前的指令,你调用命令objdump �d faulty.o。
在文件的汇编列表中,字串faulty_read+45/60标记为失效行。有关objdump的更多的信息和它的命令行选项可以参见该命令的手册。
即便你构建了你自己的修订版符号表,上面提到的有关调用轨迹的问题仍然存在:虽然0x202xxxx指针被解码了,但仍然是假的。
学会解码oops消息需要一定的经验,但是确实值得一做。用来学习的时间很快就会有所回报。不过由于机器指令的Unix语法与Intel语法不同,唯一的问题在于从哪获得有关汇编语言的文档;尽管你了解PC汇编语言,但你的经验都是用Intel语法的编程获得的。在参考书目中,我给一些有所补益的书籍。
使用oops
使用ksymoops有些繁琐。你需要C++编译器编译它,你还要构建你自己的符号表来充分发挥程序的能力,你还要将原始消息和ksymoops输出合在一起组成可用的信息。
如果你不想找这么多麻烦,你可以使用oops程序。oops在本书的O’Reilly FTP站点给出的源码中。它源自最初的ksymoops工具,现在它的作者已经不维护这个工具了。oops是用C语言写成的,而且直接查看/proc/ksyms而无需用户每次加载模块后构建新的符号表。
该程序试图解码所有的处理器寄存器并堆栈轨迹解析为符号值。它的缺点是,它要比ksymoops罗嗦些,但通常你所有的信息越多,你发现错误也就越快。oops的另一个优点是,它可以解析x86,Alpha和Sparc的oops消息。与内核源码相同,这个程序也按GPL发行。
oops产生的输出与ksymoops的类似,但是更完全。这里给出前一个oops输出的开始部分�由于在这个oops消息中堆栈没保存什么有用的东西,我不认为应该显示整个堆栈轨迹:
(代码)
当你调试“真正的”模块(faulty太短了,没有什么意义)时,将寄存器和堆栈解码是非常有益的,而且如果被调试的所有模块符号都开放出来时更有帮助。在失效时,处理器寄存器一般不会指向模块的符号,只有当符号表开放给/proc/ksyms时,你才能输出中标别它们。
我们可以用一下步骤制作一张更完整的符号表。首先,我们不应在模块中声明静态变量,否则我们就无法用insmod开放它们了。第二,如下面的截取自scull的init_module函数的代码所示,我们可以用#ifdef SCULL_DEBUG或类似的宏屏蔽register_symtab调用。
(代码)
我们在第2章“编写和运行模块”的“注册符号表”一节中已经看到了类似内容,那里说,如果模块不注册符号表,所有的全局符号就都开放。尽管这一功能仅在SCULL_DEBUG被激活时才有效,为了避免内核中的名字空间污染,所有的全局符号有合适的前缀(参见第2章的“模块与应用程序”一节)。
使用klogd
klogd守护进程的近期版本可以在oops存放到记录文件前对oops消息解码。解码过程只由版本1.3或更新版本的守护进程完成,而且只有将-k /usr/src/linux/System.map做为参数传递给守护进程时才解码。(你可以用其他符号表文件代替System.map)
有新的klogd给出的faulty的oops如下所示,它写到了系统记录中:
(代码)
我想能解码的klogd对于调试一般的Linux安装的核心来说是很好的工具。由klogd解码的消息包括大部分ksymoops的功能,而且也要求用户编译额外的工具,或是,当系统出现故障时,为了给出完整的错误报告而合并两个输出。当oops发生在内核时,守护进程还会正确地解码指令指针。它并不反汇编代码,但这不是问题,当错误报告给出消息时,二进制数据仍然存在,可以离线反汇编代码。
守护进程的另一个功能就是,如果符号表版本与当前内核不匹配,它会拒绝解析符号。如果在系统记录中解析出了符号,你可以确信它是正确的解码。
然而,尽管它对Linux用户很有帮助,这个工具在调试模块时没有什么帮助。我个人没有在开放软件的电脑里使用解码选项。klogd的问题是它不解析模块中的符号;因为守护进程在程序员加载模块前就已经运行了,即使读了/proc/ksyms也不会有什么帮助。记录文件中存在解析后的符号会使oops和ksymoops混淆,造成进一步解析的困难。
如果你需要使用klogd调试你的模块,最新版本的守护进程需要加入一些新的特殊支持,我期待它的完成,只要给内核打一个小补丁就可以了。
系统挂起
尽管内核代码中的大多数错误仅会导致一个oops消息,有时它们困难完全将系统挂起。如果系统挂起了,没有消息能够打印出来。例如,如果代码遇到一个死循环,内核停止了调度过程,系统不会再响应任何动作,包括魔法键Ctrl-Alt-Del组合。
处理系统挂起有两个选择――一个是防范与未然,另一个就是亡羊补牢,在发生挂起后调试代码。
通过在策略点上插入schedule调用可以防止死循环。schedule调用(正如你所猜想到的)调用调度器,因此允许其他进程偷取当然进程的CPU时间。如果进程因你的驱动程序中的错误而在内核空间循环,你可以在跟踪到这种情况后杀掉这个进程。
在驱动程序代码中插入schedule调用会给程序员带来新的“问题”:函数,,以及调用轨迹中的所有函数,必须是可重入的。在正常环境下,由于不同的进程可能并发地访问设备,驱动程序做为整体是可重入的,但由于Linux内核是不可抢占的,不必每个函数都是可重入的。但如果驱动程序函数允许调度器中断当前进程,另一个不同的进程可能会进入同一个函数。如果schedule调用仅在调试期间打开,如果你不允许,你可以避免两个并发进程访问驱动程序,所以并发性倒不是什么非常重要的问题。在介绍阻塞型操作时(第5章的“写可重入代码”)我们再详细介绍并发性问题。
如果要调试死循环,你可以利用Linux键盘的特殊键。默认情况下,如果和修饰键一起按了PrScr键(键码是70),系统会向当前控制台打印有关机器状态的有用信息。这一功能在x86和Alpha系统都有。Linux的Sparc移植也有同样的功能,但它使用了标记为“Break/Scroll Lock”的键(键码是30)。
每一个特殊函数都有一个名字,并如下面所示都有一个按键事件与之对应。组合键之后的括号里是函数名。
Shift-PrScr(Show_Memory)
打印若干行关于内存使用的信息,尤其是有关缓冲区高速缓存的使用情况。
Control-PrScr(Show_State)
针对系统里的每一个处理器打印一行信息,同时还打印内部进程树。对当前进程进行标记。
RightAlt-PrScr(Show_Registers)
由于它可以打印按键时的处理器寄存器内容,它是系统挂起时最重要的一个键了。如果有当前内核的系统表的话,查看指令计数器以及它如何随时间变化,对了解代码在何处循环非常有帮助。
如果想将这些函数映射到不同的键上,每一个函数名都可以做为参数传递给loadkeys。键盘映射表可以任意修改(这是“策略无关的”)。
如果console_loglevel足够到的话,这些函数打印的消息会出现在控制台上。如果不是你运行了一个旧klogd和一个新内核的话,默认记录级应该足够了。如果没有出现消息,你可以象以前说的那样提升记录级。“足够高”的具体值与你使用的内核版本有关。对于Linux 2.0或更新的版本来说是5。
即便当系统挂起时,消息也会打印到控制台上,确认记录级足够高是非常重要的。消息是在产生中断时生成的,因此即便有错的进程不释放CPU也可以运行――当然,除非中断被屏蔽了,不过如果发生这种情况既不太可能也非常不幸。
有时系统看起来象是挂起了,但其实不是。例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况。这种假挂起可以通过查看你为探明此种情况而运行的程序输出来判断。我有一个程序会不断地更新LED显示器上的时钟,我发现这个对于验证调度器尚在运行非常有用。你可以不必依赖外部设备就可以检查调度器,你可以实现一个程序让键盘LED闪烁,或是不断地打开关闭软盘马达,或是不断触动扬声器――不过我个人认为,通常的蜂鸣声很烦人,应该尽量避免。看看ioctl命令KDMKTONE。O’Reilly FTP站点上的例子程序(misc-progs/heartbeat.c)中有一个是让键盘LED不断闪烁的。
如果键盘不接收输入了,最佳的处理手段是从网络登录在系统中,杀掉任何违例的进程,或是重新设置键盘(用kdb_mode -a)。然而,如果你没有网络可用来恢复的话,发现系统挂起是由键盘锁死造成的一点儿用也没有。如果情况确实是这样,你应该配置一种替代输入设备,至少可以保证正常地重启系统。对于你的计算机来说,关闭系统或重启比起所谓的按“大红钮”要更方便一些,至少它可以免去长时间地fsck扫描磁盘。
这种替代输入设备可以是游戏杆或是鼠标。在sunsite.edu.cn上有一个游戏杆重启守护进程,gpm-1.10或更新的鼠标服务器可以通过命令行选项支持类似的功能。如果键盘没有锁死,但是却误入“原始”模式,你可以看看kdb包中文档介绍的一些小技巧。我建议最好在问题出现以前就看看这些文档,否则就太晚了。另一种可能是配置gpm-root菜单,增添一个“reboot”或“reset keyboard”菜单项;gpm-root一个响应控制鼠标事件的守护进程,它用来在屏幕上显示菜单和执行所配置的动作。
最好,你会可以按“留意安全键”(SAK),一个用于将系统恢复为可用状态的特殊键。由于不是所有的实现都能用,当前Linux版本的默认键盘表中没有为此键特设一项。不过你还是可以用loadkeys将你的键盘上的一个键映射为SAK。你应该看看drivers/char目录中的SAK实现。代码中的注释解释了为什么这个键在Linux 2.0中不是总能工作,这里我就不多说了。
不过,如果你运行版本2.1.9或是更新的版本,你就可以使用非常可靠地留意安全键了。此外,2.1.43及后续版本内核还有一个编译选项选择是否打开“SysRq魔法键”;我建议你看一看drivers/char/sysrq.c中的代码并使用这项新技术。
如果你的驱动程序真的将系统挂起了,而且你有不知道在哪插入schedule调用,最佳的处理方法就是加一些打印消息,并将它们打印到控制台上(通过修改console_loglevel变量值)。在重演挂起过程时,最好将所有的磁盘都以只读方式安装在系统上。如果磁盘是只读的或没有安装,就不会存在破坏文件系统或使其进入不一致状态的危险。至少你可以避免在复位系统后运行fsck。另一中方法就是使用NFS根计算机来测试模块。在这种情况下,由于NFS服务器管理文件系统的一致性,而它又不会受你的驱动程序的影响,你可以避免任何的文件系统崩溃。
使用调试器
最后一种调试模块的方法就是使用调试器来一步步地跟踪代码,查看变量和机器寄存器的值。这种方法非常耗时,应该尽可能地避免。不过,某些情况下通过调试器对代码进行细粒度的分析是非常有益的。在这里,我们所说的被调试的代码运行在内核空间――除非你远程控制内核,否则不可能一步步跟踪内核,这会使很多事情变得更加困难。由于远程控制很少用到,我们最后介绍这项技术。所幸的是,在当前版本的内核中可以查看和修改变量。
在这一级上熟练地使用调试器需要精通gdb命令,对汇编码有一定了解,并且有能够将源码与优化后的汇编码对应起来的能力。
不幸的是,gdb更适合与调试核心而不是模块,调试模块化的代码需要更多的技术。这更多的技术就是kdebug包,它利用gdb的“远程调试”接口控制本地内核。我将在介绍普通调试器后介绍kdebug。
使用gdb
gdb在探究系统内部行为时非常有用。启动调试器时必须假想内核就是一个应用程序。除了指定内核文件名外,你还应该在命令行中提供内存镜象文件的名字。典型的gdb调用如下所示:
(代码)
第一个参数是未经压缩的内核可执行文件(在你编译完内核后,这个文件在/usr/src/linux目录中)的名字。只有x86体系结构有zImage文件(有时称为vmlinuz),它是一种解决Intel处理器实模式下只有640KB限制的一种技巧;而无论在哪个平台上,vmlinux都是你所编译的未经压缩的内核。
gdb命令行的第二个参数是是内存镜象文件的名字。与其他在/proc下的文件类似,/proc/kcore也是在被读取时产生的。当read系统调用在/proc文件系统执行时,它映射到一个用于数据生成而不是数据读取的函数上;我们已在“使用/proc文件系统”一节中介绍了这个功能。系统用kcore来表示按内存镜象文件格式存储的内核“可执行文件”;由于它要表示整个内核地址空间,它是一个非常巨大的文件,对应所有的物理内存。利用gdb,你可以通过标准gdb命令查看内核标量。例如,p jiffies可以打印从系统启动到当前时刻的时钟滴答数。
当你从gdb打印数据时,内核还在运行,不同数据项会在不同时刻有不同的数值;然而,gdb为了优化对内存镜象文件的访问会将已经读到的数据缓存起来。如果你再次查看jiffies变量,你会得到和以前相同的值。缓存变量值防止额外的磁盘操作对普通内存镜象文件来说是对的,但对“动态”内存镜象文件来说就不是很方便了。解决方法是在你想刷新gdb缓存的时候执行core-file /proc/kcore命令;调试器将使用新的内存镜象文件并废弃旧信息。但是,读新数据时你并不总是需要执行core-file命令;gdb以1KB的尺度读取内存镜象文件,仅仅缓存它所引用的若干块。
你不能用普通gdb做的是修改内核数据;由于调试器需要在访问内存镜象前运行被调试程序,它是不会去修改内存镜象文件的。当调试内核镜象时,执行run命令会导致在执行若干指令后导致段违例。出于这个原因,/proc/kcore都没有实现write方法。
如果你用调试选项(-g)编译了内核,结果产生的vmlinux比没有用-g选项的更适合于gdb。不过要注意,用-g选项编译内核需要大量的磁盘空间――支持网络和很少几个设备和文件系统的2.0内核在PC上需要11KB。不过不管怎样,你都可以生成zImage文件并用它来其他系统:在生成可启动镜象时由于选项-g而加入的调试信息最终都被去掉了。如果我有足够的磁盘空间,我会一致打开-g选项的。
在非PC计算机上则有不同的方法。在Alpha上,make boot会在生成可启动镜象前将调试信息去掉,所以你最终会获得vmlinux和vmlinux.gz两个文件。gdb可以使用前者,但你只能用后者启动。在Sparc上,默认情况下内核(至少是2.0内核)不会被去掉调试信息,所以你需要在将其传递给silo(Sparc的内核加载器)前将调试信息去掉,这样才能启动。由于尺寸的问题,无论milo(Alpha的内核加载器)还是silo都不能启动未去掉调试信息的内核。
当你用-g选项编译内核并且用vmlinux和/proc/kcore一起使用调试器,gdb可以返回很多有关内核内部结构的信息。例如,你可以使用类似于这样的命令,p *module_list,p *module_list->next和p *chrdevs[4]->fops等显示这些结构的内容。如果你手头有内核映射表和源码的话,这些探测命令是非常有用的。
另一个gdb可以在当前内核上执行的有用任务是,通过disassemble命令(它可以缩写)或是“检查指令”(x/i)命令反汇编函数。disassemble命令的参数可以是函数名或是内存区范围,而x/i则使用一个内存地址做为参数,也可以用符号名。例如,你可以用x/20i反汇编20条指令。注意,你不能反汇编一个模块的函数,这是因为调试器处理vmlinux,它并不知道你的模块的信息。如果你试图用模块的地址反汇编代码,gdb很有可能会报告“不能访问xxxx处的内存(Cannot access memory at xxxx)”。基于同样的原因,你不查看属于模块的数据项。如果你知道你的变量的地址,你可以从/dev/mem中读出它的值,但很难弄明白从系统内存中分解出的数据是什么含义。
如果你需要反汇编模块函数,你最好对用objdump工具处理你的模块文件。很不幸,该工具只能对磁盘上的文件进行处理,而不能对运行中的模块进行处理;因此,objdump中给出的地址都是未经重定位的地址,与模块的运行环境无关。
如你所见,当你的目的是查看内核的运行情况时,gdb是一个非常有用的工具,但它缺少某些功能,最重要的一些功能就是修改内核项和访问模块的功能。这些空白将由kdebug包填补。
使用kdebug
你可用从一般的FTP站点下的pcmcia/extras目录下拿到kdebug,但是如果你想确保拿到的是最新的版本,你最好到ftp://hyper.stanford.edu/pub/pcmcia/extras/去找。该工具与pcmcia没有什么关系,但是这两个包是同一个作者写的。
kdebug是一个使用gdb“远程调试”接口与内核通信的小工具。使用时首先向内核加载一个模块,调试器通过/dev/kdebug访问内核数据。gdb将该设备当成一个与被调试“应用”通信的串口设备,但它仅仅是一个用于访问内核空间的通信通道。由于模块本身运行在内核空间,它可以看到普通调试器无法访问的内核空间地址。正如你所猜想到的,模块是一个字符设备驱动程序,并且使用了主设备号动态分配技术。
kdebug的优点在于,你无需打补丁或重新编译:无论是内核还是调试器都无需修改。你所需要做的就是编译和安装软件包,然后调用kgdb,kgdb是一个完成某些配置并调用gdb,通过新接口访问内核部件结构的脚本程序。
但是,即便是kdebug也没有提供单步跟踪内核代码和设置断点的功能。这几乎是不可避免的,因为内核必须保持运行状态以保证系统的出于运行状态,跟踪内核代码的唯一方法就是后面将要谈到的从另外一台计算机上通过串口控制系统。不过kgdb的实现允许用户修改被调试应用(即当前内核)的数据项,可以传递给内核任意数目的参数,并以读写方式访问模块所属的内存区。
最后一个功能就是通过gdb命令将模块符号表增加到调试器内部的符号表中。这个工作是由kgdb完成的。然后当用户请求访问某个符号时,gdb就知道它的地址是哪了。最终的访问是由模块里的内核代码完成的。不过要注意,kdebug的当前版本(1.6)在映射模块化代码地址方面还有些问题。你最好通过打印一些符号并与/proc/ksyms中的值进行比较来做些检查。如果地址没有匹配,你可以使用数值,但必须将它们强行转换为正确的类型。下面就是一个强制类型转换的例子:
(代码)
kdebug的另一个强于gdb的优点是,它允许你在数据结构被修改后读取到最新的值,而不必刷新调试器的缓存;gdb命令set remotecache 0可以用来关闭数据缓存。
由于kdebug与gdb使用起来很相似,这里我就不过多地罗列使用这个工具的例子了。对于知道如何使用调试器的人来说,这种例子很简单,但对于那些对调试器一无所知的人来说就很晦涩了。能够熟练地使用调试器需要时间和经验,我不准备在这里承担老师的责任。
总而言之,kdebug是一个非常好的程序。在线修改数据结构对于开发人员来说是一个非常大的进步(而且一种将系统挂起的最简单方法)。现在有许多工具可以使你的开发工作更轻松――例如,在开发scull期间,当模块的使用计数器增长后*,我可以使用kdebug来将其复位为0。这就不必每次都麻烦我重启机器,登录,再次启动我的应用程序等等。
远程调试
调试内核镜象的最后一个方法是使用gdb的远程调试能力。
当执行远程调试的时候,你需要两台计算机:一台运行gdb;另一台运行你要调试的内核。这两台计算机间用普通串口连接起来。如你所料,控制gdb必须能够理解它所控制的内核的二进制格式。如果这两台计算机是不同的体系结构,必须将调试器编译为可以支持目标平台的。
在2.0中,Linux内核的Intel版本不支持远程调试,但是Alpha和Sparc版本都支持。在Alpha版本中,你必须在编译时包含对远程调试的支持,并在启动时通过传递给内核命令行参数kgdb=1或只有kgdb打开这个功能。在Sparc上,始终包含了对远程调试的支持。启动选项kgdb=ttyx可以用来选择在哪个串口上控制内核,x可以是a或b。如果没有使用kgdb=选项,内核就按正常方式启动。
如果在内核中打开了远程调试功能,系统在启动时就会调用一个特殊的初始化函数,配置被调试内核处理它自己的断点,并且跳转到一个编译自程序中的断点。这会暂停内核的正常执行,并将控制转移给断点服务例程。这一处理函数在串口线上等待来自于gdb的命令,当它获得gdb的命令后,就执行相应的功能。通过这一配置,程序员可以单步跟踪内核代码,设置断点,并且完成gdb所允许的其他任务。
在控制端,需要一个目标镜象的副本(我们假设它是linux.img),还需要一个你要调试的模块副本。如下命令必须传递给gdb:
file linux.img
file命令告诉gdb哪个二进制文件需要调试。另一种方法是在命令行中传递镜象文件名。这个文件本身必须和运行在另一端的内核一模一样。
target remote /dev/ttyS1
这条命令通知gdb使用远程计算机做为调试过程的目标。/dev/ttyS1是用来通信的本地串口,你可以指定任一设备。例如,前面介绍的kdebug软件包中的kgdb脚本使用target remote /dev/kdebug。
add-symbol-file module.o address
如果你要调试已经加载到被控内核的模块的话,在控制系统上你需要一个模块目标文件的副本。add-symbol-file通知gdb处理模块文件,假定模块代码被定位在地址address上了。
尽管远程调试可以用于调试模块,但你还是要加载模块,并且在模块上插入断点前还需要触发另一个断点,调试模块还是需要很多技巧的。我个人不会使用远程调试去跟踪模块,除非异步运行的代码,如中断处理函数,出了问题。
形式
#include <sys/ptrace.h>
int ptrace(int request, int pid, int addr, int data);
描述
Ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。
Request参数决定了系统调用的功能:
PTRACE_TRACEME
本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
从内存地址中读取一个字节,内存地址由addr给出。
PTRACE_PEEKUSR
从USER区域中读取一个字节,偏移量为addr。
PTRACE_POKETEXT, PTRACE_POKEDATA
往内存地址中写入一个字节。内存地址由addr给出。
PTRACE_POKEUSR
往USER区域中写入一个字节。偏移量为addr。
PTRACE_SYSCALL, PTRACE_CONT
重新运行。
PTRACE_KILL
杀掉子进程,使它退出。
PTRACE_SINGLESTEP
设置单步执行标志
PTRACE_ATTACH
跟踪指定pid 进程。
PTRACE_DETACH
结束跟踪
Intel386特有:
PTRACE_GETREGS
读取寄存器
PTRACE_SETREGS
设置寄存器
PTRACE_GETFPREGS
读取浮点寄存器
PTRACE_SETFPREGS
设置浮点寄存器
init进程不可以使用此函数
返回值
成功返回0。错误返回-1。errno被设置。
错误
EPERM
特殊进程不可以被跟踪或进程已经被跟踪。
ESRCH
指定的进程不存在
EIO
请求非法
功能详细描述
PTRACE_TRACEME
形式:ptrace(PTRACE_TRACEME,0 ,0 ,0)
描述:本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
形式:ptrace(PTRACE_PEEKTEXT, pid, addr, data)
描述:从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。
PTRACE_POKETEXT, PTRACE_POKEDATA
形式:ptrace(PTRACE_POKETEXT, pid, addr, data)
描述:往内存地址中写入一个字节。pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据。
PTRACE_PEEKUSR
形式:ptrace(PTRACE_PEEKUSR, pid, addr, data)
描述:从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。USER结构为core文件的前面一部分,它描述了进程中止时的一些状态,如:寄存器值,代码、数据段大小,代码、数据段开始地址等。在Linux(i386)中通过PTRACE_PEEKUSER和PTRACE_POKEUSR可以访问USER结构的数据有寄存器和调试寄存器。
PTRACE_POKEUSR
形式:ptrace(PTRACE_POKEUSR, pid, addr, data)
描述:往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据。
PTRACE_CONT
形式:ptrace(PTRACE_CONT, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
PTRACE_SYSCALL
形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。
PTRACE_KILL
形式:ptrace(PTRACE_KILL,pid)
描述:杀掉子进程,使它退出。pid表示被跟踪的子进程。
PTRACE_SINGLESTEP
形式:ptrace(PTRACE_KILL, pid, 0, signle)
描述:设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。
PTRACE_ATTACH
形式:ptrace(PTRACE_ATTACH,pid)
描述:跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入中止状态。
PTRACE_DETACH
形式:ptrace(PTRACE_DETACH,pid)
描述:结束跟踪。 pid表示被跟踪的子进程。结束跟踪后被跟踪进程将继续执行。
PTRACE_GETREGS
形式:ptrace(PTRACE_GETREGS, pid, 0, data)
描述:读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有17个基本寄存器的值。
PTRACE_SETREGS
形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有17个基本寄存器的值。
PTRACE_GETFPREGS
形式:ptrace(PTRACE_GETFPREGS, pid, 0, data)
描述:读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有浮点协处理器387的所有寄存器的值。
PTRACE_SETFPREGS
形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有浮点协处理器387的所有寄存器的值。
cpp.info手册对此有详细说明。
我们在阅读linux源代码时都有这样的体会:核心的组织相对松散,在看一个文件时往往要牵涉到其他的头文件、源代码文件。如此来回跳转寻找变量、常量、函数的定义十分不方便,这样折腾几次,便使读代码的心情降到了低点。
lxr(linux cross reference)就是一个解决这个问题的工具:他对你指定的源代码文件建立索引数据库,利用perl脚本CGI动态生成包含源码的web页面,你可以用任何一种浏览器查阅。在此web页中,所有的变量、常量、函数都以超连接的形式给出,十分方便查阅。比如你在阅读/usr/src/linux/net/socket.c的源代码,发现函数get_empty_inode不知道是如何以及在哪里定义的,这时候你只要点击 get_empty_inode,lxr将返回此函数的定义、实现以及各次引用是在什么文件的哪一行,注意,这些信息也是超连接,点击将直接跳转到相应的文件相应的行。另外lxr还提供标识符搜索、文件搜索,结合程序glimpse还可以提供对所有的源码文件进行全文检索,甚至包括注释!
下面将结合实例介绍一下lxr和glimpse的基本安装和使用,由于glimpse比较简单,就从它开始:
首先访问站点: http://glimpse.cs.arizona.edu/ 得到glimpse 的源码,比如我得到的是glimpse-4.12.5.tar.gz .
用root登录,在: 任一目录下用tar zxvf glimpse-4.12.5.tar.gz解开压缩包,在当前目录下出现新目录glimpse-4.12.5 .进入该目录,执行make即可。进入bin目录,将文件glimpse和glimpseindex拷贝到/bin或/usr/bin下即可。如果单独使用glimpse,那么只要简单的执行glimpseindex foo 即可,其中foo是你想要索引的目录,比如说是/usr/src/linux .glimpseindex 的执行结果是在你的起始目录下产生若干.glimpse*的索引文件。然后你只要执行glimpse yourstring即可查找/usr/src/linux下所有包含字符串yourstring的文件。
对于lxr,你可以访问 http://lxr.linux.no/得到它的源代码。解包后,遵循如下步骤:
/*下面的文字来源于lxr的帮助文档以及本人的安装体会*/
1)修改Makefile中的变量PERLBIN和INSTALLPREFIX,使它们分别为 perl程序的位置和你想lxr安装的位置.在我的机器上,PERLBIN的值为 /usr/bin/perl .至于INSTALLPREFIX,有如下原则,lxr的安装路径必须是web服务器能有权限访问。因此它的值简单一点可取 /home/httpd/html/lxr (对于Apache web server)。
2)执行 make install
3)修改$INSTALLPREFIX/http/lxr.conf :
baseurl : http://yourIP/lxr/http/
htmlhead: /home/httpd/html/lxr/http/template-head
htmltail: /home/httpd/html/lxr/http/template-tail
htmldir: /home/httpd/html/lxr/http/template-dir
sourceroot : /usr/src/linux # 假如对linux核心代码索引
dbdir : /home/httpd/html/lxr/dbdir/ #dbdirk可任意起名,且位置任意
glimpsebin: /usr/bin/glimpse #可执行程序glimpse的位置
4)在$INSTALLPREFIX/http/下增加一个文件.htaccess 内容:
SetHandler cgi-script
上面这个文件保证Apache server将几个perl文件作为cgi-script.
5)按照lxr.conf中的设置建立dbdir ,按照上例,建立目录 /home/httpd/html/lxr/dbdir
进入这个目录执行$INSTALLPREFIX/bin/genxref yourdir
其中yourdir是源码目录,比如/usr/src/linux
如果要结合glimpse,则执行glimpseindex -H . yourdir
6)修改 /etc/httpd/conf/access.conf ,加入
《Directory /home/httpd/html/lxr/http》 Options All AllowOverride All order allow,deny allow from all
《/Directory》
7)进入/etc/rc.d/init.d/ 执行
killall httpd
./httpd start
进入X ,用浏览器 http://yourIP/lxr/http/blurb.html
大功告成 ,这下你可以舒心的读源码了。
注意:以上只是lxr和glimpse的基本用法,进一步的说明可以参考连机文档。
***文中的“《”“》”,实际为“<”“>”,sorry,不这么写就不显示了:(
Linux的内核源代码可以从很多途径得到。一般来讲,在安装的linux系统下,/usr/src/linux目录下的东西就是内核源代码。另外还可以从互连网上下载,解压缩后文件一般也都位于linux目录下。内核源代码有很多版本,目前最新的稳定版是2.2.14。
许多人对于阅读Linux内核有一种恐惧感,其实大可不必。当然,象Linux内核这样大而复杂的系统代码,阅读起来确实有很多困难,但是也不象想象的那么高不可攀。只要有恒心,困难都是可以克服的。也不用担心水平不够的问题,事实上,有很多事情我们不都是从不会到会,边干边学的吗?
任何事情做起来都需要有方法和工具。正确的方法可以指导工作,良好的工具可以事半功倍。对于Linux 内核源代码的阅读也同样如此。下面我就把自己阅读内核源代码的一点经验介绍一下,最后介绍Window平台下的一种阅读工具。
对于源代码的阅读,要想比较顺利,事先最好对源代码的知识背景有一定的了解。对于linux内核源代码来讲,我认为,基本要求是:1、操作系统的基本知识;2、对C语言比较熟悉,最好要有汇编语言的知识和GNU C对标准C的扩展的知识的了解。另外在阅读之前,还应该知道Linux内核源代码的整体分布情况。我们知道现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序、网络等组成。看一下Linux内 核源代码就可看出,各个目录大致对应了这些方面。Linux内核源代码的组成如下(假设相对于linux目录):
arch 这个子目录包含了此核心源代码所支持的硬件体系结构相关的核心代码。如对于X86平台就是i386。
include 这个目录包括了核心的大多数include文件。另外对于每种支持的体系结构分别有一个子目录。
init 此目录包含核心启动代码。
mm 此目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,如对应于X86的就是arch/i386/mm/fault.c 。
drivers 系统中所有的设备驱动都位于此目录中。它又进一步划分成几类设备驱动,每一种也有对应的子目录,如声卡的驱动对应于drivers/sound。
ipc 此目录包含了核心的进程间通讯代码。
modules 此目录包含已建好可动态加载的模块。
fs Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext2文件系统对应的就是ext2子目录。
kernel 主要核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。
net 核心的网络部分代码。里面的每个子目录对应于网络的一个方面。
lib 此目录包含了核心的库代码。与处理器结构相关库代码被放在arch/*/lib/目录下。
scripts此目录包含用于配置核心的脚本文件。
Documentation 此目录是一些文档,起参考作用。
清楚了源代码的结构组成后就可以着手阅读。对于阅读方法或者说顺序,有所谓的纵向与横向之分。所谓纵向就是顺着程序的执行顺序逐步进行;所谓横向,就是分模块进行。其实他们之间不是绝对的,而是经常结合在一起进行。对于Linux源代码来讲,启动的代码就可以顺着linux的启动顺序一步一步来,它的大致流程如下(以X86平台为例):
./larch/i386/boot/bootSect.S-->./larch/i386/boot/setup.S-->./larch/i386/kernel/head.S-->./init/main.c中的start_kernel()。而对于象内存管理等部分,则可以单独拿出来进行阅读分析。我的体会是:开始最好按顺序阅读启动代码,然后进行专题阅读,如进程部分,内存管理部分等。在每个功能函数内部应该一步步来。实际上这是一个反复的过程,不可能读一遍就理解。
俗话说:“工欲善其事,必先利其器”。 阅读象Linux核心代码这样的复杂程序令人望而生畏。它象一个越滚越大的雪球,阅读核心某个部分经常要用到好几个其他的相关文件,不久你将会忘记你原来在干什么。所以没有一个好的工具是不行的。由于大部分爱好者对于Window平台比较熟悉,并且还是常用Window系列平台,所以在此我介绍一个Window下的一个工具软件:Source Insight。这是一个有30天免费期的软件,可以从www.sourcedyn.com下载。安装非常简单,和别的安装一样,双击安装文件名,然后按提示进行就可以了。安装完成后,就可启动该程序。这个软件使用起来非常简单,是一个阅读源代码的好工具。它的使用简单介绍如下:先选择Project菜单下的new,新建一个工程,输入工程名,接着要求你把欲读的源代码加入(可以整个目录加)后,该软件就分析你所加的源代码。分析完后,就可以进行阅读了。对于打开的阅读文件,如果想看某一变量的定义,先把光标定位于该变量,然后点击工具条上的相应选项,该变量的定义就显示出来。对于函数的定义与实现也可以同样操作。别的功能在这里就不说了,有兴趣的朋友可以装一个Source Insight,那样你阅读源代码的效率会有很大提高的。怎么样,试试吧!
内核重编译对很多Linux爱好者来说是一个不小的挑战。笔者认为,很多Linux用户对内核通常有一种误解,他们认为普通用户是不能调整内核的。其实,就实际情况而言,这种认识是不全面的。应该说,内核重编译是具有一定深度和复杂性的,同时也是易失败的配置工作。
如果大家留意那些比较权威的Linux参考工具书的话,就会发现很多原版书籍都把内核重编译作为一个很重要的章节进行介绍。本文将要向读者介绍的并不是如何去一步步的对Linux内核进行重编译,而是收集整理了几个在Linux内核重编译中最常见的故障及其解决方法,如果您在编译内核过程中遇到了类似的故障,那么本文将会对您有所帮助。
Linux内核重编译常见故障介绍
在介绍Linux内核重编译常见故障前,假设我们已经按照参考工具书上的步骤对Linux内核进行了相应的配置。
Linux内核重编译通常包括了许多步骤。如果“幸运”的话,Linux内核重编译是可以一次性编译成功的。如果在编译完成后,启动计算机或者内核的时候系统有错误信息的提示,那么最有可能出现的是以下5个:1.内核不能启动;2.异常I/O错误;3.内核反映缓慢;4.内核不能正确编译;5.系统重复启动。
故障分析及其解决方法
·内核不能启动
当我们重新创建Linux内核时,主要是选择用户需要或不需要在系统中使用的设备及服务。从2.0版内核开始,Linux引入了一个全新的设计特征到内核中并提供了折中方案:组件可以动态的、随时的调入和调出内核。例如我们在修改/etc/lilo.conf之后都要重新启动系统,如果你的内核不能启动,并且在屏幕上看到了下面的信息:
Warning: unable to open an initial console Kernel panic: no init found. Try passing init= option to kernel
这个错误最大的可能就是我们没有正确的给/etc/lilo.conf 中的“root=”提供参数。例如,在一个Linux系统中有root=dev/hdc5这样的配置方式,那么这是错误的,正确的应该是root=/dev/hdc5,不要小看只是多了一个 “/”,这是给root提供的重要参数。没有“/”则Linux内核无法确认root到底该从哪里启动。很多朋友往往忽略了这个小细节而造成内核引导失败。下面给出/etc/lilo.conf的一个正确的配置清单,仅供参考。
/etc/lilo.conf示例
―――――――――――
boot=/dev/hdc5
map=/boot/map
prompt
timeout=50
image=/boot/vmlinuz-2.2.2-1
label=Linux
root=/dev/hdc5
inirtd=/boot/initrd-2.2.2-1.img
read-only
other=/dev/hda1
label=dos
table=/dev/hda
·异常I/O错误
如果您重新创建了一个Linux内核,并且能正确启动,但在使用新内核过程中,系统经常出现崩溃、死机等异常现象。那么很可能是I/O出现了问题。I/O异常除了使得系统频繁出现莫名其妙的故障之外,更重要的是会使Linux内核降级,导致整个系统系能严重下降。
究其异常I/O错误的原因,是用户在编译Linux内核结束的时候没有执行“make dep clean”这一步骤。一般来说,我们在保存Kernel configuration选项中的“menuconfig”或“xconfig”时并不包含“make dep clean”这个步骤。因此,这里建议用户在保存配置后的Kernel configuration选项时注意确认是否已经进行了“make dep clean”这一步。
另外,请注意硬件问题,很多时候编译内核是因为系统新增加了一些硬件设施。在硬件安装的时候有可能造成插槽松动等问题。
·内核反映缓慢
目前很多计算机都采用了高速的CPU和大容量内存。可有时候在创建新内核后系统显得比没有配置内核之前的反映速度慢得多。出现这个情况,很可能是用户在修改Kernel configuration options的时候,在“menuconfig”或者“xconfig”选择了过多的选项。这样不仅使得计算机在编译新内核的时候要花费更多的时间,也使得系统在工作的时候占用了太多的内存。由于很多内存都是被无用的选项所占用,这就导致了系统运行的缓慢。解决方法很简单,尽量选择我们需要的选项,那些无用的,太过于复杂的选项就无需去修改了,有时候使用默认的参数效果会显得更好。
进一步,例如redhat这样的发行版,为了支持尽可能多的硬件,所以选择支持了几乎所有的板卡驱动程序。在自己编译的时候,就可以不客气地“排头砍去”,只保留自己系统特有的硬件驱动。这样你才会得到比一般系统更好的性能。
·内核不能编译
当用户输入“make bzImage modules”并按下输入键的时候,出现了内核不能编译或者其他的奇怪现象。此时最好的方法就是重新启动系统,然后使用“rpm -e”命令移除Kernel configuration tools这个内核配置工具。接着再重新安装这个工具(请使用“rpm -I”或“rpm -Uvh”命令来安装),如果能正确安装,那么问题就简单多了,此时再重新配置内核和再编译应该就能成功。如果还是不能编译内核,请检查硬件设备是不是有问题。
·系统重复启动
出现这种情况,十有八九是因为在对内核做完修改之后忘记使用LILO来注册新的映象到启动加载程序。LILO需要内核的扇区位置,因此即使是拷贝映象也会将其迁移到新的扇区中,这将使得LILO存储的老指针挂在一个深渊上。
为了解决这个问题,请从软驱启动并运行LILO命令,或使用挽救磁盘启动并安装启动分区到“/mnt”,同时使用如下命令:lilo -r /mnt。