Linux Kernel: IO
Table of Contents
1. Linux Kernel: IO
1.1. Accessing Files
1.1.1. Reading File
读文件的最上层入口是 f_op->read, Linux 提供了一个 generic 的实现叫做 generic_file_read, 如果你想利用 IO architectures 中的其它部分, 例如 Page Cache, Generic Block Layer, IO Scheduler 等的功能, 必须指定 generic_file_read 做为 f_op->read 的实现.
1.1.1.1. VFS
generic_file_read (struct file *filp, char __user *buf, size_t count, loff_t *ppos): // iovec 记录了本次读操作要填充的 buffer 和大小 struct iovec local_iov = { .iov_base = buf, .iov_len = count }; __generic_file_aio_read(&kiocb, &local_iov, 1, ppos); read_descriptor_t desc; desc.arg.buf = iov[seg].iov_base; desc.count = iov[seg].iov_len; do_generic_file_read(filp,ppos,&desc,file_read_actor); do_generic_mapping_read(filp->f_mapping,&filp->f_ra,filp,ppos,desc,actor);
1.1.1.2. Page Cache
do_generic_mapping_read(filp->f_mapping,&filp->f_ra,filp,ppos,desc,actor); // ppos 是要读取的 offset (字节), index 是这个 offset 对应的 page cache 中的 // page index. page index 与 offset 是线性的映射关系 index = *ppos >> PAGE_CACHE_SHIFT; // mapping 是 inode->address_space // !! 查找 page cache !! page = find_get_page(mapping, index); return radix_tree_lookup(&mapping->page_tree, offset); // !! page cache miss !! if (unlikely(page == NULL)): goto no_cached_page; else: // !! page cache hit !! goto page_ok; no_cached_page: // page cache hit, 分配一个新的 page, 加到 page cache 中, 并 readpage cached_page = page_cache_alloc_cold(mapping); // 新 page 与 mapping 的 index 关联, 加到 page cache 中 add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL); // lock page, readpage 是个异步的调用 (dma), do_generic_mapping_read 需要通过 lock_page 阻塞住 SetPageLocked(page); goto read_page; read_page: // NEXT STAGE // The read will unlock the page mapping->a_ops->readpage(filp, page); // 因为 readpage 是异步的, readpage 返回时 page 一般是 !update // 这里的 lock_page 会使函数阻塞住, 等待底层读操作的结束 if (!PageUptodate(page)): lock_page(page); // TASK_UNINTERRUPTIBLE, 所谓的 D 状态 __wait_on_bit_lock(page_waitqueue(page), &wait, sync_page, TASK_UNINTERRUPTIBLE); unlock_page(page); goto page_ok; page_ok: // actor 实际是 file_read_actor, 负责将 page 的数据复制到 desc, 即 // user space 的 buffer 中 ret = actor(desc, page, offset, nr);
1.1.1.3. File System
a_ops->readpage 是和具体文件系统打交道的函数, 它的作用是真正从设备读取一个 page.
但与 generic_file_read 类似, VFS 也提供了一个通用的函数, 比如 mpage_readpage 或 block_read_full_page.
有了这些函数, 具体文件系统不再需要考虑如何和下一层的 Generic Block Layer 打交道, 它们只需要实现几个回调即可, 比如 get_block
以 ext2 为例, 它定义的 readpage 为:
static int ext2_readpage(struct file *file, struct page *page) { return mpage_readpage(page, ext2_get_block); }
其中的 ext2_get_block 是 get_block 回调, 这个函数是和具体文件系统相关的, 它的作用是将 readpage 传过来的 block_in_file 映射为 logical block (sector_t)
1.1.1.4. Generic Block Layer
1.1.1.4.1. mpage_readpage
// mpage_readpage 并不需要 file 指针, 因为 page 中已经包含了足够的信息: // 1. page->index, 表示 page 在 address_space 的 index (或对应的文件的 offset) // 2. page->mapping->host, 表示 page 对应的 inode // inode 和 index 可以标识本次要读取的内容 mpage_readpage(struct page *page, get_block_t get_block): bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio, get_block); struct inode *inode = page->mapping->host; // PAGE_CACHE_SHIFT 为 12 (4KB), blkbits 一般为 9 (512B) // 所以 block_in_file = page_index * 8 block_in_file = page->index << (PAGE_CACHE_SHIFT - blkbits); // blocks_per_page = 4K >> 9 = 8, 表示一个 page 容纳 8 个 block (sector) blocks_per_page = PAGE_CACHE_SIZE >> blkbits; for (page_block = 0; page_block < blocks_per_page; page_block++, block_in_file++): // 对这个 page 中的每个 block // 调用文件系统相关的 get_block, 以便得到 block_in_file 与真正的 sector_t 的对应 // 关系, 真正的 sector_t 保存在 bh->b_blocknr 中 // 这里的 block_in_file 只是相当于文件的偏移量, 真正的 logical block (sector_t) // 只有文件系统才知道 get_block(inode, block_in_file, &bh, 0) // blocks 数组保存着循环过程中对应各个 block_in_file 的 sector_t 的值 // 如果这个数组中的数据不是连续递增的 (例如 23456789 是连续递增的), 表示 // 这个 page 的数据在物理设备上不是连续的, 调用 block_read_full_page if (page_block && blocks[page_block-1] != bh.b_blocknr-1): block_read_full_page(page, get_block); return // 物理设备上连续, 仅构造一个 BIO bio = mpage_alloc(bdev, blocks[0],nr_pages,GFP_KERNEL); bio_add_page(bio, page, length, 0) mpage_bio_submit(READ, bio);
1.1.1.4.2. block_read_full_page
block_read_full_page 与 mpage_readpage 类似, 也是构造相应的 BIO 并提交, 但它并不会像 mpage_readpage 一样尝试用一个 BIO 表示连续的多个 block: block_read_full_page 对一个 page 会构造 8 个 buffer_head, 然后对每个 buffer_header 构造一个 BIO, 同时 page 会被转换为一个 buffer page.
实际上, 具体文件系统中大部分读取 block 的操作 (例如 __bread) 都是采用类似的方法: 读取单个 block 时构造 buffer_head, submit BIO 并使用一个 buffer page 来 cache 这个 buffer_head.
// block_read_full_page 与 mpage_readpage 类似, 但省去了合并 bio 的过程 block_read_full_page(struct page *page, get_block_t *get_block): struct inode *inode = page->mapping->host; // block_read_full_page 结束后, page 必然会是一个 buffer page if (!page_has_buffers(page)) // 生成 8 个 buffer_head, 并将 page 转换为 buffer page create_empty_buffers(page, blocksize, 0); head = page_buffers(page); iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits); for_each (iblock, bh): get_block(inode, iblock, bh, 0) arr[nr++] = bh; for_each bh in arr: submit_bh(READ, bh);
1.1.1.4.3. submit_bio
无论是 block_read_full_page 的 submit_bh 或 mpage_read 的 mpage_bio_submit, 最终都会调用到 submit_bio:
- 对于 submit_bh, 它会将 bio->bi_end_io 设置为 end_bio_bh_io_sync, 后者会调用 end_buffer_async_read
- 对于 mpage_bio_submit, 它会将 bio->bi_end_io 设置为 mpage_end_io_read
bio->bi_end_io 都会在底层 IO 结束时被调用, 并且都会 unlock_page 从而唤醒上层的 do_generic_mapping_read
1.1.2. Reading Block Device File
读取块设备文件时与普通文件有些差别:
- get_block 回调使用的是 blkdev_get_block
- readpage 使用的是 block_read_full_page 而不是 mpage_readpage
直接读取块设备文件时, 并不需要具体文件系统提供的 Mapping Layer (get_block), 因为它是和具体文件系统无关的, 实际上, blkdev_get_block 只是一个 noop 函数, 它将 block index 直接映射为 sector_t
blkdev_get_block(struct inode *inode, sector_t iblock, struct buffer_head *bh): bh->b_blocknr = iblock;
另外, readpage 使用 block_read_full_page (而不是 mpage_readpage), 可能原因是:
具体文件系统通常需要直接读取设备的 block, 例如 super_block, inode_block 等, 这些操作一般都是通过 __bread (__getblk) 完成, 后者会使用 buffer page 来 cache 这些 block 对应的 buffer_head. 而且这些 buffer page 是挂在块设备文件对应的 inode 的 address_space 下的.
上层 readpage 使用 block_read_full_page, 可以与底层的 __bread 互通, 在两者看来, 它们操作的是同一个 inode, 看到的是相同的 buffer page 及 buffer_head
1.1.3. Writing File
1.1.3.1. VFS
generic_file_write(struct file *file, const char __user *buf,size_t count, loff_t *ppos): __generic_file_write_nolock(file, &local_iov, 1, ppos); __generic_file_aio_write_nolock(&kiocb, iov, nr_segs, ppos); generic_file_buffered_write(iocb, iov, nr_segs, pos, ppos, count, written);
1.1.3.2. Page Cache
generic_file_buffered_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos, loff_t *ppos, size_t count, ssize_t written): struct file *file = iocb->ki_filp; struct address_space * mapping = file->f_mapping; struct address_space_operations *a_ops = mapping->a_ops; struct inode *inode = mapping->host; index = pos >> PAGE_CACHE_SHIFT; // 查找 page cache, 若不存在, 则新建一个 page page = __grab_cache_page(mapping,index,&cached_page,&lru_pvec); page = find_lock_page(mapping, index); if (!page): *cached_page = page_cache_alloc(mapping); add_to_page_cache(*cached_page, mapping, index, GFP_KERNEL); return *cached_page // prepare_write 与具体文件系统相关, 这个函数返回后, 可以保证 // page 中已经有相应的 buffer_head, 并且 page 中已经包含原始的数据 // (write 操作需要在原始数据的基础上进行覆盖) a_ops->prepare_write(file, page, offset, offset+bytes); // 数据被写到 page filemap_copy_from_user(page, offset, buf, bytes); // 根据 offset 和 size 确定 page 中各个 buffer_head 的状态只有被修改 // 到的 buffer_head 才会改为 dirty, 后续 sync 时只需要 sync 这一个 // buffer_head a_ops->commit_write(file, page, offset, offset+bytes); // balance dirty page (vm_dirty_ratio) balance_dirty_pages_ratelimited() // 除非指定了 O_SYNC 或 O_DIRECT, 否则 write 操作在 commit_write 就结束了, // 真正的 IO 操作发生在 sync 时
1.1.3.3. File System
与 read 时的 readpage 类似, prepare_write 和 commit_write 是和文件系统相关的, 但有 VFS 有相应的通用实现, 具体文件系统只需要提供一个 get_block 回调.
1.1.3.4. Generic Block Layer
1.1.3.4.1. prepare_write
prepare_write 的主要功能是: 确保 page 是一个 buffer page, 并且保证 page 中已经有对应的原始数据, 所以它的代码与 block_read_full_page 有些类似.
int block_prepare_write(struct page *page, unsigned from, unsigned to, get_block_t *get_block): struct inode *inode = page->mapping->host; __block_prepare_write(inode, page, from, to, get_block);
1.1.3.4.2. commit_write
commit_write 主要功能是修改 buffer_head 和 page 本身的状态, 以便随后 writepage 时能找到哪些 buffer_head 是 dirty 的.
static int __block_commit_write(struct inode *inode, struct page *page, unsigned from, unsigned to): for_each bh in page_buffers(page): unless (block_end <= from || block_start >= to): set_buffer_uptodate(bh); mark_buffer_dirty(bh);
1.1.4. Writing Dirty Pages
1.1.5. Read ahead
每次调用 do_generic_mapping_read 时, 在查找 page cache 之前, 会通过 page_cache_readahead 尝试进行 read ahead (ra), 具体 page_cache_readahead 的行为(是否进行 ra, ra 哪些 page 等) 由以下几方面确定:
- 通过 (ra->start, ra->size) 维护一个 current window, 通过 (ra->ahead_start, ra->ahead_size) 维护一个 ahead window, 两个 window 是前后连续的
- 若 read 是对文件开头的读操作, 则通过 get_init_ra_size 计算初始 ra->size, 其大小与 read 的 size 正相关, 大小从 16 K 到 128K 不等, 并对 current window 进行 ra
- 若本次 read 要读取的 page_index = ra->prev_page + 1, 表示连续的读操作, 则通过 get_next_ra_size 根据 ra->size 对 ra->ahead_size 进行倍增 (最大 128K), 增大 ahead window, 并对 ahead window 进行 ra
- 若本次 read 的 page_index 与 ra->prev_page 不连续, 则重置 ra (例如 ra->size 置 -1), 但 ra 并不禁用.
- 若 get_next_ra_size 时发现之前对该文件的读取出现过 page cache miss (handle_ra_miss), 表示内存可以比较紧张, 则上一步的 get_next_ra_size 会减少 ahead window 的大小 (最小 16K) 而不是倍增
- 若当前读操作已经超越 current window, 则重置 ahead window 为 current window, 并通过 get_next_ra_size 分配一个新的(更大的) ahead window
- 若连续对 256 个 page 的 ra 都因为 page cache hit 没有执行, 则禁用 ra
1.1.5.1. page_cache_readahead
1.1.5.2. handle_ra_miss
1.1.5.3. utils
- readahead
- posix_fadvise
- madvise
- blockdev
1.1.6. Memory Mapping
1.1.6.1. vm_area_struct
struct vm_area_struct { // vma 中和 file map 相关的成员 unsigned long vm_start; struct rb_node vm_rb; struct vm_operations_struct * vm_ops; unsigned long vm_pgoff; struct file * vm_file; // ... }
1.1.6.2. filemap_nopage
// 实际代码中 handle_pte_fault 对于 file mapping 的调用路径是 // do_file_page -> filemap_getpage 而不是 do_no_page -> filemap_nopage // 但两者代码基本类似 struct page * filemap_nopage(struct vm_area_struct * area, address, int *type): struct file *file = area->vm_file; struct address_space *mapping = file->f_mapping; struct file_ra_state *ra = &file->f_ra; struct inode *inode = mapping->host; pgoff = ((address - area->vm_start) >> PAGE_CACHE_SHIFT) + area->vm_pgoff; // madvise (MADV_SEQUENTIAL) if (VM_SequentialReadHint(area)): page_cache_readahead(mapping, ra, file, pgoff, 1); // some logic related to ra // ... // 查找 page cache page = find_get_page(mapping, pgoff); if (!page) goto no_cached_page; else: // assume page is updated for simple case // return minor fault, since no IO issued *type = VM_FAULT_MINOR; return page no_cached_page: // 分配 page cache 并 readpage page_cache_read(file, pgoff); page_cache_alloc_cold(mapping); add_to_page_cache_lru(page, mapping, offset, GFP_KERNEL); mapping->a_ops->readpage(file, page); // find page cache again, should hit page = find_get_page(mapping, pgoff); // major fault here *type = VM_FAULT_MAJOR; return page
1.1.6.3. do_page_fault
do_page_fault: __asm__("movl %%cr2,%0":"=r" (address)); vma = find_vma(mm, address); switch (handle_mm_fault(mm, vma, address, write)) { handle_pte_fault(mm, vma, address, write_access, pte, pmd); do_no_page(mm, vma, address, write_access, pte, pmd); new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret); filemap_nopage(vma, address & PAGE_MASK, &ret) entry = mk_pte(new_page, vma->vm_page_prot); set_pte(page_table, entry); case VM_FAULT_MINOR: tsk->min_flt++; break; case VM_FAULT_MAJOR: tsk->maj_flt++; break; // case xxx
1.2. Architectures
1.2.1. Page Cache
1.2.2. File system
1.2.2.1. readpage
1.2.2.2. prepare_write
1.2.2.3. commit_write
1.2.2.4. writepage
1.2.2.5. getblock
1.2.3. Generic Block Layer
1.2.3.1. Data Structure
1.2.3.1.1. bio
1.2.3.1.2. block_device
1.2.3.1.3. gendisk
1.2.3.1.4. request_queue
1.2.3.1.5. request
1.2.3.2. submit_bio
submit_bio: generic_make_request(bio); // 计算 bio 包括多少个 sector int nr_sectors = bio_sectors(bio); q = bdev_get_queue(bio->bi_bdev); // block_device->gendisk->request_queue return bdev->bd_disk->queue; // 若 bio->bi_dev 指向一个分区, 将 bio->bi_sector // 转换为相对于 bio->bi_dev->bd_contains 的 sector // 并将 bio->bi_dev 变为 bio->bi_dev->bd_contains blk_partition_remap(bio); // q->make_request_fn 是在 block device driver 初始时 // 通过 blk_init_queue 指定的, 一般默认的实现为 __make_request q->make_request_fn(q, bio);
1.2.3.3. IO Scheduler
1.2.3.3.1. driver plug/unplug
IO scheduler 作用是将多个 bio delay 一段时间再统一交给 driver 的 request_fn, 这段 delay 使得 IO scheduler 可以将 bio merger 到某个之前的 request 中,并实现对这些 request 的排队, 那么 delay 时间有多久?
IO scheduler 通过 plug/unplug 机制控制这个 delay:
- 通过 blk_plug_device 将 device 的 request_queue 设为 plugged 状态, 这时 request_queue 的所有 request 不会被 driver 处理, request_queue 处于等待接受 quest 以便能 merge request 的状态
- blk_plug_device 时会启动一个 timer (request_queue->unplug_timer), timer 的 delay 为 request_queue->unplug_delay (3ms). 这个 timer 可以保证至多 3ms 后 device 会被 unplug, 以便 driver 可以开始处理 request.
- 若 device 处理 plugged 状态, 但 request_queue 中的 pending request 个数大于 request_queue->unplug_thresh, 表示当前有足够的 pending request, 则 device 此时也会被 unplug
1.2.3.3.2. __make_request
__make_request 是 request_queue->make_request_fn 的默认实现, 如果 driver 希望使用 IO Scheduler 的功能, 则需要指定 make_request_fn 为 __make_request
__make_request 会直接与 IO scheduler 打交道.
__make_request: sector = bio->bi_sector; nr_sectors = bio_sectors(bio); if (elv_queue_empty(q)): blk_plug_device(q); goto get_rq; // req 保存着 elevator 返回的结果: elevator 认为 bio 可以与 req // merge 在一起至于为什么 elevator 认为可以与 req merge, 参考 // elevator 算法, 简单的以 noop elevator 为例, 如果 elevator 发现 // bio 与某个 req 是连续的 (例如 req->sector + req->nr_sectors = // bio->bi_sector, 表示 bio 可以 merge 到 req 末尾), 则认为 bio 与 // req 是可以 merge 的 el_ret = elv_merge(q, &req, bio); switch (el_ret): // bio 可以 merge 到 req 的末尾 case ELEVATOR_BACK_MERGE: req->biotail->bi_next = bio; req->biotail = bio; // 因为 bio 是 merge 到 req 的末尾, 所以 req->sector (req 开始的 // sector) 不变 req->nr_sectors += nr_sectors; case ELEVATOR_FRONT_MERGE: req->bio = bio; req->sector = sector; req->nr_sectors += nr_sectors; case ELEVATOR_NO_MERGE: break; get_rq: // 无法 merge, 生成一个新的 request req->nr_sectors = nr_sectors; req->bio = req->biotail = bio; // ... add_request(q, req); __elv_add_request() q->elevator->ops->elevator_add_req_fn(q, rq, where); int nrq = q->rq.count[READ] + q->rq.count[WRITE] - q->in_flight; if (nrq == q->unplug_thresh): __generic_unplug_device(q); // !! DRIVER !! q->request_fn(q);
总的来说, __make_request 会:
- 通过 request_queue->elevator 找到 elevator
- 调用 elevator 的相关方法 (elv_merge 等), 将 bio 转换为 request 放在 request_queue 中, 或将 bio merge 到某个已经存在的 request 中.
- 根据需要 (例如 unplug_thresh) 调用 generic_unplug_device 来通知 driver, 后者会调用 request_fn 来真正启动 IO
1.2.3.3.3. IO scheduler algorithm
- common purposes
- To minimize time wasted by hard disk seeks
- To prioritize a certain processes' I/O requests
- To give a share of the disk bandwidth to each running process
- To guarantee that certain requests will be issued before a particular deadline
- elevator_ops
elevator_ops 是各个 elevator 算法需要实现的回调函数. __make_request 时调用的 elv_queue_empty, elv_merge, elv_add_request, elv_next_request 等都是对 elevator_ops 相应函数的封装.
IO scheduler 是位于 __make_request 与 driver 之间的一层, 两者都需要调用 elevator_ops 相关的函数, 例如:
- __make_request 需要调用 elevator_merge_fn 尝试进行 merge, 调用 elevator_add_req_fn 添加一个新的 request
- driver 的 request_fn 需要调用 elevator_next_req_fn 从 request_queue 拿一个 request 进行真正的 IO
- elevator_queue_empty_fn
- elevator_merge_fn
虽然不同的 elevator 都会实现自己的 elevator_merge_fn, 但实现起来基本都是差不多的:
- 尝试 elv_try_last_merge, 看看 request->last_merge (上次 merge 的 request) 是否也可以满足这次 merge
- 若不行, 在 request_queue 中找一个可以 merge 的 request
`可以 merge` 的标准是某个 request 与 bio 是连续的 (adjacent).
不同的 elevator 的这个"找"的过程会有所区别, 例如 noop 直接是遍历整个 request_queue, deadline 会使用 hash, 而 anticipatory 会使用 rbtree
- elevator_add_req_fn
- elevator_next_req_fn
merge 只是 elevator 的一部分功能, elv_next_request (或 elevator_next_req_fn) 才是 elevator 的主要功能.
这部分也是 IO scheduler 被称为 elevator 的原因: 它负责对多个 request 进行排序以便减少磁盘 seek 的时间, 但与生活中的电梯不同的是, IO scheduler 的功能要简单一些: 它只需要将 request 按照 sector (楼层) 从小到大排序, 并不需要考虑磁头当前的位置 (电梯当前的楼层), 例如, 假设当前有几个 request, 其 sector 分别为: 4 8 10 3, 则 IO scheduler 会将它们排序为 3 4 8 10, 然后从 3 开始操作数据. 而对于真实的电梯来说, 假设电梯此时在 9 层, 则 10 8 4 3 更好一些.
为什么 IO scheduler 不考虑磁头的位置并采用简单的按 sector 升序扫描的方法?
磁盘的寻址, 以 CHS 模式为例, 需要定位三个部件:
- Cylinder, 柱面
- Header, 磁头, 1+2 可以定位 Track, 称为寻道
- Sector, 扇区, 1+2+3 可以定位最终的扇区
其中定位 C 和 H 称为寻道, 需要移动磁头, 时间一般为 10ms 左右, 定位 S 称为 latency, 依靠磁盘的旋转, 一般平均为 4ms (以 7200 转/分的硬盘为例, 60*1000/7200/2 = 4ms )
以下全是猜的…
- 寻址是一个很慢的过程 (15ms 左右) , 但我认为寻址的速度和距离可能关系不大, 即磁头 1 -> 2 和 1 -> 10 可能时间是差不多的 (磁头并不需要像电梯一样 1 -> 2 -> 3 .. -> 10, 而是可以直接 1 -> 10), 所以不需要太在意初始磁头的位置
- 一次 IO 的初始扇区定位后, 后续对连续扇区的操作可能只需要旋转磁盘(S) 即可, 不需要寻道 (CH), 将 sector 升序排序可以尽可能的利用这一性质
- 磁盘片的转动应该是固定一个方向, 所以固定的升序也是必要的.
综上, IO scheduler 采取的直接排序后线性扫描的方式应该也能尽可能的减少磁盘寻址的次数
猜测结束…
不同的 elevator 的 elevator_next_req_fn 基本都是这种简单的排序, 但各个 elevator 在解决 IO 优先级, 防止 starvation 等方面会有不同.
- algorithm
1.2.4. bdev fs
bdev 文件系统是一个很特殊的伪文件系统, 它和其它文件系统一样, 提供了各种 ops, 但并不会被 mount 到某个 dentry…
1.2.4.1. bdev fs 的作用
- 访问 block device file 时会关联到 bdev fs 定义的各种 ops, 但这种关联并不是通过 mount 确立的, 而是通过 init_special_inode 这种代码实现的.
- bdev fs 还是一个作用是通过 inode cache 来维护 dev_t 和 block_device 的关联, 例如 bdget
1.2.4.2. bdev_cache_init
1.2.4.3. init_special_inode
1.2.4.4. bdget
1.2.5. Block Device Driver
1.2.5.1. Initialization
1.2.5.1.1. register_blkdev
1.2.5.1.2. blk_init_queue
1.2.5.2. request_fn
do_xxx_request