I want to be a complete engineer - technical genius and sensitive humanist all in one!

Showing posts with label 内核. Show all posts
Showing posts with label 内核. Show all posts

Friday, July 29, 2011

跑步与减肥

长跑,除了锻炼身体外还能磨练一个人的意志力,越是忍受不了的事情越是要坚持不懈,毫不放松!

减肥除了重塑自己的形体外同时也是很能磨练人意志力的事情。当饥肠辘辘的时候,兜里揣着足够的钱,望着丰盛可口的美食,你是否有足够的意志力来抗拒这种诱惑?

所以,长跑和减肥要贯穿日常生活,成为日常生活的一部分,习惯成自然!

Thursday, September 3, 2009

Linux 虚拟文件系统概述

[翻译说明:Richard Gooch 的这份 Overview of the Linux Virtual File System 位于内核
源代码的 Documentation/filesystems/下, 文件名是 vfs.txt。 该文档在 Understanding
the Linux Kernel 中被推荐, 从 2.6.26 官方内核中拷贝出来翻译,详细情况看原文。

介绍
====

虚拟文件系统(也被称做虚拟文件系统切换)是内核中为应用程序提供文件系统接口的一个软件层。
同时, 它也为不同的文件系统共存, 提供了一种抽象。

open(2),stat(2),read(2),write(2),chmod(2)等VFS系统调用, 是在进程上下文中被
调用的。 文件锁的描述在 Documnetation/filesystems/Locking中。


目录项缓存(dcache)
-----------------

VFS实现了诸如 open(2),stat(2),chmod(2)等系统调用。 路径名作为参数传递给这些系统
调用, 然后VFS使用路径名, 在目录项缓存(也叫 dentry cache 或 dcache)中查找相应的目
录项(dentry)。 通过目录项缓存, 路径名(文件名)能迅速得转换成相应的目录项。 目录项只存
在于内存中, 不会保存到磁盘上:它们存在的意义在于性能。

目录项缓存(dcache)旨在维护整个文件空间的一个视图。 但大多数计算机无法将文件系统的整
个目录结构缓存在内存中, 因此有些文件没有被缓存。 为了把路径名解析成相应目录项(dentry),
VFS得一步步的将路径名分解,创建目录项(dentry),读取相应的Inode。 这个过程是通过
Inode中的查找方法做到的。

Inode 对象
---------

一个目录项 (dentry) 通常包含指向 inode 的指针。 Inode是文件系统中的对象, 诸如常规文件,
目录, 管道(FIFO) 和其他的文件系统对象。 它们要么保存在于磁盘上(块设备上的文件系统), 要么
存在于内存中(伪文件系统)。 当需要时, 在磁盘上的 inode会被读取到内存中; 对inode的修改也会
写回到磁盘上。 一个 Inode可以被多个目录项(dentry)指向(例如硬链接就是这样)。

查找一个文件的Inode, VFS需要调用它的父目录对应Inode的lookup()方法。 该方法由Inode所在
的文件系统实现来定义。 一旦 VFS找到了dentry(因此也就知道了inode), open(2)系统调用就能
打开文件、 stat(2)就能查看inode内的数据。 Stat(2)操作非常简单: 一旦 VFS找到了dentry,
它就查看相关的inode并把部分数据返回给用户空间。

File 对象
--------

打开文件时还需要一个操作: 分配一个file结构(就是内核对文件描述符的实现)。 初始化这个新分配的
file结构时, 其中的一个指针会指向目录项(dentry),另一个指针会指向文件操作成员函数表──这是
从 inode中取来的。 然后函数表中的open()方法会被调用, 这样, 就调用了文件系统自己的open方法。
File结构被放置在进程的文件描述符表中。

读、写和关闭文件(以及其他与 VFS有关的操作)时, 首先使用用户空间提供的的文件描述符找到相应的
file结构, 然后调用file结构指向的函数表中的相应函数, 去执行相应的操作。只要文件还在打开着, 目录
项(dentry)就处于使用状态, 也就意味着inode也处在使用状态。


注册和挂载一个文件系统
=====================

注册或注销一个文件系统, 使用如下的 API 函数:

#include

extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);

传递进来的 file_system_type 结构描述了一个文件系统。 当有将把某个设备挂载到文件空间的某个目录
上的请求时, VFS会调用对应文件系统(由 file_system_type 结构代表)的 get_sb()方法。 挂载点的
目录项(dentry)将被更新, 指向inode的指针将指向新挂载的文件系统的根目录inode。
( 这里更新挂载点d_inode的描述是错的 )

在/proc/filesystems文件中, 你可以看到内核中所有已注册的文件系统。

file_system_type 结构
---------------------

该结构描述了文件系统。 例如 2.6.22 内核中的代码, 该结构具有下列成员:

struct file_system_type {
const char *name;
int fs_flags;
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
};


name: 文件系统的名字, 例如"ext2"、"iso9660"、"msdos"等

fs_flags: 各种标志(亦即: FS_REQUIRES_DEV, FS_NO_DCACHE 等)

get_sb: 每当该类型的文件系统被挂载时, 调用该方法

kill_sb: 每当该类型的文件系统被卸载时, 调用该方法

owner: VFS 内部使用:多数情况下该被赋值为 THIS_MODULE

next: VFS 内部使用: 多数情况下该被赋值为 NULL

s_lock_key, s_umount_key: 处理锁依赖相关

get_sb()方法有如下参数:

struct file_system_type *fs_type: 描述文件系统, 由特定文件系统初始化

int flags: 挂载标志

const char *dev_name:要挂载的设备名

void *data: 任意的挂载选项, 通常是 ASCII 字符串的形式

struct vfsmount *mnt: 挂载点的VFS内部呈现

get_sb()方法必须探测 dev_name指定的块设备所包含的文件系统类型, 判断该方法是否支持fs_type
指定的文件系统。 如果dev_name指定的块设备被成功打开, 它将为块设备中的文件系统初始化一个
super_block结构, 如果失败, 将返回错误代码。

由get_sb()方法填充的 super_block结构的一些域中, s_op 最值得关注。 它指向是一个super_operations结构,
这个结构描述了文件系统的下一层的实现。

通常, 一个文件系统会使用通用的 get_sb()实现并自己提供一个 fill_super()方法。 通用的 get_sb()实现有:

get_sb_bdev: 挂载一个基于块设备的文件系统

get_sb_nodev: 挂载不存在于磁盘上的文件系统

get_sb_single: 挂载所有挂载点共享一个super_block结构的文件系统

fill_super()方法具有下列参数:

struct super_block *sb: superblock 结构, fill_super()方法必须初始化它

void *data: 任意的挂载选项, 通常由 ASCII 字符串组成

int silent: 出错时是否打印错误信息


Superblock 对象
==============

一个 superblock 对象代表一个挂载的文件系统。

super_operations 结构
---------------------

该结构描述了 VFS 如何操作文件系统上的 superblock, 以 2.6.22 为例, 它具有下列成员:

struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);

void (*read_inode) (struct inode *);

void (*dirty_inode) (struct inode *);
int (*write_inode) (struct inode *, int);
void (*put_inode) (struct inode *);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
void (*write_super_lockfs) (struct super_block *);
void (*unlockfs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);

int (*show_options)(struct seq_file *, struct vfsmount *);

ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
};

除非特别说明, 所有这些方法都在不持有锁的情况下调用, 这意味着这些方法可以安全的阻塞,
这些方法都必须在进程上下文调用(亦即:不是在中断处理函数或者 boottom half 中)。

alloc_inode: 由 inode_alloc()调用, 来为 inode分配空间并初始化它。 如果不定义该
方法,则分配的空间只够inode结构使用。 而通常情况下,alloc_inode会分配一个更大
的数据结构, 其中包含一个inode结构"。

destroy_inode: 由 destroy_inode()调用, 撤消为 inode分配的资源。 该方法只有在
alloc_inode被定义的时候才有效,它撤消alloc_inode做的所有事。

read_inode: 从某个挂载的文件系统中读取相关的 inode。 inode 结构的 i_ino 成员由 VFS
初始化, 来指定要读取的 inode; 其他成员由本方法填充。
你可以将该字段设置为NULL,这样就不能使用iget了, 而要使用iget5_locked来读取inode。
对于不能只通过inode号找到相应inode的文件系统, 则必需使用这种方法。

dirty_inode: 由 VFS 调用, 来把一个 inode 标记为脏。

write_inode: 当 VFS 需要把某个 inode 写入到磁盘上时, 调用本方法。 第二个参数指定
了写操作是否需要同步, 并非所有的文件系统都会检查这个标志。

put_inode: 当inode被从inode缓存(inode cache)中移除时调用

drop_inode: 当所有对inode的引用都被移除时调用, 调用时必须持有 inode_lock自旋锁。
该方法或者为 NULL(正常的 Unix 文件系统语义), 或者为 "generic_delete_inode"
(那些不想 缓存inode的文件系统。 这会导致不管 inode的引用计数的值是多少, delete_inode
总会被调用)"generic_delete_inode"的行为, 与 put_inode()中使用"force_delete"是等价的, 但
不会象后者那样会引发竞争情形。

delete_inode: 当 VFS 想删除一个 inode 时调用

put_super: 当 VFS 想释放 superblock(比如卸载时)时调用。 调用前应先调用lock_super锁住superblock

wirte_super: 当 VFS 需要将superblock写入到磁盘时调用, 该方法是可选的。

sysc_fs: 当 VFS需要把有隶属于 superblock的脏数据写入到磁盘上时调用。 第二个参 数指示了该方法是否需要
一直等待写操作的完成。 可选。

write_super_lockfs: 当 VFS需要锁定一个文件系统, 强制使它进入一致状态时调用该方法。
该方法目前用于逻辑卷管理 (Logical Volume Manager, LVM)

unlockfs: 当 VFS解锁一个文件系统, 并标记它为可写的, 此时调用本方法。

statfs: 当 VFS 想获得文件系统的一些统计数据时调用。 调用时需要持有内核锁(翻译疑 问:看 2.6.16 的 vfs_statfs 函数调用 sb->s_op->statfs 时并没有持有锁, 不知道作者指的是哪把锁?)

remount_fs: 当文件系统被 remount 时调用, 调用需持有内核锁

clear_inode: 当 VFS 清除 inode 时调用。 可选。

umount_begin: 当 VFS 卸载一个文件系统时调用

show_options: VFS 需要在/proc//mounts 显示挂载选项时调用

quota_read: VFS 想读取文件系统的磁盘配额文件时调用

quota_write: VFS 想写入文件系统的磁盘配额文件时调用

read_inode()方法负责填充"i_op"域, 它是一个指向"struct inode_operations"的指针, 该
结构描述了那些操作于每个 inode 的方法。

Inode 对象
==========

一个 inode 对象代表了文件系统内的一个对象。

inode_operations 结构
---------------------

描述了 VFS 如何操作你的文件系统中的一个 inode。 例如在 2.6.22 内核中, 有如下的成员:

struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char __user *,int);
void * (*follow_link) (struct dentry *, struct nameidata *);
void (*put_link) (struct dentry *, struct nameidata *, void *);
void (*truncate) (struct inode *);
int (*permission) (struct inode *, int, struct nameidata *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*truncate_range)(struct inode *, loff_t, loff_t);
};

正如 super_operations 的方法一样, inode_operations 的成员也是在无锁情形下调用──
除非有特别说明。

create: 由 open(2)和 creat(2)系统调用来调用。 只有你想在文件系统中支持正规文件时,
才需要提供该方法的实现。 该方法参数中的 dentry应该还没有跟 inode 相关连(亦即:是一个负的
dentry)。 该方法中一般会调用d_instantiate()将 dentry于inode相关联。

lookup: VFS 需要在父目录中寻找一个 inode 时调用。 要查找的文件名在 dentry 中。 该方
法必须调用 d_add()来将dentry与找到的inode相关联 , 该 inode 的"i_count"域随之增
加。 如果该名字的 inode 未找到, 则 dentry 与 NULL 关联(亦即:是一个负的 dentry)。
本方法只有在真正遇到不可恢复的错误时才返回错误, 否则的话, 那些会创建 inode 的系
统调用, 如 create(2)、mknod(2)、mkdir(2)等将无法工作。 如果你想重载 dentry 的方法,
那么就初始化 dentry 的"d_dop"域, 它是一个指向"struct dentry_operations"的指针。

link: 由系统调用 link(2)来调用。 只有你想在自己的文件系统内支持硬链接时, 才需要提供该方法的实现。
多数情形下, 需要象 create 方法那样调用 d_instantiate()函数。

unlink: 由 unlink(2)系统调用来调用。 只有你想支持删除 inode 时才提供。

symlink: 由 symlink(2)系统调用来调用。 只有你想在自己的文件系统里支持符号链接时,
才需要提供该方法的实现。 多数情形下, 需要象 create 方法那样调用 d_instantiate()函数。

mkdir: 由系统调用 mkdir(2)来调用。 只有你想在文件系统中支持创建子目录时, 才需要提供其实现。
需要象 create 方法那样调用 d_instantiate()函数

rmdir: 由系统调用 rmdir(2)来调用。 只有想支持删除子目录时, 才需要。

mknod: 由系统调用 mknod(2)来调用, 以创建设备文件或者命名管道(FIFO)或者
本地socket。 只有你想支持创建这些类型的 inode 时, 你的文件系统才需要提供该方法的实现。
需要象 create 方法那样调用 d_instantiate()方法。

rename: 由系统调用 rename(2)来调用, 以重命名一个对象, 使之具有由第二个inode和 denrty
给定的父目录和名字。


readlink: 由系统调用readlink(2)来调用. 只有在文件系统支持符号链接时才需提供

follow_link: 由 VFS 调用, 以跟踪符号链接到它所指向的 inode。 只有你想支持符号链接
时才需要提供。 该方法返回了一个 void 型指针 cookie 传递给 put_link(), 参考下面的 put_link()方法。

put_link: 由 VFS 调用, 来释放由 follow_link 方法申请的临时性资源。 由follow_link()返回的
cookie 作为最后一个参数传递给本方法。 由一些诸如 NFS 这样的文件系统使用, 因为在这样的文
件系统中, page cache 是不稳定的(亦即, 随着跟踪符号链接的过程中建立起的 page cache 可能在
跟踪的最后已经不存在了)。

truncate: 由 VFS 调用以改变文件的大小。 在调用本方法之前, VFS 先把 inode 的 i_size
域设为期望的值。 本方法主要由 truncate(2)系统调用等使用。

permission: 由 VFS 调用, 来检查 POSIX 类文件系统的访问权限。

setattr: 由 VFS 调用, 来设定文件的属性, 该方法主要由 chmod(2)等使用

truncate_range: 该方法由文件系统提供, 用于截去文件中一个连续区域中块. (例如在文件中制造一个空洞)

getattr: 由 VFS 调用, 来设定文件的属性, 主要由 stat(2)等系统调用使用

setxattr: 由 VFS 调用, 来设置文件的扩展属性。 所谓扩展属性,就是和 inode 相关的一 对「名称-值」,
它是在inode 分配的时候与之关联的。 由 setxattr(2)系统调用使用。

getxattr: 由 VFS 调用, 获取根据扩展属性的名称, 获取其值。 由 getxattr(2)系统调用使用。

listxattr: 由 VFS 调用, 来列出给定文件的所有扩展属性。 给 listxattr(2)系统调用使用。

removexattr: 由 VFS 调用, 移除给定文件的扩展属性。 给 removexattr(2)系统调用使用。

truncate_range: 由文件系统实现提供的方法, 用于截去指定范围内的块 (例如: 在文件中制造一个空洞)

地址空间对象(The Address Space Object)
======================================

地址空间对象用来对Page Cache中的页进行分组、管理。 它可以用来跟踪文件中的(或其他地方的)
页面,也可以用来跟踪文件映射到进程地址空间的映射区。

地址空间对象有多种用处,其关系有时并不紧密。 这些用处包括:动态调整内存使用情况
根据地址来查找页面,跟踪那些标记为Dirty和Writeback的页面。

在这用处中,第一项独立于其他项。VM可以把脏页写入磁盘,从而使得它变为clean;也可以释放clean的页,
以便重新使用它。重新使用页前, 脏页需要调用->writepage方法;clean的页, 如果有PagePrivate标记,
则需要调用->releasepage方法; 没有PagePrivate标记的Clean页,如果又没有外部的引用,可以直
接释放,而不用通知它所属的地址空间对象。

为了做到这点,需要将页面放入一个"最近最少使用"链表,每当页面被使用,就调用lru_cache_add和
mark_page_active。

通常情况下地址空间中的页面要位于基树(radix tree)中,由page结构的->index成员来索引。该基树为每个
页面维护了PG_Dirty和PG_Writeback的信息,这样,设置了这些标志的页面可以通过基树很快 找到。

Dirty标签主要是由mpage_writeages──默认的->writepages方法──使用的,用来寻找那么需要调用
->writepages方法的页面。 如果未使用mpage_writepages函数(亦即,地址空间提供了自己定义的
->writepages方法),PAGECACHE_TAG_DIRTY标签就基本没用了。 write_inode_now和sync_inode函
数通过该标签,来检查->writepages方法是否成功把地址空间对象里的所有脏页都写出去了。
/* "PAGECACHE_TAG_DIRTY标签就基本没用了", 这句肯定是不对的 */

Writeback标签由 filemap*_wait* 和 sync_page* 等函数使用(通过wait_on_page_writeback_range),
以等待写回操作完成。 在等待的时候,会针对每个在写回的页面调用->sync_page方法(如果定义了)。

地址空间对象的操作可能会给页面附加一些信息,通常会使用'struct page'的'private'。 如果附加了这样
的信息,那么就应该设置页面的PG_Private标志,这样,各个VM子系统就会在相应的场合调用地址空
间对象的处理函数来处理这些附加信息。

地址空间对象是联系应用程序和磁盘存储的媒介。 数据是以页为单位,从存储读入地址空间的;再提供
给应用程序时,要么是拷贝该页中的数据到程序空间,要么是通过内存映射。同样的, 程序将 数据写入到地址空间中,
然后写回到磁盘存储--通常也是以页为单位,但实际上地址空间会的精确的控制写回操作大小。

读的过程很简单,只需要'readpage'方法;相对来说,写的过程要复杂的多,使用prepare_write/
commit_write或set_page_dirty来把数据写入到地址空间中,再使用writepage,sync_page和
writepages来把数据从地址空间中写入到磁盘存储上。

从地址空间中添加和删除页面,都必须得持有inode的i_mutex互斥锁。

当有数据写入到页面,就应设置该页面的PG_Dirty标志。 该标志会一直保持着,直到writepage请求把该页写回存
储--这会清除PG_Dirty标志,而设置PG_Writeback标志。 写回操作实际上是发生在PG_Dirty标志清除后的任意时刻。
当页成功写回到存储后,PG_Writeback标志会被清除。

Writeback使用了一个writeback_control结构。


address_space_operations结构
----------------------------

该结构描述了 VFS 如何把文件映射到 page cache 中。 例如在 2.6.22 内核中, 它有以下成
员:

struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*writepages)(struct address_space *, struct writeback_control *);
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
sector_t (*bmap)(struct address_space *, sector_t);
int (*invalidatepage) (struct page *, unsigned long);
int (*releasepage) (struct page *, int);
ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
loff_t offset, unsigned long nr_segs);
struct page* (*get_xip_page)(struct address_space *, sector_t,
int);
/* migrate the contents of a page to the specified target */
int (*migratepage) (struct page *, struct page *);
int (*launder_page) (struct page *);
};

writepage: 由 VM 用来把脏页写到磁盘上。 这可能是为了数据的完整性(亦即,'sync'),
或者是为了释放内存(flush)。二者的区别见wbc->sync_mode。
writepage方法调用前应保证PG_Dirty标志已清除,PageLocaked已设置。该方法执行回写操作,
并在写操作执行前设置PG_Writeback标志,清楚页面的PageLocaked标记。

如果wbc->syn_mode的值为WB_SYNC_NONE, ->writepage方法就不必费力的完成工作,
它可以选择容易写的其他页面(例如,由于内部依赖的问题)。 如果它决定不回写页面,则应
当返回AOP_WRITEPAGE_ACTIVE,以便VM不再对该页调用->writepage方法了。

更多细节可以参考"Locking"文件。

readpage: 由 VM 用来从磁盘上读取页面。
调用readpage方法前应保证在已经对页面加锁, 当读操作完成时会解锁该页,并将页面标记为最新。
如果出于某种原因,->readpage方法需要解锁该页,它倒也可以这么做,然后返回AOP_TRUNCATED_PAGE。
这种情况下,该页将被重定位、重新加锁,都成功了后VFS会重新调用->readpage方法。

sync_page: VM 调用它来通知存储设备, 执行所有与某一页有关的正等待的 I/O 操作。 同一
address_space 中的其他页的 I/O 也可能被执行。

该方法是可选的,只有在等待writeback完成时才会调用,而且只针对那些设置了PG_Writeback的页调用。

writepages; VM 调用, 把该 address_space中的"脏"页回写到磁盘。 如果wbc->sync_mode
为WBC_SYNC_ALL,那么writeback_control将指定需要回写页面的范围。如果是WBC_SYNC_NONE,
就要给一个nr_to_write值, writepages应该尽可能的回写这么多个的页面到存储设备。
如果未提供->writepages方法,那么就使用mpage_writepages函数。它会选择地址空间
对象中那些标记为脏的页面,并传给->write_page方法。
/* 没提供writepages, VFS会使用generic_writepages */

set_page_dirty: VM调用它把某页标记为脏。
如果一个地址空间对象给页面附加了数据,且当页面变脏时那些数据需要更新,那么就需要
该本方法了。 例如,当一个内存映射的页被修改了,本方法就被调用。它需要设置页的PageDirty标志,
在基树中为该页加上PAGECACHE_TAG_DIRTY标签。

readpages: VM 调用, 从磁盘上读取跟该 address_space 有关的页。
基本可以说本方法就是readpage方法的向量化版本(a vector version of readpage)。
readpage方法只读一个页面,而本方法读多个页面。
readpages方法只用与预读(read-ahead),所以其错误可以被忽略,不管什么地方出错
了,只管放弃,没问题的。

prepare_write: 在通常的写操作路径中, VM调用它来设置跟页有关的写请求。 它表示页面中的一定区域将会
写入数据。 地址空间应该检查该写操作请求是否能成功完成, 如果需要的话,分配磁盘空间, 做其他需要的处理。
如果改写操作只修改一个文件系统中数据块中的部分数据, 这些被部分修改的数据块中的数据需要预先读入 (如果还
没有被读入)。 只有这样, 这些被部分修改的数据块才能正确的更新。 和readpage一样, 如果prepare_write
需要解锁页面, 它也可以这么做,然后返回AOP_TRUNCATED_PAGE.

提示: 不要将页面标记为最新, 除非页面真的为最新。 如果标记为最新, 其他的并发的读操作可能会读到错误的数据。


commit_write: 如果prepare_write成功, 写入的数据将拷贝到页面中, 然后commit_write会被调用。 一般情况下,
它会更新文件的大小、 将inode标记为脏、 作一些相关的处理。 如果可能, 该函数应该避免返回错误, 错误应该在 prepare_write发现


bmap: VFS 调用它, 把对象内的逻辑块号映射到物理块号上。 传统的 FIBMAP ioctl 系统调和激活交换文件时
使用该函数。 为了将页面换出到交换文件中, 交换文件必须直接映射到块设备上。 Swap子系统读写操作不会通过
文件系统, 而是使用bmap将文件的逻辑块映射到设备的物理块上,读写操作时直接使用物理块号。

invalidatepage: 如果一个页面有PagePrivate标记, 当要把页面中部分或全部数据从地址空间移出时, 需要调用该方法。在截断(truncate)地址空间时和使整个地址空间无效时会将页面数据从地址空间移出(在后一种情况, 传递给该函数offset
参数的值是0)。 页面的私有数据也应该依照截断(truncate)的情况更新, 如果offset参数的值为0, 则应该释放私有数据。
这是因为, 当offset为0时, 整个页面的数据都会被丢弃, 这有点象releasepage的功能, 但在这种情况下, invalidatepage必须成功的释放页面。

releasepage: 该方法在想要释放一个有PagePrivate标记的页时被调用。->releasepage应该移除页面的私有数据, 清除PagePrivate标记。 它也可能会将页面从地址空间中移出。 如果出于某种原因, 操作失败, 则返回0。 该函数在以下两种情况下被调用。 第一种情况:虚拟内存子系统(VM)找到一个干净(clean)的页面 , 并且该页面没有活动用户使用时, 虚拟内存子系统会尝试回收该页面。如果releasepage中的操作都成功, 页面会从地址空间中移出, 变为空闲。第二中情况: 收到将地址空间中的部分或全部页面变为无效的请求时。 invalidate_inode_pages2处理这种请求, 它可能由 fadvice(2)
系统调用发出, 也可能由一些文件系统显示的发出,比如nfs, 9fs (当文件系统发现地址空间中的数据已经过期)。 如果是文件系统发出这种请求, 并且所有指定的页面都要变为无效, 则需要通过->releasepage来保证页面都要变为无效。 当某些页面的私有数据不能清除时, ->releasepage可能会清除页面的Update标记。

direct_IO: VM 为直接 I/O 的读/写操作调用它。 直接 I/O是指跳过页面缓存(page cache), 直接在用程序地址空间和存储之间传输数据。

get_xip_page: VM 调用它, 把块号转换成页。 在相关的文件系统卸载之前, 该页保持有效。 那些想实现「适当执行」
(execute-in-place,XIP)的文件系统需要提供该方法的实现。在 fs/ext2/xip.c 文件中可以找到例子。
/* 感觉将XIP翻译成适当执行不是很贴切 */

migrate_page: 用于压缩内存的使用。 如果虚拟内存子系统(VM)希望重定位一个页面(比如一个内存即将失效), 它将传递新页面和被替换的页面的描述符 给该函数。 该函数应该将被替换的页面的私有数据转移到新页面上, 并且更新所有对被替换的页面的引用。
launder_page: 释放一个页面之间调用, 它将脏页面回写到存储设备。 它可以防止页面再次被标记为脏, 整个操作在持有页面锁的情况下进行。



文件对象(The File Object)
=========================

一个文件对象, 代表了进程的一个打开文件。

file_operations 结构
--------------------

该结构描述了 VFS 如何操作一个打开的文件。 例如在内核 2.6.13 中, 它有如下成员:

struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t,
loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned
long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
loff_t *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void
*);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
};

除非特别说明, 否则这些方法都可以在不加锁的情况下调用。

llseek: VFS 想移动文件的读写位置指针时调用

read: 由 read(2)及其它相关系统调用调用

aio_read: 由 io_submit(2)及其他异步 I/O 操作调用

write: 由 write(2)及相关系统调用调用

aio_write: 由 io_submit(2)及其他异步 I/O 操作调用

readdir: VFS 想读取目录内容时调用

poll: VFS 调用。 调用的时机为: 当进程想检查某一文件上是否出现特定特征, 并且
(可选地)阻塞, 直到所等待特征出现。 给 select(2)和 poll(2)系统调用使用。

ioctl: 由 ioctl(2)调用

unlocked_ioctl: 由 ioctl(2)调用。 那些并不获取 BKL(译注:Big Kernel Lock, 大内核
锁,一种同一时刻只允许一个 CPU 在内核态、允许递归获取的锁,详见 lib/kernel_lock.c 代码注释)

compat_ioctl: 由 ioctl(2)调用。 调用时机为: 在 64 位内核上执行 32 位的 ioctl 系统调用。

mmap: 由 mmap(2)调用

open: 当 VFS 想打开一个 inode 时调用。 VFS 打开文件时, 先创建一个新的 struct file, 然后
调用该 file 结构的 open 方法。 嗯, 你可能会想:open 方法为什么不放在
struct inode_operations 里呢? 可能这种想法也有道理, 但我觉得象内核这样设计, 可以简
化文件系统的实现。 并且, 该 open()方法适合初始化 file 结构的"private_data"成员──如
果你想让该成员指向某个设备的数据结构。

flush: 由 close(2)调用, 来冲刷文件。

release: 当最后一个对 file 结构的指向也被关闭时调用

fsync: 由 fsync(2)调用

fasync: 由 fcntl(2)调用, 前提是该 file 的异步(非阻塞)模式已被激活

lock: 由带 F_GETLK,F_SETLK 和 F_SETLKW 命令的 fcntl(2)调用

readv: 由 readv(2)调用

writev: 由 writev(2)调用

sendfile: 由 sendfile(2)调用

get_unmapped_aera: 由 mmap(2)调用

check_flags: 由带 F_SETFL 命令的 fcntl(2)调用

dir_notify: 由带 F_NOTIFY 命令的 fcntl(2)调用

flock: 由 flock(2)调用

注意, 文件操作的这些方法, 是由其 inode 所在的分区的文件系统来实现的。 当打开一
个设备文件(字符设备或块设备特殊文件)时, 多数文件系统会调用 VFS 的一些例程来定
位该设备所属的驱动程序信息。 这些例程将用设备驱动程序中实现的的 file operations
替换文件系统中实现的的那个, 并继续调用新的 open 方法, 这是「为什么打开文件系统
中的设备文件,会最终导致调用设备驱动中的 open()方法」的原因。


目录项 Cache(Directory Entry Cache, dcache)
===========================================

dentry_operations 结构
----------------------

该结构描述了一个文件系统如何重载标准的 dentry 操作集。 Dentry 和 dcache 是 VFS 和具
体文件系统实现的概念, 设备驱动程序就和他们不搭边了。 这些方法可以被置为 NULL,
因为它们是可选的, 如果你不实现, VFS 就使用默认的。 例如 2.6.13 内核中, 该结构有
如下成员:

struct dentry_operations {
int (*d_revalidate)(struct dentry *, struct nameidata *);
int (*d_hash) (struct dentry *, struct qstr *);
int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
int (*d_delete)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
};

d_revalidate: 当 VFS 想重新使一个 dentry 有效时调用, 这一般发生在某次查找中在dcache
中找到了 dentry。 多数文件系统会把这个方法置为 NULL, 因为它们留在 dache 中的 dentry 还是有效的。

d_hash: 当 VFS 把一个 dentry 加入到哈希表中时调用

d_compare: 比较两个 dentry 时调用

d_delete: 当 dentry 的最后一个引用被删除时调用。 这意味着没有人在使用这个 dentry
了, 但它依然是有效的, 并且在 dcache 中。

d_release: 当 dentry 真正被销毁时调用。

d_input: 当一个 denrty 失去了它所属的 inode 时(正好在 dentry 被销毁之前)调用。
如果这个方法置为 NULL, VFS 就会调用 iput(); 如果你自己定义了该方法, 必须在自己
的实现中调用 iput()。

每个 dentry 含有一个指向父 dentry 的指针, 还有一个所有子 dentries 的哈希链表。 基
本上, 子 dentries 就象目录中的文件一样。



Dcache API
----------

内核中定义了许多函数, 供文件系统来操作 dentries:

dget: 打开一个已存在的 dentry 的句柄(在这里,只是增加引用计数而已)

dput: 关闭 dentry 的一个句柄(减少引用计数)。 如果引用计数减到了 0, 就调用

d_delete 方法, 把该 dentry 置入「未使用」队列。 「把 dentry 置入未使用队列」意味着,
如果内存不够用了, 将遍历「未使用队列」并调用 deallocates 方法来销毁 dentries,
以腾出内存。 如果 dentry 已经是「unhashed」(译注:指不在父 dentry 的 hash 链中)且
引用计数为 0, 这时候调用 d_delete 方法然后销毁它。

d_drop: 该方法把一个 dentry 从它的父 dentry 的 hash 链中脱链。 如果它的引用计数变为
0, 随后的调用 dput()将销毁该 dentry。

d_delete: 删除一个 dentry。 如果该 dentry 没有其他的引用了, 则变为「负的 dentry」
并调用 d_iput()方法; 如果还有其他引用, 就不走这些而调用 d_drop()。

d_add: 把一个 dentry 放入它的父 dentry 的哈希链表, 并调用 d_instantiate()。

d_instantiate: 把一个 dentry 链入 inode 的「别名哈希链表」并更新 d_inode 域。 inode
结构的i_count 域应该被设置/增加。 如果 dentry 不和任何 inode 关联, 则它就是一个
「负的 dentry」。 该函数一般在为负的 dentry 新创建一个 inode 时调用。

d_lookup: 给出父 dentry 和名字等信息, 在 dcache 哈希表中查找一个 dentry。 如果找到,
增加其引用计数并返回其地址。 调用者在使用完毕时, 必须调用 d_put()方法来释放
dentry。

关于访问 dentry 时加锁的更多信息, 请参考文档 Documentation/filesystems/dentry-locking.txt。

Thursday, July 16, 2009

自旋锁spinlock

自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点可以应用在多处理机器、或运行在单处理器上的抢占式内核中需要的锁定服务。

顺便介绍下信号量的概念,因为其和自旋锁的用法有颇多相似之处。

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。

自旋锁和信号量对比

在很多地方自旋锁和信号量可以选择任何一个使用,但也有一些地方只能选择某一种。下面对比一些两者的用法。

表1-1自旋锁和信号量对比

应用场合

信号量or自旋锁

低开销加锁(临界区执行时间较快)

优先选择自旋锁

低开销加锁(临界区执行时间较长)

优先选择信号量

临界区可能包含引起睡眠的代码

不能选自旋锁,可以选择信号量

临界区位于非进程上下文时,此时不能睡眠

优先选择自旋锁,即使选择信号量也只能用down_trylock非阻塞的方式

自旋锁与linux内核进程调度关系

以上表第3种情况(其它几种情况比较好理解)为例,如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁。

那么为什么信号量保护的代码可以睡眠而自旋锁就不能呢?

先看下自旋锁的实现方法吧,自旋锁的基本形式如下:

spin_lock(&mr_lock);

//临界区

spin_unlock(&mr_lock);

跟踪一下spin_lock(&mr_lock)的实现

#define spin_lock(lock) _spin_lock(lock)

#define _spin_lock(lock) __LOCK(lock)

#define __LOCK(lock) \

do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

注意到“preempt_disable()”,这个调用的功能是“关抢占”(在spin_unlock中会重新开启抢 占功能)。从中可以看出,使用自旋锁保护的区域是工作在非抢占的状态;即使获取不到锁,在“自旋”状态也是禁止抢占的。了解到这,我想咱们应该能够理解为 何自旋锁保护的代码不能睡眠了。试想一下,如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段 代码。而我们现在知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度 发生了,那么死锁自然就发生了。

咱们可以总结下自旋锁的特点:

● 单处理器非抢占内核下:自旋锁会在编译时被忽略;

● 单处理器抢占内核下:自旋锁仅仅当作一个设置内核抢占的开关;

● 多处理器下:此时才能完全发挥出自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

linux抢占发生的时间

最后在了解下linux抢占发生的时间,抢占分为用户抢占和内核抢占。

用户抢占在以下情况下产生:

● 从系统调用返回用户空间

● 从中断处理程序返回用户空间

内核抢占会发生在:

● 当从中断处理程序返回内核空间的时候,且当时内核具有可抢占性;

● 当内核代码再一次具有可抢占性的时候。(如:spin_unlock时)

● 如果内核中的任务显式的调用schedule()

● 如果内核中的任务阻塞。

基本的进程调度就是发生在时钟中断后,并且发现进程的时间片已经使用完了,则发生进程抢占。通常我们会利用中断处理程序 返回内核空间的时候可以进行内核抢占这个特性来提高一些I/O操作的实时性,如:当I/O事件发生的是时候,对应的中断处理程序被激活,当它发现有进程在 等待这个I/O事件的时候,它会激活等待进程,并且设置当前正在执行进程的need_resched标志,这样在中断处理程序返回的时候,调度程序被激 活,原来在等待I/O事件的进程(很可能)获得执行权,从而保证了对I/O事件的相对快速响应(毫秒级)。可以看出,在I/O事件发生的时候,I/O事件 的处理进程会抢占当前进程,系统的响应速度与调度时间片的长度无关。

更详细的内容可以参考Robert Love所著的《Linux kernel Development》SE, 经典著作啊,很值得一看!


Tuesday, July 14, 2009

Linux2.6 内核的 Initrd 机制解析

1.什么是 Initrd

initrd 的英文含义是 boot loader initialized RAM disk,就是由 boot loader 初始化的内存盘。在 linux内核启动前, boot loader 会将存储介质中的 initrd 文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的 initrd 文件系统。在 boot loader 配置了 initrd 的情况下,内核启动被分成了两个阶段,第一阶段先执行 initrd 文件系统中的"某个文件",完成加载驱动模块等任务,第二阶段才会执行真正的根文件系统中的 /sbin/init 进程。这里提到的"某个文件",Linux2.6 内核会同以前版本内核的不同,所以这里暂时使用了"某个文件"这个称呼,后面会详细讲到。第一阶段启动的目的是为第二阶段的启动扫清一切障爱,最主要的是 加载根文件系统存储介质的驱动模块。我们知道根文件系统可以存储在包括IDE、SCSI、USB在内的多种介质上,如果将这些设备的驱动都编译进内核,可 以想象内核会多么庞大、臃肿。

Initrd 的用途主要有以下四种:

1. linux 发行版的必备部件

linux 发行版必须适应各种不同的硬件架构,将所有的驱动编译进内核是不现实的,initrd 技术是解决该问题的关键技术。Linux 发行版在内核中只编译了基本的硬件驱动,在安装过程中通过检测系统硬件,生成包含安装系统硬件驱动的 initrd,无非是一种即可行又灵活的解决方案。

2. livecd 的必备部件

同 linux 发行版相比,livecd 可能会面对更加复杂的硬件环境,所以也必须使用 initrd。

3. 制作 Linux usb 启动盘必须使用 initrd

usb 设备是启动比较慢的设备,从驱动加载到设备真正可用大概需要几秒钟时间。如果将 usb 驱动编译进内核,内核通常不能成功访问 usb 设备中的文件系统。因为在内核访问 usb 设备时, usb 设备通常没有初始化完毕。所以常规的做法是,在 initrd 中加载 usb 驱动,然后休眠几秒中,等待 usb设备初始化完毕后再挂载 usb 设备中的文件系统。

4. 在 linuxrc 脚本中可以很方便地启用个性化 bootsplash。

2.Linux2.4内核对 Initrd 的处理流程

为 了使读者清晰的了解Linux2.6内核initrd机制的变化,在重点介绍Linux2.6内核initrd之前,先对linux2.4内核的 initrd进行一个简单的介绍。Linux2.4内核的initrd的格式是文件系统镜像文件,本文将其称为image-initrd,以区别后面介绍 的linux2.6内核的cpio格式的initrd。 linux2.4内核对initrd的处理流程如下:

1. boot loader把内核以及/dev/initrd的内容加载到内存,/dev/initrd是由boot loader初始化的设备,存储着initrd。

2. 在内核初始化过程中,内核把 /dev/initrd 设备的内容解压缩并拷贝到 /dev/ram0 设备上。

3. 内核以可读写的方式把 /dev/ram0 设备挂载为原始的根文件系统。

4. 如果 /dev/ram0 被指定为真正的根文件系统,那么内核跳至最后一步正常启动。

5. 执行 initrd 上的 /linuxrc 文件,linuxrc 通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。

6. /linuxrc 执行完毕,真正的根文件系统被挂载。

7. 如果真正的根文件系统存在 /initrd 目录,那么 /dev/ram0 将从 / 移动到 /initrd。否则如果 /initrd 目录不存在, /dev/ram0 将被卸载。

8. 在真正的根文件系统上进行正常启动过程 ,执行 /sbin/init。 linux2.4 内核的 initrd 的执行是作为内核启动的一个中间阶段,也就是说 initrd 的 /linuxrc 执行以后,内核会继续执行初始化代码,我们后面会看到这是 linux2.4 内核同 2.6 内核的 initrd 处理流程的一个显著区别。


3.Linux2.6 内核对 Initrd 的处理流程

linux2.6 内核支持两种格式的 initrd,一种是前面第 3 部分介绍的 linux2.4 内核那种传统格式的文件系统镜像-image-initrd,它的制作方法同 Linux2.4 内核的 initrd 一样,其核心文件就是 /linuxrc。另外一种格式的 initrd 是 cpio 格式的,这种格式的 initrd 从 linux2.5 起开始引入,使用 cpio 工具生成,其核心文件不再是 /linuxrc,而是 /init,本文将这种 initrd 称为 cpio-initrd。尽管 linux2.6 内核对 cpio-initrd和 image-initrd 这两种格式的 initrd 均支持,但对其处理流程有着显著的区别,下面分别介绍 linux2.6 内核对这两种 initrd 的处理流程。

cpio-initrd 的处理流程

1. boot loader 把内核以及 initrd 文件加载到内存的特定位置。

2. 内核判断initrd的文件格式,如果是cpio格式。

3. 将initrd的内容释放到rootfs中。

4. 执行initrd中的/init文件,执行到这一点,内核的工作全部结束,完全交给/init文件处理。

image-initrd的处理流程

1. boot loader把内核以及initrd文件加载到内存的特定位置。

2. 内核判断initrd的文件格式,如果不是cpio格式,将其作为image-initrd处理。

3. 内核将initrd的内容保存在rootfs下的/initrd.image文件中。

4. 内核将/initrd.image的内容读入/dev/ram0设备中,也就是读入了一个内存盘中。

5. 接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。

6. .如果/dev/ram0被指定为真正的根文件系统,那么内核跳至最后一步正常启动。

7. 执行initrd上的/linuxrc文件,linuxrc通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动, 以及加载根文件系统。

8. /linuxrc执行完毕,常规根文件系统被挂载

9. 如果常规根文件系统存在/initrd目录,那么/dev/ram0将从/移动到/initrd。否则如果/initrd目录不存在, /dev/ram0将被卸载。

10. 在常规根文件系统上进行正常启动过程 ,执行/sbin/init。

通 过上面的流程介绍可知,Linux2.6内核对image-initrd的处理流程同linux2.4内核相比并没有显著的变化, cpio-initrd的处理流程相比于image-initrd的处理流程却有很大的区别,流程非常简单,在后面的源代码分析中,读者更能体会到处理的 简捷。

4.cpio-initrd同image-initrd的区别与优势

没有找到正式的关于cpio-initrd同image-initrd对比的文献,根据笔者的使用体验以及内核代码的分析,总结出如下三方面的区别,这些区别也正是cpio-initrd的优势所在:

cpio-initrd的制作方法更加简单

cpio-initrd的制作非常简单,通过两个命令就可以完成整个制作过程


#假设当前目录位于准备好的initrd文件系统的根目录下
bash# find . | cpio -c -o > ../initrd.img
bash# gzip ../initrd.img

而传统initrd的制作过程比较繁琐,需要如下六个步骤


#假设当前目录位于准备好的initrd文件系统的根目录下
bash# dd if=/dev/zero of=../initrd.img bs=512k count=5
bash# mkfs.ext2 -F -m0 ../initrd.img
bash# mount -t ext2 -o loop ../initrd.img /mnt
bash# cp -r * /mnt
bash# umount /mnt
bash# gzip -9 ../initrd.img

本文不对上面命令的含义作细节的解释,因为本文主要介绍的是linux内核对initrd的处理,对上面命令不理解的读者可以参考相关文档。

cpio-initrd的内核处理流程更加简化

通过上面initrd处理流程的介绍,cpio-initrd的处理流程显得格外简单,通过对比可知cpio-initrd的处理流程在如下两个方面得到了简化:

1. cpio-initrd并没有使用额外的ramdisk,而是将其内容输入到rootfs中,其实rootfs本身也是一个基于内存的文件系统。这样就省掉了ramdisk的挂载、卸载等步骤。

2. cpio-initrd启动完/init进程,内核的任务就结束了,剩下的工作完全交给/init处理;而对于image-initrd,内核在执行完 /linuxrc进程后,还要进行一些收尾工作,并且要负责执行真正的根文件系统的/sbin/init。通过图1可以更加清晰的看出处理流程的区别:


图1内核对cpio-initrd和image-initrd处理流程示意图
图1内核对cpio-initrd和image-initrd处理流程示意图

cpio-initrd的职责更加重要

如 图1所示,cpio-initrd不再象image-initrd那样作为linux内核启动的一个中间步骤,而是作为内核启动的终点,内核将控制权交给 cpio-initrd的/init文件后,内核的任务就结束了,所以在/init文件中,我们可以做更多的工作,而不比担心同内核后续处理的衔接问题。 当然目前linux发行版的cpio-initrd的/init文件的内容还没有本质的改变,但是相信initrd职责的增加一定是一个趋势。

5.linux2.6内核initrd处理的源代码分析

上 面简要介绍了Linux2.4内核和2.6内核的initrd的处理流程,为了使读者对于Linux2.6内核的initrd的处理有一个更加深入的认 识,下面将对Linuxe2.6内核初始化部分同initrd密切相关的代码给予一个比较细致的分析,为了讲述方便,进一步明确几个代码分析中使用的概 念:

rootfs: 一个基于内存的文件系统,是linux在初始化时加载的第一个文件系统,关于它的进一步介绍可以参考文献[4]。

initramfs: initramfs同本文的主题关系不是很大,但是代码中涉及到了initramfs,为了更好的理解代码,这里对其进行简单的介绍。Initramfs 是在 kernel 2.5中引入的技术,实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个 cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显 的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。Linux 2.6.12内核的 initramfs还没有什么实质性的东西,一个包含完整功能的initramfs的实现可能还需要一个缓慢的过程。对于initramfs的进一步了解 可以参考文献[1][2][3]。

cpio-initrd: 前面已经定义过,指linux2.6内核使用的cpio格式的initrd。

image-initrd: 前面已经定义过,专指传统的文件镜像格式的initrd。

realfs: 用户最终使用的真正的文件系统。

内 核的初始化代码位于 init/main.c 中的 static int init(void * unused)函数中。同initrd的处理相关部分函数调用层次如下图,笔者按照这个层次对每一个函数都给予了比较详细的分析,为了更好的说明,下面列 出的代码中删除了同本文主题不相关的部分:


图2 initrd相关代码的调用层次关系图
图2 initrd相关代码的调用层次关系图

init函数是内核所有初始化代码的入口,代码如下,其中只保留了同initrd相关部分的代码。


static int init(void * unused){
[1] populate_rootfs();

[2] if (sys_access((const char __user *) "/init", 0) == 0)
execute_command = "/init";
else
prepare_namespace();
[3] if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
[4] if (execute_command)
run_init_process(execute_command);
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}

代码[1]:populate_rootfs函数负责加载initramfs和cpio-initrd,对于populate_rootfs函数的细节后面会讲到。

代码[2]:如果rootfs的根目录下中包含/init进程,则赋予execute_command,在init函数的末尾会被执行。否则执行prepare_namespace函数,initrd是在该函数中被加载的。

代码[3]:将控制台设置为标准输入,后续的两个sys_dup(0),则复制标准输入为标准输出和标准错误输出。

代 码[4]:如果rootfs中存在init进程,就将后续的处理工作交给该init进程。其实这段代码的含义是如果加载了cpio-initrd则交给 cpio-initrd中的/init处理,否则会执行realfs中的init。读者可能会问:如果加载了cpio-initrd, 那么realfs中的init进程不是没有机会运行了吗?确实,如果加载了cpio-initrd,那么内核就不负责执行realfs的init进程了, 而是将这个执行任务交给了cpio-initrd的init进程。解开fedora core4的initrd文件,会发现根目录的下的init文件是一个脚本,在该脚本的最后一行有这样一段代码:


………..
switchroot --movedev /sysroot

就是switchroot语句负责加载realfs,以及执行realfs的init进程。

对cpio-initrd的处理

对cpio-initrd的处理位于populate_rootfs函数中。


void __init populate_rootfs(void){
[1] char *err = unpack_to_rootfs(__initramfs_start,
__initramfs_end - __initramfs_start, 0);
[2] if (initrd_start) {
[3] err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 1);

[4] if (!err) {
printk(" it is\n");
unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 0);
free_initrd_mem(initrd_start, initrd_end);
return;
}
[5] fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start,
initrd_end - initrd_start);
sys_close(fd);
free_initrd_mem(initrd_start, initrd_end);
}
}

代码[1]:加载initramfs, initramfs位于地址__initramfs_start处,是内核在编译过程中生成的,initramfs的是作为内核的一部分而存在的,不是 boot loader加载的。前面提到了现在initramfs没有任何实质内容。

代码[2]:判断是否加载了initrd。无论哪种格式的initrd,都会被boot loader加载到地址initrd_start处。

代码[3]:判断加载的是不是cpio-initrd。实际上 unpack_to_rootfs有两个功能一个是释放cpio包,另一个就是判断是不是cpio包, 这是通过最后一个参数来区分的, 0:释放 1:查看。

代码[4]:如果是cpio-initrd则将其内容释放出来到rootfs中。

代码[5]:如果不是cpio-initrd,则认为是一个image-initrd,将其内容保存到/initrd.image中。在后面的image-initrd的处理代码中会读取/initrd.image。

对image-initrd的处理 在prepare_namespace函数里,包含了对image-initrd进行处理的代码,相关代码如下:


void __init prepare_namespace(void){
[1] if (initrd_load())
goto out;
out:
umount_devfs("/dev");
[2] sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
security_sb_post_mountroot();
mount_devfs_fs ();
}

代码[1]:执行initrd_load函数,将initrd载入,如果载入成功的话initrd_load函数会将realfs的根设置为当前目录。

代码[2]:将当前目录即realfs的根mount为Linux VFS的根。initrd_load函数执行完后,将真正的文件系统的根设置为当前目录。

initrd_load函数负责载入image-initrd,代码如下:


int __init initrd_load(void)
{
[1] if (mount_initrd) {
create_dev("/dev/ram", Root_RAM0, NULL);
[2] if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
sys_unlink("/initrd.image");
handle_initrd();
return 1;
}
}
sys_unlink("/initrd.image");
return 0;
}

代码[1]:如果加载initrd则建立一个ram0设备 /dev/ram。

代 码[2]:/initrd.image文件保存的就是image-initrd,rd_load_image函数执行具体的加载操作,将image- nitrd的文件内容释放到ram0里。判断ROOT_DEV!=Root_RAM0的含义是,如果你在grub或者lilo里配置了 root=/dev/ram0 ,则实际上真正的根设备就是initrd了,所以就不把它作为initrd处理 ,而是作为realfs处理。

handle_initrd()函数负责对initrd进行具体的处理,代码如下:


 static void __init handle_initrd(void){
[1] real_root_dev = new_encode_dev(ROOT_DEV);
[2] create_dev("/dev/root.old", Root_RAM0, NULL);
mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
[3] sys_mkdir("/old", 0700);
root_fd = sys_open("/", 0, 0);
old_fd = sys_open("/old", 0, 0);
/* move initrd over / and chdir/chroot in initrd root */
[4] sys_chdir("/root");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
mount_devfs_fs ();
[5] pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
if (pid > 0) {
while (pid != sys_wait4(-1, &i, 0, NULL))
yield();
}
/* move initrd to rootfs' /old */
sys_fchdir(old_fd);
sys_mount("/", ".", NULL, MS_MOVE, NULL);
/* switch root and cwd back to / of rootfs */
[6] sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);
umount_devfs("/old/dev");
[7] if (new_decode_dev(real_root_dev) == Root_RAM0) {
sys_chdir("/old");
return;
}
[8] ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();
[9] printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay\n");
else {
int fd = sys_open("/dev/root.old", O_RDWR, 0);
printk("failed\n");
printk(KERN_NOTICE "Unmounting old root\n");
sys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = sys_ioctl(fd, BLKFLSBUF, 0);
sys_close(fd);
}
printk(!error ? "okay\n" : "failed\n");
}

handle_initrd函数的主要功能是执行initrd的linuxrc文件,并且将realfs的根目录设置为当前目录。

代码[1]:real_root_dev,是一个全局变量保存的是realfs的设备号。

代码[2]:调用mount_block_root函数将initrd文件系统挂载到了VFS的/root下。

代码[3]:提取rootfs的根的文件描述符并将其保存到root_fd。它的作用就是为了在chroot到initrd的文件系统,处理完initrd之后要,还能够返回rootfs。返回的代码参考代码[7]。

代码[4]:chroot进入initrd的文件系统。前面initrd已挂载到了rootfs的/root目录。

代码[5]:执行initrd的linuxrc文件,等待其结束。

代码[6]:initrd处理完之后,重新chroot进入rootfs。

代码[7]:如果real_root_dev在 linuxrc中重新设成Root_RAM0,则initrd就是最终的realfs了,改变当前目录到initrd中,不作后续处理直接返回。

代码[8]:在linuxrc执行完后,realfs设备已经确定,调用mount_root函数将realfs挂载到root_fs的 /root目录下,并将当前目录设置为/root。

代码[9]:后面的代码主要是做一些收尾的工作,将initrd的内存盘释放。

到此代码分析完毕。

6.结束语

通过本文前半部分对cpio-initrd和imag-initrd的阐述与对比以及后半部分的代码分析,我相信读者对Linux 2.6内核的initrd技术有了一个较为全面的了解。在本文的最后,给出两点最重要的结论:

1. 尽管Linux2.6既支持cpio-initrd,也支持image-initrd,但是cpio-initrd有着更大的优势,在使用中我们应该优先考虑使用cpio格式的initrd。

2. cpio-initrd相对于image-initrd承担了更多的初始化责任,这种变化也可以看作是内核代码的用户层化的一种体现,我们在其它的诸如 FUSE等项目中也看到了将内核功能扩展到用户层实现的尝试。精简内核代码,将部分功能移植到用户层必然是linux内核发展的一个趋势。

come from:http://www.ibm.com/developerworks/cn/linux/l-k26initrd/

Sunday, December 7, 2008

MontaVista linux 4.0.1 for TMS320DM6446 EVM 驱动一览

数字 --- 索引, D代表设备,S代表drivers source code
以下代码位置均以lsp1.3为准,lsp1.2的大同小异。
1、VPFE linux driver --- V4L2
D /dev/video0
S /drivers/media/video/davinci/davinci_vpfe.c

2、VPBE linux driver --- FBDev
D /dev/fb/x (x = 0 to 3)
S /drivers/video/davincifb.c
S /drivers/video/davinci/davincifb.c

3、MMC/SD card controller
D /dev/mmcblk0
S /drivers/mmc

4、ATA/CF controller
D ......
S /drivers/ide

5、Async EMIF linux driver
D /dev/mtdblock x(0 or 1)
S /drivers/mtd/nand/nand_davinci.c

6、EMAC linux driver
D /dev/eth0
S /drivers/net/davinci_emac.c
S /davinci/net/davinci_emac_phy.c

7、USB
D /dev/usb0
S /drivers/usb

8、ASP --- ALC33 Audio codec
D /dev/dsp
S /sound/oss/dm644x/davinci-*.c
S /arch/arm/mach-davinci/mcbsp.c

9、I2C master mode
D /dev/i2c/0
S /drivers/i2c/busses/i2c_davinci.c

10、 UART0
D /dev/ttyS0
S /drivers/serial/.......

11、Timer
D .......
S /arch/arm/mach-davinci/timer.c

Thursday, December 4, 2008

compiler error:KBUILD_NOPEDANTIC & Fix it to use EXTRA_CFLAGS

For the "Linux device driver III", the example of all

Delete the //include/config.h
Add the //include/sched.h

Ok, now change into its derectory and complile it like this:

root@microtiger-tiger:/home/microtiger/test/examples/scull# make
make -C /lib/modules/2.6.24-22-generic/build M=/home/microtiger/test/examples/scull LDDINC=/home/microtiger/test/examples/scull/../include modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.24-22-generic'
scripts/Makefile.build:46: *** CFLAGS was changed in "/home/microtiger/test/examples/scull/Makefile". Fix it to use EXTRA_CFLAGS. Stop.
make[1]: *** [_module_/home/microtiger/test/examples/scull] Error 2
make[1]: Leaving directory `/usr/src/linux-headers-2.6.24-22-generic'
make: *** [modules] Error 2

There are two kind of method to solve this problem:

(1)Rplace all CFLAGS with EXTRA_CFLAGS in the Makefile.

(2)Using the KBUILD_NOPEDANTIC arg.

root@microtiger-tiger:/home/microtiger/test/examples/scull# make KBUILD_NOPEDANTIC=1
make -C /lib/modules/2.6.24-22-generic/build M=/home/microtiger/test/examples/scull LDDINC=/home/microtiger/test/examples/scull/../include modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.24-22-generic'
CC [M] /home/microtiger/test/examples/scull/access.o
LD [M] /home/microtiger/test/examples/scull/scull.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/microtiger/test/examples/scull/scull.mod.o
LD [M] /home/microtiger/test/examples/scull/scull.ko
make[1]: Leaving directory `/usr/src/linux-headers-2.6.24-22-generic'

Tuesday, December 2, 2008

Makefile & Kconfig in kernel 2.6.xx

kernel源码树的目录下都有两个文件:Kconfig和Makefile。分布到各目录的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文档相关的内核配置菜单。在内核配置make gconfig(或xconfig等)时,从Kconfig中读出菜单,用户选择后保到.config的内核配置文档中。 在编译内核时,主Makefile调用这个.config文件就确定用户的选择。

以上说明Kconfig即对应内核的配置菜单。假如要添加新的驱动代码到内核的源码,修改Kconfig,这样就能够选择这个驱动;要使这个驱动被编译,就要修改Makefile。

因此添加新的驱动代码时必须要修改的文档有两种Kconfig和Makefile。
要想知道怎么修改这两种文档,就要知道两种文档的语法结构。

Kconfig 语法

linux2.6.x/Documentation/kbuild/kconfig-language.txt
每个菜单都有一个关键字标识,最常见的就是config。

语法:
config
symbol是个新的标记的菜单项,options是在这个新的菜单项下的属性和选项
其中options部分有:

1、类型定义:
每个config菜单项都要有类型定义,bool布尔类型、 tristate三态:内建、模块、移除 string字符串、 hex十六进制、 integer整型
例如config HELLO_MODULE
bool "hello test module"
bool类型的只能选中或不选中,tristate类型的菜单项多了编译成内核模块的选项,假如选择编译成内核模块,则会在.config中生成一个 CONFIG_HELLO_MODULE=m的配置,假如选择内建,就是直接编译成内核影响,就会在.config中生成一个 CONFIG_HELLO_MODULE=y的配置.

2、依赖型定义depends on或requires
指此菜单的出现和否依赖于另一个定义
config HELLO_MODULE
bool "hello test module"
depends on ARCH_PXA
这个例子表明HELLO_MODULE这个菜单项只对XScale处理器有效。

3、帮助性定义
只是增加帮助用关键字help或---help---

内核Makefile

在linux2.6.x/Documentation/kbuild/makefiles.txt目录下有周详的介绍有关kernel makefile的知识。内核的Makefile分为5个组成部分:
Makefile 顶层的Makefile
.config 内核的当前配置文档,编译时成为顶层Makefile的一部分
arch/$(ARCH)/Makefile 和体系结构相关的Makefile
s/ Makefile.* 一些Makefile的通用规则
kbuild Makefile 各级目录下的大概约500个文档,编译时根据上层Makefile传下来的宏定义和其他编译规则,将源代码编译成模块或编入内核。
顶层的Makefile文档读取 .config文档的内容,并总体上负责build内核和模块。Arch Makefile则提供补充体系结构相关的信息。 s目录下的Makefile文档包含了任何用来根据kbuild Makefile 构建内核所需的定义和规则。(其中.config的内容是在make gconfig的时候,通过Kconfig文档配置的结果)。

举个例子:
假设想把自己写的一个flash的驱动程式加载到工程中,而且能够通过menuconfig配置内核时选择该驱动该怎么办呢?能够分三步:
第一:将您写的flashtest.c 文档添加到/driver/mtd/maps/ 目录下。
第二:修改/driver/mtd/maps目录下的kconfig文档:
config MTD_flashtest
tristate “ap71 flash"
这样当make menuconfig时 ,将会出现 ap71 flash选项。
第三:修改该目录下makefile文档。
添加如下内容:obj-$(CONFIG_MTD_flashtest) += flashtest.o
这样,当您运行make menucofnig时,您将发现ap71 flash选项,假如您选择了此项。该选择就会保存在.config文档中。当您编译内核时,将会读取.config文档,当发现ap71 flash 选项为yes 时,系统在调用/driver/mtd/maps/下的makefile 时,将会把 flashtest.o 加入到内核中。

Monday, December 1, 2008

原始kernel对TI davinci平台的支持

kernel.org上的原始内核自linux-2.6.22开始支持TI davinci平台,改变的文件有如下:
27 files changed:

arch/arm/Kconfig diff | blob | history
arch/arm/Makefile diff | blob | history
arch/arm/mach-davinci/Kconfig [new file with mode: 0644] blob |
arch/arm/mach-davinci/Makefile [new file with mode: 0644] blob |
arch/arm/mach-davinci/Makefile.boot [new file with mode: 0644] blob |
arch/arm/mach-davinci/board-evm.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/id.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/io.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/irq.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/psc.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/serial.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/time.c [new file with mode: 0644] blob |
arch/arm/mm/Kconfig diff | blob | history
include/asm-arm/arch-davinci/common.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/debug-macro.S [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/dma.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/entry-macro.S [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/hardware.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/io.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/irqs.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/memory.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/psc.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/serial.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/system.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/timex.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/uncompress.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/vmalloc.h [new file with mode: 0644] blob |

在随后发布的2.6.23内核代码,增加了对davinci: GPIO support (commit), clock control support (commit), pin mux support (commit)的支持, 代码分别位于:

GPIO support
[ARM] 4457/2: davinci: GPIO support
Support GPIO driver for TI DaVinci SoC

arch/arm/Kconfig diff | blob | history
arch/arm/mach-davinci/Makefile diff | blob | history
arch/arm/mach-davinci/gpio.c [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/gpio.h [new file with mode: 0644] blob |
include/asm-arm/arch-davinci/hardware.h diff | blob | history
-------------------------------------------------------------------------------------------
clock control support
[ARM] 4430/1: davinci: clock control support
Support clock control driver for TI DaVinci SoC

arch/arm/mach-davinci/Makefile diff | blob | history
arch/arm/mach-davinci/board-evm.c diff | blob | history
arch/arm/mach-davinci/clock.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/clock.h [new file with mode: 0644] blob |
arch/arm/mach-davinci/io.c diff | blob | history
include/asm-arm/arch-davinci/clock.h [new file with mode: 0644] blob |
-------------------------------------------------------------------------------------------
pin mux support
[ARM] 4432/5: davinci: pin mux support
Support pin multiplexing configurations driver for TI DaVinci SoC

arch/arm/mach-davinci/Makefile diff | blob | history
arch/arm/mach-davinci/mux.c [new file with mode: 0644] blob |
arch/arm/mach-davinci/psc.c diff | blob | history
include/asm-arm/arch-davinci/mux.h [new file with mode: 0644] blob |

针对这些文件进行分析,可以理清原始内核逐步对TI Davinci 平台支持的框架,对编写驱动有一定的帮助。

与kernel同步,了解内核的变化

從事 Embedded Linux(GNU/Linux systems on devices)工作的朋友,除了日常的讀書功課外,另外一個重要的工作就是「隨時注意 Linux kernel 的發展狀況」。要能與 Linux kernel 的發展同步,嚴格來說,已經是一件吃重的工作了,不過還是可以列出幾個基本的工作原則如下:

1. 每天閱讀 linux-kernel 郵遞論壇(mailing-list)的「標題」。

2. 隨時上 kernel.org 看看最新釋出的 kernel 版本(或留意 linux-kernel-announce mailing-list)。

3. 閱讀釋出版本的 Changelog。

4. 不要與 git 系統的距離太遠,定時看看 git 系統,保持一定的「短距離」。

Mailing List

linux-kernel 上的 posts 每天的量大約在 200~350 篇左右,數量並不算少,要把每一篇都看過是不太可能的,因此以我自己的閱讀心態來說,我會建議以下的閱讀方式:

1. 看標題,如果是自己有興趣或正在留意的更新,我就會標記下來持續追蹤。

2. 如果有 Bugfix 的 patch 出現,我會看看這項修正的起因與原理,但為了不讓自己花費太多時間,如果我對這項 patch 的修正「原理」不甚熟悉,我便會跳過此 post。

以下是閱讀 linux-kernel list 必須知道的幾件事:

1. 如果有新的修正,第一時間都會發佈在此 list 上,並且標題的起頭一定是 "[PATCH n/m] subject" 這樣的格式。PATCH 表示這是一個 patch 的發佈,由於一個 patch 會以多篇 post 發佈,因此就用「n/m」來表示「這是第幾篇 patch,總共有幾篇。」,例如:

[PATCH 0/7] KVM: Kernel-based Virtual Machine
[PATCH 1/7] KVM: userspace interface
[PATCH 2/7] KVM: Intel virtual mode extensions definitions
[PATCH 3/7] KVM: kvm data structures
[PATCH 5/7] KVM: mmu virtualization
[PATCH 6/7] KVM: x86 emulator
[PATCH 7/7] KVM: plumbing

2. 不能在這裡詢問與 kernel 發展無關的問題,例如:工具的使用、系統設定、詢問是否有XXX驅動程式、請求協助測試等等,這些都是不能張貼的文章。另外,原本就該留意的非成文禮節一定要注意,像是 FAQ 能找到的東西,就不要去麻煩人家。

3. list 裡大部份都是 device driver 的討論,並且很多都是架構面或觀念面的討論與修正建議,所以當您參與討論時,千萬不要用「個人的主觀看法,或是沒有事實與理論根據」的角度發表意見;由於「觀念」的修正是 kernel 2.6 驅動程式的大討論方向,所以必須先把主題相關的東西先看懂看熟後再參與討論。

我要怎麼知道 kernel 更新了什麼東西?

這是一個時常被問到的問題,由於 kernel 的變動快速,因此 Jollen 在日前便寫了一篇「讓 kernel 常在我心:探討如何與 kernel 的發展同步」的日記,內容是大略介紹如何與 kernel 的發展同步(day-by-day)。

但是,如果並不是很需要每天去注意 kernel 的動態的話,只要在每個 kernel 穩定版(stable)釋出後去看 Changlog 就好了。特別是 kernel 2.6.1x 的更新項目(update)、臭蟲修正(big fix)、新的驅動程式與 filesystem 更是以可怕的「量」在 patch,特別是最近半年的 3 個 kernel 版本(2.6.17~19),變化量真是到了油門全開的狀態,所以每天看 kernel 會是沈重的負擔。

話說回來,改變是好事。期待的是,看著這麼多的改變與越來越多的新驅動程式加入,以及企業級(enterprise-class)功能的成熟,我們已經可以拿到越來越棒的「成熟」kernel 了。誠如 Linus 在 2.6.19 stable 釋出時的玩笑話「It's one of those rare 'perfect' kernels」。

言歸正傳,如果要 keep 新 kernel 做了什麼改變,或是了解「什麼功能在幾版的 kernel 才開始有」、「某些 bug 在幾版做修正」、「這個版本是否對理器架構面做修正」等,建議可以直接由 kernelnewbies.org 做查詢,例如,我想知道 kernel 2.6.17 改了什麼東西,就可以輸入以下的 URL:

http://kernelnewbies.org/Linux_2_6_17

同理,我想知道幾天前才丟出來的 kernel 2.6.19 加了什麼、改了什麼,就輸入以下的 URL:

http://kernelnewbies.org/Linux_2_6_19

就如前面提到的,kernel 2.6.1x 的修改變化相當大,特別是在 kernel 2.6.16(大約)後,每每都有重大更新,修正範圍也「波及」到「Kernel Core」。如果工作場合與 kernel 有關係,確實有必要仔細閱讀每一個版本的 Changelog。
files come from Jollen's Blog at http://www.jollen.org/blog/2006/11/reading_kernel_kernel_patch.html

Friday, November 14, 2008

編譯Android內核

1. 獲得 toolchains 從 http://www.codesourcery.com/gnu_toolchains/arm/portal/subscription?@template=lite (選擇 ARM EABI 和 IA32 GNU/Linux).

2. 獲得內核源代碼從 http://code.google.com/p/android/downloads/list

3.部署 toolcains 和 內核源碼進入內核源碼樹。

$ mkdir -p android
$ cd android
$ tar xjvf ../arm-2007q3-51-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2
$ tar xzvf ../linux-2.6.23-android-m3-rc20.tar.gz
$ cd kernel

4. 獲得 config.gz 來自正在運行的emulator 通過下面你的方法。

$ adb pull /proc/config.gz .

5. 解壓縮 config.gz 并重命名為 .config

$ gunzip config.gz
$ mv config .config

6. 指定 CROSS_COMPILE (譯注:交叉編譯器)在 Makefile 中。

CROSS_COMPILE=../arm-2007q3/bin/arm-none-linux-gnueabi-

7. 评论指出 LDFLAGS_BUILD_ID 在相同的 Makefile。
The LDFLAGS_BUILD_ID enables --build-id 選擇 ld 如果可以得到。
The --build-id 選擇相對較新的。
目前的模擬器似乎不支持內核鏈表這個選項。

http://fedoraproject.org/wiki/Releases/FeatureBuildId 如果你有興趣這些詳細的選項。

#LDFLAGS_BUILD_ID = $(patsubst -Wl$(comma)%,%,\
$(call ld-option, -Wl$(comma)--build-id,))

8. Make the kernel(制作內核)

$ make

9. 檢查 zImage 是被創建的和大小相近的kernel-qemu 在 Andorid SDK 中。

$ ls -l arch/arm/boot/zImage
-rwxrwxr-x 1 motz motz 1234712 2007-12-01 18:06 arch/arm/boot/zImage
$ ls -l $SDK/tools/lib/images/kernel-qemu
-rwxrwxr-x 1 motz motz 1245836 2007-11-12 5:59 ...sdk/tools/lib/images/kernel-qemu

10. 運行 emulator 被創建的內核

$ emulator -kernel arch/arm/boot/zImage

Wednesday, November 12, 2008

kernel 支持yaffs2文件系统

first download yaffs2 source code,
(1) copy "yaffs2" to "linux-2.6.26/fs/"
(2) edit "fs/Makefile"
obj-$(CONFIG_YAFFS_FS) += yaffs2
(3) edit "fs/Kconfig"
# Patched by YAFFS
source "fs/yaffs2/Kconfig"

config JFFS2_FS # add here above

(4) make menuconfig
File systems --->
Miscellaneous filesystems --->
<*> YAFFS2 file system support
-*- 512 byte / page devices

#cd fs/yaffs/utils/
#make
#./mkyaffs2image
#./mkyaffsimage

Monday, November 10, 2008

在文本界面(Text Console)下播放音视频

前一段时间在youtube看到了一段使用Linux文本字符流来播放电影,网址

http://www.youtube.com/watch?v=ji0A3kOAc9U,感觉非常有意思,拿出来大家一起娱乐,也巩固一下内核编译和驱动的知识。

首先,确保内核支持VESA(视频电子标准)驱动。并保证内核选择了正确的声卡驱动程序。
以我的机器为例,我的是普通VGA显卡(主板集成,8M显存),声卡是Intel AC97,则在内核中选择以下

必须要支持以下功能:
(1)VESA模式的支持
Device Drivers ---> Graphics support --->
VGA 16-color graphics support
[*] VESA VGA graphics support

(2)相关音频接口的支持,例如:Sequencer support(MIDI接口的支持)
Device Drivers ---> Sound ---> Advanced Linux Sound Architecture --->
Advanced Linux Sound Architecture
Sequencer support
Sequencer dummy client
OSS Mixer API
OSS PCM (digital audio) API
[*] OSS PCM (digital audio) API - Include plugin system
......

(3)声卡驱动
Device Drivers ---> Sound ---> Advanced Linux Sound Architecture ---> PCI devices --->
Intel/SiS/nVidia/AMD/ALi AC97 Controller (AC97声卡)

确保内核支持以上功能后,就可以安装软件了。下面是安装软件的步骤:


1. 下载软件,全部存储到/mnt/source目录下
mkdir -p /mnt/source
cd /mnt/source
wget http://prdownloads.sourceforge.net/aa-project/aalib-1.4rc4.tar.gz
wget http://www4.mplayerhq.hu/MPlayer/releases/MPlayer-1.0rc1.tar.bz2
wget http://www4.mplayerhq.hu/MPlayer/releases/codecs/essential-20061022.tar.bz2
wget http://downloads.sourceforge.net/lrmi/lrmi-0.10.tar.gz?modtime=1133743627&big_mirror=0

2. 准备vesautils(自持VESA需要)
svn checkout svn://svn.mplayerhq.hu/vesautils/trunk vesautils

3. 准备解码器库文件
tar essential-20061022.tar.bz2
mkdir /usr/local/lib/codecs
cp essential-20061022/* /usr/local/lib/codecs

4. 修改/etc/ld.so.conf,添加下面的行
/usr/local/lib

5. 编译lrmi
cd /mnt/source
tar -zxvf lrmi-0.10.tar.gz
cd lrmi-0.10
make
make install

6. 编译vesautils
cd /mnt/source/vesautils/libvbe/
make
make install

7. 编译软件aalib
tar -zxvf aalib-1.4rc4.tar.gz
cd aalib-1.4.0
./configure
make
make install

8. 编译Mplayer
tar -jxvf MPlayer-1.0rc1.tar.bz2
cd MPlayer-1.0rc1
编辑configure文件,找到4195行,即:
4195 _ld_aa=`aalib-config --libs | cut -d " " -f 2,5,6`
在该行的后面添加下面两行:
else
_ld_aa="-laa"

./configure --with-codecsdir=/usr/local/lib/codecs --with-win32libdir=/usr/local/lib/codecs

--with-reallibdir=/usr/local/lib/codecs --enable-aa --enable-vesa
make
make install

9. 测试
下载就可以在Text Console下播放电影了。
mplayer -vo vesa 电影文件名

要测试Videos in ASCII Art
mplayer -vo aa 电影文件名

10. 如果VESA播放时分辨率不高,修改/etc/grub.conf文件,在kernel参数后添加选项vga=791。
例如:
kernel /vmlinuz-2.6.18 ro root=/dev/VolGroup00/LogVol00 rhgb quiet vga=791

Monday, October 27, 2008

关于kernel里面的asmlinkage

kernelnewibes上的解释是:

What is asmlinkage?
The asmlinkage tag is one other thing that we should observe aboutthis simple function.
 This is a #define for some gcc magic that tellsthe compiler that the function should not
 expect to find any of itsarguments in registers (a common optimization), but only on 
theCPU’s stack. Recall our earlier assertion that system_call consumesits first argument, 
the system call number, and allows up to four morearguments that are passed along to the 
real system call. system_callachieves this feat simply by leaving its other arguments (which 
werepassed to it in registers) on the stack. All system calls are markedwith the asmlinkage tag,
 so they all look to the stack for arguments.Of course, in sys_ni_syscall’s case, this doesn’t make
 any difference,because sys_ni_syscall doesn’t take any arguments, but it’s an issuefor most 
other system calls. And, because you’ll be seeingasmlinkage in front of many other functions, 
I thought you shouldknow what it was about.

在kernel2.6.26
include/linux/linkage.h里面定义如下:

#ifndef asmlinkage_protect
# define asmlinkage_protect(n, ret, args...) do { } while (0)
#endif

CPP_ASMLINKAGE的定义为:
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern “C”
#else
#define CPP_ASMLINKAGE
#endif

而在include/asm-i386/linkage.h里则有更明确的定义:

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

__attribute__是GCC的C语言扩展语法。regparm(0)表示不从寄存器中传递参数。

另外,如果是__attribute__((regparm(3))),那么调用函数的时候参数不是通过栈传递,而是直接放到寄存器里,被调用函数直接从寄存器取参数。

通俗而言,asmlinkage是声明在函数之前,表示该函数可以被汇编代码调用。 加上"asmlinkage" 后,C function 就会由 stack 取参数,而不是从 register 取参数。asmlinkage大都用在系统调用中,系统调用需要在entry.s文件中用汇编语言调用,所以必须要保证它符合C语言的参数传递规则,才能用汇编语言正确调用它。

Saturday, September 27, 2008

zImage内核镜像解压过程详解

本文以linux-2.6.14内核在S3C2410平台上运行为例,讲解内核的解压过程。内核编译完成后会生成zImage内核镜像文件。关于bootloader加载zImage到内核,并且跳转到zImage开始地址运行zImage的过 程,相信大家都很容易理解。但对于zImage是如何解压的过程,就不是那么好理解了。本文将结合部分关键代码,讲解zImage的解压过程。

先看看zImage的组成吧。在内核编译完成后会在arch/arm/boot/下生成zImage。

在arch/armboot/Makefile中:

$(obj)/zImage: $(obj)/compressed/vmlinux FORCE

$(call if_changed,objcopy)

由此可见,zImage的是elf格式的arch/arm/boot/compressed/vmlinux二进制化得到的

在arch/armboot/compressed/Makefile中:

$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o \

$(addprefix $(obj)/, $(OBJS)) FORCE

$(call if_changed,ld)

$(obj)/piggy.gz: $(obj)/../Image FORCE

$(call if_changed,gzip)

$(obj)/piggy.o: $(obj)/piggy.gz FORCE

其中Image是由内核顶层目录下的vmlinux二进制化后得到的。注意:arch/arm/boot/compressed/vmlinux是位置无关的,这个有助于理解后面的代码。,链接选项中有个 –fpic参数:

EXTRA_CFLAGS := -fpic

总结一下zImage的组成,它是由一个压缩后的内核piggy.o,连接上一段初始化及解压功能的代码(head.o misc.o),组成的。

下面就要看内核的启动了,那么内核是从什么地方开始运行的呢?这个当然要看lds文件啦。zImage的生成经历了两次大的链接过程:一次是顶层 vmlinux的生成,由arch/arm/boot/vmlinux.lds(这个lds文件是由arch/arm/kernel /vmlinux.lds.S生成的)决定;另一次是arch/arm/boot/compressed/vmlinux的生成,是由arch/arm /boot/compressed/vmlinux.lds(这个lds文件是由arch/arm/boot/compressed /vmlinux.lds.in生成的)决定。zImage的入口点应该由arch/arm/boot/compressed/vmlinux.lds决 定。从中可以看出入口点为‘_start’

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = 0;

_text = .;

.text : {

_start = .;

*(.start)

*(.text)

……

}

在arch/arm/boot/compressed/head.S中找到入口点。

看看head.S会做些什么样的工作:

• 对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作;

•设置kernel开始和结束地址,保存architecture ID;

• 如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断

• 分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。

•需要重载内核地址,将r0的偏移量加到BSS region和GOT table中的每一项。

对于位置无关的代码,程序是通过GOT表访问全局数据目标的,也就是说GOT表中中记录的是全局数据目标的绝对地址,所以其中的每一项也需要重载。

• 清空bss堆栈空间r2-r3

•建立C程序运行需要的缓存

•这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址

•用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。

可能大家看了上面的文字描述还是不清楚解压的动态过程。还是先用图表的方式描述下代码的搬运解压过程。然后再针对中间的一些关键过程阐述。

假定zImage在内存中的初始地址为0x30008000(这个地址由bootloader决定,位置不固定)

1、初始状态

.text

0x30008000开始,包含piggydata段(即压缩的内核段)

. got

?

. data

?

.bss

?

.stack

4K大小

2、head.S调用misc.c中的decompress_kernel刚解压完内核后

.text

0x30008000开始,包含piggydata段(即压缩的内核段)

. got

?

. data

?

.bss

?

.stack

4K大小

解压函数所需缓冲区

64K大小

解压后的内核代码

小于4M

3、此时会将head.S中的部分代码重定位

.text

0x30008000开始,包含piggydata段(即压缩的内核段)

. got

?

. data

?

.bss

?

.stack

4K大小

解压函数所需缓冲区

64K大小

解压后的内核代码

小于4M

head.S中的部分重定位代码代码

reloc_startreloc_end

4、跳转到重定位后的reloc_start处,由reloc_start至reloc_end的代码复制解压后的内核代码到0x30008000处,并调用call_kernel跳转到0x30008000处执行。

解压后的内核

0x30008000开始

在通过head.S了解了动态过程后,大家可能会有几个问题:

问题1:zImage是如何知道自己最后的运行地址是0x30008000的?

问题2:调用decompress_kernel函数时,其4个参数是什么值及物理含义?

问题3:解压函数是如何确定代码中压缩内核位置的?

先回答第1个问题

这个地址的确定和Makefile和链接脚本有关,在arch/arm/Makefile文件中的

textaddr-y := 0xC0008000 这个是内核启动的虚拟地址

TEXTADDR := $(textaddr-y)

在arch/arm/mach-s3c2410/Makefile.boot中

zreladdr-y := 0x30008000 这个就是zImage的运行地址了

在arch/arm/boot/Makefile文件中

ZRELADDR := $(zreladdr-y)

在arch/arm/boot/compressed/Makefile文件中

zreladdr=$(ZRELADDR)

在arch/arm/boot/compressed/Makefile中有

.word zreladdr @ r4

内核就是用这种方式让代码知道最终运行的位置的

接下来再回答第2个问题

decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,

int arch_id)

l output_start:指解压后内核输出的起始位置,此时它的值参考上面的图表,紧接在解压缓冲区后;

l free_mem_ptr_p:解压函数需要的内存缓冲开始地址;

l ulg free_mem_ptr_end_p:解压函数需要的内存缓冲结束地址,共64K;

l arch_id :architecture ID,对于SMDK2410这个值为193;

最后回答第3个问题

首先看看piggy.o是如何生成的,在arch/arm/boot/compressed/Makefie中

$(obj)/piggy.o: $(obj)/piggy.gz FORCE

Piggy.o是由piggy.S生成的,咱们看看piggy.S的内容:

.section .piggydata,#alloc

.globl input_data

input_data:

.incbin "arch/arm/boot/compressed/piggy.gz"

.globl input_data_end

input_data_end:

再看看misc.c中decompress_kernel函数吧,它将调用gunzip()解压内核。gunzip()在lib/inflate.c中定义,它将调用NEXTBYTE(),进而调用get_byte()来获取压缩内核代码。

在misc.c中

#define get_byte() (inptr <>

查看fill_inbuf函数

int fill_inbuf(void)

{

if (insize != 0)

error("ran out of input data");

inbuf = input_data;

insize = &input_data_end[0] - &input_data[0];

inptr = 1;

return inbuf[0];

}

发现什么没?这里的input_data不正是piggy.S里的input_data吗?这个时候应该明白内核是怎样确定piggy.gz在zImage中的位置了吧。

利用udev、sys动态创建linux设备结点

在Linux2.6内核中,devfs被认为是过时的方法,并最终被抛弃,udev取代了它。Devfs的一个很重要的特点就是可以动态创建设备结点。那我们现在如何通过udev和sys文件系统动态创建设备结点呢?

下面通过一个实例,说明udev、sys动态创建设备结点的方法。注意代码中红色的部分是为了实现动态创建设备结点添加的。


#include linux/module.h/

#include /linux/kernel.h

#include /linux/init.h/
#include /
linux/fs.h/
#include /
linux/cdev.h/
#include /
asm/uaccess.h/
#include /
linux/device.h/


MODULE_LICENSE ("GPL");

int hello_major = 252;
int hello_minor = 0;
int number_of_devices = 1;
char data[50]="foobar not equal to barfoo";
struct cdev cdev;
dev_t dev = 0;
static int hello_open (struct inode *inode, struct file *file)
{
printk (KERN_INFO "Hey! device opened\n");
return 0;
}
static int hello_release (struct inode *inode, struct file *file)
{
printk (KERN_INFO "Hmmm... device closed\n");
return 0;
}
ssize_t hello_read (struct file *filp, char *buff, size_t count, loff_t *offp)
{
ssize_t result = 0;
if (copy_to_user (buff, data, sizeof(data)-1))
result = -EFAULT;
else
printk (KERN_INFO "wrote %d bytes\n", count);

return result;
}
ssize_t hello_write (struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
ssize_t ret = 0;
printk (KERN_INFO "Writing %d bytes\n", count);
if (count>127)

return -ENOMEM;
if (count<0)>

return -EINVAL;

if (copy_from_user (data, buf, count))

{

ret = -EFAULT;

}

else

{

data[127]='\0';

printk (KERN_INFO"Received: %s\n", data);

ret = count;

}

return ret;

}

struct file_operations hello_fops = {

.owner = THIS_MODULE,

.open = hello_open,

.release = hello_release,

.read = hello_read,

.write = hello_write

}; struct class *my_class;

static void char_reg_setup_cdev (void)
{
int error, devno = MKDEV (hello_major, hello_minor);
cdev_init (&cdev, &hello_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &hello_fops;
error = cdev_add (&cdev, devno , 1);
if (error)
printk (KERN_NOTICE "Error %d adding char_reg_setup_cdev", error);
/* creating your own class */
my_class =class_create(THIS_MODULE, "farsight_class");
if(IS_ERR(my_class))

{
printk("Err: failed in creating class.\n");
return ;
}
/* register your own device in sysfs, and this will cause udevd to create corresponding device node */
class_device_create(my_class,NULL, devno, NULL,"farsight_dev");
// device_create(my_class,NULL, devno,"farsight_dev");
}

static int __init hello_2_init (void)
{
int result;
dev = MKDEV (hello_major, hello_minor);
result = register_chrdev_region (dev, number_of_devices, "test");
if (result<0)>

{

printk (KERN_WARNING "hello: can't get major number %d\n", hello_major);

return result;

}

char_reg_setup_cdev ();

printk (KERN_INFO "char device registered\n");

return 0;

}

static void __exit hello_2_exit (void)

{

dev_t devno = MKDEV (hello_major, hello_minor);

cdev_del (&cdev);

unregister_chrdev_region (devno,number_of_devices);

class_device_destroy(my_class, devno);

class_destroy(my_class);

}

module_init (hello_2_init); module_exit (hello_2_exit);

在编译了驱动后,可以查看/dev/farsight_dev设备结点,和 /sys/class/farsight_class/farsight_dev/ 本代码的测试环境是Ubantu7.04,内核版本是2.6.20-15-generi。在不同版本的内核中,有些系统函数的参数可能不太一样。

Wednesday, September 24, 2008

Linux驱动程序开发学习步骤

1. 学会写简单的makefile

2. 编一应用程序,可以用makefile跑起来

3. 学会写驱动的makefile

4. 写一简单char驱动,makefile编译通过,可以insmod, lsmod, rmmod. 在驱动的init函数里打印hello world, insmod后应该能够通过dmesg看到输出。

5. 写一完整驱动, 加上read, write, ioctl, polling等各种函数的驱动实现。 在ioctl里完成从用户空间向内核空间传递结构体的实现。

6. 写一block驱动, 加上read,write,ioctl,poll等各种函数实现。

7. 简单学习下内存管理, 这个是最难的,明白各种memory alloc的函数实现细节。这是linux开发的基本功。

8. 学习锁机制的应用,这个不是最难的但是最容易犯错的,涉及到很多同步和并发的问题。

9. 看内核中实际应用的驱动代码。 你会发现最基本的你已经知道了, 大的框架都是一样的, 无非是read, write, ioctl等函数的实现, 但里面包含了很多很多细小的实现细节是之前不知道的。 这时候就要考虑到很多别的问题而不仅仅是基本功能的实现。

推荐您看2.6.20中integrated的一个驱动 kvm,记得是在driver/lguest下,很好玩的, 就是linux下的虚拟机驱动,代码不长,但功能强大。有能力的可以自己写一操作系统按照要求做成磁盘镜像加载到虚拟机中, 然后客户机可以有自己的4G虚拟地址空间。

10. 看完驱动欢迎您进入Linux kernel学习中来。

最简单的方法,跟着ldd(linux devive driver)做一遍。

Labels

Followers