加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

[转载]memory学习总结读写操作

(2015-11-04 07:27:16)
标签:

转载

分类: 内核开发
原文地址:memory学习总结读写操作作者:silvcn

一、概述

通过调用读写操作接口来访问emmc设备,在内核中的接口为read、write,应用通过系统调用访问到kernel中的read和write函数,linux将驱动分为很多层,层层调用之后最终通过读写emmc的接口完成。

二、打开设备

无论是读还是写,都必须要先完成开发操作open,下面先介绍open,系统调用如下,SYSCALL_DEFINE3表示open函数后面带三个参数。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)

open主要调用了do_sys_open接口,完成下列工作

1、分析open的入参flag和mode,check是否需要创建新文件;

2、getname从names_cachep缓存中分配内存区,然后将路径名从用户空间复制到该内存区中,最后返回字符串的首地址赋给temp指针;

3、get_unused_fd_flags在当前进程的文件描述符表中找一个空位,如果没有位置,在expand_files中(expand_fdtable)会申请空间构造一个新的文件描述符表,将旧文件描述符表拷贝到新表的前面,表的后面清0,释放旧表空间。

4、do_filp_open根据文件的路径以及打开标志等信息得到要操作的文件指针,具体操作如下:

a、调用get_empty_filp返回一个空的文件结构并将指针给局部指针filp;

b、path_init把根路径赋给nd的当前path,其中flag标志置上了LOOKUP_PARENT(要查找路径名最后一个分量的父目录)和LOOKUP_JUMPED,查看应用代码,open的filename最前面都要带‘/’;

c、调用link_path_walk(pathname, &nd)查找路径名最后一个分量的父目录的路径path结构体(通过d_hash指针接口完成),路径名的最后一个分量有可能是不存在的而需要创建,将this变量(路径名最后一个分量的名字、长度等信息)和type赋给nd->last和nd->last_type;

d、do_last是最后一个关键操作,根据上面保存的nd->last_type信息(可能是”.”、”..”、根目录或者符号链接)做相应处理,创建文件、打开文件、退回到上一级目录或者返回错误,创建文件调用vfs_create,最后在nameidata_to_filp中调用__dentry_open将inode->i_fop挂到了文件f的f_op指针上,而inode->i_fop则在fat_fill_inode中(创建文件系统msdos_create的时候调用)挂上file_operations的操作接口首地址,同理fat_aops的操作接口首地址也赋给inode->i_mapping->a_ops,进而在__dentry_open中赋给f->f_mapping;

5、调用fsnotify_open标识文件打开;

6、调用fd_install将文件f安装到fs数组(文件描述表)中;

文件打开操作的核心是在当前进程下的文件描述表中放置一个file结构体,在文件描述表空间不够的时候扩展空间申请一个文件描述符挂到描述符表的后面,把file结构体放进去。

 

三、读设备

当打开文件后,就可以进行读文件了。上层接口是read,系统调用定义如下:

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count),read函数有三个参数。

操作顺序如下:

1、调用fget_light从fd获取当前进程相应文件对象的地址file;

2、调用file_pos_read获取要读取内容的位置;

3、vfs_read实际读文件,无论同步读还是异步读都最终调用了异步读文件,并更新读指针;

4、调用file_pos_write将刚更新的读指针更新到文件的读取位置上;

 

读设备的具体操作流程

1、file->f_op->read/ filp->f_op->aio_read是同步/异步读文件的函数指针,对应结构是file_operations,不同的文件系统在这一层的接口一般是相同的,fat文件系统的全局变量是fat_file_operations,同步接口是do_sync_read,异步接口是generic_file_aio_read,最终都调用generic_file_aio_read,文件的操作接口是在open中挂接的(参考上一小节)。

struct file_operations {

         struct module *owner;

         loff_t (*llseek) (struct file *, loff_t, int);

         ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

         ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

         ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

         ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

         int (*readdir) (struct file *, void *, filldir_t);

         int (*open) (struct inode *, struct file *);

         int (*flush) (struct file *, fl_owner_t id);

         int (*release) (struct inode *, struct file *);

         int (*fsync) (struct file *, int datasync);

         int (*aio_fsync) (struct kiocb *, int datasync);

    …………..

};

2、generic_file_aio_read调用do_generic_file_read,在do_generic_file_read/

do_generic_mapping_read中会先调用find_get_page查看是否被cache命中(即文件是否被缓冲),如果可以命中找到cache的page,则读取page头,更新page的up-to-date,调用指针actor(file_read_actor接口)把数据拷贝到用户空间;如果没有命中,即没有被缓冲,则需要创建新page(page_cache_alloc_cold会分配一个页面),并将页面添加到page_cache的lru表中(add_to_page_cache_lru),filp->f_mapping->a_ops->readpage进行传统意义上实际的读操作,从设备中读取一个页面大小的数据到这个新创建的页面缓存上,结构address_space_operations的全局变量是fat_aops,address_space_operations是address_space的操作函数,address_space 用于管理文件(inode)映射到内存的页面(struct page),address_space_operations用来操作该文件映射到内存的页面,比如把内存中的修改写回文件、从文件中读入数据到页面缓冲等。

读操作(fat_readpage)执行完后还会进入调用指针actor(file_read_actor接口)把数据拷贝到用户空间的流程,用户控件的buffer指针是从最上层的系统调用传下来的。

如果请求读取的数据长度已完成,则函数返回,否则跳转到步骤a重复执行。

static const struct address_space_operations fat_aops = {

         .readpage         = fat_readpage,

         .readpages       = fat_readpages,

         .writepage       = fat_writepage,

         .writepages      = fat_writepages,

         .write_begin    = fat_write_begin,

         .write_end       = fat_write_end,

         .direct_IO         = fat_direct_IO,

         .bmap                = _fat_bmap

};

3、fat_readpage最终调用mpage_bio_submit实现bio的访问请求,之前先调用do_mpage_readpage在一页范围内做从物理块到内存页面的映射。

4、mpage_bio_submit调用submit_bio(generic_make_request)实现块设备的I/O操作,参数bio中描述要操作的数据的关键属性,其中bi_io_vec描述内存buffer信息,bi_bdev和bi_sector描述设备信息,bi_end_io是I/0请求结束的处理接口。bdev_get_queue获取当前bio对应的工作队列q,q->make_request_fn触发请求(blk_init_allocated_queue中blk_queue_make_request会注册__make_request到q->make_request_fn上,和mmc_request的注册在同一个流程中),__make_request调用__blk_run_queue函数进而调用q->request_fn ,即mmc_request函数。在mmc_init_queue中blk_init_queue函数向下一直调用到blk_init_allocated_queue会将mmc_request挂到指针q->request_fn上。

bio是上层内存和下层块设备的纽带,包含了上下层的信息,代表了正在现场活动的以片段(segment)链表形式组织的块I/O操作,一个片段是一小块连续的内存缓冲区,每个块I/0请求都是通过一个bio结构来表示,每个请求都包含一个或多个块,这些块存储在bio_vec中,每个bio_vec都包含一个segment,每个segment都包含几个连续的buffer。核心接口submit_bio在后面有详细介绍。

5、mmc_request函数判断当前队列上是否有queuedata,如果没有则到队列上获取其他请求,如果有其他请求则置标志位quiet结束所有请求并退出(因为这些请求不是我们想要的),如果有queuedata,则直接唤醒mmc的队列任务mmc_queue_thread,请求队列是I/O调度程序管理块设备的媒介。

6、mmc_queue_thread线程在死循环中blk_fetch_request获取请求,如果有当前请求或者有之前的请求则调用mq->issue_fn(mmc_blk_issue_rq)处理请求;如果没有则判断当前线程是否应该被stop,如果是则置当前任务为running并退出线程,否则启动background管理,获取信号量启动调度器调度。

7、mmc_blk_issue_rq/ mmc_blk_issue_rw_rq中调用mmc_blk_rw_rq_prep获取要操作的sectors个数等,调用mmc_start_req发起mmc的请求。host->ops->pre_req做准备工作,__mmc_start_req->mmc_start_request完成触发I/O设备的请求host->ops->request(msmsdcc_

request),mmc_post_req实现post完整的请求。mmc_pre_req、__mmc_start_req和mmc_post_req是完成mmc请求的三个步骤。

8、请求完成后调用rq_data_dir区分读写操作,如果是读的话mmc_queue_bounce_post拷贝读到的数据到mqrq->bounce_buf,如果成功则置位md->reset_done。

 

msmsdcc设备关键接口实现

msmsdcc的关键接口全局变量定义如下:

static const struct mmc_host_ops msmsdcc_ops = {

         .enable     = msmsdcc_enable,

         .disable    = msmsdcc_disable,

         .pre_req  = msmsdcc_pre_req,

         .post_req = msmsdcc_post_req,

         .request   = msmsdcc_request,

         .set_ios    = msmsdcc_set_ios,

         .get_ro              = msmsdcc_get_ro,

         .enable_sdio_irq = msmsdcc_enable_sdio_irq,

         .start_signal_voltage_switch = msmsdcc_switch_io_voltage,

         .execute_tuning = msmsdcc_execute_tuning

};

 

1、msmsdcc_request

在上述流程中,msmsdcc_request是核心操作,如果host->plat->is_sdio_al_client置位的话则使sdio的AL变成低电管理状态,如果host->eject置位则调用mmc_request_done结束mmc请求流程,释放资源;如果sdcc下面三个状态 pwr、clks_on、sdcc_irq_disabled有一个不满足则调用mmc_request_done结束mmc请求流程,释放资源;设置sdcc控制器的请求超时时间为10秒,置位host->curr.req_tout_ms并调用mod_timer修改超时寄存器;将传参传进来的request结构体指针赋给host->curr.mrq,如果是写数据操作(结合mrq->data是否为1和mrq->data->flags是否为MMC_DATA_WRITE判断),置标志位;最后调用msmsdcc_request_start完成请求。

在msmsdcc_request_start函数中如果不是数据操作则直接调用接口msmsdcc_start_command,否则判断是否是读操作或者写一个block(MMC_WRITE_BLOCK)、写多个block(MMC_WRITE_MULTIPLE_BLOCK)中的一种,如果是的话则调用msmsdcc_start_data接口,否则调用msmsdcc_start_command。

 

2、msmsdcc_pre_req

该接口用来完成sdcc控制器访问emmc设备前的准备工作,调用接口msmsdcc_is_dma_possible判断当前设备使用DMA模式传输数据,根据is_dma_mode是否被置位以及传输的数据大小是否是64字节的倍数决定(实际每次都返回DMA模式),调用msmsdcc_prep_xfer准备数据传输,置data->host_cookie标志为1。

msmsdcc_prep_xfer中根据data->flags是否是MMC_DATA_READ给dir方向赋值,DMA_FROM_DEVICE或者DMA_TO_DEVICE,dma_map_sg配置sg的buffer做DMA传输的准备。for_each_sg查找scatterlist中的每个元素s,sg_page根据输入的s返回当前s对应的页号地址page,__dma_map_page将s对应的页地址转换成对应的DMA地址赋给本list的s->dma_address。具体操作如下,

1)、__dma_page_cpu_to_dev将page地址信息从cpu映射到设备,并处理L2cache。调用dma_cache_maint_page将要操作到的内存放到DMA的cache上(通过注册的cpu_cache.dma_map_area接口),在映射前先通过PageHighMem判断输入的页地址page是否是高端内存,如果是则通过kmap_high_get获取到highmem上的地址,否则直接计算offset之后的地址,如果数据是从设备到CPU为提高DMA读取数据的速度调用outer_inv_range使L2 cache无效,函数指针上注册的是feroceon_l2_inv_range/l2x0_inv_range/tauros2_inv

_range/xsc3_l2_inv_range中的一个,反之如果从CPU到设备则调用outer_clean_range清空并同步外部L2 cache上指定的地址范围,操作cache前通过page_to_phys根据page页地址得到实际的物理地址。

2)、pfn_to_dma(DMA映射内部专用API) 提供DMA地址,调用__pfn_to_bus计算页号,其中page_to_pfn中将page地址减去mem_map再加上ARCH_PFN_OFFSET得到对应的虚拟地址。

 

3、msmsdcc_post_req

post接口是prepare接口的逆过程,调用__dma_page_dev_to_cpu,如果不是到设备的DMA操作,则禁止L2cache;调用dma_cache_maint_page释放刚才加入到DMA中的内存页资源,并置当前page的标志page->flags为PG_dcache_clean(该标识表示已经被pte清除过了,在返回给用户之前不用再clean)。

有了prepare和post,详细介绍msmsdcc_start_data和msmsdcc_start_command如何完成传送数据和命令。

msmsdcc_start_data先调用msmsdcc_config_dma完成DMA的配置,如果在msmsdcc_pre_req 没有被成功调用过(data->host_cookie没有置1)则再调用一次msmsdcc_prep_xfer,给控制位datactrl先后置上MCI_DPSM_ENABLE、MCI_DPSM_DMAENABLE及读写标识,设置超时clk(host->cmd_timeout)、控制字段(host->cmd_datactrl)、命令字段(cmd_cmd)、DMA执行接口(host->dma.hdr.exec_func赋上msmsdcc_dma_exec_func接口)、busy标识(host->dma.busy) ,如果use了sdcc上命令字段,则调用接口msmsdcc_start_command_deferred,给cmd寄存器赋值并清除DLL配置寄存器的CDR位,最后给MCI_INT_MASK寄存器赋值(给cpu send实际的中断), mb保证cpu顺序操作指令。

 

MCI有几个关键寄存器data和ctrlcmd,在有多块(multi block)数据传输时cmd用来给sdio的状态机传信号;在操作时datactrl用来当cpu (receive)到正常RESPONSE后自动start DPSM。

DPSM数据通路状态机,datactrl控制位的各bit含义:

#define MCI_DPSM_ENABLE            (1 << 0)

#define MCI_DPSM_DIRECTION     (1 << 1)

#define MCI_DPSM_MODE              (1 << 2)

#define MCI_DPSM_DMAENABLE  (1 << 3)

#define MCI_DATA_PEND                 (1 << 17)

#define MCI_AUTO_PROG_DONE  (1 << 19)    spec上没有,应该是扩展位

#define MCI_RX_DATA_PEND (1 << 20)        spec上没有,应该是扩展位

 

CPSM命令通路状态机,CMD各bit含义:

#define MCI_CPSM_RESPONSE        (1 << 6)

#define MCI_CPSM_LONGRSP         (1 << 7)

#define MCI_CPSM_INTERRUPT     (1 << 8)

#define MCI_CPSM_PENDING         (1 << 9)

#define MCI_CPSM_ENABLE            (1 << 10)

#define MCI_CPSM_PROGENA        (1 << 11)

#define MCI_CSPM_DATCMD          (1 << 12)

#define MCI_CSPM_MCIABORT      (1 << 13)

#define MCI_CSPM_CCSENABLE      (1 << 14)

#define MCI_CSPM_CCSDISABLE     (1 << 15)

#define MCI_CSPM_AUTO_CMD19         (1 << 16)   spec上没有,应该是扩展位

Sdcc有两个时钟域,分别是MCLK和HCLK,CPSM和DPSM被MCLK管理,而FIFO/DMA控制器被HCLK管理。 

 

写设备流程简介

1、概述

写设备的调用关系如下,write->vfs_write->do_sync_write->generic_file_aio_write->

__generic_file_aio_write,先检查要写的数据以及写入文件的位置是否正确,然后再删除suid,更新文件的时间,标志位为O_DIRECT时,表示立即写入调用函数generic_file_direct_write将数据直接写入设备。如果直接写入数据出错或者没有将数据完全写入则调用函数generic_file_buffered_write将数据(剩余数据)写入缓存,然后调用函数filemap_write_and_wait_range将缓存中的数据写入设备。如果不是直接写入设备则用generic_file_buffered_write把数据写入缓存并提交写数据请求。

 

2、直接写入设备generic_file_direct_write

generic_file_direct_write中调用filemap_write_and_wait_range将数据写回设备,调用关系如下:

__filemap_fdatawrite_range->do_writepages->a_ops->writepage,如果函数指针a_ops->writepage有定义(对于fat文件系统是fat_writepages)则调用系统注册的接口,否则调用通用的写文件接口generic_writepages完成操作。__filemap_fdatawrite_range用mapping_cap_writeback_dirty给对应的写入数据置上写回信息标志,之后再调用do_writepages。

fat_writepages/mpage_writepages中先获取fat文件系统下的block,如果获取的大小是0,仍然使用通用的写文件接口generic_writepages来完成操作。否则用write_cache_pages浏览给定地址空间内的所有脏页list,把对应数据全部写回。write_cache_pages是数据写到cache上,通过mpage_bio_submit接口完成实际的写设备操作。

write_cache_pages通过pagevec_lookup_tag获取实际要操作的page数,用for循环遍历每一个脏页,通过PageDirty、PageWriteback判断是否脏页、是否需要写回等信息,最后调用注册到writepage上的__mpage_writepage接口。遍历完之后释放page向量的资源pagevec_release。

__mpage_writepage函数是写文件的核心接口。代码大致流程如下:如果page有buffer_head,则完成磁盘映射,代码只支持所有page都被设为脏页的写,除非没有设为脏页的page放到文件的尾部,即要求page设置脏页的连续性。如果page没有buffer_head,在接口中所有page被设为脏页。如果所有的block都是连续的则直接进入bio请求流程,否则重新回到writepage的映射流程。

用page_has_buffers判断当前page是否有buffer_head(bh),如果有则用page_buffers将当前page转换为buffer_head的bh指针,之后用bh->b_this_page遍历当前page的所有bh,即使出现一个bh没有被映射都会进入confused流程,first_unmapped记录了第一个没有映射的bh,除了要保证所有的bh都被映射,还要保证所有的bh都被置为脏页并且完成了uptodate。如果每个page的block数不为0(通过判断first_unmapped是否非0),则直接进入当前page已经被映射的流程page_is_mapped,否则进入confused流程。

如果当前page没有buffer_head(bh),需要将当前page映射到磁盘上,使用buffer_head变量map_bh封装,做buffer_head和bio之间的转换。

page_is_mapped流程中如果有bio资源并且检测到当前的页面和前面一个页面的磁盘块号不连续(代码对应bio && mpd->last_block_in_bio != blocks[0] – 1,blocks[0]表示第一个磁盘块),则用mpage_bio_submit来提交一个积累bio请求,将之前的连续block写到设备中。否则进入alloc_new流程。

alloc_new流程中,判断bio为空(表示前面刚刚提交了一个bio)则需要用mpage_alloc重新申请一个bio资源,之后用bio_add_page向bio中添加当前page,如果bio中的长度不能容纳下这次添加page的整个长度,则先将添加到bio上的数据提交bio请求mpage_bio_submit,剩下的数据重新进入到alloc_new流程做bio的申请操作。如果一次性将page中的所有数据全部添加到bio上,在page有buffer的情况下要将所有的buffer全部清除脏页位。用set_page_writeback设置该page为写回状态,给page解锁(unlock_page)。当bh的boundary被设置或者当前页面和前面一个页面的磁盘块号不连续,就先提交一个累积连续block的bio。否则说明当前page中的所有block都是连续的,并且与之前的page中block也是连续的,这种情况下不需要提交bio,只更新前面一个页面的磁盘块号mpd->last_block_in_bio为当前page的最后一个block号,之后退出进行下一个page的连续性检查,直到碰到不连续的再做bio提交。

confused流程中会提交bio操作,但是会设置映射错误。

 

内核机制相关

mpage_writepages中在整个写文件流程的前后会使用blk_start_plug/blk_finish_plug给块设备加上/去掉塞子,start_plug中将should_sort置0,给当前任务的plug赋值,作用是当make_request(bio请求)时不会马上运行请求队列,而是加到plug队列里面统一处理。

write_cache_pages中写完所有的page之后会调用cond_resched,这个函数具有主动被调度的作用。为了及时响应实时过程,需要中断线程化,而在中断线程化的过程中,需要调用cond_resched 这个函数,主动放弃cpu供优先级更高的任务使用。

 

submit_bio

提交bio请求的接口submit_bio,一次bio的提交要求提交的页面都是连续的数据。对所有的页面循环,最大限度最高效的进行mpage方式的io,就是最大限度的寻找连续块,然后最大限度的将多个页面合并在一个bio中,最后最大量的提交bio,因为mpage机制保证所有按照页面顺序加入bio的page中的block都是连续的,这样的话就可以使磁盘工作更加有效。

先查看io上是否有数据,然后判断读写,count_vm_events给读增加PGPGIN项并且更新当前任务的ioac.read_bytes数值,给写增加PGPGOUT项,最后调用发起请求generic_make_request。

generic_make_request中如果判断当前任务的bio_list不为空,则说明bio已经激活(即generic_make_request函数正在while循环中处理bio请求,还没有处理完,如果处理完current->bio_list会被清空bio不会处于激活的状态),直接bio_list_add将本次bio添加到bio_list中返回,否则初始化bio_list并把指针付给当前任务的bio_list调用__generic_make_request完成请求,之后用bio_list_pop找到下一个bio,继续__generic_make_request,直到所有的bio请求结束。

__generic_make_request中用bio_check_eod校验操作的bio是否已经超过了设备的结尾,如果超过了则作为坏块处理,否则通过bdev_get_queue获取bio对应的队列,如果设备有分区表则调用blk_partition_remap将分区表a的block B映射到从disk开始的第C个block。

用blk_throtl_bio限制上层发送到io层的速度,

q->make_request_fn调用函数指针,在初始化(blk_init_allocated_queue)的时候blk_queue_make_request注册__make_request接口.

__make_request先调用blk_queue_bounce弹出块队列中的bio资源,如果是forced unit access或者flush的请求直接进入获取请求(get_rq)的操作,否则用attempt_plug_merge将bio资源加入到当前任务中,也进入获取bio请求的流程

get_rq流程中用get_request_wait获取一个空闲的request请求结构,用init_request_from_bio初始化bio的请求,如果当前有plug,用list_add_tail将当前请求添加在plug list的尾部,drive_stat_acct完成rcu的映射,如果没有plug,关中断,用add_acct_request添加acct请求,用__blk_run_queue运行块队列,开中断。

 

__make_request中的内核机制

1、  spin_lock_irq和spin_unlock_irq,自旋锁

2、  io_schedule的机制,

3、  elevator的机制

4、  put_cpu、get_cpu的机制

get_cpu() 和 put_cpu() 的定义如下:

#define get_cpu()        ({ preempt_disable(); smp_processor_id(); })
#define put_cpu()        preempt_enable()
所以,在需要禁止任务抢占的区域,要得到cpu id 的话,可以使用 get_cpu()/put_cpu() 函数对, 否则直接使用 smp_processor_id() 即可。

5、  plug的机制,应该是start plig后,make request时不会立即运行队列,而是把请求加到plug的list中, blk_start_plug和blk_finish_plug成对使用,blk_finish_plug会处理之前添加到plug list上的所有请求;

看当前代码,都应用在bio/IO请求的场景下,submit_bio、generic_make_request、io_submit_one、write_dirty_buffer、journal_submit_data_buffers

part_stat_lock和part_stat_unlock的机制

 

3、将数据写入缓冲区

如果不是立即写入则调用generic_file_buffered_write将数据写入缓存,它调用generic_perform_write继而调用address_space->a_ops->write_begin和address_space->a_ops

->write_end,由于写操作时间相对过长,write_end之后会用cond_resched释放CPU,使得其他任务可以被调度执行。再度切回来之后,判断当前i变量上的数据是否全部写完,如果未写完则继续用write_begin和write_end执行写操作的循环。直至所有要写的数据都写完,最后用balance_dirty_pages_ratelimited来检测当前系统内脏页的数量是否超过了最大比率,那些page新被置为脏页,如果超过比率值mapping->backing_dev_info->dirty_exceeded会置位,则降低比率vm_dirty_ratio,在balance_dirty_pages置写回初始化标志位,如果置位失败,则用bdi_start_background_writeback唤醒flush线程完成实际的写回操作。该接口不应该被周期性调用,只是在写操作执行之后check一下当时状态。

generic_file_buffered_write执行完之后,会逐层返回直至系统调用结束。但此时要写的数据,只是拷贝到内核缓冲区,并将相应的页面标记为脏,并未真正写到磁盘上。
在下面条件下pdflush线程把脏页写回磁盘:

-page-cache太满或脏页数量非常大;
-脏页停留在内存中的时间过长;
-某个进程要求更改的块设备或文件数据被刷新,通常通过调用sync,fsync,fdatasync来实现。

pdflush内核线程有两个作用:
1、系统地扫描page-cache,以找到要刷新的脏页;
2、保证所有的页不会长期处于dirty状态。

系统中pdflush线程数量是动态变化的,太少时就创建新的,太多时就杀死部分。在系统空闲内存低于一个特定的阈值时,pdflush将脏页刷新回磁盘。flush线程在bdi的初始化中线程bdi_forker_thread中创建,接口是bdi_writeback_thread,会定时地完成写回设备的任务,如果没有脏页并且队列中也没有要做的事情,则直接调用reshedule,否则用schedule_timeout起一个5秒的超时timer。

 

balance_dirty_pages_ratelimited_nr涉及到的内核机制

1、  preempt_disable和preempt_enable

是禁止内核抢占和使能内核抢占的意思,即禁止和使能任务切换,锁任务。

2、  __get_cpu_var(bdp_ratelimits)

用来获取per_CPU变量,每个CPU都有一个副本,per-cpu 变量的引入有效的解决了SMP系统中处理器对锁得竞争,每个cpu只需访问自己的本地变量。tasklet、timer_list等机制都使用了per-CPU技术。

以tasklet为例,tasklet是I/O驱动程序中可延迟执行的内核函数

Tasklet中用per_cpu接口将每个cpu的tasklet队列链表保存在变量tasklet_vec中,在 (start_kernel->softirq_init)中创建list,同时用open_softirq注册了软中断tasklet_action和tasklet_hi_action。

软中断的实际调用接口为__do_softirq(软中断服务程序)。

Tasklet的使用机制:

1)、应用在初始化函数中用tasklet_init构建一个tasklet_struct结构的信息;

2)、在应用自己的中断服务函数或者回调函数中用tasklet_schedule将之前构建的信息添加到tasklet_vec链表的头部,同时用wakeup_softirqd唤醒(wake_up_process)后台线程ksoftirqd。

3)、线程ksoftirqd调用__do_softirq进而执行在中断向量表softirq_vec里中断号TASKLET_SOFTIRQ对应的tasklet_action函数,然后tasklet_action遍历 tasklet_vec链表,调用每个tasklet的函数完成软中断操作,包括应用刚刚添加到tasklet_vec链表节点;

4)、显然tasklet_vec链表中的节点不能太多,否则会长时间占用CPU,利用软中断实现的timer的运行周期会更加不准。

5)、除了软中断中会调用__do_softirq之外,在硬中断irq_enter和irq_exit中也会根据条件调用,当没有处于软中断正在执行的过程并且没有处于中断嵌套中,就会用__do_softirq、__local_bh_enable或者invoke_softirq->__do_softirq,执行软中断中的所有action,从而调用tasklet_action函数完成tasklet钩子的延时功能。

6)、显示调用local_bh_enable和local_bh_enable_ip也会触发do_softirq接口的调用。

 

3、软中断简介

上面同时简略地介绍了软中断的机制,用open_softirq注册,用__do_softirq完成注册钩子的调用,用全局变量irq_stat保存当前cpu对应的各软中断元素,只用当irq_stat[cpu].member的对应bit值为pengding时(值为1)才调用对应软中断的action服务函数,当前一共有9种软中断,详细列表在下面给出。通过wakeup_softirqd或者直接用wake_up_process(per_cpu(ksoftirqd, hotcpu))唤醒线程ksoftirqd工作,每个CPU对应一个线程ksoftirqd,还有就是在硬中断irq进入和退出的时候满足条件的情况下用__do_softirq、__local_bh_enable或invoke_softirq完成软中断的调用,第三种触发场景就是显示调用local_bh_enable和local_bh_enable_ip的调用。

内核定义了9种软中断

enum

{

         HI_SOFTIRQ=0,      

         TIMER_SOFTIRQ,     

         NET_TX_SOFTIRQ,    

         NET_RX_SOFTIRQ,    

         BLOCK_SOFTIRQ,      

         BLOCK_IOPOLL_SOFTIRQ, 

         TASKLET_SOFTIRQ,       

         SCHED_SOFTIRQ,        

         HRTIMER_SOFTIRQ,      

         RCU_SOFTIRQ,   

         NR_SOFTIRQS

};

4、软中断与Tasklet差异

软中断的分配是静态的(编译时定义),而tasklet的分配和初始化可以在运行时进行(比如安装一个内核模块)。

软中断(即使是同一类型的软中断)可以并发地运行在多个CPU上。因此,软中断是可重入函数,而且必须明确地使用自旋锁保护其数据结构。tasklet不必担心这些问题,因为内核对tasklet的执行进行了更加严格的控制。相同类型的tasklet总是被串行地执行,换句话说:不能在两个CPU上同时运行相同类型的tasklet。但是,类型不同的tasklet可以在几个CPU上并发进行。tasklet的串行化使得tasklet函数不必是可重入的,因此简化了设备驱动程序开发的工作。

5、preempt_count 字段说明

判断是否处于中断嵌套的接口in_interrupt,最终通过preempt_count接口完成,下面对preempt_count 字段做详细说明。

字段共32bit,用来跟踪内核抢占和内核控制路径的嵌套。其放在每个进程描述符thread_info字段中。preempt_count各字段含义如下:

bit7~0 抢占计数器(preempt)     PREEMPT_MASK

bit15~8 软中断计数器 softirq    SOFTIRQ_MASK

bit25~16 硬中断计数器 HardIrq  HARDIRQ_MASK

bit26    NMI中断标识位       NMI_MASK

preempt_count相关操作函数:

irq_enter-> __irq_enter调用add_preempt_count(HARDIRQ_OFFSET), preempt_count变量的HARDIRQ部分+1,即标识一个hardirq的上下文,所以可以认为do_IRQ()调用irq_enter函数意味着中断处理进入hardirq阶段。

sub_preempt_count(IRQ_EXIT_OFFSET); sub_preempt_count(val)  preempt_count() -= (val);

IRQ_EXIT_OFFSET    # define IRQ_EXIT_OFFSET (HARDIRQ_OFFSET-1)

    in_interrupt()  检查硬中断计数器和软中断计数器,只要这两个计数器中一个为正值,该宏产生一个非零值。

#define in_interrupt() ({ const int __cpu = smp_processor_id(); /

(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })

in_interrupt()函数的叙述很明确,主要用意是根据当前preempt_count变量,来判断当前代码是否在一个中断上下文中执行。

HARDIRQ、SOFTIRQ以及NMI 都属于interrupt范畴。

 

balance_dirty_pages涉及到的内核机制

1、__set_current_state(TASK_UNINTERRUPTIBLE);

__set_current_state是在设置CPU的状态,TASK_UNINTERRUPTIBLE简写为D,意指不可中断的睡眠状态,这种情况下一般任务都是在等待外部硬件的IO,此时的不响应中断指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。也不会发生任务抢占。

2、io_schedule_timeout(pause);

设置一个超时的io调度,通过定时器来实现定时,到时间后会重新唤醒该任务(将任务放到running队列上)。

 

bdi_start_background_writeback涉及到的内核机制

1、spin_lock_bh(&bdi->wb_lock)和spin_unlock_bh(&bdi->wb_lock)

buffer_head的自旋锁,自旋锁不会引起调用者睡眠(即将任务踢出running队列),自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用,可以用到中断中,适用于等待非常小的时间段,自旋锁生效期间是禁止任务发生抢占的。

如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问最好使用spin_lock_bh和spin_unlock_bh来保护,他们的执行时间快。

2、bdi_wakeup_flusher(bdi)和wake_up_process

bdi_wakeup_flusher用来唤醒pdflush线程更新脏页,wake_up_process会将指定的任务放到内核的running队列中。

0

  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有