------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
文件的读写是文件系统中最核心也是最复杂的一部份,它牵涉到了很多的概念.之前分析文件系统其它操作的时候,遇到与文件系统相关的读写部份都忽略过去了.在这一节里,来讨论一下文件的读写是怎样实现的.
二:I/O请求的概述
如之前所提到的,为了提高文件的操作效率,文件系统中的内容都是缓存在内存里的.每当发起一个Rear/Write请求的时候,都会到页面高速缓存中寻找具体的页面.如果页面不存在,则在页面高速缓存中建立相关页面的缓存.如果当前的页面不是最新的.那就必须要到具体的文件系统中读取数据了.一般来说,内核提供了这样的界面:它产生一个I/O请求.这个界面为上层隐藏了下层的不同实现.在这个界面中,将产生的I/O请求提交给I/O调度.再与I/O调度调用具体的块设备驱动程序.
整个过程如下图所示:
上图中的Generic Block Layer就是上面描述中所说的I/O的界面.
接下来我们以上图从下到上的层次进行讨论.
三:块设备驱动
块设备与字符设备的区别在于:块设备可以随机的访问,例如磁盘.正是因为它可以随机访问,内核才需要一个高效的手段去管理每一个块设备.例如对磁盘的操作,每次移动磁针都需要花不少的时候,所以尽量让其处理完相同磁道内的请求再将磁针移动到另外的磁道.而对于字符设备来说,不存在这样的顾虑,只需按顺序从里面读/写就可以了.
先来看一下块设备驱动所涉及到的数据结构.
3.1: block_device结构:
struct block_device {
//主次驱备号
dev_t              bd_dev;  /* not a kdev_t - it's a search key */
//指向bdev文件系统中块设备对应的文件索引号
struct inode *         bd_inode; /* will die */
//计数器,统计块驱备被打开了多少次
int           bd_openers;
// 块设备打开和关闭的信号量
struct semaphore   bd_sem;  /* open/close mutex */
//禁止在块设备上建行新安装的信号量
struct semaphore   bd_mount_sem; /* mount mutex */
//已打开的块设备文件inode链表
struct list_head   bd_inodes;
//块设备描述符的当前拥有者
void *             bd_holder;
//统计字段,统计对bd_holder进行更改的次数
int           bd_holders;
//如果当前块设备是一个分区,此成员指向它所属的磁盘的设备
//否则指向该描述符的本身
struct block_device *  bd_contains;
//块大小
unsigned      bd_block_size;
//指向分区描述符的指针
struct hd_struct * bd_part;
/* number of times partitions within this device have been opened. */
//统计字段,统计块设备分区被打开的次数
unsigned      bd_part_count;
//读取块设备分区表时设置的标志
int           bd_invalidated;
//指向块设备所属磁盘的gendisk
struct gendisk *   bd_disk;
//指向块设备描述符链表的指针
struct list_head   bd_list;
//指向块设备的专门描述符backing_dev_info
struct backing_dev_info *bd_inode_backing_dev_info;
/*
* Private data.  You must have bd_claim'ed the block_device
* to use this.  NOTE:  bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
//块设备的私有区
unsigned long      bd_private;
}
通常,对于块设备来说还涉及到一个分区问题.分区在内核中是用hd_struct来表示的.
3.2: hd_struct结构:
struct hd_struct {
//磁盘分区的起始扇区
sector_t start_sect;
//分区的长度,即扇区的数目
sector_t nr_sects;
//内嵌的kobject
struct kobject kobj;
//分区的读操作次数,读取扇区数,写操作次数,写扇区数
unsigned reads, read_sectors, writes, write_sectors;
//policy:如果分区是只读的,置为1.否则为0
//partno:磁盘中分区的相对索引
int policy, partno;
}
每个具体的块设备都会都应一个磁盘,在内核中磁盘用gendisk表示.
3.3: gendisk结构:
struct gendisk {
//磁盘的主驱备号
int major;             /* major number of driver */
//与磁盘关联的第一个设备号
int first_minor;
//与磁盘关联的设备号范围
int minors;                     /* maximum number of minors, =1 for
* disks that can't be partitioned. */
//磁盘的名字
char disk_name[32];         /* name of major driver */
//磁盘的分区描述符数组                                      
struct hd_struct **part;    /* [indexed by minor] */
//块设备的操作指针
struct block_device_operations *fops;
//指向磁盘请求队列指针
struct request_queue *queue;
//块设备的私有区
void *private_data;
//磁盘内存区大小(扇区数目)
sector_t capacity;
//描述磁盘类型的标志
int flags;
//devfs 文件系统中的名字
char devfs_name[64];        /* devfs crap */
//不再使用
int number;            /* more of the same */
//指向磁盘中硬件设备的device指针
struct device *driverfs_dev;
//内嵌kobject指针
struct kobject kobj;
//记录磁盘中断定时器
struct timer_rand_state *random;
//如果只读,此值为1.否则为0
int policy;
//写入磁盘的扇区数计数器
atomic_t sync_io;      /* RAID */
//统计磁盘队列使用情况的时间戳
unsigned long stamp, stamp_idle;
//正在进行的I/O操作数
int in_flight;
//统计每个CPU使用磁盘的情况
#ifdef   CONFIG_SMP
struct disk_stats *dkstats;
#else
struct disk_stats dkstats;
#endif
}
以上三个数据结构的关系,如下图所示:
如上图所示:
每个块设备分区的bd_contains会指它的总块设备节点,它的bd_part会指向它的分区表.bd_disk会指向它所属的磁盘.
从上图中也可以看出:每个磁盘都会对应一个request_queue.对于上层的I/O请求就是通过它来完成的了.它的结构如下:
3.4:request_queue结构:
struct request_queue
{
/*
* Together with queue_head for cacheline sharing
*/
//待处理请求的链表
struct list_head   queue_head;
//指向队列中首先可能合并的请求描述符
struct request         *last_merge;
//指向I/O调度算法指针
elevator_t         elevator;
/*
* the queue request freelist, one for reads and one for writes
*/
//为分配请请求描述符所使用的数据结构
struct request_list    rq;
//驱动程序策略例程入口点的方法
request_fn_proc        *request_fn;
//检查是否可能将bio合并到请求队列的最后一个请求的方法
merge_request_fn   *back_merge_fn;
//检查是否可能将bio合并到请求队列的第一个请求中的方法
merge_request_fn   *front_merge_fn;
//试图合并两个相邻请求的方法
merge_requests_fn  *merge_requests_fn;
//将一个新请求插入请求队列时所调用的方法
make_request_fn        *make_request_fn;
//该方法反这个处理请求的命令发送给硬件设备
prep_rq_fn         *prep_rq_fn;
//去掉块设备方法
unplug_fn     *unplug_fn;
//当增加一个新段时,该方法驼回可插入到某个已存在的bio  结构中的字节数
merge_bvec_fn      *merge_bvec_fn;
//将某个请求加入到请求队列时,会调用此方法
activity_fn        *activity_fn;
//刷新请求队列时所调用的方法
issue_flush_fn         *issue_flush_fn;
/*
* Auto-unplugging state
*/
//插入设备时所用到的定时器
struct timer_list  unplug_timer;
//如果请求队列中待处理请求数大于该值,将立即去掉请求设备
int           unplug_thresh;     /* After this many requests */
//去掉设备之间的延迟
unsigned long      unplug_delay; /* After this many jiffies */
//去掉设备时使用的操作队列
struct work_struct unplug_work;
//
struct backing_dev_info backing_dev_info;
/*
* The queue owner gets to use this for whatever they like.
* ll_rw_blk doesn't touch it.
*/
//指向块设备驱动程序中的私有数据
void          *queuedata;
//activity_fn()所用的参数
void          *activity_data;
/*
* queue needs bounce pages for pages above this limit
*/
//如果页框号大于该值,将使用回弹缓存冲
unsigned long      bounce_pfn;
//回弹缓存区页面的分配标志
int           bounce_gfp;
/*
* various queue flags, see QUEUE_* below
*/
//描述请求队列的标志
unsigned long      queue_flags;
/*
* protects queue structures from reentrancy
*/
//指向请求队列锁的指针
spinlock_t         *queue_lock;
/*
* queue kobject
*/
//内嵌的kobject
struct kobject kobj;
/*
* queue settings
*/
//请求队列中允许的最大请求数
unsigned long      nr_requests;  /* Max # of requests */
//如果待请求的数目超过了该值,则认为该队列是拥挤的
unsigned int       nr_congestion_on;
//如果待请求数目在这个阀值下,则认为该队列是不拥挤的
unsigned int       nr_congestion_off;
//单个请求所能处理的最大扇区(可调的)
unsigned short         max_sectors;
//单个请求所能处理的最大扇区(硬约束)
unsigned short         max_hw_sectors;
//单个请求所能处理的最大物理段数
unsigned short         max_phys_segments;
//单个请求所能处理的最大物理段数(DMA的约束)
unsigned short         max_hw_segments;
//扇区中以字节 为单位的大小
unsigned short         hardsect_size;
//物理段的最大长度(以字节为单位)
unsigned int       max_segment_size;
//段合并的内存边界屏弊字
unsigned long      seg_boundary_mask;
//DMA缓冲区的起始地址和长度的对齐
unsigned int       dma_alignment;
//空闲/忙标记的位图.用于带标记的请求
struct blk_queue_tag   *queue_tags;
//请求队列的引用计数
atomic_t      refcnt;
//请求队列中待处理的请求数
unsigned int       in_flight;
/*
* sg stuff
*/
//用户定义的命令超时
unsigned int       sg_timeout;
//Not Use
unsigned int       sg_reserved_size;
}
request_queue表示的是一个请求队列,每一个请求都是用request来表示的.
3.5: request结构:
struct request {
//用来形成链表
struct list_head queuelist; /* looking for ->queue? you must _not_
* access it directly, use
* blkdev_dequeue_request! */
//请求描述符的标志              
unsigned long flags;        /* see REQ_ bits below */
/* Maintain bio traversal state for part by part I/O submission.
* hard_* are block layer internals, no driver should touch them!
*/
//要传送的下一个扇区
sector_t sector;       /* next sector to submit */
//要传送的扇区数目
unsigned long nr_sectors;   /* no. of sectors left to submit */
/* no. of sectors left to submit in the current segment */
//当前bio段传送扇区的数目
unsigned int current_nr_sectors;
//要传送的下一个扇区号
sector_t hard_sector;       /* next sector to complete */
//整个过程中要传送的扇区号
unsigned long hard_nr_sectors;   /* no. of sectors left to complete */
/* no. of sectors left to complete in the current segment */
//当前bio段要传送的扇区数目
unsigned int hard_cur_sectors;
/* no. of segments left to submit in the current bio */
//
unsigned short nr_cbio_segments;
/* no. of sectors left to submit in the current bio */
unsigned long nr_cbio_sectors;
struct bio *cbio;      /* next bio to submit */
//请求中第一个没有完成的bio
struct bio *bio;       /* next unfinished bio to complete */
//最后的bio
struct bio *biotail;
//指向I/O调度的私有区
void *elevator_private;
//请求的状态
int rq_status;     /* should split this into a few status bits */
//请求所引用的磁盘描述符
struct gendisk *rq_disk;
//统计传送失败的计数
int errors;
//请求开始的时间
unsigned long start_time;
/* Number of scatter-gather DMA addr+len pairs after
* physical address coalescing is performed.
*/
//请求的物理段数
unsigned short nr_phys_segments;
/* Number of scatter-gather addr+len pairs after
* physical and DMA remapping hardware coalescing is performed.
* This is the number of scatter-gather entries the driver
* will actually have to deal with after DMA mapping is done.
*/
//请求的硬段数
unsigned short nr_hw_segments;
//与请求相关的标识
int tag;
//数据传送的缓冲区,如果是高端内存,此成员值为NULL
char *buffer;
//请求的引用计数
int ref_count;
//指向包含请求的请求队列描述符
request_queue_t *q;
struct request_list *rl;
//指向数据传送终止的completion
struct completion *waiting;
//对设备发达“特殊请求所用到的指针”
void *special;
/*
* when request is used as a packet command carrier
*/
//cmd中的数据长度
unsigned int cmd_len;
//请求类型
unsigned char cmd[BLK_MAX_CDB];
//data中的数据长度
unsigned int data_len;
//为了跟踪所传输的数据而使用的指针
void *data;
//sense字段的数据长度
unsigned int sense_len;
//指向输出sense缓存区
void *sense;
//请求超时
unsigned int timeout;
/*
* For Power Management requests
*/
//指向电源管理命令所用的结构
struct request_pm_state *pm;
}
请求队列描述符与请求描述符都很复杂,为了简化驱动的设计,内核提供了一个API,供块设备驱动程序来初始化一个请求队列.这就是blk_init_queue().它的代码如下:
//rfn:驱动程序自动提供的操作I/O的函数.对应请求队列的request_fn
//lock:驱动程序提供给请求队列的自旋锁
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
{
request_queue_t *q;
static int printed;
//申请请求队列描述符
q = blk_alloc_queue(GFP_KERNEL);
if (!q)
return NULL;
//初始化q->request_list
if (blk_init_free_list(q))
goto out_init;
if (!printed) {
printed = 1;
printk("Using %s io scheduler\n", chosen_elevator->elevator_name);
}
//初始化请求队列描述符中的各项操作函数
q->request_fn      = rfn;
q->back_merge_fn       = ll_back_merge_fn;
q->front_merge_fn      = ll_front_merge_fn;
q->merge_requests_fn   = ll_merge_requests_fn;
q->prep_rq_fn      = NULL;
q->unplug_fn       = generic_unplug_device;
q->queue_flags         = (1 << QUEUE_FLAG_CLUSTER);
q->queue_lock      = lock;
blk_queue_segment_boundary(q, 0xffffffff);
//设置q->make_request_fn函数,初始化等待队对列的定时器和等待队列
blk_queue_make_request(q, __make_request);
//设置max_segment_size,max_hw_segments,max_phys_segments
blk_queue_max_segment_size(q, MAX_SEGMENT_SIZE);
blk_queue_max_hw_segments(q, MAX_HW_SEGMENTS);
blk_queue_max_phys_segments(q, MAX_PHYS_SEGMENTS);
/*
* all done
*/
//设置等待队列的I/O调度程序
if (!elevator_init(q, chosen_elevator))
return q;
//失败的处理
blk_cleanup_queue(q);
out_init:
kmem_cache_free(requestq_cachep, q);
return NULL;
}
这个函数中初始化了很多操作指针,这个函数在所有块设备中都是一样的,这样就为通用块设备层提供了一个统一的接口.对于块设备驱动的接口就是我们在blk_init_queue中设置的策略例程了.留意一下关于请求队列的各操作的设置,这在后续的分析中会用到.
另外,在请求结构中涉及到了bio结构.bio表示一个段.目前内核中关于I/O的所有操作都是由它来表示的.它的结构如下所示:
struct bio {
//段的起始扇区
sector_t      bi_sector;
//下一个bio
struct bio         *bi_next; /* request queue link */
//段所在的块设备
struct block_device    *bi_bdev;
//bio的标志
unsigned long      bi_flags; /* status, command, etc */
//Read/Write
unsigned long      bi_rw;        /* bottom bits READ/WRITE,
* top bits priority
*/
//bio_vec的项数
unsigned short         bi_vcnt; /* how many bio_vec's */
//当前正在操作的bio_vec
unsigned short         bi_idx;       /* current index into bvl_vec */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
//结合后的片段数目
unsigned short         bi_phys_segments;
/* Number of segments after physical and DMA remapping
* hardware coalescing is performed.
*/
//重映射后的片段数目
unsigned short         bi_hw_segments;
//I/O计数
unsigned int       bi_size; /* residual I/O count */
/*
* To keep track of the max hw size, we account for the
* sizes of the first and last virtually mergeable segments
* in this bio
*/
//第一个可以合并的段大小
unsigned int       bi_hw_front_size;
//最后一个可以合并的段大小
unsigned int       bi_hw_back_size;
//最大的bio_vec项数
unsigned int       bi_max_vecs;  /* max bvl_vecs we can hold */
//bi_io_vec数组
struct bio_vec         *bi_io_vec;   /* the actual vec list */
//I/O完成的方法
bio_end_io_t       *bi_end_io;
//使用计数
atomic_t      bi_cnt;       /* pin count */
//拥有者的私有区
void          *bi_private;
//销毁此bio的方法
bio_destructor_t   *bi_destructor;    /* destructor */
}
bio_vec的结构如下:
struct bio_vec {
//bi_vec所表示的页面
struct page   *bv_page;
//数据区的长度
unsigned int  bv_len;
//在页面中的偏移量
unsigned int  bv_offset;
}
关于bio与bio_vec的关系,用下图表示:
现在,我们来思考一个问题:
当一个I/O请求提交给请求队列后,它是怎么去调用块设备驱动的策略例程去完成这次I/O的呢?还有,当一个I/O请求被提交给请求队列时,会不会立即调用驱动中的策略例程去完成这次I/O呢?
实际上,为了提高效率,所有的I/O都会在一个特定的延时之后才会调用策略例程去完成本次I/O.我们来看一个反面的例子,假设I/O在被提交后马上得到执行.例如.磁盘有磁针在磁盘12.现在有一个磁道1的请求.就会将磁针移动到磁道1.操作完后,又有一个请求过来了,它要操作磁道11.然后又会将磁针移到磁道11.操作完后,又有一个请求过来,要求操作磁道4.此时会将磁针移到磁道4.这个例子中,磁针移动的位置是:12->1->11->4.实际上,磁针的定位是一个很耗时的操作.这样下去,毫无疑问会影响整个系统的效率.我们可以在整个延时内,将所有I/O操作按顺序排列在一起,然后再调用策略例程.于是上例的磁针移动就会变成12->11->4->1.此时磁针只会往一个方向移动.
至于怎么样排列请求和选取哪一个请求进行操作,这就是I/O调度的任务了.这部份我们在通用块层再进行分析.
内核中有两个操作会完成上面的延时过程.即:激活块设备驱动程序和撤消块设备驱动程序.
3.6:块设备驱动程序的激活和撤消
激活块设备驱动程序和撤消块设备驱动程序在内核中对应的接口为blk_plug_device()和blk_remove_plug().分别看下它们的操作:
void blk_plug_device(request_queue_t *q)
{
WARN_ON(!irqs_disabled());
/*
* don't plug a stopped queue, it must be paired with blk_start_queue()
* which will restart the queueing
*/
//如果设置了QUEUE_FLAG_STOPPED.直接退出
if (test_bit(QUEUE_FLAG_STOPPED, &q->queue_flags))
return;
//为请求队列设置QUEUE_FLAG_PLUGGED.
if (!test_and_set_bit(QUEUE_FLAG_PLUGGED, &q->queue_flags))
//如果之前请求队列的状态不为QUEUE_FLAG_PLUGGED,则设置定时器超时时间
mod_timer(&q->unplug_timer, jiffies + q->unplug_delay);
}
int blk_remove_plug(request_queue_t *q)
{
WARN_ON(!irqs_disabled());
//将队列QUEUE_FLAG_PLUGGED状态清除
if (!test_and_clear_bit(QUEUE_FLAG_PLUGGED, &q->queue_flags))
//如果请求队列之前不为QUEUE_FLAG_PLUGGED标志,直接返回
return 0;
//如果之前是QUEUE_FLAG_PLUGGED标志,则将定时器删除
del_timer(&q->unplug_timer);
return 1;
}
如果请求队列状态为QUEUE_FLAG_PLUGGED,且定时器超时,会有什么样的操作呢?
回忆在请求队列初始化函数中,blk_init_queue()会调用blk_queue_make_request().它的代码如下:
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn)
{
……
……
q->unplug_delay = (3 * HZ) / 1000;   /* 3 milliseconds */
if (q->unplug_delay == 0)
q->unplug_delay = 1;
INIT_WORK(&q->unplug_work, blk_unplug_work, q);
q->unplug_timer.function = blk_unplug_timeout;
q->unplug_timer.data = (unsigned long)q;
……
……
}
上面设置了定时器的时间间隔为(3*HZ)/1000.定时器超时的处理函数为blk_unplug_timeout().参数为请求队列本身.
blk_unplug_timeout()的代码如下:
static void blk_unplug_timeout(unsigned long data)
{
request_queue_t *q = (request_queue_t *)data;
kblockd_schedule_work(&q->unplug_work);
}
从上面的代码看出,定时器超时之后,会唤醒q->unplug_work这个工作对列.
在blk_queue_make_request()中,对这个工作队列的初始化为:
INIT_WORK(&q->unplug_work, blk_unplug_work, q)
即工作队列对应的函数为blk_unplug_work().对应的参数为请求队列本身.代码如下:
static void blk_unplug_work(void *data)
{
request_queue_t *q = data;
q->unplug_fn(q);
}
到此,就会调用请求队列的unplug_fn()操作.
在blk_init_queue()对这个成员的赋值如下所示:
q->unplug_fn       = generic_unplug_device;
generic_unplug_device()对应的代码如下:
void __generic_unplug_device(request_queue_t *q)
{
//如果请求队列是QUEUE_FLAG_STOPPED 状态,返回
if (test_bit(QUEUE_FLAG_STOPPED, &q->queue_flags))
return;
//如果请求队列的状态是QUEUE_FLAG_PLUGGED.就会返回1
if (!blk_remove_plug(q))
return;
/*
* was plugged, fire request_fn if queue has stuff to do
*/
//如果请求对列中的请求,则调用请求队列的reauest_fn函数.也就是驱动程序的
//策略例程
if (elv_next_request(q))
q->request_fn(q);
}
blk_remove_plug()在上面已经分析过了.这里不再赘述.
归根到底,最后的I/O完成操作都会调用块设备驱动的策略例程来完成.
四:I/O调度层
I/O调度对应的结构如下所示:
struct elevator_s
{
//当要插入一个bio时会调用
elevator_merge_fn *elevator_merge_fn;
elevator_merged_fn *elevator_merged_fn;
elevator_merge_req_fn *elevator_merge_req_fn;
//取得下一个请求
elevator_next_req_fn *elevator_next_req_fn;
//往请求队列中增加请求
elevator_add_req_fn *elevator_add_req_fn;
elevator_remove_req_fn *elevator_remove_req_fn;
elevator_requeue_req_fn *elevator_requeue_req_fn;
elevator_queue_empty_fn *elevator_queue_empty_fn;
elevator_completed_req_fn *elevator_completed_req_fn;
elevator_request_list_fn *elevator_former_req_fn;
elevator_request_list_fn *elevator_latter_req_fn;
elevator_set_req_fn *elevator_set_req_fn;
elevator_put_req_fn *elevator_put_req_fn;
elevator_may_queue_fn *elevator_may_queue_fn;
//初始化与退出操作
elevator_init_fn *elevator_init_fn;
elevator_exit_fn *elevator_exit_fn;
void *elevator_data;
struct kobject kobj;
struct kobj_type *elevator_ktype;
//调度算法的名字
const char *elevator_name;
}
我们以最简单的NOOP算法为例进行分析.
NOOP算法只是做简单的请求合并的操作.的定义如下:
elevator_t elevator_noop = {
.elevator_merge_fn     = elevator_noop_merge,
.elevator_merge_req_fn      = elevator_noop_merge_requests,
.elevator_next_req_fn       = elevator_noop_next_request,
.elevator_add_req_fn        = elevator_noop_add_request,
.elevator_name              = "noop",
}
挨个分析里面的各项操作:
elevator_noop_merge():在请求队列中寻找能否有可以合并的请求.代码如下:
int elevator_noop_merge(request_queue_t *q, struct request **req,
struct bio *bio)
{
struct list_head *entry = &q->queue_head;
struct request *__rq;
int ret;
//如果请求队列中有last_merge项.则判断last_merge项是否能够合并
//在NOOP中一般都不会设置last_merge
if ((ret = elv_try_last_merge(q, bio))) {
*req = q->last_merge;
return ret;
}
//遍历请求队列中的请求
while ((entry = entry->prev) != &q->queue_head) {
__rq = list_entry_rq(entry);
if (__rq->flags & (REQ_SOFTBARRIER | REQ_HARDBARRIER))
break;
else if (__rq->flags & REQ_STARTED)
break;
//如果不是一个fs类型的请求?
if (!blk_fs_request(__rq))
continue;
//判断能否与这个请求合并   
if ((ret = elv_try_merge(__rq, bio))) {
*req = __rq;
q->last_merge = __rq;
return ret;
}
}
return ELEVATOR_NO_MERGE;
}
Elv_try_merge()用来判断能否与请求合并,它的代码如下:
inline int elv_try_merge(struct request *__rq, struct bio *bio)
{
int ret = ELEVATOR_NO_MERGE;
/*
* we can merge and sequence is ok, check if it's possible
*/
//判断rq与bio是否为同类型的请求
if (elv_rq_merge_ok(__rq, bio)) {
//如果请求描述符中的起始扇区+ 扇区数= bio的起始扇区
//则将bio加到_rq的后面.
//返回ELEVATOR_BACK_MERGE
if (__rq->sector + __rq->nr_sectors == bio->bi_sector)
ret = ELEVATOR_BACK_MERGE;
//如果请求描述符中的起始扇区- 扇区数=bio的起始扇区
//则将bio加到_rq的前面
//返回ELEVATOR_FRONT_MERGE
else if (__rq->sector - bio_sectors(bio) == bio->bi_sector)
ret = ELEVATOR_FRONT_MERGE;
}
//如果不可以合并,返回ELEVATOR_NO_MERGE (值为0)
return ret;
}
elv_rq_merge_ok()代码如下:
inline int elv_rq_merge_ok(struct request *rq, struct bio *bio)
{
//判断rq是否可用
if (!rq_mergeable(rq))
return 0;
/*
* different data direction or already started, don't merge
*/
//操作是否相同
if (bio_data_dir(bio) != rq_data_dir(rq))
return 0;
/*
* same device and no special stuff set, merge is ok
*/
//要操作的对象是否一样
if (rq->rq_disk == bio->bi_bdev->bd_disk &&
!rq->waiting && !rq->special)
return 1;
return 0;
}
注意:如果检查成功返回1.失败返回0.
elevator_noop_merge_requests():将next 从请求队列中取出.代码如下:
void elevator_noop_merge_requests(request_queue_t *q, struct request *req,
struct request *next)
{
list_del_init(&next->queuelist);
}
从上面的代码中看到,NOOP算法从请求队列中取出请求,只需要取链表结点即可.不需要进行额外的操作.
elevator_noop_next_request():取得下一个请求.代码如下:
struct request *elevator_noop_next_request(request_queue_t *q)
{
if (!list_empty(&q->queue_head))
return list_entry_rq(q->queue_head.next);
return NULL;
}
很简单,取链表的下一个结点.
elevator_noop_add_request():往请求队列中插入一个请求.代码如下:
void elevator_noop_add_request(request_queue_t *q, struct request *rq,
int where)
{
//默认是将rq插和到循环链表末尾
struct list_head *insert = q->queue_head.prev;
//如果要插到请求队列的前面
if (where == ELEVATOR_INSERT_FRONT)
insert = &q->queue_head;
//不管是什么样的操作,都将新的请求插入到请求队列的末尾
list_add_tail(&rq->queuelist, &q->queue_head);
/*
* new merges must not precede this barrier
*/
if (rq->flags & REQ_HARDBARRIER)
q->last_merge = NULL;
else if (!q->last_merge)
q->last_merge = rq;
}
五:通用块层的处理
通用块层的入口点为generic_make_request().它的代码如下:
void generic_make_request(struct bio *bio)
{
request_queue_t *q;
sector_t maxsector;
//nr_sectors:要操作的扇区数
int ret, nr_sectors = bio_sectors(bio);
//可能会引起睡眠
might_sleep();
/* Test device or partition size, when known. */
//最大扇区数目
maxsector = bio->bi_bdev->bd_inode->i_size >> 9;
if (maxsector) {
//bio操作的起始扇区
sector_t sector = bio->bi_sector;
//如果最大扇区数<要操作的扇区数or 最大扇区数与起始扇区的差值小于要操作的扇区数
//非法的情况
if (maxsector < nr_sectors ||
maxsector - nr_sectors < sector) {
char b[BDEVNAME_SIZE];
/* This may well happen - the kernel calls
* bread() without checking the size of the
* device, e.g., when mounting a device. */
printk(KERN_INFO
"attempt to access beyond end of device\n");
printk(KERN_INFO "%s: rw=%ld, want=%Lu, limit=%Lu\n",
bdevname(bio->bi_bdev, b),
bio->bi_rw,
(unsigned long long) sector + nr_sectors,
(long long) maxsector);
set_bit(BIO_EOF, &bio->bi_flags);
goto end_io;
}
}
/*
* Resolve the mapping until finished. (drivers are
* still free to implement/resolve their own stacking
* by explicitly returning 0)
*
* NOTE: we don't repeat the blk_size check for each new device.
* Stacking drivers are expected to know what they are doing.
*/
do {
char b[BDEVNAME_SIZE];
//取得块设备的请求对列
q = bdev_get_queue(bio->bi_bdev);
if (!q) {
//请求队列不存在
printk(KERN_ERR
"generic_make_request: Trying to access "
"nonexistent block-device %s (%Lu)\n",
bdevname(bio->bi_bdev, b),
(long long) bio->bi_sector);
end_io:
//最终会调用bio->bi_end_io
bio_endio(bio, bio->bi_size, -EIO);
break;
}
//非法的情况
if (unlikely(bio_sectors(bio) > q->max_hw_sectors)) {
printk("bio too big device %s (%u > %u)\n",
bdevname(bio->bi_bdev, b),
bio_sectors(bio),
q->max_hw_sectors);
goto end_io;
}
//如果请求队列为QUEUE_FLAG_DEAD
//退出
if (test_bit(QUEUE_FLAG_DEAD, &q->queue_flags))
goto end_io;
/*
* If this device has partitions, remap block n
* of partition p to block n+start(p) of the disk.
*/
//如果当前块设备是一个分区,则转到分区所属的块设备
blk_partition_remap(bio);
//调用请求队列的make_request_fn()
ret = q->make_request_fn(q, bio);
} while (ret);
}
在blk_init_queue()中对请求队列的make_request_fn的设置如下所示:
blk_init_queue()—> blk_queue_make_request(q, __make_request)
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn)
{
……
……
q->make_request_fn = mfn;
……
}
这里,等待队对的make_request_fn就被设置为了__make_request.这个函数的代码如下:
static int __make_request(request_queue_t *q, struct bio *bio)
{
struct request *req, *freereq = NULL;
int el_ret, rw, nr_sectors, cur_nr_sectors, barrier, err;
sector_t sector;
//bio的起始扇区
sector = bio->bi_sector;
//扇区数目
nr_sectors = bio_sectors(bio);
//当前bio中的bio_vec的扇区数目
cur_nr_sectors = bio_cur_sectors(bio);
//读/写
rw = bio_data_dir(bio);
/*
* low level driver can indicate that it wants pages above a
* certain limit bounced to low memory (ie for highmem, or even
* ISA dma in theory)
*/
//建立一个弹性回环缓存
blk_queue_bounce(q, &bio);
spin_lock_prefetch(q->queue_lock);
barrier = bio_barrier(bio);
if (barrier && !(q->queue_flags & (1 << QUEUE_FLAG_ORDERED))) {
err = -EOPNOTSUPP;
goto end_io;
}
again:
spin_lock_irq(q->queue_lock);
//请求队列是空的
if (elv_queue_empty(q)) {
//激活块设备驱动
blk_plug_device(q);
goto get_rq;
}
if (barrier)
goto get_rq;
//调用I/O调度的elevator_merge_fn方法,判断这个bio能否和其它请求合并
//如果可以合并,req参数将返回与之合并的请求描述符
el_ret = elv_merge(q, &req, bio);
switch (el_ret) {
//可以合并.且bio加到req的后面
case ELEVATOR_BACK_MERGE:
BUG_ON(!rq_mergeable(req));
if (!q->back_merge_fn(q, req, bio))
break;
req->biotail->bi_next = bio;
req->biotail = bio;
req->nr_sectors = req->hard_nr_sectors += nr_sectors;
drive_stat_acct(req, nr_sectors, 0);
if (!attempt_back_merge(q, req))
elv_merged_request(q, req);
goto out;
//可以合并.且bio加到req的前面
case ELEVATOR_FRONT_MERGE:
BUG_ON(!rq_mergeable(req));
if (!q->front_merge_fn(q, req, bio))
break;
bio->bi_next = req->bio;
req->cbio = req->bio = bio;
req->nr_cbio_segments = bio_segments(bio);
req->nr_cbio_sectors = bio_sectors(bio);
/*
* may not be valid. if the low level driver said
* it didn't need a bounce buffer then it better
* not touch req->buffer either...
*/
req->buffer = bio_data(bio);
req->current_nr_sectors = cur_nr_sectors;
req->hard_cur_sectors = cur_nr_sectors;
req->sector = req->hard_sector = sector;
req->nr_sectors = req->hard_nr_sectors += nr_sectors;
drive_stat_acct(req, nr_sectors, 0);
if (!attempt_front_merge(q, req))
elv_merged_request(q, req);
goto out;
/*
* elevator says don't/can't merge. get new request
*/
//不可以合并.申请一个新的请求,将且加入请求队列
case ELEVATOR_NO_MERGE:
break;
default:
printk("elevator returned crap (%d)\n", el_ret);
BUG();
}
/*
* Grab a free request from the freelist - if that is empty, check
* if we are doing read ahead and abort instead of blocking for
* a free slot.
*/
get_rq:
//freereq:是新分配的请求描述符
if (freereq) {
req = freereq;
freereq = NULL;
} else {
//分配一个请求描述符
spin_unlock_irq(q->queue_lock);
if ((freereq = get_request(q, rw, GFP_ATOMIC)) == NULL) {
/*
* READA bit set
*/
//分配失败
err = -EWOULDBLOCK;
if (bio_rw_ahead(bio))
goto end_io;
freereq = get_request_wait(q, rw);
}
goto again;
}
req->flags |= REQ_CMD;
/*
* inherit FAILFAST from bio (for read-ahead, and explicit FAILFAST)
*/
if (bio_rw_ahead(bio) || bio_failfast(bio))
req->flags |= REQ_FAILFAST;
/*
* REQ_BARRIER implies no merging, but lets make it explicit
*/
if (barrier)
req->flags |= (REQ_HARDBARRIER | REQ_NOMERGE);
//初始化新分配的请求描述符
req->errors = 0;
req->hard_sector = req->sector = sector;
req->hard_nr_sectors = req->nr_sectors = nr_sectors;
req->current_nr_sectors = req->hard_cur_sectors = cur_nr_sectors;
req->nr_phys_segments = bio_phys_segments(q, bio);
req->nr_hw_segments = bio_hw_segments(q, bio);
req->nr_cbio_segments = bio_segments(bio);
req->nr_cbio_sectors = bio_sectors(bio);
req->buffer = bio_data(bio);     /* see ->buffer comment above */
req->waiting = NULL;
//将bio 关联到请求描述符
req->cbio = req->bio = req->biotail = bio;
req->rq_disk = bio->bi_bdev->bd_disk;
req->start_time = jiffies;
//请将求描述符添加到请求队列中
add_request(q, req);
out: (R)
if (freereq)
__blk_put_request(q, freereq);
//如果定义了BIO_RW_SYNC.
//将调用__generic_unplug_device将块设备驱动,它会直接调用驱动程序的策略例程
if (bio_sync(bio))
__generic_unplug_device(q);
spin_unlock_irq(q->queue_lock);
return 0;
end_io:
bio_endio(bio, nr_sectors << 9, err);
return 0;
}
这个函数的逻辑比较简单,它判断bio能否与请求队列中存在的请求合并,如果可以合并,将其它合并到现有的请求.如果不能合并,则新建一个请求描述符,然后把它插入到请求队列中.上面的代码可以结合之前分析的NOOP算法进行理解.
重点分析一下请求描述符的分配过程:
分配一个请求描述符的过程如下所示:
if ((freereq = get_request(q, rw, GFP_ATOMIC)) == NULL) {
/*
* READA bit set
*/
//分配失败
err = -EWOULDBLOCK;
if (bio_rw_ahead(bio))
goto end_io;
freereq = get_request_wait(q, rw);
}
在分析这段代码之前,先来讨论一下关于请求描述符的分配方式.记得我们在分析请求队列描述符的时候,request_queue中有一个成员:struct request_list  rq;
它的数据结构如下:
struct request_list {
//读/写请求描述符的分配计数
int count[2];
//分配缓存池
mempool_t *rq_pool;
//如果没有空闲内存时.读/写请求的等待队列
wait_queue_head_t wait[2];
};
如果当前空闲内存不够.则会将请求的进程挂起.如果分配成功,则将请求队列的rl字段指向这个分配的request_list.
释放一个请求描述符,将会将其归还给指定的内存池.
request_list结构还有一个避免请求拥塞的作用:
每个请求队列都有一个允许处理请求的最大值(request_queue->nr_requests).如果队列中的请求超过了这个数值,则将队列置为QUEUE_FLAG_READFULL/QUEUE_FLAG_WRITEFULL.后续试图加入到队列的进程就会被放置到request_list结构所对应的等待队列中睡眠.如果一个队列中的睡眠进程过程也多也会影响系统的效率.如果待处理的请求大于request_queue-> nr_congestion_on就会认为这个队列是拥塞的.就会试图降低新请求的创建速度.如果待处理请求小于request_queue->nr_congestion_off.则会认为当前队列是不拥塞的.
get_request()的代码如下:
static struct request *get_request(request_queue_t *q, int rw, int gfp_mask)
{
struct request *rq = NULL;
struct request_list *rl = &q->rq;
struct io_context *ioc = get_io_context(gfp_mask);
spin_lock_irq(q->queue_lock);
//如果请求数超过了请求队列允许的最大请求值(q->nr_requests)
//就会将后续的请求进程投入睡眠
if (rl->count[rw]+1 >= q->nr_requests) {
/*
* The queue will fill after this allocation, so set it as
* full, and mark this process as "batching". This process
* will be allowed to complete a batch of requests, others
* will be blocked.
*/
//判断是否将队列置为了QUEUE_FLAG_READFULL/QUEUE_FLAG_WRITEFULL
//如果没有,则置此标志.并且设置当前进程为batching
if (!blk_queue_full(q, rw)) {
ioc_set_batching(ioc);
blk_set_queue_full(q, rw);
}
}
//如果队列满了,进程不为batching 且I/O调度程序不能忽略它
//不能分配.直接返回
if (blk_queue_full(q, rw)
&& !ioc_batching(ioc) && !elv_may_queue(q, rw)) {
/*
* The queue is full and the allocating process is not a
* "batcher", and not exempted by the IO scheduler
*/
spin_unlock_irq(q->queue_lock);
goto out;
}
//要分配请求描述符了,递增计数
rl->count[rw]++;
//如果待请求数量超过了request_queue-> nr_congestion_on
//则队列是阻塞的,设置阻塞标志
if (rl->count[rw] >= queue_congestion_on_threshold(q))
set_queue_congested(q, rw);
spin_unlock_irq(q->queue_lock);
//分配请求描述符
rq = blk_alloc_request(q, gfp_mask);
if (!rq) {
/*
* Allocation failed presumably due to memory. Undo anything
* we might have messed up.
*
* Allocating task should really be put onto the front of the
* wait queue, but this is pretty rare.
*/
spin_lock_irq(q->queue_lock);
//分配失败了,要减小分配描述的引用计数
freed_request(q, rw);
spin_unlock_irq(q->queue_lock);
goto out;
}
if (ioc_batching(ioc))
ioc->nr_batch_requests--;
//初始化请求的各字段
INIT_LIST_HEAD(&rq->queuelist);
/*
* first three bits are identical in rq->flags and bio->bi_rw,
* see bio.h and blkdev.h
*/
rq->flags = rw;
rq->errors = 0;
rq->rq_status = RQ_ACTIVE;
rq->bio = rq->biotail = NULL;
rq->buffer = NULL;
rq->ref_count = 1;
rq->q = q;
rq->rl = rl;
rq->waiting = NULL;
rq->special = NULL;
rq->data_len = 0;
rq->data = NULL;
rq->sense = NULL;
out:
//减少ioc的引用计数
put_io_context(ioc);
return rq;
}
由于在分配之前递增了统计计数,所以在分配失败后,要把这个统计计数减下来,这是由freed_request()完成的.它的代码如下:
static void freed_request(request_queue_t *q, int rw)
{
struct request_list *rl = &q->rq;
rl->count[rw]--;
//如果分配计数小于request_queue->nr_congestion_off.队列已经不拥塞了
if (rl->count[rw] < queue_congestion_off_threshold(q))
clear_queue_congested(q, rw);
//如果计数小于允许的最大值.那可以分配请求了,将睡眠的进程唤醒
if (rl->count[rw]+1 <= q->nr_requests) {
//唤醒等待进程
if (waitqueue_active(&rl->wait[rw]))
wake_up(&rl->wait[rw]);
//清除QUEUE_FLAG_READFULL/QUEUE_FLAG_WRITEFULL
blk_clear_queue_full(q, rw);
}
}
在这里我们可以看到,如果待处理请求小于请求队列所允许的最大值,就会将睡眠的进程唤醒.
如果请求描述符分配失败,会怎么样呢?我们接着看__make_request()中的代码:
if ((freereq = get_request(q, rw, GFP_ATOMIC)) == NULL) {
/*
* READA bit set
*/
//分配失败
err = -EWOULDBLOCK;
//如果此次操作是一次预读,且不阻塞
if (bio_rw_ahead(bio))
goto end_io;
//挂起进程
freereq = get_request_wait(q, rw);
}
如果分配失败,会调用get_request_wait()将进程挂起.它的代码如下:
static struct request *get_request_wait(request_queue_t *q, int rw)
{
//初始化一个等待队列
DEFINE_WAIT(wait);
struct request *rq;
struct io_context *ioc;
//撤消块设备驱动.这里会直接调用块设备驱动的策略例程
generic_unplug_device(q);
ioc = get_io_context(GFP_NOIO);
do {
struct request_list *rl = &q->rq;
//将当前进程加入等待队列.并设置进程状态为TASK_UNINTERRUPTIBLE
prepare_to_wait_exclusive(&rl->wait[rw], &wait,
TASK_UNINTERRUPTIBLE);
//再次获得等待队列
rq = get_request(q, rw, GFP_NOIO);
if (!rq) {
//如果还是失败了,睡眠
io_schedule();
/*
* After sleeping, we become a "batching" process and
* will be able to allocate at least one request, and
* up to a big batch of them for a small period time.
* See ioc_batching, ioc_set_batching
*/
//这里是被唤醒之后运行
ioc_set_batching(ioc);
}
//将进程从等待队列中删除
finish_wait(&rl->wait[rw], &wait);
} while (!rq);
put_io_context(ioc);
return rq;
}
这段代码比较简单,相似的代码我们在之前已经分析过很多次了.这里不做重点分析.
此外.在__make_request()中还需要注意一件事情.在bio中的内存可能是高端内存的.但是内核不能直接访问,这里就必须要对处理高端内存的bio_vec做下处理.即将它临时映射之后copy到普通内存区.这就是所谓的弹性回环缓存.相关的操作是在blk_queue_bounce()中完成的.这个函数比较简单,可以自行分析.
到这里,通用块层的处理分析就结束了.我们继续分析其它的层次.

 解析linux根文件系统的挂载过程 2008-08-05 15:55:45

分类: LINUX

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------

一:前言
前段时间在编译kernel的时候发现rootfs挂载不上。相同的root选项设置旧版的image却可以。为了彻底解决这个问题。研究了一下rootfs的挂载过程。特总结如下,希望能给这部份知识点比较迷茫的朋友一点帮助。
二:rootfs的种类
总的来说,rootfs分为两种:虚拟rootfs和真实rootfs.现在kernel的发展趋势是将更多的功能放到用户空间完成。以保持内核的精简。虚拟rootfs也是各linux发行厂商普遍采用的一种方式。可以将一部份的初始化工作放在虚拟的rootfs里完成。然后切换到真实的文件系统.
在虚拟rootfs的发展过程中。又有以下几个版本:
initramfs:
Initramfs是在 kernel 2.5中引入的技术,实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。这种这种方式的rootfs是包含在kernel image之中的.

cpio-initrd: cpio格式的rootfs

image-initrd:传统格式的rootfs
关于这两种虚拟文件系统的制作请自行参阅其它资料
三:rootfs文件系统的挂载过程
这里说的rootfs不同于上面分析的rootfs。这里指的是系统初始化时的根结点。即/结点。它是其于内存的rootfs文件系统。这部份之前在<< linux启动过程分析>>和文件系统中已经分析过。为了知识的连贯性这里再重复一次。
Start_kernel()àmnt_init():

void __init mnt_init(void)

{
……
……
init_rootfs();
init_mount_tree();
}
Init_rootfs的代码如下:

int __init init_rootfs(void)

{
int err;

err = bdi_init(&ramfs_backing_dev_info);

if (err)
return err;
err = register_filesystem(&rootfs_fs_type);
if (err)
bdi_destroy(&ramfs_backing_dev_info);
return err;
}
这个函数很简单。就是注册了rootfs的文件系统.
init_mount_tree()代码如下:

static void __init init_mount_tree(void)

{
struct vfsmount *mnt;
struct mnt_namespace *ns;
struct path root;

mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);

if (IS_ERR(mnt))

panic("Can't create rootfs");

ns = kmalloc(sizeof(*ns), GFP_KERNEL);
if (!ns)

panic("Can't allocate initial namespace");

atomic_set(&ns->count, 1);
INIT_LIST_HEAD(&ns->list);
init_waitqueue_head(&ns->poll);
ns->event = 0;

list_add(&mnt->mnt_list, &ns->list);

ns->root = mnt;
mnt->mnt_ns = ns;
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);
root.mnt = ns->root;
root.dentry = ns->root->mnt_root;
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
}
在这里,将rootfs文件系统挂载。它的挂载点默认为”/”.最后切换进程的根目录和当前目录为”/”.这也就是根目录的由来。不过这里只是初始化。等挂载完具体的文件系统之后,一般都会将根目录切换到具体的文件系统。所以在系统启动之后,用mount命令是看不到rootfs的挂载信息的.
四:虚拟文件系统的挂载
根目录已经挂上去了,可以挂载具体的文件系统了.
在start_kernel()àrest_init()àkernel_init():

static int __init kernel_init(void * unused)

{
……
……
do_basic_setup();

if (!ramdisk_execute_command)

ramdisk_execute_command = "/init";

if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {

ramdisk_execute_command = NULL;

prepare_namespace();
}
/*

* Ok, we have completed the initial bootup, and

* we're essentially up and running. Get rid of the

* initmem segments and start the user-mode stuff..

*/
init_post();
return 0;
}
do_basic_setup()是一个很关键的函数,所有直接编译在kernel中的模块都是由它启动的。代码片段如下:

static void __init do_basic_setup(void)

{
/* drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();
init_irq_proc();
do_initcalls();
}
Do_initcalls()用来启动所有在__initcall_start和__initcall_end段的函数,而静态编译进内核的modules也会将其入口放置在这段区间里。
跟根文件系统相关的初始化函数都会由rootfs_initcall()所引用。注意到有以下初始化函数:
rootfs_initcall(populate_rootfs);
也就是说会在系统初始化的时候会调用populate_rootfs进行初始化。代码如下:

static int __init populate_rootfs(void)

{
char *err = unpack_to_rootfs(__initramfs_start,
__initramfs_end - __initramfs_start, 0);
if (err)
panic(err);
if (initrd_start) {

#ifdef CONFIG_BLK_DEV_RAM

int fd;

printk(KERN_INFO "checking if image is initramfs...");

err = unpack_to_rootfs((char *)initrd_start,

initrd_end - initrd_start, 1);

if (!err) {

printk(" it is\n");

unpack_to_rootfs((char *)initrd_start,

initrd_end - initrd_start, 0);

free_initrd();
return 0;
}

printk("it isn't (%s); looks like an initrd\n", err);

fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);

if (fd >= 0) {

sys_write(fd, (char *)initrd_start,

initrd_end - initrd_start);

sys_close(fd);
free_initrd();
}
#else

printk(KERN_INFO "Unpacking initramfs...");

err = unpack_to_rootfs((char *)initrd_start,

initrd_end - initrd_start, 0);

if (err)
panic(err);
printk(" done\n");
free_initrd();
#endif
}
return 0;
}
unpack_to_rootfs:顾名思义就是解压包,并将其释放至rootfs。它实际上有两个功能,一个是释放包,一个是查看包,看其是否属于cpio结构的包。功能选择是根据最后的一个参数来区分的.
在这个函数里,对应我们之前分析的三种虚拟根文件系统的情况。一种是跟kernel融为一体的initramfs.在编译kernel的时候,通过链接脚本将其存放在__initramfs_start至__initramfs_end的区域。这种情况下,直接调用unpack_to_rootfs将其释放到根目录.如果不是属于这种形式的。也就是__initramfs_start和__initramfs_end的值相等,长度为零。不会做任何处理。退出.
对应后两种情况。从代码中看到,必须要配制CONFIG_BLK_DEV_RAM才会支持image-initrd。否则全当成cpio-initrd的形式处理。
对于是cpio-initrd的情况。直接将其释放到根目录。对于是image-initrd的情况。将其释放到/initrd.image.最后将initrd内存区域归入伙伴系统。这段内存就可以由操作系统来做其它的用途了。
接下来,内核对这几种情况又是怎么处理的呢?不要着急。往下看:
回到kernel_init()这个函数:

static int __init kernel_init(void * unused)

{
…….
…….
do_basic_setup();
/*

* check if there is an early userspace init.  If yes, let it do all

* the work
*/
if (!ramdisk_execute_command)

ramdisk_execute_command = "/init";

if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {

ramdisk_execute_command = NULL;

prepare_namespace();
}
/*

* Ok, we have completed the initial bootup, and

* we're essentially up and running. Get rid of the

* initmem segments and start the user-mode stuff..

*/
init_post();
return 0;
}
ramdisk_execute_command:在kernel解析引导参数的时候使用。如果用户指定了init文件路径,即使用了“init=”,就会将这个参数值存放到这里。
如果没有指定init文件路径。默认为/init
对应于前面一段的分析,我们知道,对于initramdisk和cpio-initrd的情况,都会将虚拟根文件系统释放到根目录。如果这些虚拟文件系统里有/init这个文件。就会转入到init_post()。
Init_post()代码如下:

static int noinline init_post(void)

{
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();

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);
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);

printk(KERN_WARNING "Failed to execute %s\n",

ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*

* The Bourne shell can be used instead of init if we are

* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);

printk(KERN_WARNING "Failed to execute %s.  Attempting "

"defaults...\n", 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.");
}
从代码中可以看中,会依次执行指定的init文件,如果失败,就会执行/sbin/init, /etc/init,, /bin/init,/bin/sh
注意的是,run_init_process在调用相应程序运行的时候,用的是kernel_execve。也就是说调用进程会替换当前进程。只要上述任意一个文件调用成功,就不会返回到这个函数。如果上面几个文件都无法执行。打印出没有找到init文件的错误。
对于image-hdr或者是虚拟文件系统中没有包含 /init的情况,会由prepare_namespace()处理。代码如下:

void __init prepare_namespace(void)

{
int is_floppy;
if (root_delay) {

printk(KERN_INFO "Waiting %dsec before mounting root device...\n",

root_delay);
ssleep(root_delay);
}

/* wait for the known devices to complete their probing */

while (driver_probe_done() != 0)
msleep(100);
//mtd的处理
md_run_setup();
if (saved_root_name[0]) {

root_device_name = saved_root_name;

if (!strncmp(root_device_name, "mtd", 3)) {

mount_block_root(root_device_name, root_mountflags);

goto out;
}

ROOT_DEV = name_to_dev_t(root_device_name);

if (strncmp(root_device_name, "/dev/", 5) == 0)

root_device_name += 5;

}
if (initrd_load())
goto out;

/* wait for any asynchronous scanning to complete */

if ((ROOT_DEV == 0) && root_wait) {

printk(KERN_INFO "Waiting for root device %s...\n",

saved_root_name);

while (driver_probe_done() != 0 ||

(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)

msleep(100);
}

is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;

if (is_floppy && rd_doload && rd_load_disk(0))

ROOT_DEV = Root_RAM0;
mount_root();
out:

sys_mount(".", "/", NULL, MS_MOVE, NULL);

sys_chroot(".");
}
这里有几个比较有意思的处理,首先用户可以用root=来指定根文件系统。它的值保存在saved_root_name中。如果用户指定了以mtd开始的字串做为它的根文件系统。就会直接去挂载。这个文件是mtdblock的设备文件。
否则将设备结点文件转换为ROOT_DEV即设备节点号
然后,转向initrd_load()执行initrd预处理后,再将具体的根文件系统挂载。
注意到,在这个函数末尾。会调用sys_mount()来移动当前文件系统挂载点到”/”目录下。然后将根目录切换到当前目录。这样,根文件系统的挂载点就成为了我们在用户空间所看到的”/”了.
对于其它根文件系统的情况,会先经过initrd的处理。即

int __init initrd_load(void)

{
if (mount_initrd) {

create_dev("/dev/ram", Root_RAM0);

/*

* Load the initrd data into /dev/ram0. Execute it as initrd

* unless /dev/ram0 is supposed to be our actual root device,

* in that case the ram disk is just set up here, and gets

* mounted in the normal path.
*/

if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {

sys_unlink("/initrd.image");
handle_initrd();
return 1;
}
}
sys_unlink("/initrd.image");
return 0;
}
建立一个ROOT_RAM)的设备节点,并将/initrd/.image释放到这个节点中,/initrd.image的内容,就是我们之前分析的image-initrd。
如果根文件设备号不是ROOT_RAM0( 用户指定的根文件系统不是/dev/ram0就会转入到handle_initrd()
如果当前根文件系统是/dev/ram0.将其直接挂载就好了。
handle_initrd()代码如下:

static void __init handle_initrd(void)

{
int error;
int pid;

real_root_dev = new_encode_dev(ROOT_DEV);

create_dev("/dev/root.old", Root_RAM0);

/* mount initrd on rootfs' /root */

mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);

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 */

sys_chdir("/root");

sys_mount(".", "/", NULL, MS_MOVE, NULL);

sys_chroot(".");
/*

* In case that a resume from disk is carried out by linuxrc or one of

* its children, we need to tell the freezer not to wait for us.

*/
current->flags |= PF_FREEZER_SKIP;

pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);

if (pid > 0)

while (pid != sys_wait4(-1, NULL, 0, NULL))

yield();

current->flags &= ~PF_FREEZER_SKIP;

/* move initrd to rootfs' /old */
sys_fchdir(old_fd);

sys_mount("/", ".", NULL, MS_MOVE, NULL);

/* switch root and cwd back to / of rootfs */

sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);

if (new_decode_dev(real_root_dev) == Root_RAM0) {

sys_chdir("/old");
return;
}
ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();

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);

if (error == -ENOENT)

printk("/initrd does not exist. Ignored.\n");

else
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");

}
}
先将/dev/ram0挂载,而后执行/linuxrc.等其执行完后。切换根目录,再挂载具体的根文件系统.
到这里。文件系统挂载的全部内容就分析完了.
五:小结
在本小节里。分析了根文件系统的挂载流程。并对几个虚拟根文件系统的情况做了详细的分析。理解这部份,对我们构建linux嵌入式开发系统是很有帮助的.
PS:参考资料:ibm技术论坛的<>                 
附根文件系统挂载流程图

Linux文件系统之文件的读写相关推荐

  1. 嵌入式 Linux 入门(二、Linux 文件系统、文件类型及权限管理)

    嵌入式 Linux入 门第二课, linux 文件系统.文件类型及权限管理. ...... 矜辰所致 目录 前言 一.Linux 文件属性 1.1 Linux 文件类型 1.2 Linux 文件权限及 ...

  2. Linux文件系统及文件储存方式

    前言 Linux文件系统构成 文件式的文件结构 Linux的一个具体文件 系统对文件的访问方式 Linux系统的删除方式 shred与rm的区别 rm删除文件的恢复 前言 闲来无事复习了下Linux文 ...

  3. Linux文件系统及文件储存方式【转】

    本文转载自:https://blog.csdn.net/qyp199312/article/details/54927843 前言 Linux文件系统构成 文件式的文件结构 Linux的一个具体文件 ...

  4. Linux 文件系统类型 文件系统结构 与Windows文件系统的比较

    Linux 文件系统类型 磁盘文件系统. 包括硬盘.CD-ROM.DVD.USB存储器.磁盘阵列等.常见文件系统格式有:autofs.coda.Ext(Extended File sytem,扩展文件 ...

  5. linux文件系统、文件系统结构、虚拟文件系统

    参考:linux文件系统及其目录结构.虚拟文件系统 作者:丶PURSUING 发布时间: 2021-02-15 09:33:29 网址:https://blog.csdn.net/weixin_447 ...

  6. Spark修炼之道(基础篇)——Linux大数据开发基础:第二节:Linux文件系统、文件夹(一)...

    本节主要内容 怎样获取帮助文档 Linux文件系统简单介绍 文件夹操作 訪问权限 1. 怎样获取帮助文档 在实际工作过程其中,常常会忘记命令的使用方式.比如ls命令后面能够跟哪些參数,此时能够使用ma ...

  7. linux文件系统中文件基本权限,Linux文件权限基本属性图文详解

    [概述] 在 Linux 系统中,不同的用户处于不同的地位,拥有不同的权限,为保护系统安全性,Linux 系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定. Linux 系统的每一个文 ...

  8. 利用stat命令获取Linux文件系统和文件的详细状态信息

    用途:stat命令用于显示文件或文件系统的状态信息,来自于coreutils软件包,一般系统自带此命令工具,它能获取与文件系统及文件相关的许多信息,具体用途见stat的功能选项.这些信息包括inode ...

  9. linux文件系统打开不了,linux文件系统之文件的打开与关闭

    文件的操作主要包括了文件的打开关闭和读写.在这节中主要分析了linux内核中的文件操作的实现.还是同前两节一样,涉及到块设备与页面缓存的部份先放一边.后续有会有专题分析与此相关的内容. 二:文件的打开 ...

最新文章

  1. ASP.NET 2.0 中配合 Master Page 使用的优化 CSS 模型
  2. SQL数据库面试题以及答案
  3. DFS 之 poj 2386 Lake Counting
  4. IDEA在当前类中查找方法快捷键--转
  5. python与图书编辑
  6. SAP报表开发工具 Report Painter
  7. 网易云音乐网络库跨平台化实践
  8. Moose File System分布文件系统测试
  9. Android中简单的日期格式化
  10. long 转为string_面试必问 Redis数据结构底层原理String、List篇
  11. 论文浅尝 | 基于图匹配神经网络的跨语言知识图对齐 (ACL 2019)
  12. 字体怎么转换html代码,如何设置下列html代码的字体大小和 font-family 以进行水平转换?_html_开发99编程知识库...
  13. 【工程项目经验】之多个静态库合并成一个
  14. 使用IntelliJ IDEA同步Github代码
  15. JS中Null与Undefined的区别
  16. 超强 | 保险单据在线OCR,秒速识别保单信息
  17. 数据库关闭的四种方式
  18. 桌面图标分类软件Fences的缺点
  19. dreamweaver cs6 连接mysql_dreamweaver cs6连接数据库的具体流程讲述
  20. Html5中鼠标经过图片,图片在盒子内部实现放大效果。

热门文章

  1. php empty详解
  2. LeetCode-334. Increasing Triplet Subsequence
  3. 最大输入hdu 2534 规律水题 求任意个a,b的和 不能表示的最大的数
  4. The Elements of C# Style -Naming
  5. iOS 快速定位到系统设置界面
  6. Redis 系列之一
  7. 库(静态库和动态库)
  8. ueditor初始化
  9. IOS8的新特性:简洁易用的毛玻璃效果
  10. Lisp-1和Lisp-2