Linux文件系统之文件的读写

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: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

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: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技术论坛的<>                 
附根文件系统挂载流程图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值