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

Sunday, September 13, 2009

识人

一个人让对方不放心,怎样做都是徒劳。

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。

Wednesday, September 2, 2009

ubuntu上使用scim和gajim技巧两则

Howto run Gajim with root privileges

Gajim 0.12.1 says that he can’t run with root privileges. This is new feature that appeared in Fedora 10. In earlier versions it was normaly running under root. Anyway let’s turn off this feature in case that you need to run Gajim under root.

Open /usr/bin/gajim with your favorite text editor, find 24th line and just comment out this part of code:

if test $(id -u) -eq 0; then
echo "You must not launch Gajim as root, it is INSECURE"
exit 1
fi

After commenting it should look like

#if test $(id -u) -eq 0; then
# echo "You must not launch Gajim as root, it is INSECURE"
# exit 1
#fi

Save file. Thats it.

-------------------------------------------------------------------------------

root 下的中文显示问题解决方法如下

9.04中选择root进入系统,无法显示中文桌面,而且选择中文后也没有作用, 而在/etc/environment中,"LANGUAGE=zh_CN:zh:en_US:en"是不用修改的,改后有没问题还不清楚。

解决方法:

在root下打开隐藏文件.profile。
修改root下隐藏文件.profile,把最后的两行  LANG=C 和LANGUAGE=C改为如下所示后重启。
以下是修改过的。

# ~/.profile: executed by Bourne-compatible login shells.

if [ "$BASH" ]; then
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
fi

mesg n
# Installed by Debian Installer:
# no localization for root because zh_CN.UTF-8
# cannot be properly displayed at the Linux console
LANG=C #修改此行为:LANG="zh_CN.UTF-8"
LANGUAGE=C #修改此行为:LANGUAGE="zh_CN:zh"

然后重启,选择以root进入系统,即可显示中文。

Monday, August 3, 2009

sendfile函数

$man 2 sendfile
显示如下:

SENDFILE(2) Linux Programmer’s Manual SENDFILE(2)

NAME
sendfile - transfer data between file descriptors

SYNOPSIS
#include

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

DESCRIPTION
sendfile() copies data between one file descriptor and another.
Because this copying is done within the kernel, sendfile() is more
efficient than the combination of read(2) and write(2), which would
require transferring data to and from user space.

in_fd should be a file descriptor opened for reading and out_fd should
be a descriptor opened for writing.

If offset is not NULL, then it points to a variable holding the file
offset from which sendfile() will start reading data from in_fd. When
sendfile() returns, this variable will be set to the offset of the byte
following the last byte that was read. If offset is not NULL, then
sendfile() does not modify the current file offset of in_fd; otherwise
the current file offset is adjusted to reflect the number of bytes read
from in_fd.

count is the number of bytes to copy between the file descriptors.

Presently (Linux 2.6.9): in_fd, must correspond to a file which sup‐
ports mmap(2)-like operations (i.e., it cannot be a socket); and out_fd
must refer to a socket.

Applications may wish to fall back to read(2)/write(2) in the case
where sendfile() fails with EINVAL or ENOSYS.

RETURN VALUE
If the transfer was successful, the number of bytes written to out_fd
is returned. On error, -1 is returned, and errno is set appropriately.
这些日子编写网络相关的程序,偶尔发现这个api,据说效率奇高。sendfile()在在两个文件描述符之间完成数据拷贝操作,该操作是内核中完成的,所以称为"零拷贝"。sendfile函数比起read和write函数高效的原因在于read和write是要把数据拷贝到用户应用层操作而sendfile不需要。
参数说明:
out_fd 是已经打开了,用于写操作(write)的文件描述符;
in_fd 是已经打开了,用于读操作(read)的文件描述符;
offset 偏移量;表示sendfile函数从in_fd中的哪一偏移量开始读取数据.如果是零表示从文件的开始读,否则从相应的便宜量读取.如果是循环读取的时候,下一次offset值应为sendfile函数返回值加上本次的offset的值;
count是在两个描述符之间拷贝的字节数(bytes)。

返回值:
如果成功的拷贝,返回写操作到out_fd的字节数,错误返回-1,并相应的设置error信息.
EAGAIN 无阻塞I/O设置O_NONBLOCK时,写操作(write)阻塞了;
EBADF 输出或者输入的文件描述符没有打开;
EFAULT 错误的地址;
EINVAL 描述符不可用或者锁定了,或者用mmap()函数操作的in_fd不可用;
EIO 当读取(read)in_fd时发生未知错误;
ENOMEM 读(read)in_fd时内存不足。

目前手头时间比较紧迫,抽空出来写个测试程序看看使用这个api与使用其他api到底效率差别有多大。

man手册分类

熟练使用linux提供的man在线帮助系统是任何一个开发者必备的基本素质之一。系统命令,系统调用函数等等用法都可以很方便的参考man。
man手册分为以下几个章节:
(1)standard commands (标准命令)
(2)system calls (系统调用)
(3)library functions (库函数)
(4)special devices (设备说明)
(5)file formats (文件格式)
(6)games and toys (游戏和娱乐)
(7)miscellaneous (杂项)
(8)administrative Commands (管理员命令)

如需要查阅网络编程里面send系统调用的帮助信息,如果直接输入$man send则显示的是send命令的帮助信息,要想正确得到send系统调用的信息,需要$man 2 send 。
NOTE:man是通过手册的章节号来搜索帮助信息的。

Tuesday, July 21, 2009

Install XMMS on Ubuntu

First Step:
We need to download the required packages to compile XMMS
# sudo apt-get install autotools-dev automake1.9 libtool gettext libasound2-dev libaudiofile-dev \
libgl1-mesa-dev libglib1.2-dev libgtk1.2-dev libesd0-dev libice-dev libmikmod2-dev libogg-dev \
libsm-dev libvorbis-dev libxxf86vm-dev libxml-dev libssl-dev build-essential make

Depending on your internet connection and your machine this may take some minutes.

Second Step:
Prepare the XMMS for compiling
Create a directory in your HOME directory:
# mkdir ~/build
Change the working directory to it:
# cd ~/build
Download XMMS sources:
# wget http://xmms.org/files/1.2.x/xmms-1.2.11.tar.gz
Unpack it:
# tar xvf xmms-1.2.11.tar.gz
Change the working directory to the source directory:
# cd xmms-1.2.11/
Third Step:
Compiling it:
This generates the necessary files, and checks your system:
# ./configure --prefix=/usr
The actually compiling
# make
Fourth Step:
Install it:
# sudo make install
After install we no longer need the source directory:
# cd
# rm -rf ~/build

That's it, now we can run XMMS: press ALT+F2 write xmms there and hit enter and enjoy your music.

NOTE: many people say that XMMS is old, buggy,no UTF-8 support, etc. Yes maybe all is true, but it's an audio player not a media library organizer, so it plays music and you listen, that's all and it does that job.

UPDATE:
Flac Plugin

We need to get the build dependencies
# sudo apt-get build-dep flac

You already know what's this ;)
# mkdir ~/build
# cd ~/build

Get flac's sources
# apt-get source flac

Another well known step:)
# cd flac-1.2.1
# ./configure
# make

It's enough to copy the plugin, not to install the whole flac stuff, this is good if will be flac update, note, an update wont break the plugin.
# cp src/plugin_xmms/.libs/libxmms-flac.so ~/.xmms/Plugins
# cd
# rm -rf ~/build

linux清理内存命令

经常操作内存之后不免使系统变的比较慢,内存的碎块也较多,有必要进行清理,搜索下网络之后,得到如下清理方法,效果还不错。

Writing to this will cause the kernel to drop clean caches, dentries and inodes from memory, causing that memory to become free.

To free pagecache:

  • echo 1 > /proc/sys/vm/drop_caches

To free dentries and inodes:

  • echo 2 > /proc/sys/vm/drop_caches

To free pagecache, dentries and inodes:

  • echo 3 > /proc/sys/vm/drop_caches

As this is a non-destructive operation, and dirty objects are not freeable, the user should run “sync” first in order to make sure all cached objects are freed.

This tunable was added in 2.6.16.

the last:
echo 0 > /proc/sys/vm/drop_caches

should operator in root !

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, 经典著作啊,很值得一看!


命令行下清空回收站

#sudo rm -rf /root/.Trash/

Wednesday, July 15, 2009

不要脸的中国社会和不要脸的中国人

不要告诉我说中国是什么社会主义或资本主义,这些跟中国扯不上边,现在的中国就是一个不要脸的社会,中国人就是一群不要脸的人。
中国的不要脸首先体现在中国人价值观和是非观的丧失。国人社会价值的衡量标准就是钱的多寡;判断是非的标准就是是否符合经济效益;一个人社会地位的高低取决于其拥有财富的多少,而不管这些财富是如何来的。不要脸的中国人一边不断地诅咒这那些拥有财富的人一边盘算着如何效仿他们以便挤入他们的行列。
在中国最不要脸的组织就是我们的政府,榨取农民几十年的血汗,一旦免除了农民的农业税就把自己装扮成一个施恩者。整天在各种场合大谈关心农民和农民工就是迟迟不动林林种种的不合理体制,甚至不承认这些不合理。“以租待征”怎么就不行了,为什么到了现在还要喝我们农民的血?农民的金融合作就这么危险吗?
其次不要脸的组织是国有垄断企业,凭借着行政授予的垄断权大肆榨取消费者的血汗。更让人气愤的这些人一边喝着我们的血还一边一本正经地挖苦我们太不理解他们的困难。不是某位石油大亨的代言人说了嘛:“中国石油企业的利润率还不到50%”,我不知道这个人是不是白痴,综观发达国家的石油企业,还有谁的利润率能达到10%!无知的言论透露出的是这些部门的猖狂和无耻。有银行系统的代言人说话了:“银行员工的工资之所以高是因为银行属于高危险行业”。奶奶的!不要脸是不?要是这么危险你们怎么不和高空作业的农民工换换,还挤破头地把自己的小子闺女七大姑八大姨家的孩子往“火坑”里推?公路局也跟着不要脸,不就是坐着收费吗,凭什么你们一个月工资就是别人工资的十倍?电信局坐不住了,迫不及待地加入了不要脸的行列,无线上网就这么难吗?电信就是不发展,为什么?控制啊!要说不要脸的可别忘记我们的邮电局,不要脸的商业银行退出农村了,这个不要脸的进来了,光吸收存款不发放贷款,贫穷的中国农民又一次在为中国的城市作贡献了。

还有一个不要脸的组织就是大部分民营企业,资本家的血腥在中国的最近20多年已经把中国人浸透了,黑工厂、黑煤窑、黑砖窑、黑中介,所有的行业都有透着铜臭和血腥的组织。对于民营企业,除了个别的,有几个是干净的?榨取劳动者的血汗,规避法制,偷税漏税,使得朱总理为某个会计学院题词时实在想不起来其他更好的话,没办法才写了个“不做假帐!”这个就不说了,地球人都知道了!以至于美国工会过来为中国工人讨说法,丢人啊,中国政府,中国人!!
要说不要脸的,还有我们自己。胆小怯懦,目送英雄归西;狼狈为奸,冷眼事态炎凉;不负责任,事不关己高高挂起;极度自私,人不为己天诛地灭!
十年文革把中国旧有的价值观念砸的粉碎,改革开放使得国外的思想潮流蜂拥而至。中国是个没有宗教信仰的国家,中华民族是个没有精神寄托的民族。粉碎了旧有的价值体系,新的价值体系根本没有建立起来,每个人都在迷惘,都在彷徨,吃饱了肚子的人精神空虚的如同行尸走肉,于是,李洪志来了,说跟他走,搞什么真善美,于是,邪教诞生了!
他妈的,神奇的国度!神奇的事件!

Tuesday, July 14, 2009

TCP/IP 应用程序的通信连接模式

TCP/IP 应用层与应用程序

TCP/IP 起源于二十世纪 60 年代末美国政府资助的一个分组交换网络研究项目,它是一个真正的开放协议,很多不同厂家生产各种型号的计算机,它们运行完全不同的操作系统,但 TCP/IP 协议组件允许它们互相进行通信。现在 TCP/IP 已经从一个只供一些科学家使用的小实验网成长为一个由成千上万的计算机和用户构成的全球化网络,TCP/IP 也已成为全球因特网(Internet)的基础,越来越多的 TCP/IP 互联网应用和企业商业应用正在改变着世界。

TCP/IP 通讯协议采用了四层的层级模型结构(注:这与 OSI 七层模型不相同),每一层都调用它的下一层所提供的网络任务来完成自己的需求。TCP/IP 的每一层都是由一系列协议来定义的。这 4 层分别为:

 

  • 应用层 (Application):应用层是个很广泛的概念,有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应用。
  • 传输层 (Transport):传输层包括 UDP 和 TCP,UDP 几乎不对报文进行检查,而 TCP 提供传输保证。
  • 网络层 (Network):网络层协议由一系列协议组成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。
  • 链路层 (Link):又称为物理数据网络接口层,负责报文传输。

图1显示了 TCP/IP 层级模型结构,应用层之间的协议通过逐级调用传输层(Transport layer)、网络层(Network Layer)和物理数据链路层(Physical Data Link)而可以实现应用层的应用程序通信互联。

应用层需要关心应用程序的逻辑细节,而不是数据在网络中的传输活动。应用层其下三层则处理真正的通信细节。在 Internet 整个发展过程中的所有思想和着重点都以一种称为 RFC(Request For Comments)的文档格式存在。针对每一种特定的 TCP/IP 应用,有相应的 RFC 文档。一些典型的 TCP/IP 应用有 FTP、Telnet、SMTP、SNTP、REXEC、TFTP、LPD、SNMP、NFS、INETD 等。RFC 使一些基本相同的 TCP/IP 应用程序实现了标准化,从而使得不同厂家开发的应用程序可以互相通信。


图 1 TCP/IP 层级模型结构

然而除了这些已经实现标准化的系统级 TCP/IP 应用程序外,在企业商业应用和互联网应用开发中,存在着大量的商业应用程序通信互联问题。如图 1 显示,其中的应用层所包含应用程序主要可以分成两类,即系统级应用和商业应用,互联网商业应用是商业应用中的主要形式之一。

不同开发商和用户在开发各自商业应用通信程序时也存在有许多不同的设计方式。关于 TCP/IP 应用层以下的技术文献与书籍早已是汗牛充栋,但是关于 TCP/IP 应用本身,尤其是关于商业应用的通信设计模式技术讨论方面的文章还是比较少的。TCP/IP 应用通信设计模式实际上是在 TCP/IP 基础编程之上的一种应用编程设计方式,也属于一种应用层协议范畴,其可以包含有 TCP/IP 地址族模式设计、I/O 模式设计、通信连接模式设计以及通信数据格式设计等。鉴于目前讨论 TCP/IP 商业应用程序设计模式问题这方面的文章还很少见,本文尝试给出一些通信连接模式设计中共同的概念与一些典型的设计模式,在以后的文章中将继续讨论地址族模 式设计、I/O 模式设计、以及通信数据格式设计等方面的模式设计实现话题。

通信连接模式设计主要考虑内容有:

  • 通信两端程序建立通信方式
  • 通信连接方式
  • 通信报文发送与接收方式

以下内容将介绍建立通信的 Client/Server 模型,然后逐一介绍通信连接模式设计所需要考虑的这些内容。

传输层接口 APIs 与 TCP/IP 应用程序 C/S 模型

传输层接口 APIs

TCP/IP 应用层位于传输层之上,TCP/IP 应用程序需要调用传输层的接口才能实现应用程序之间通信。目前使用最广泛的传输层的应用编程接口是套接字接口(Socket)。Socket APIs 是于 1983 年在 Berkeley Socket Distribution (BSD) Unix 中引进的。 1986 年 AT&T 公司引进了另一种不同的网络层编程接口 TLI(Transport Layer Interface),1988 年 AT&T 发布了一种修改版的 TLI,叫做 XTI(X/open Transport interface)。XTI/TLI 和 Socket 是用来处理相同任务的不同方法。关于 TCP/IP APIs 使用文章与书籍已相当多,本文则是侧重于如何组合使用这些 APIs 来进行 TCP/IP 应用程序连接模式设计,并归纳出几种基本应用连接模式。

如图 2 显示,应用层是通过调用传输层接口 APIs(Socket 或 XTI/TLI)来与传输层和网络层进行通信的。


图 2 传输层接口

不管是使用何种编程接口,要在两个机器或两个程序之间建立通信,通信双方必须建立互相一致的通信模式。如果双方的通信设计模式不一致就无法建立有效的通信连接。

以下是经常使用的 socket APIs,是建立 TCP/IP 应用程序的标准接口,也是影响 TCP/IP 应用程序通信方式的几个主要 APIs,不同 APIs 组合再结合系统调用可以实现不同方式的应用。Sockets 支持多种传输层和网络层协议,支持面向连接和无连接的数据传输,允许应用分布式工作。

  • socket():是用来创建一个 socket,socket 表示通信中的一个节点,其可以在一个网络中被命名,用 socket 描述符表示,socket 描述符类似于 Unix 中的文件描述符。
  • bind():是用来把本地 IP 层地址和 TCP 层端口赋予 socket。
  • listen() :把未连接的 socket 转化成一个等待可连接的 socket,允许该 socket 可以被请求连接,并指定该 socket 允许的最大连接数。
  • accept():是等待一个连接的进入,连接成功后,产生一个新的 socket 描述符,这个新的描述符用来建立与客户端的连接。
  • connect():用来建立一个与服务端的连接。
  • send():发送一个数据缓冲区,类似 Unix 的文件函数 write()。另外 sendto() 是用在无连接的 UDP 程序中,用来发送自带寻址信息的数据包。
  • recv():接收一个数据缓冲区,类似 Unix 的文件函数 readI()。另外 recvfrom() 是用在无连接的 UDP 程序中,用来接收自带寻址信息的数据包。
  • close():关闭一个连接

Client/Server 模型

Sockets 是以 Client 和 Server 交互通信方式来使用的。典型的系统配置是把 Server 放在一台机器中,而把 Client 放在另一台机器中,Client 连接到 Server 交换信息。一个 socket 有一系列典型的事件流。例如,在面向连接的 Client/Server 模型中,Server 端的 socket 总是等待一个 Client 端的请求。要实现这个请求,Server 端首先需要建立能够被 Client 使用的地址,当地址建立后,Server 等待 Client 请求服务。当一个 Client 通过 socket 连接到 Server 后,Client 与 Server 之间就可以进行信息交换。Client/Server 是通信程序设计的基本模式。从软件开发的角度讲,TCP/IP 应用程序都是基于 Client/Server 方式的。注意本篇文章以下 Client/Server 概念是针对程序内部调用 Socket API 所讲的概念,与针对整个程序甚至针对机器而讲的客户端 / 服务器概念有所不同。用 Server APIs 建立的程序可以被当作客户端使用,用 Client APIs 建立的程序也可以被用作服务器端使用。建立 Server 需要的 APIs 有 socket(), bind(), listen(), accept(),建立 Client 需要的 APIs 有 Socket(), Connect()。在实际应用开发中,同一个程序里往往同时可以有 Client 和 Server 的代码,或者多种形式的组合。在实际应用编程中,针对 Socket APIs 不同有效组合,结合系统调用可以有多种复杂的设计变化。

面向连接的应用编程存在三类基本的不同级别的设计方式范畴,根据 Socket APIs 从上到下顺序依次是:

  • Client/Server 通信建立方式
  • Client/Server 通信连接方式
  • Client/Server 通信发送与接收方式

下面内容以面向连接的 Socket 应用编程为例来说明这几种不同通信范畴的设计实现。

Client/Server 建立方式设计概述

一个 Client 连接一个 Server

如果只有两台机器之间连接,那么一个是 Client,另一个是 Server,如下面图 3 所示。这是最简单的 TCP/IP 的应用,也是 TCP/IP 应用早期的 Peer to Peer (P2P) 概念。其流程基本如图 4 所示。


图 3 TCP/IP 应用单点 Client/Server

图 4 显示了 TCP/IP 应用编程最基本的 Client/Server 模式,显示了基本的 Client/Server 通信所需要调用的 Socket APIs 以及顺序。


图 4 TCP/IP 应用编程基本 Client/Server 模式

多个 Client 连接一个 Server

多个 Client 同时连接一个 Server 是 TCP/IP 应用的主流形式,如图 5 所示,其中 Client 连接数可以从几个到成千上万。


图 5 TCP/IP 应用多 Client 端的 Client/Server

由于 socket APIs 缺省方式下都是阻塞方式的,实现多个 Client 同时连接一个 Server 就需要特别的设计。其实现方式可以有多种不同的设计,这其中也涉及 I/O 模式设计。下面将展开介绍其中几种设计形式。

利用一个 Client 连接一个 Server 形式实现多 Client 连接

从程序设计角度讲,只要 Client 和 Server 端口是一对一形式,那么就属于一个 Client 连接一个 Server 形式。在处理多个 Client 端连接时,Server 端轮流使用多个端口建立多个 Client-Server 连接,连接关闭后,被释放端口可以被循环使用。在这种多连接形式中需要谨慎处理 Client 端如何获取使用 Server 端的可用端口。比如图 6 显示 Server 有一个服务于所有进程的进程可以先把 Server 端的可用端口发送给 Client 端,Client 端再使用该端口建立连接来处理业务。Server 针对每一个 Client 连接用一个专门的进程来处理。由于可用端口数有限,Server 用一个有限循环来处理每一个可用的端口连接。由于新端口需要用 bind() 来绑定,所以需要从 bind() 开始到 close() 结束都需要包含在循环体内。


图 6 利用一对一 Client-Server 模式实现多 Client 连接

使用多个 accept() 实现多 Client 连接

多进程 Server 一般有一个专注进程是服务于每一个连接的。当 Client 端完成连接后,专注进程可以循环被另外的连接使用。使用多个 accept() 也可以实现处理多 Client 连接。多 accept() 的 Server 也只有一个 socket(),一个 bind(),一个 listen(),这与通常情况一样。但是它建立许多工作子进程,每一个工作子进程都有 accept(),这样可以为每一个 Client 建立 socket 描述符。如图 7 所示,由于 accept() 连接成功后,会产生一个新的 socket 描述符,这样通过循环多进程利用 accept() 产生的多 socket 描述符就可以与多个 Client 进行连接通信。循环体是从 accept() 开始到 close() 结束的。


图 7 使用多 accept() 实现多 Client 连接

使用并发 Server 模式实现多 Client 连接

并发服务器模式曾经是 TCP/IP 的主流应用程序设计模式,得到广泛使用,目前互联网上仍有相当多的应用使用此种模式。其设计思路是在 accept 之后 fork 出一个子进程。因为 socket 会产生监听 socket 描述符 listenfd,accept 会产生连接 socket 描述符 connfd。连接建立后,子进程继承连接描述符服务于 Client,父进程则继续使用监听描述符等待另外一个 Client 的连接请求,以产生另外一个连接 socket 描述符和子进程。如图 8 所示,accept() 接收到一个 Client 连接后,产生一个新的 socket 描述符,通过 fork() 系统调用,用一个子进程来处理该 socket 描述符的连接服务。而父进程可以立即返回到 accept(),等待一个新的 Client 请求,这就是典型的并发服务器模式。并发服务器模式同时处理的最大并发 Client 连接数由 listen() 的第二个参数来指定。


图 8 TCP/IP 应用并发 Server

使用 I/O 多路技术实现多 Client 连接

以上三种连接设计,多 Server 端口、多 accept() 和并发服务器模式,都是通过 fork() 系统调用产生多进程来实现多 Client 连接的。使用 I/O 多路技术也可以同时处理多个输入与输出问题,即用一个进程同时处理多个文件描述符。I/O 多路技术是通过 select() 或 poll() 系统调用实现的。poll() 与 select() 功能完全相同,但是 poll() 可以更少使用内存资源以及有更少的错误发生。select() 调用需要与操作文件描述符集的 APIs 配合使用。select() 系统调用可以使一个进程检测多个等待的 I/O 是否准备好,当没有设备准备好时,select() 处于阻塞状态中,其中任一设备准备好后,select() 函数返回调用。select() API 本身也有一个超时时间参数,超时时间到后,无论是否有设备准备好,都返回调用。其流程如图 9 所示。在 socket APIs listen() 和 accept() 之间插入 select() 调用。使用这三个宏 FD_ZERO()、FD_CLR() 和 FD_SET(),在调用 select() 前设置 socket 描述符屏蔽位,在调用 select() 后使用 FD_ISSET 来检测 socket 描述符集中对应于 socket 描述符的位是否被设置。 FD_ISSET() 就相当通知了一个 socket 描述符是否可以被使用,如果该 socket 描述符可用,则可对该 socket 描述符进行读写通信操作。通常,操作系统通过宏 FD_SETSIZE 来声明在一个进程中 select() 所能操作的文件或 socket 描述符的最大数目。更详细的 I/O 多路技术实现,可以参考其他相关文献。


图 9 I/O 多路技术实现多连接的 Server

一个 Client 连接多个 Server

一个 Client 连接多个 Server 这种方式很少见,主要用于一个客户需要向多个服务器发送请求情况,比如一个 Client 端扫描连接多个 Server 端情况。如图 10 所示。此种方式设计主要是 Client 端应用程序的逻辑设计,通常需要在 Client 端设计逻辑循环来连接多个 Server,在此不做更多描述。


图 10 单 Client 对多 Server

复杂 Client/Server 设计与现代 P2P

最近几年,对等网络技术 ( Peer-to-Peer,简称 P2P) 迅速成为计算机界关注的热门话题之一,以及影响 Internet 未来的科技之一。与早期点对点 (Peer to Peer) 的 Client/Server 模式不同,现在的 P2P 模式是指每个结点既可充当服务器,为其他结点提供服务,同时也可作为客户端享用其他结点提供的服务。实际上 P2P 模式仍然是基于 Client/Server 模式的,每个通信节点都既是 Server,又是 Client,P2P 是基于复杂 Client/Server 设计的 TCP/IP 应用。图 11 显示 P2P 模式下两个用户 PC 之间的对等连接。


图 11 P2P 模式

在技术上,P2P 本身是基于 TCP/IP Client/Server 技术的一种设计模式思想, P2P 也属于网络应用层技术,与 Web 和 FTP 等应用是并列的。只是 P2P 应用在设计实现上更要复杂的多。P2P 技术实现的协同工作是无需专门的服务器支持的 (Serverless),这里的服务器概念与 Client/Server 中的 Server 概念是不一样的。在传统意义上中心服务器机器上往往运行的是 TCP/IP 应用的 Server 端程序,所以传统意义上的 Server 概念在机器与应用上是重合的。如果更改 TCP/IP 的应用设计,使应用程序既可做 Server 又可做 Client,就可以实现无中心服务器的 P2P 模式。

在设计模式上,P2P 模式实现了网络终端用户不依赖中心服务器或者服务商而直接进行信息和数据交换的可能,因此 P2P 正在改变着整个互联网的一些基础应用,从而极大地增加了用户之间的信息沟通和交流能力。目前互联网的 P2P 应用与网络都正在飞速发展,一些典型的 P2P 应用程序比如有 BitTorrent, eDonkey 等,另外一些即时通信(IM)类软件比如 MSN、QQ 等也正在向无中心服务器模式转变。无中心服务器的 Internet 应用程序大大降低应用提供商的运营成本,而且减少人们对于 Server 稳定性的依赖。

Client/Server 通信连接方式设计

Client/Server 通信方式建立后,下一步就需要考虑通信连接的方式,主要有两种方式的连接,即长连接通信与短连接通信。通信连接方式涉及到的 APIs 主要是 connect() 和 accept()。要实现某种 Client/Server 方式,就必须考虑用某种特定的连接方式。

短连接通信

短连接通信是指 Client 方与 Server 方每进行一次通信报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于多个 Client 连接一个 Server 情况,常用于机构与用户之间通信,比如 OLTP(联机事务处理)类应用。在短连接情况下,Client 端完成任务后,就关闭连接并退出。在 Server 端,可以通过循环 accept(),使 Server 不会退出,并连续处理 Client 的请求。图 12 显示了一般情况下短连接通信模式的 Socket 事件流,不同设计的连接多 Client 的 Server 有不同的循环流程。


图 12 短连接模式通信

长连接通信

长连接通信是指 Client 方与 Server 方先建立通讯连接,连接建立后不会断开,然后再进行报文发送和接收,报文发送与接收完毕后,原来连接不会断开而继续存在,因此可以连续进行交易报文的发送 与接收。这种方式下由于通讯连接一直存在,其 TCP/IP 状态是 Established,可以用操作系统的命令 netstat 查看连接是否建立。由于在长连接情况下,Client 端和 Server 端一样可以固定使用一个端口,所以长连接下的 Client 也需要使用 bind() 来绑定 Client 的端口。在长连接方式下,需要循环读写通信数据。为了区分每一次交易的通信数据,每一次交易数据常常需要在数据头部指定该次交易的长度,接收 API 需要首先读出该长度,然后再按该长度读出指定长度的字节。长连接方式常用于一个 Client 端对一个 Server 端的通讯,一般常用于机构与机构之间的商业应用通信,以处理机构之间连续的大量的信息数据交换。或者说可用于两个系统之间持续的信息交流情况。通常为了加 快两个系统之间的信息交流,通常还需要建立几条长连接的并行通信线路。图 13 显示了一般情况下长连接通信模式的 socket 事件流,可见其最大特点是 Client 和 Server 都有循环体,而且循环体只包含读写 APIs。


图 13 长连接模式通信

Client/Server 通信发送与接收方式设计

在通信数据发送与接收之间也存在不同的方式,即同步和异步两种方式。这里的同步和异步与 I/O 层次的同异步概念不同。主要涉及 socket APIs recv() 和 send() 的不同组合方式。

同步发送与接收

从应用程序设计的角度讲,报文发送和接收是同步进行的,既报文发送后,发送方等待接收方返回消息报文。同步方式一般需要考虑超时问题,即报文发出去后发送 方不能无限等待,需要设定超时时间,超过该时间后发送方不再处于等待状态中,而直接被通知超时返回。同步发送与接收经常与短连接通信方式结合使用,称为同 步短连接通信方式,其 socket 事件流程可如上面的图 12 所示。

异步发送与接收

从应用程序设计的角度讲,发送方只管发送数据,不需要等待接收任何返回数据,而接收方只管接收数据,这就是应用层的异步发送与接收方式。要实现异步方式, 通常情况下报文发送和接收是用两个不同的进程来分别处理的,即发送与接收是分开的,相互独立的,互不影响。异步发送与接收经常与长连接通信方式结合使用, 称为异步长连接通信方式。从应用逻辑角度讲,这种方式又可分双工和单工两种情况。

异步双工

异步双工是指应用通信的接收和发送在同一个程序中,而有两个不同的子进程分别负责发送和接收,异步双工模式是比较复杂的一种通信方式,有时候经常会出现在 不同机构之间的两套系统之间的通信。比如银行与银行之间的信息交流。它也可以适用在现代 P2P 程序中。如图 14 所示,Server 和 Client 端分别 fork 出两个子进程,形成两对子进程之间的连接,两个连接都是单向的,一个连接是用于发送,另一个连接用于接收,这样方式的连接就被称为异步双工方式连接。


图 14 长连接异步双工模式

异步单工

应用通信的接收和发送是用两个不同的程序来完成,这种异步是利用两对不同程序依靠应用逻辑来实现的。图 15 显示了长连接方式下的异步单工模式,在通信的 A 和 B 端,分别有两套 Server 和 Client 程序,B 端的 Client 连接 A 端的 Server,A 端的 Server 只负责接收 B 端 Client 发送的报文。A 端的 Client 连接 B 端的 Server,A 端 Client 只负责向 B 端 Server 发送报文。


图 15 长连接异步单工模式


典型通信连接模式

综上所述,在实际 TCP/IP 应用程序设计中,就连接模式而言,我们需要考虑 Client/Server 建立方式、Client/Server 连接方式、Client/Server 发送与接收方式这三个不同级别的设计方式。实际 TCP/IP 应用程序连接模式可以是以上三类不同级别 Client/Server 方式的组合。比如一般 TCP/IP 相关书籍上提供的 TCP/IP 范例程序大都是同步短连接的 Client/Server 程序。有的组合是基本没有实用价值的,比较常用的有价值的组合是以下几种:

  • 同步短连接 Server/Client
  • 同步长连接 Server/Client
  • 异步短连接 Server/Client
  • 异步长连接双工 Server/Client
  • 异步长连接单工 Server/Client

其中异步长连接双工是较为复杂的一种通信方式,有时候经常会出现在不同银行或不同城市之间的两套系统之间的通信,比如国家金卡工程。由于这几种通信方式比较固定,所以可以预先编制这几种通信方式的模板程序。

总结

本文探讨了 TCP/IP 应用程序中连接模式的设计。在以后的文章中还将继续讨论 TCP/IP 应用程序设计中的其他方面的设计话题,包括地址族模式设计、I/O 模式设计、以及通信数据格式设计等。

come from:http://www.ibm.com/developerworks/cn/aix/library/0807_liugb_tcpip/

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/

Linux 多线程应用中如何编写安全的信号处理函数

在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 pthread_mutex 去保护全局变量。如果应用中使用了信号,而且信号的产生不是因为程序运行出错,而是程序逻辑需要,譬如 SIGUSR1、SIGRTMIN 等,信号在被处理后应用程序还将正常运行。在编写这类信号处理函数时,应用层面的开发人员却往往忽略了信号处理函数执行的上下文背景,没有考虑编写安全的 信号处理函数的一些规则。本文首先介绍编写信号处理函数时需要考虑的一些规则;然后举例说明在多线程应用中如何构建模型让因为程序逻辑需要而产生的异步信 号在指定的线程中以同步的方式处理。

线程和信号

Linux 多线程应用中,每个线程可以通过调用 pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。

当一个线程调用 pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。

POSIX.1 标准定义了一系列线程函数的接口,即 POSIX threads(Pthreads)。Linux C 库提供了两种关于线程的实现:LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已经过时,一些函数的实现不遵循POSIX.1 规范。NPTL 依赖 Linux 2.6 内核,更加遵循 POSIX..1 规范,但也不是完全遵循。

基于 NPTL 的线程库,多线程应用中的每个线程有自己独特的线程 ID,并共享同一个进程ID。应用程序可以通过调用 kill(getpid(),signo) 将信号发送到进程,如果进程中当前正在执行的线程没有阻碍此信号,则会被中断,线号处理函数会在此线程的上下文背景中执行。应用程序也可以通过调用 pthread_kill(pthread_t thread, int sig) 将信号发送给指定的线程,则线号处理函数会在此指定线程的上下文背景中执行。

基于 LinuxThreads 的线程库,多线程应用中的每个线程拥有自己独特的进程 ID,getpid() 在不同的线程中调用会返回不同的值,所以无法通过调用 kill(getpid(),signo) 将信号发送到整个进程。


编写安全的异步信号处理函数

信号的产生可以是:

  • 用户从控制终端终止程序运行,如 Ctrk + C 产生 SIGINT;
  • 程序运行出错时由硬件产生信号,如访问非法地址产生 SIGSEGV;
  • 程序运行逻辑需要,如调用 killraise 产生信号。

因为信号是异步事件,即信号处理函数执行的上下文背景是不确定的,譬如一个线程在调用某个库函数时可能会被信号中断,库函数提前出错返回,转而去执 行信号处理函数。对于上述第三种信号的产生,信号在产生、处理后,应用程序不会终止,还是会继续正常运行,在编写此类信号处理函数时尤其需要小心,以免破 坏应用程序的正常运行。关于编写安全的信号处理函数主要有以下一些规则:

  • 信号处理函数尽量只执行简单的操作,譬如只是设置一个外部变量,其它复杂的操作留在信号处理函数之外执行;
  • errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;
  • 信号处理函数只能调用可以重入的 C 库函数;譬如不能调用 malloc(),free()以及标准 I/O 库函数等;
  • 信号处理函数如果需要访问全局变量,在定义此全局变量时须将其声明为 volatile,以避免编译器不恰当的优化。

从整个 Linux 应用的角度出发,因为应用中使用了异步信号,程序中一些库函数在调用时可能被异步信号中断,此时必须根据errno 的值考虑这些库函数调用被信号中断后的出错恢复处理,譬如socket 编程中的读操作:

     rlen = recv(sock_fd, buf, len, MSG_WAITALL);
if ((rlen == -1) && (errno == EINTR)){
// this kind of error is recoverable, we can set the offset change
//‘rlen’ as 0 and continue to recv
}

在指定的线程中以同步的方式处理异步信号

如上文所述,不仅编写安全的异步信号处理函数本身有很多的规则束缚;应用中其它地方在调用可被信号中断的库函数时还需考虑被中断后的出错恢复处理。这让程序的编写变得复杂,幸运的是,POSIX.1 规范定义了sigwait()、 sigwaitinfo() pthread_sigmask() 等接口,可以实现:

这种在指定的线程中以同步方式处理信号的模型可以避免因为处理异步信号而给程序运行带来的不确定性和潜在危险。

sigwait

sigwait() 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:

  • 多个相同的信号可不可以在信号队列中排队?
  • 如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?
  • 实时信号和非实时信号在处理时有没有什么区别?

笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。


清单 1. sigwait_test.c
#include signal.h
#include /
errno.h
#include /
pthread.h
#include /
unistd.h
#include /
sys/types.h

void sig_handler(int signum)
{
printf("Receive signal. %d\n", signum);
}

void* sigmgr_thread()
{
sigset_t waitset, oset;
int sig;
int rc;
pthread_t ppid = pthread_self();

pthread_detach(ppid);

sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGRTMIN+2);
sigaddset(&waitset, SIGRTMAX);
sigaddset(&waitset, SIGUSR1);
sigaddset(&waitset, SIGUSR2);

while (1) {
rc = sigwait(&waitset, &sig);
if (rc != -1) {
sig_handler(sig);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}


int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;

sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGRTMIN+2);
sigaddset(&bset, SIGRTMAX);
sigaddset(&bset, SIGUSR1);
sigaddset(&bset, SIGUSR2);

if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");

kill(pid, SIGRTMAX);
kill(pid, SIGRTMAX);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGUSR2);
kill(pid, SIGUSR2);
kill(pid, SIGUSR1);
kill(pid, SIGUSR1);

// Create the dedicated thread sigmgr_thread() which will handle signals synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);

sleep(10);

exit (0);
}

程序编译运行在 RHEL4 的结果如下:


图 1. sigwait 测试程序执行结果
sigwait 测试程序执行结果

从以上测试程序发现以下规则:

  • 对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。
  • 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。

sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。

Linux 多线程应用中的信号处理模型

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 sigwait()使用同步模型进行处理。其程序流程如下:

  1. 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
  2. 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
  3. 主线程创建工作线程。

图 2. 在指定的线程中以同步方式处理异步信号的模型

代码示例

以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。

主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程 worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()

程序编译:gcc -o signal_sync signal_sync.c -lpthread

程序执行:./signal_sync

从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。


清单 2. signal_sync.c
#include signal.h
#include /
errno.h
#include /
pthread.h
#include /
unistd.h
#include /
sys/types.h

void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
// used to show which thread the signal is handled in.

if (signum == SIGUSR1) {
printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
//SIGRTMIN should not be considered constants from userland,
//there is compile error when use switch case
} else if (signum == SIGRTMIN) {
printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}

void* worker_thread()
{
pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {
printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}

void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();

pthread_detach(ppid);

sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);

while (1) {
rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {
printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}


int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;


// Block SIGRTMIN and SIGUSR1 which will be handled in
//dedicated thread sigmgr_thread()
// Newly created threads will inherit the pthread mask from its creator
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");

// Create the dedicated thread sigmgr_thread() which will handle
// SIGUSR1 and SIGRTMIN synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);

// Create 5 worker threads, which will inherit the thread mask of
// the creator main thread
for (i = 0; i < i =" 0;">

注意事项

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:

  • 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
  • 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  • 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
  • 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
  • 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。

小结

在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全; 同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。

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

Monday, July 13, 2009

Linux 线程模型LinuxThreads 和 NPTL的比较

目前手头的工作需要用到linux线程编程,所以就草草的查找了点关于线程方面的资料,针对目前linux下面最主要的两大线程库做一简要的描述,以便将来查阅。
当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个 线程模型也不符合 POSIX 的要求。

LinuxThreads 设计细节

线程 将应用程序划分成一个或多个同时运行的任务。线程与传统的多任务进程 之间的区别在于:线程共享的是单个进程的状态信息,并会直接共享内存和其他资源。同一个进程中线程之间的上下文切换通常要比进程之间的上下文切换速度更 快。因此,多线程程序的优点就是它可以比多进程应用程序的执行速度更快。另外,使用线程我们可以实现并行处理。这些相对于基于进程的方法所具有的优点推动 了 LinuxThreads 的实现。

LinuxThreads 最初的设计相信相关进程之间的上下文切换速度很快,因此每个内核线程足以处理很多相关的用户级线程。这就导致了一对一 线程模型的革命。

让我们来回顾一下 LinuxThreads 设计细节的一些基本理念:

  • LinuxThreads 非常出名的一个特性就是管理线程(manager thread)。管理线程可以满足以下要求:

    • 系统必须能够响应终止信号并杀死整个进程。
    • 以堆栈形式使用的内存回收必须在线程完成之后进行。因此,线程无法自行完成这个过程。
    • 终止线程必须进行等待,这样它们才不会进入僵尸状态。
    • 线程本地数据的回收需要对所有线程进行遍历;这必须由管理线程来进行。
    • 如果主线程需要调用 pthread_exit(),那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在所有线程都被杀死之后来唤醒这个主线程。

  • 为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。

  • 原语的同步是使用信号 来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。

  • 在克隆系统的最初设计之下,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。

  • 终止信号可以杀死所有的线程。LinuxThreads 接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。

  • 根据 LinuxThreads 的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程。如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起。这是因为管理线程无法将这个信号发送给进程;相反,每个线程都是作为一个进程在执行。

  • 线程之间的调度是由内核调度器来处理的。

LinuxThreads 及其局限性

LinuxThreads 的设计通常都可以很好地工作;但是在压力很大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。下面让我们来看一下 LinuxThreads 设计的一些局限性:

  • 它使用管理线程来创建线程,并对每个进程所拥有的所有线程进行协调。这增加了创建和销毁线程所需要的开销。

  • 由于它是围绕一个管理线程来设计的,因此会导致很多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。

  • 由于管理线程只能在一个 CPU 上运行,因此所执行的同步操作在 SMP 或 NUMA 系统上可能会产生可伸缩性的问题。

  • 由于线程的管理方式,以及每个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其他与 POSIX 相关的线程库并不兼容。

  • 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守 POSIX 中处理信号的方法。

  • LinuxThreads 中对信号的处理是按照每线程的原则建立的,而不是按照每进程的原则建立的,这是因为每个线程都有一个独立的进程 ID。由于信号被发送给了一个专用的线程,因此信号是串行化的 —— 也就是说,信号是透过这个线程再传递给其他线程的。这与 POSIX 标准对线程进行并行处理的要求形成了鲜明的对比。例如,在 LinuxThreads 中,通过 kill() 所发送的信号被传递到一些单独的线程,而不是集中整体进行处理。这意味着如果有线程阻塞了这个信号,那么 LinuxThreads 就只能对这个线程进行排队,并在线程开放这个信号时在执行处理,而不是像其他没有阻塞信号的线程中一样立即处理这个信号。

  • 由于 LinuxThreads 中的每个线程都是一个进程,因此用户和组 ID 的信息可能对单个进程中的所有线程来说都不是通用的。例如,一个多线程的 setuid()/setgid() 进程对于不同的线程来说可能都是不同的。

  • 有一些情况下,所创建的多线程核心转储中并没有包含所有的线程信息。同样,这种行为也是每个线程都是一个进程这个事实所导致的结果。如果任何线程 发生了问题,我们在系统的核心文件中只能看到这个线程的信息。不过,这种行为主要适用于早期版本的 LinuxThreads 实现。

  • 由于每个线程都是一个单独的进程,因此 /proc 目录中会充满众多的进程项,而这实际上应该是线程。

  • 由于每个线程都是一个进程,因此对每个应用程序只能创建有限数目的线程。例如,在 IA32 系统上,可用进程总数 —— 也就是可以创建的线程总数 —— 是 4,090。

  • 由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的访问速度都很慢。另外一个缺点是用户无法可信地指定堆栈的大小,因为用户可能会意外地将堆栈地址映射到本来要为其他目的所使用的区域上了。按需增长(grow on demand) 的概念(也称为浮动堆栈 的概念)是在 2.4.10 版本的 Linux 内核中实现的。在此之前,LinuxThreads 使用的是固定堆栈。

关于 NPTL

NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。

Ulrich Drepper 和 Ingo Molnar 是 Red Hat 参与 NPTL 设计的两名员工。他们的总体设计目标如下:

  • 这个新线程库应该兼容 POSIX 标准。

  • 这个线程实现应该在具有很多处理器的系统上也能很好地工作。

  • 为一小段任务创建新线程应该具有很低的启动成本。

  • NPTL 线程库应该与 LinuxThreads 是二进制兼容的。注意,为此我们可以使用 LD_ASSUME_KERNEL,这会在本文稍后进行讨论。

  • 这个新线程库应该可以利用 NUMA 支持的优点。

NPTL 的优点

与 LinuxThreads 相比,NPTL 具有很多优点:

  • NPTL 没有使用管理线程。管理线程的一些需求,例如向作为进程一部分的所有线程发送终止信号,是并不需要的;因为内核本身就可以实现这些功能。内核还会处理每个 线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理,这样可以避免僵尸进程的问题。

  • 由于 NPTL 没有使用管理线程,因此其线程模型在 NUMA 和 SMP 系统上具有更好的可伸缩性和同步机制。

  • 使用 NPTL 线程库与新内核实现,就可以避免使用信号来对线程进行同步了。为了这个目的,NPTL 引入了一种名为 futex 的新机制。futex 在共享内存区域上进行工作,因此可以在进程之间进行共享,这样就可以提供进程间 POSIX 同步机制。我们也可以在进程之间共享一个 futex。这种行为使得进程间同步成为可能。实际上,NPTL 包含了一个 PTHREAD_PROCESS_SHARED 宏,使得开发人员可以让用户级进程在不同进程的线程之间共享互斥锁。

  • 由于 NPTL 是 POSIX 兼容的,因此它对信号的处理是按照每进程的原则进行的;getpid() 会为所有的线程返回相同的进程 ID。例如,如果发送了 SIGSTOP 信号,那么整个进程都会停止;使用 LinuxThreads,只有接收到这个信号的线程才会停止。这样可以在基于 NPTL 的应用程序上更好地利用调试器,例如 GDB。

  • 由于在 NPTL 中所有线程都具有一个父进程,因此对父进程汇报的资源使用情况(例如 CPU 和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。

  • NPTL 线程库所引入的一个实现特性是对 ABI(应用程序二进制接口)的支持。这帮助实现了与 LinuxThreads 的向后兼容性。这个特性是通过使用 LD_ASSUME_KERNEL 实现的,下面就来介绍这个特性。

LD_ASSUME_KERNEL 环境变量

正如上面介绍的一样,ABI 的引入使得可以同时支持 NPTL 和 LinuxThreads 模型。基本上来说,这是通过 ld (一个动态链接器/加载器)来进行处理的,它会决定动态链接到哪个运行时线程库上。

举例来说,下面是 WebSphere® Application Server 对这个变量所使用的一些通用设置;您可以根据自己的需要进行适当的设置:

  • LD_ASSUME_KERNEL=2.4.19:这会覆盖 NPTL 的实现。这种实现通常都表示使用标准的 LinuxThreads 模型,并启用浮动堆栈的特性。
  • LD_ASSUME_KERNEL=2.2.5:这会覆盖 NPTL 的实现。这种实现通常都表示使用 LinuxThreads 模型,同时使用固定堆栈大小。

我们可以使用下面的命令来设置这个变量:

export LD_ASSUME_KERNEL=2.4.19

注意,对于任何 LD_ASSUME_KERNEL 设置的支持都取决于目前所支持的线程库的 ABI 版本。例如,如果线程库并不支持 2.2.5 版本的 ABI,那么用户就不能将 LD_ASSUME_KERNEL 设置为 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 则需要 2.4.1。

如果您正运行的是一个启用了 NPTL 的 Linux 发行版,但是应用程序却是基于 LinuxThreads 模型来设计的,那么所有这些设置通常都可以使用。

GNU_LIBPTHREAD_VERSION 宏

大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:

$ getconf GNU_LIBPTHREAD_VERSION

在我的ubuntu9.04下面的输出结果:

NPTL 2.9

VerilogHDL里面的阻塞与非阻塞赋值

接触过Verilog HDL的都对阻塞与非阻塞赋值略知一二,这也是经常强调的重点之一。在编写可综合代码的时候,建议大家不要忘了打开RTL网表查看器看看自己综合出来的电路是不是想要的逻辑。

1、连续赋值

连续赋值语句的硬件实现是:从赋值语句(=)右边提取出的逻辑,用于驱动赋值语句左边的线网(net)连续赋值语句

module continousassignment(a,b,c);
input a,b;
output c;
assign c=a&b;
endmodule


综合以后,通过网表查看器为上图的结果,线网c由赋值语句的右边的逻辑是组合逻辑a&b简单驱动

2、过程赋值

过程赋值语句的硬件实现是,从赋值语句的(=或<=)右边提取出的逻辑用于驱动赋值语句左边的变量(必需是reg类 型)。必须注意的是虽然过程赋值语句是可以出现在initial语句中(仅用于仿真),也可以出现在“always”块语句中,但是只有“always” 中的过程赋值语句才能被综合。有两种类型的过程赋值语句:阻塞赋值语句(Blocking Assignment statement)、非阻塞赋值语句(non-Blocking Assignment statement)

2.1、阻塞赋值语句

阻塞赋值语句可以简单描述为,在一个always块中,语句按照从上到下的顺序执行

module blockingassignment (clk ,q1,q2);
input clk;
output [2:0] q1,q2;
reg[2:0] q1,q2;
always @ (posedge clk)
begin
q1=q1+3'b1;
q2=q1;
end
endmodule

综合后的RTL视图如上图所示,每个时钟上升沿触发后,变量q2、q1的值是同步的,q2被赋予了q1更新后的值。

2.2、非阻塞赋值语句

非阻塞赋值语句简述为:在一个always 块中,语句是并行执行的

module nonblockingassignment (clk ,q1,q2);
input clk;
output [2:0] q1,q2;
reg[2:0] q1,q2;
always @ (posedge clk)
begin
q1<=q1+3'b1; q2<=q1; end endmodule

从综合结果的RTL查看器可以看到如上图所示,每个时钟触发后,q2被赋予的q1值时上个时钟周期生成的值。从上面可以看出,第一条语句综合出来的结果是一样的,阻塞与非阻塞不同在于它们会影响到后面引用该条语句的逻辑。

3、阻塞与非阻塞建模建模原则

1)、组合逻辑使用阻塞语句、时序逻辑使用非阻塞语句;

2)、在同一个模块里,同一个变量不能既有阻塞赋值,又有非阻塞赋值。

ARM VS X86 的时代

自从订阅了以“arm+linux”为关键字的google新闻简报以来,这几年每天都可以收到几个相关的报道,我也时刻关心着技术的发展方向,毕竟身在江湖嘛!
嵌入式处理器从十年前的群雄并起到现在ARM和Intel两强争雄,波澜壮阔,演绎了一出纷繁芜杂而又让人感慨万千的大戏!
自从英特尔携凌动杀入嵌入式市场,就与在该市场获得广泛应用的ARM及相关DSP平台展开激烈的市场争夺战。
两大平台相互学习,似乎将殊途同归,应用的交叉会更多。不同的人会选择不同的方案。将来,可能是用英特尔的平台也可以,用ARM的平台也可以。但有一点很重要,硬件只不过是一个躯壳,软件才是产生价值的灵魂。项目会不会有竞争力,并不取决于你采用的是英特尔还是ARM,我认为取决于Design,即软件的价值,还有商业模式、生态环境和创新的功能。在这方面,苹果的成功就是一个例子。我们今天都是站在巨人的肩膀上前行,一个idea出来,一个创新出来了,附加值产生了,这个附加值很可能就是软件带来的。

Thursday, July 9, 2009

团队-TEAM

这个世界上没有完美的人,但是有些人热爱追求完美的事物,所以他门走到了一起。他们用他人的长处来弥补自己的短处,这个队伍称之为团队。

Tuesday, July 7, 2009

我的战术刀具

呵呵,生活就是充满了乐趣的,男人爱刀就像爱女人一样,你永远不知道哪一把是最好的,追求美好的步伐永无止境,呵呵,不过追求玩具和追求女人毕竟是不一样的!女朋友,我爱的就是最好的!
我承认我是个军刀爱好者,我喜欢购买各种战术刀具,从廉价的国产刀具到正宗进口的国外品牌刀具我都喜欢,刀子这种东西,重在实用,我买这些东西不是为了收藏,当然,工业化量产的商品收藏起来又有多大意义呢?

我的第一把刀, 国产高仿Strider.BT



买的第二把刀,SCHRADA EXTREME 野战救生刀



第三把刀,ColdSteel(冷钢)13RTK 侦察兵



卡巴1217


哈哈,我的刀具很少,我喜欢户外运动,这也是我玩刀的一个主要原因,欢迎喜欢玩刀的网友与我联系,共同交流心得,呵呵!

Friday, July 3, 2009

linux socket select笔记

有了epoll之后有人说select是应该扔到垃圾堆的东西,呵呵,我不这么认为,我是网络编程的菜鸟,实质上epoll只是select的改进而已,本质上区别不大。
select()机制提供一个fd_set数据结构,实际上是long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件可读,下面具体解释:

struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合 FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。

struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

使用select需要包含以下linux头文件:

#include /sys/types.h/
#include /sys/times.h/
#include /sys/select.h/

函数原型:
int select(
int nfds,
fd_set * readfds,
fd_set * writefds,
fd_set * exceptfds,
struct timeval * timeout);

参数解释:

ndfs:select监视的文件句柄数,视进程中打开的文件数而定,即所有文件描述符的最大值加1。
readfds:select监视的可读文件句柄集合。readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

writefds: select监视的可写文件句柄集合。writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

exceptfds:select监视的异常文件句柄集合。

timeout:本次select()的超时结束时间。(见/usr/sys/select.h,可精确至百万分之一秒)timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值下述。

返回值:
负值:select错误
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件

当readfds或writefds中映象的文件可读或可写或超时,本次select()就结束返回。程序员利用一组系统提供的宏在select()结束时便可判断哪一文件可读或可写。对Socket编程特别有用的就是readfds。
几只相关的宏解释如下:

FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否可读写,>0表示可读写。
(关于fd_set及相关宏的定义见/usr/include/sys/types.h)

编程框架:
int main()
{
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout={2,0}; //select等待2秒,2秒轮询,要非阻塞就置0
char buffer[256]={0}; //256字节的接收缓冲区

/* 假定已经建立TCP连接,具体过程忽略,
*UDP同理,主机ip和port都已经给定,要写的文件已经打开
*/
sock=socket(...);
bind(...);
fp=fopen(...);

while(1)
{
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加描述符

FD_SET(fp,&fds); //同上
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1

switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
{
case -1:
exit(-1);
break; //select错误,退出程序
case 0:
break; //再次轮询
default:
if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
{
recvfrom(sock,buffer,256,.....); //接受网络数据
if(FD_ISSET(fp,&fds)) //测试文件是否可写
fwrite(fp,buffer...); //写入文件
buffer清空;
}// end if
break;
}// end switch
}//end while
}//end main

Labels

Followers