一、mmap原理

在虚拟内存空间那一节,我们知道,每一个进程都有一个列表vm_area_struct,指向虚拟地址空间的不同内存块,这个变量名字叫mmap

struct mm_struct {struct vm_area_struct *mmap;     /* list of VMAs */
......
}struct vm_area_struct {/** For areas with an address space and backing store,* linkage into the address_space->i_mmap interval tree.*/struct {struct rb_node rb;unsigned long rb_subtree_last;} shared;/** A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma* list, after a COW of one of the file pages.   A MAP_SHARED vma* can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack* or brk vma (with NULL file) can only be in an anon_vma list.*/struct list_head anon_vma_chain; /* Serialized by mmap_sem &* page_table_lock */struct anon_vma *anon_vma;    /* Serialized by page_table_lock *//* Function pointers to deal with this struct. */const struct vm_operations_struct *vm_ops;/* Information about our backing store: */unsigned long vm_pgoff;     /* Offset (within vm_file) in PAGE_SIZEunits */struct file * vm_file;       /* File we map to (can be NULL). */void * vm_private_data;      /* was vm_pte (shared mem) */

其实内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间,这个时候访问内存空间就能够访问到文件里面的数据。

而仅有物理内存和虚拟内存的映射是一种特殊情况

1、mmap系统调用

1、如何分配一大块内存

如果申请一大块内存就用mmap,mmap是映射内存空间到物理内存

另外,如果一个进程想映射一个文件到自己的虚拟内存空间,也要通过mmap系统调用这个时候mmap是映射内存空间到物理内存再到文件。可见mmap这个系统调用时核心,

2、我们现在来看mmap这个系统调用

SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, off)
{
......error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
......
}SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, pgoff)
{struct file *file = NULL;
......file = fget(fd);
......retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);return retval;
}

如果映射到文件,fd会传进来一个文件描述符,并且mmap_pgoff里面通过fget函数,根据文件描述符获得struct file、struct file表示打开一个文件

接下来的调用链是:

这里主要干了两件事情

1、调用 get_unmapped_area 找到一个没有映射的区域

2、调用 mmap_region 映射这个区域。

3、我们先来看 get_unmapped_area 函数。

unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags)
{unsigned long (*get_area)(struct file *, unsigned long,unsigned long, unsigned long, unsigned long);
......get_area = current->mm->get_unmapped_area;if (file) {if (file->f_op->get_unmapped_area)get_area = file->f_op->get_unmapped_area;}
......
}

const struct file_operations ext4_file_operations = {
.......mmap           = ext4_file_mmap.get_unmapped_area = thp_get_unmapped_area,
};unsigned long __thp_get_unmapped_area(struct file *filp, unsigned long len,loff_t off, unsigned long flags, unsigned long size)
{unsigned long addr;loff_t off_end = off + len;loff_t off_align = round_up(off, size);unsigned long len_pad;len_pad = len + size;
......addr = current->mm->get_unmapped_area(filp, 0, len_pad,off >> PAGE_SHIFT, flags);addr += (off - addr) & (size - 1);return addr;
}

 4、我们再来看 mmap_region 函数,看它如何映射这个虚拟内存区域

unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,struct list_head *uf)
{struct mm_struct *mm = current->mm;struct vm_area_struct *vma, *prev;struct rb_node **rb_link, *rb_parent;/** Can we just expand an old mapping?*/vma = vma_merge(mm, prev, addr, addr + len, vm_flags,NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);if (vma)goto out;/** Determine the object being mapped and call the appropriate* specific mapper. the address has already been validated, but* not unmapped, but the maps are removed from the list.*/vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);if (!vma) {error = -ENOMEM;goto unacct_error;}vma->vm_mm = mm;vma->vm_start = addr;vma->vm_end = addr + len;vma->vm_flags = vm_flags;vma->vm_page_prot = vm_get_page_prot(vm_flags);vma->vm_pgoff = pgoff;INIT_LIST_HEAD(&vma->anon_vma_chain);if (file) {vma->vm_file = get_file(file);error = call_mmap(file, vma);addr = vma->vm_start;vm_flags = vma->vm_flags;}
......vma_link(mm, vma, prev, rb_link, rb_parent);return addr;
.....

1、还记得咱们刚找到了虚拟内存区域的前一个 vm_area_struct,我们首先要看,是否能够基于它进行扩展,也即调用 vma_merge,和前一个 vm_area_struct 合并到一起。

2、如果不能,就需要调用 kmem_cache_zalloc,在 Slub 里面创建一个新的 vm_area_struct对象,设置起始和结束位置,将它加入队列。如果是映射到文件,则设置 vm_file 为目标文件,

  调用 call_mmap。其实就是调用 file_operations 的 mmap 函数

3、对于 ext4 文件系统,调用的是 ext4_file_mmap。从这个函数的参数可以看出,这一刻文件和内存开始发生关系了。这里我们将vm_area_struct 的内存操作设置为文件系统操作,也就是说,

  读写内存其实就是读写文件系统。

static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{return file->f_op->mmap(file, vma);
}static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
......vma->vm_ops = &ext4_file_vm_ops;
......
}

5、我们再回到 mmap_region 函数。

最终,vma_link 函数将新创建的 vm_area_struct 挂在了 mm_struct 里面的红黑树上。

这个时候,从内存到文件的映射关系,至少要在逻辑层面建立起来。那从文件到内存的映射关系呢?vma_link 还做了另外一件事情,就是 __vma_link_file。这个东西要用于建立这层映射关系。

对于打开的文件,会有一个结构 struct file 来表示。它有个成员指向 struct address_space 结构,这里面有棵变量名为 i_mmap 的红黑树,vm_area_struct 就挂在这棵树上。

struct address_space {struct inode        *host;      /* owner: inode, block_device */
......struct rb_root        i_mmap;     /* tree of private and shared mappings */
......const struct address_space_operations *a_ops; /* methods */
......
}static void __vma_link_file(struct vm_area_struct *vma)
{struct file *file;file = vma->vm_file;if (file) {struct address_space *mapping = file->f_mapping;vma_interval_tree_insert(vma, &mapping->i_mmap);}

到这里,内存映射的内容要告一段落,你可能会困惑,好像还没有和物理内存法神过任何关系、还是在虚拟内存里面折腾呀?对的,因为到目前为止,我们还没有开始真正访问内存呀!

这个时候,内存管理并不直接分配物理内存,因为物理内存相对于虚拟地址空间太宝贵了,只要等你真正用的那一刻才会开始分配

二、用户态缺页异常

一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就出发缺页中断,调用do_page_fault

dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{unsigned long address = read_cr2(); /* Get the faulting address */
......__do_page_fault(regs, error_code, address);
......
}/** This routine handles page faults.  It determines the address,* and the problem, and then passes it off to one of the appropriate* routines.*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,unsigned long address)
{struct vm_area_struct *vma;struct task_struct *tsk;struct mm_struct *mm;tsk = current;mm = tsk->mm;if (unlikely(fault_in_kernel_space(address))) {if (vmalloc_fault(address) >= 0)return;}
......vma = find_vma(mm, address);
......fault = handle_mm_fault(vma, address, flags);
......

1、在do_page_fault里面,先要判断缺页中断是否发生在内核,如果发生在内核则调用vmalloc_fault,这就是和咱们前面学过的虚拟内存的布局对应上了

2、在内核里面,vmalloc区域需要内核页表映射到物理页,咱们这里把内核的这部分放放,接着看用户空间的部分

3、接下来在用户空间里面,找到你访问的那个地址所在的区域 vm_area_struct,然后调用 handle_mm_fault 来映射这个区域。

static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,unsigned int flags)
{struct vm_fault vmf = {.vma = vma,.address = address & PAGE_MASK,.flags = flags,.pgoff = linear_page_index(vma, address),.gfp_mask = __get_fault_gfp_mask(vma),};struct mm_struct *mm = vma->vm_mm;pgd_t *pgd;p4d_t *p4d;int ret;pgd = pgd_offset(mm, address);p4d = p4d_alloc(mm, pgd, address);
......vmf.pud = pud_alloc(mm, p4d, address);
......vmf.pmd = pmd_alloc(mm, vmf.pud, address);
......return handle_pte_fault(&vmf);
}

到这里,终于看到了我们熟悉的 PGD、P4G、PUD、PMD、PTE,这就是前面讲页表的时候,讲述的四级页表的概念,因为暂且不考虑五级页表,我们暂时忽略P4G

1、pgd_t 用于全局页目录项,pud_t 用于上层页目录项,pmd_t 用于中间页目录项,pte_t 用于直接页表项。

2、每个进程都有独立的地址空间,为了这个进程独立完成映射,每个进程都有独立的进程页表,这个页表的最顶级的 pgd 存放在 task_struct 中的 mm_struct 的 pgd变量里面

3、在一个进程新创建的时候,会调用 fork,对于内存的部分会调用 copy_mm,里面调用 dup_mm

/** Allocate a new mm structure and copy contents from the* mm structure of the passed in task structure.*/
static struct mm_struct *dup_mm(struct task_struct *tsk)
{struct mm_struct *mm, *oldmm = current->mm;mm = allocate_mm();memcpy(mm, oldmm, sizeof(*mm));if (!mm_init(mm, tsk, mm->user_ns))goto fail_nomem;err = dup_mmap(mm, oldmm);return mm;
}

在这里,除了创建一个新的 mm_struct,并且通过 memcpy 将它和父进程的弄成一模一样之外,我们还需要调用 mm_init 进行初始化。接下来,

mm_init 调用 mm_alloc_pgd,分配全局、页目录项,赋值给 mm_struct 的 pdg 成员变量。

static inline int mm_alloc_pgd(struct mm_struct *mm)
{mm->pgd = pgd_alloc(mm);return 0;
}

pgd_alloc 里面除了分配 PDG 之外,还做了很重要的一个事情,就是调用 pgd_ctor

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{/* If the pgd points to a shared pagetable level (either theptes in non-PAE, or shared PMD in PAE), then just copy thereferences from swapper_pg_dir. */if (CONFIG_PGTABLE_LEVELS == 2 ||(CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||CONFIG_PGTABLE_LEVELS >= 4) {clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,swapper_pg_dir + KERNEL_PGD_BOUNDARY,KERNEL_PGD_PTRS);}
......
}

1、pgd_ctor干了什么

2、load_new_mm_cr3 为什么要将虚拟地址转换为物理地址?

因为cr3存放的是物理地址,只有将虚拟地址转换为物理地址才能加载到 cr3 里面去

3、load_new_mm_cr3将虚拟地址转换为虚拟地址的调用链

4、地址转换的过程无需进入内核态

5、触发缺页异常调用链

只有访问内存的时候发现没有映射多物理内存,页表也没有创建过,才触发缺页异常

绕了一大圈,终于将页表整个机制的各个部分串了起来。但是咱们的故事还没讲完,物理的内存、还没找到。我们还得接着分析 handle_pte_fault 的实现。

static int handle_pte_fault(struct vm_fault *vmf)
{pte_t entry;
......vmf->pte = pte_offset_map(vmf->pmd, vmf->address);vmf->orig_pte = *vmf->pte;
......if (!vmf->pte) {if (vma_is_anonymous(vmf->vma))return do_anonymous_page(vmf);elsereturn do_fault(vmf);}if (!pte_present(vmf->orig_pte))return do_swap_page(vmf);
......
}

匿名页调用

这个函数你还记得吗?就是咱们伙伴系统的核心函数,专门用来分配物理页面的。do_anonymous_page 接下来要调用 mk_pte,将页表项指向新分配的物理页,set_pte_at 会将页表项塞到页表里面。

static int do_anonymous_page(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;struct mem_cgroup *memcg;struct page *page;int ret = 0;pte_t entry;
......if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))return VM_FAULT_OOM;
......page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
......entry = mk_pte(page, vma->vm_page_prot);if (vma->vm_flags & VM_WRITE)entry = pte_mkwrite(pte_mkdirty(entry));vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,&vmf->ptl);
......set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
......
}

第二种情况映射到文件 do_fault,最终我们会调用 __do_fault

int swap_readpage(struct page *page, bool do_poll)
{struct bio *bio;int ret = 0;struct swap_info_struct *sis = page_swap_info(page);blk_qc_t qc;struct block_device *bdev;
......if (sis->flags & SWP_FILE) {struct file *swap_file = sis->swap_file;struct address_space *mapping = swap_file->f_mapping;ret = mapping->a_ops->readpage(swap_file, page);return ret;}
......
}

这里调用了struct vm_operations_struct vm_ops的fault函数,还记得咱们上面用mmap映射文件的时候,对于ext4文件系统,vm_ops指向了ext4_file_vm_ops也就是调用了函数ext4_filemap_fault

static const struct vm_operations_struct ext4_file_vm_ops = {.fault       = ext4_filemap_fault,.map_pages    = filemap_map_pages,.page_mkwrite   = ext4_page_mkwrite,
};int ext4_filemap_fault(struct vm_fault *vmf)
{struct inode *inode = file_inode(vmf->vma->vm_file);
......err = filemap_fault(vmf);
......return err;
}

ext4_filemap_fault里面的逻辑我们很容易就能读懂,vm_file就是咱们当时mmap的时候映射的那个文件,然后我们需要调用filemap_fault

对于文件映射来说,一般这个文件会在物理内存里面有页面作为它的缓存,find_get_page就是找那个页,如果找到了,就调用,预读一些数据到内存里面;如果没有,就跳到no_cached_page

int filemap_fault(struct vm_fault *vmf)
{int error;struct file *file = vmf->vma->vm_file;struct address_space *mapping = file->f_mapping;struct inode *inode = mapping->host;pgoff_t offset = vmf->pgoff;struct page *page;int ret = 0;
......page = find_get_page(mapping, offset);if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {do_async_mmap_readahead(vmf->vma, ra, file, page, offset);} else if (!page) {goto no_cached_page;}
......vmf->page = page;return ret | VM_FAULT_LOCKED;
no_cached_page:error = page_cache_read(file, offset, vmf->gfp_mask);
......
}

1、如果没有物理内存中的缓存页

2、那我们就调用 page_cach—_read

3、在这里显示分配一个缓存页

4、将这一页加到 lru 表里面

5、然后在 address_space 中调用 aaddress_space_operations 的readpage 函数,将文件内容读到内存中。address_space 的作用咱们上面也介绍过了。

static int page_cache_read(struct file *file, pgoff_t offset, gfp_t gfp_mask)
{struct address_space *mapping = file->f_mapping;struct page *page;
......page = __page_cache_alloc(gfp_mask|__GFP_COLD);
......ret = add_to_page_cache_lru(page, mapping, offset, gfp_mask & GFP_KERNEL);
......ret = mapping->a_ops->readpage(file, page);
......
}

struct address_space_operations对于 ext4 文件系统的定义如下所示。这么说来,

上面的 readpage 调用的其实是 ext4_readage。因为我们还没讲到文件系统,这里我们不详细介绍

ext4_readpage 具体干了什么。你只要知道,最后会调用 ext4_read_inline_page,这里面有部分逻辑和内存映射有关就行了。

static const struct address_space_operations ext4_aops = {.readpage     = ext4_readpage,.readpages     = ext4_readpages,
......
};static int ext4_read_inline_page(struct inode *inode, struct page *page)
{void *kaddr;
......kaddr = kmap_atomic(page);ret = ext4_read_inline_data(inode, kaddr, len, &iloc);flush_dcache_page(page);kunmap_atomic(kaddr);
......
}

1、为什么要在内核里面映射一把?

1、在 ext4_read_inline_page 函数里,我们需要先调用 kmap_atomic,将物理内存映射到内核的虚拟地址空间,得到内核中的地址kaddr

2、kaddr它是用来做临时内核映射的。本来把物理内存映射到用户虚拟地址空间,不需要在内核里面映射一把。

但是,现在因为要从文件里面读取数据并写入这个物理页面,又不能使用物理地址,

我们只能使用虚拟地址,这就需要在内核里面临时映射一把。临时映射后,ext4_read_inline_data 读取文件到这个虚拟地址。读取完毕后,我们取消这个临时映射 kunmap_atomic 就行了。

我们再来看第三种情况,do_swap_page。之前我们讲过物理内存管理,你这里可以回忆一下。如果长时间不用,就要换出到硬盘,

也就是 swap,现在这部分数据又要访问了,我们还得想办法再读到内存中来。

int do_swap_page(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;struct page *page, *swapcache;struct mem_cgroup *memcg;swp_entry_t entry;pte_t pte;
......entry = pte_to_swp_entry(vmf->orig_pte);
......page = lookup_swap_cache(entry);if (!page) {page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vma,vmf->address);
......}
......swapcache = page;
......pte = mk_pte(page, vma->vm_page_prot);
......set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);vmf->orig_pte = pte;
......swap_free(entry);
......
}

1、do_swap_page函数会先查找 swap 文件有没有缓存页。

2、如果没有,就调用swapin_readahead,将 swap 文件读到内存中来,形成内存页,并通过 mk_pte 生成页表项。

3、set_pte_at 将页表项插入页表,将 swap 文件清理。因为重新加载回内存了,不再需要 swap 文件了。

4、swapin_readahead 会最终调用 swap_readpage,在这里,我们看到了熟悉的readpage 函数,也就是说读取普通文件和读取 swap 文件,

过程是一样的,同样需要用 kmap_atomickmap_atomic 做临时映射。

int swap_readpage(struct page *page, bool do_poll)
{struct bio *bio;int ret = 0;struct swap_info_struct *sis = page_swap_info(page);blk_qc_t qc;struct block_device *bdev;
......if (sis->flags & SWP_FILE) {struct file *swap_file = sis->swap_file;struct address_space *mapping = swap_file->f_mapping;ret = mapping->a_ops->readpage(swap_file, page);return ret;}
......
}

通过上面复杂的过程,用户缺页异常处理完毕了,物理内存中有了页面,页表也建立好了映射,接下来用户程序在虚拟内存空间里面,可以通过虚拟地址顺利经过页表映射的访问物理页面上的数据了

为了加快映射速度,我们不需要每次从虚拟地址到物理地址都转换走一遍页表

1、页表一般都很大,只能存放在内存中,操作系统每次访问内存要折腾两步

1、先通过查询页表得到物理地址

2、然后访问该物理地址读取指令、数据

2、TLB 页表的 Cache

为了提高映射速度,我们引入了TLB(Translation Lookaside Buffer)我们经常称为快表,专门用来做地址映射的硬件设备。

它不在内存中、可存储的数据比较少,但是比内存要快。所以,我们可以想象,TLB 就是页表的 Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。

3、有了 TLB 之后,地址映射的过程就像图中画的

1、我们先查块表,块表中有映射关系,然后直接转换为物理地址。
2、如果在 TLB 查不到映射关系时,才会到内存中查询页表。

总结时刻

用户态的内存映射机制,我们解析的差不多了,我们来总结一下,用户态的内存映射机制包含以下几个部分
用户态内存映射函数 mmap,包括用它来做匿名映射和文件映射...
用户态的页表结构,存储位置在 mm_struct 中。
在用户态访问没有映射的内存会引发缺页异常,分配物理页表,补齐页表。如果是匿名映射则
分配物理内存;如果是 swap,则将 swap 文件读入;如果是文件映射,则将文件读入

转载于:https://www.cnblogs.com/luoahong/p/10916458.html

趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...相关推荐

  1. 认真学习系列:Linux原理——《趣谈linux》学习笔记

    1.电脑一通电,先运行主板上ROM(只读存储器)里写死的程序BIOS,BIOS去找要运行什么操作系统,运行操作系统的第一段代码,创建0号进程,它是这次开机所有进程的爹, 2.然后操作系统代码里先初始化 ...

  2. Linux操作系统学习笔记(二十)网络通信之TCP协议

    一. 前言   自TCP诞生以来就改变了网络通信的格局,而TCP协议随着网络基础设施的发展也在一路演进,形成了如今庞大复杂的TCP协议簇.如何深入理解TCP的设计理念以及几十年以来TCP协议的演进,有 ...

  3. Linux操作系统学习笔记(十)内存管理之内存映射

    一. 前言   本文为内存部分最后一篇,介绍内存映射.内存映射不仅是物理内存和虚拟内存间的映射,也包括将文件中的内容映射到虚拟内存空间.这个时候,访问内存空间就能够访问到文件里面的数据.而仅有物理内存 ...

  4. Linux操作系统学习笔记(十四)块设备

    一. 前言   上文我们分析了字符设备,本文接着分析块设备.我们首先分析块设备的基本结构体,然后分析块设备生成.加载的整个过程,最后分析块设备的直接I/O访问和缓存I/O访问. 二. 块设备基本结构体 ...

  5. Linux操作系统学习笔记【入门必备】

    Linux操作系统学习笔记[入门必备] 文章目录 Linux操作系统学习笔记[入门必备] 1.Linux入门 2.Linux目录结构 3.远程登录 3.1 远程登录Linux-Xshell5 3.2 ...

  6. 趣谈Linux操作系统01:概述

    目录 1. Linux学习路径 2. Linux内核体系结构 3. 系统调用简介 3.1 系统调用分类简介 3.1.1 进程管理 3.1.2 内存管理 3.1.3 文件管理 3.1.4 信号处理 3. ...

  7. Ubuntu LInux操作系统 学习笔记及课后习题解答

    1.ubuntu基本使用 1.1 GNU GPL GNU通用公共许可证(general public license),开放.自由的精神,任何软件加上GPL协议后,即成为自由的软件,任何人均可获得,同 ...

  8. 趣谈Linux操作系统随笔——2.0 从BIOS到bootloader:创业伊始,有活儿老板自己上

    从BIOS到bootloader:创业伊始,有活儿老板自己上 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统 开发环境:Linux ...

  9. Linux操作系统学习笔记(二十八)深入理解CPU

    一. 前言   在前面一些文章中多多少少有提到一些CPU的结构以及对应的寄存器等,但是总觉得不够透彻,所以单开一文详细叙述CPU的各种知识,从而加深对操作系统和性能的理解.本文从最基本的加法器和乘法器 ...

最新文章

  1. php rsa数字签名为空,如何使用PHP将数字签名(RSA,证书等)添加到任何文件?
  2. 剑指Offer(Java实现)栈的压入、弹出序列
  3. C#操作Excel数据增删改查(转)
  4. [react] 怎么在JSX里属性可以被覆盖吗?覆盖的原则是什么?
  5. UNIX(多线程):18---异步任务提供者(Provider) 介绍(续)
  6. Python学习笔记:交互对话环境IPython
  7. SentOS 7防火墙配置与端口增删改查的命令
  8. 警方通报6少年深夜洗劫小米专卖店
  9. mysql 5.5 外键_MySQL 5.5添加外键失败,错误[HY000] [150]和[HY000] [1005]
  10. 有效缓解腰部不适,十星小双鱼腰部按摩器上手体验
  11. 制作海报,你一定会用到的这些工具网站
  12. IOS OC UIKit基本使用
  13. 阿里云的NoSQL存储服务OTS的应用分析
  14. 170716 网线接口顺序
  15. Premature optimization is the root of all evil.
  16. 浅析c4编译器--一个优秀的编译器是如何生成的
  17. 大数据培训需要有基础吗
  18. Python作业“骰子游戏”
  19. 18位身份证号码编码标准
  20. 电商物流系统技术架构进化史

热门文章

  1. SAS 2.0:中端理想“零距离”?
  2. 文件操作:在某手诗文中插入一句话
  3. hive--udf函数(开发-4种加载方式)
  4. TypeScript Type Innference(类型推断)
  5. linux运维实战练习-2015年9月13日-9月15日课程作业(练习)安排
  6. 开启apache的server-status辅助分析工具
  7. Linux程序包管理,YUM命令使用解析。
  8. position属性及实现图片垂直居中
  9. 基于python3的一次简单的请求url接口返回json类型结果实例。
  10. paramiko基础