专题:Linux内存管理专题

关键词:swapper_pd_dir、ARM PGD/PTE、Linux PGD/PTE、pgd_offset_k。

Linux下的页表映射分为两种,一是Linux自身的页表映射,另一种是ARM32 MMU硬件的映射。

1. ARM32页表映射

由于ARM32和Linux内核维护的页表项有所不同,所以维护了两套PTE。

PGD存放在swapper_pd_dir中,一个PGD目录项其实包含了两份ARM32 PGD。

所以再分配PTE的时候,共分配了1024个PTE,512个给Linux OS维护用;512个给ARM32 MMU用,对应两个PGD的页表数目。

由于Linux OS和ARM32的PTE紧邻,所以两者的转换也方便进行。

1.1 ARM32处理器查询页表

32bit的Linux采用三级映射:PGD-->PMD-->PTE,64bit的Linux采用四级映射:PGD-->PUD-->PMD-->PTE,多了个PUD。

缩写是PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。

在ARM32 Linux采用两层映射,省略了PMD,除非在定义了CONFIG_ARM_LPAE才会使用3级映射。

在ARM32架构中,可以按段(section)来映射,这是采用单层映射模式。

使用页面映射需要两层映射结构,页面可以是64KB或4KB大小。

1.1.1 ARM32架构MMU4KB页面映射过程

如果采用页表映射的方式,段映射表就变成一级映射表(Linux中称为PGD),其页表项提供的不再是物理地址,而是二级页表的基地址。

32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。

以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。

最后将这20位物理页面地址和虚拟地址的低12位拼凑在一起,得到最终的32位物理地址。

这个过程在ARM32架构中由MMU硬件完成,软件不需要接入。

ARM32架构MMU页表映射过程

1.1.2 ARMv7-AR中关于Short Descriptor映射概览图

关于4K页表的映射过程在ARMv7-AR用户架构手册有关介绍。

一个地址映射的概览图,32位虚拟地址从TTBR1中找到First-level table地址,然后取虚拟地址VA[31:20]作为序号找到Second-level table地址。

取虚拟地址VA[19:12]作为序号找到Page地址。

规格书中Small Page映射过程

Figure B3-11 Small page address translation是映射的细节:

1.2 Linux页表映射相关数据结构

我们知道在map_lowmem()使用create_mapping()创建页表映射,这个函数的参数结构是struct map_desc。

下面来研究它的相关结构,有助于理解内核是如何处理页表映射的。

arch\arm\include\asm\mach\map.h:

struct map_desc {
unsigned long virtual;------虚拟地址起始地址
unsigned long pfn;----------物理地址开始页帧号
unsigned long length;-------内存空间大小
unsigned int type;----------mem_types中的序号
};

map_desc中的type指向类型为struct mem_type的mem_types数组:

arch\arm\mm\mm.h:
struct mem_type {
pteval_t prot_pte;------------PTE属性
pteval_t prot_pte_s2;---------定义CONFIG_ARM_LPAE才有效
pmdval_t prot_l1;-------------PMD属性
pmdval_t prot_sect;-----------Section类型映射
unsigned int domain;----------定义ARM中不同的域
}; arch\arm\mm\mmu.c:
static struct mem_type mem_types[] = {
...
[MT_MEMORY_RWX] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意这里都是L_PTE_*类型,需要在写入MMU对应PTE时进行转换。
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
[MT_MEMORY_RW] = {
.prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
L_PTE_XN,
.prot_l1 = PMD_TYPE_TABLE,
.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
.domain = DOMAIN_KERNEL,
},
...
}

下面重点关注Page Table类型的一级页表和二级页表的细节,以及Linux内核中的定义:

ARM32中PGD定义

下面是First-level descriptor详细说明:

/*
* Hardware page table definitions.
*
* + Level 1 descriptor (PMD)
* - common
*/
#define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)---------------------------01对应PageTable
#define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT (_AT(pmdval_t, 2) << 0)
#define PMD_PXNTABLE (_AT(pmdval_t, 1) << 2) /* v7 */
#define PMD_BIT4 (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x) (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION (_AT(pmdval_t, 1) << 9) /* v5 */

ARM32中PTE定义

下面是Second-level descriptor详细说明:

/*
* + Level 2 descriptor (PTE)
* - common
*/
#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT (_AT(pteval_t, 0) << 0)
#define PTE_TYPE_LARGE (_AT(pteval_t, 1) << 0)
#define PTE_TYPE_SMALL (_AT(pteval_t, 2) << 0)
#define PTE_TYPE_EXT (_AT(pteval_t, 3) << 0) /* v5 */
#define PTE_BUFFERABLE (_AT(pteval_t, 1) << 2)
#define PTE_CACHEABLE (_AT(pteval_t, 1) << 3) /*
* - extended small page/tiny page
*/
#define PTE_EXT_XN (_AT(pteval_t, 1) << 0) /* v6 */
#define PTE_EXT_AP_MASK (_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0 (_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1 (_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO (_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x) (_AT(pteval_t, (x)) << 6) /* v5 */
#define PTE_EXT_APX (_AT(pteval_t, 1) << 9) /* v6 */
#define PTE_EXT_COHERENT (_AT(pteval_t, 1) << 9) /* XScale3 */
#define PTE_EXT_SHARED (_AT(pteval_t, 1) << 10) /* v6 */
#define PTE_EXT_NG (_AT(pteval_t, 1) << 11) /* v6 */

Linux中PTE定义

由于Linux对于PTE的定义和ARM硬件不一致,下面的L_开头的定义都是针对Linux的,L_MT开头的是bit[5:2]表示的内存类型。

/*
* "Linux" PTE definitions.
*
* We keep two sets of PTEs - the hardware and the linux version.
* This allows greater flexibility in the way we map the Linux bits
* onto the hardware tables, and allows us to have YOUNG and DIRTY
* bits.
*
* The PTE table pointer refers to the hardware entries; the "Linux"
* entries are stored 1024 bytes below.
*/
#define L_PTE_VALID (_AT(pteval_t, 1) << 0) /* Valid */
#define L_PTE_PRESENT (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY (_AT(pteval_t, 1) << 7)
#define L_PTE_USER (_AT(pteval_t, 1) << 8)
#define L_PTE_XN (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED (_AT(pteval_t, 1) << 10) /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE (_AT(pteval_t, 1) << 11) /*
* These are the memory types, defined to be compatible with
* pre-ARMv6 CPUs cacheable and bufferable bits: XXCB
*/
#define L_PTE_MT_UNCACHED (_AT(pteval_t, 0x00) << 2) /* 0000 */
#define L_PTE_MT_BUFFERABLE (_AT(pteval_t, 0x01) << 2) /* 0001 */
#define L_PTE_MT_WRITETHROUGH (_AT(pteval_t, 0x02) << 2) /* 0010 */
#define L_PTE_MT_WRITEBACK (_AT(pteval_t, 0x03) << 2) /* 0011 */
#define L_PTE_MT_MINICACHE (_AT(pteval_t, 0x06) << 2) /* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC (_AT(pteval_t, 0x07) << 2) /* 0111 */
#define L_PTE_MT_DEV_SHARED (_AT(pteval_t, 0x04) << 2) /* 0100 */
#define L_PTE_MT_DEV_NONSHARED (_AT(pteval_t, 0x0c) << 2) /* 1100 */
#define L_PTE_MT_DEV_WC (_AT(pteval_t, 0x09) << 2) /* 1001 */
#define L_PTE_MT_DEV_CACHED (_AT(pteval_t, 0x0b) << 2) /* 1011 */
#define L_PTE_MT_VECTORS (_AT(pteval_t, 0x0f) << 2) /* 1111 */
#define L_PTE_MT_MASK (_AT(pteval_t, 0x0f) << 2)

ARM PMD描述符bit[8:5]用于描述Domain,但ARM Linux只定义使用三个:

#define DOMAIN_KERNEL    2---------用于内核空间
#define DOMAIN_TABLE 2
#define DOMAIN_USER 1-----------用于用户空间
#define DOMAIN_IO 0-------------用于I/O地址域

1.3 设置PGD页面目录

create_mapping的参数是struct map_desc类型,用于描述一个虚拟地址区域线性映射到物理区域。基于这块区域创建PGD/PTE。

static void __init create_mapping(struct map_desc *md)
{
unsigned long addr, length, end;
phys_addr_t phys;
const struct mem_type *type;
pgd_t *pgd;
...
type = &mem_types[md->type];------------------------------找到对应的struct mem_type
...
addr = md->virtual & PAGE_MASK;---------------------------对齐到页
phys = __pfn_to_phys(md->pfn);----------------------------页到物理地址转换
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
...
pgd = pgd_offset_k(addr);---------------------------------根据addr找到对应虚拟地址对应的pgd地址
end = addr + length;
do {
unsigned long next = pgd_addr_end(addr, end); alloc_init_pud(pgd, addr, next, phys, type);----------初始化下一级页表 phys += next - addr;
addr = next;
} while (pgd++, addr != end);-----------------------------遍历区间地址,步长是PGDIR_SIZE,即2MB大小的空间。
}

这里面有三个地方需要解释:

pgd_offset_k

将虚拟地址进行转换得到PMD的指针。

#define PGDIR_SHIFT 21

/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT) #define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr)) #define pgd_offset_k(addr) pgd_offset(&init_mm, addr) struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(),
.mm_count = ATOMIC_INIT(),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
INIT_MM_CONTEXT(init_mm)
};

由虚拟内存布局图中swapper_pg_dir可知,大小为16KB,里面有详细的解释。init_mm.pgd指向swapper_pg_dir。

pgd_addr_end

include\asm-generic\pgtable.h:

#define pgd_addr_end(addr, end)                        \
({ unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK; \
(__boundary - < (end) - )? __boundary: (end); \
}) arch\arm\include\asm\pgtable-2level.h:
/*
* PMD_SHIFT determines the size of the area a second-level page table can map
* PGDIR_SHIFT determines what a third-level page table entry can map
*/
#define PMD_SHIFT 21
#define PGDIR_SHIFT 21 #define PMD_SIZE (1UL << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))

由于PGDIR_SHIFT为21,所以一个PGD页表目录对应2MB大小的空间,即[addr, addr+PGDIR_SIZE)。所以PGD的数目为2^11,2028个。整个PGD页表占用空间为2048*4B=8KB。

这和ARM硬件的4096 PGD不一致。这里涉及到Linux实现技巧,在创建PTE中进行分析。

所以此处按照2MB步长,遍历[virtual, virtual+length)空间创建PDG页表和PTE。

alloc_init_pte

由于ARM-Linux采用两级页表映射,跳过PUD/PMD,直接到alloc_init_pte创建PTE。

alloc_init_pud-->alloc_init_pmd-->alloc_init_pte

arch\arm\mm\mmu.c:
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,------------这里的pmd=pud=pgd。
unsigned long end, unsigned long pfn,
const struct mem_type *type)
{
pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);------------------使用prot_l1作为参数,创建PGD页表目录,返回addr对应的pte地址。
do {
set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), );---------调用体系结构相关汇编,配置PTE。
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);-------------------------遍历[addr, end)区间内存,以PAGE_SIZE为步长。
}

下面看看如何分配PGD页表目录:

static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
if (pmd_none(*pmd)) {---------------------------------------------------如果PGD的内容为空,即PTE还没有创建,择取建立页面。
pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);-------分配512+512个PTE页表项
__pmd_populate(pmd, __pa(pte), prot);-------------------------------生成pmd页表目录,并刷入RAM
}
BUG_ON(pmd_bad(*pmd));
return pte_offset_kernel(pmd, addr);------------------------------------返回当前addr对应的PTE地址
} early_alloc-->early_alloc_aligned:
static void __init *early_alloc_aligned(unsigned long sz, unsigned long align)
{
void *ptr = __va(memblock_alloc(sz, align));-------------------------------------基于memblock进行分配,这里分配4096B,刚好是一页大小。
memset(ptr, , sz);
return ptr;
}

所以存放PGD需要的空间通过memblock进行申请,PTE_HWTABLE_OFF和PTE_HWTABLE_SIZE都为512,所以一个1024个PTE。

下面是early_pte_alloc分配的空间示意图:前面512个表项是给Linux OS使用的,后512个表项是给ARM硬件MMU用的。

Linux内核PGD/PTE映射关系

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
pmdval_t prot)
{
pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;------------------生成pmdp[0]的内容
pmdp[] = __pmd(pmdval);
#ifndef CONFIG_ARM_LPAE
pmdp[] = __pmd(pmdval + * sizeof(pte_t));---------------------生成紧邻的pmdp[1]的内容
#endif
flush_pmd_entry(pmdp);---------------------------------------------将pmdp两个刷入到RAM中
}

Linux的PGD页表目录和ARM32不同,总数和ARM32是一样的。

在arm_mm_memblock_reserve中,通过swapper_pg_dir可以知道其大小为16KB。

就来看看SWAPPER_PG_DIR_SIZE,一共2048个PGD,但是每个PGD包含了两个相邻的PGD页面目录项。


typedef pmdval_t pgd_t[2];---------------------------------------------8字节

#define SWAPPER_PG_DIR_SIZE (PTRS_PER_PGD * sizeof(pgd_t))-------------2048*8B=16KB

/*
* Reserve the special regions of memory
*/
void __init arm_mm_memblock_reserve(void)
{
/*
* Reserve the page tables. These are already in use,
* and can only be in node 0.
*/
memblock_reserve(__pa(swapper_pg_dir), SWAPPER_PG_DIR_SIZE);
...
}

1.4 设置PTE表项

要理解是如何设置PTE表项,就需要参照B3.3.1 Translation table entry formants中关于Second-level descriptors的描述。

arch\arm\include\asm\pgtable-2level.h:
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext) arch\arm\include\asm\glue-proc.h:
#ifndef MULTI_CPU
...
#define cpu_set_pte_ext __glue(CPU_NAME,_set_pte_ext)
...
#endif arch\arm\mm\proc-v7-2level.S:
/*
* cpu_v7_set_pte_ext(ptep, pte)
*
* Set a level 2 translation table entry.
*
* - ptep - pointer to level 2 translation table entry----------放入r0
* (hardware version is stored at +2048 bytes)
* - pte - PTE value to store----------------------------------放入r1
* - ext - value for extended PTE bits------------------------放入r2
*/
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
str r1, [r0] @ linux version----------将r1的值存入r0地址的内存中 bic r3, r1, #0x000003f0--------------------------清除r1的bit[9:4],存入r3
bic r3, r3, #PTE_TYPE_MASK-----------------------PTE_TYPE_MASK为0x03,记清除低2位
orr r3, r3, r2-----------------------------------r3与r2或,存入r3
orr r3, r3, #PTE_EXT_AP0 | 2---------------------这里将bit1和bit4置位,所以是Small page。 tst r1, # << 4----------------------------------判断r1的bit4是否为0
orrne r3, r3, #PTE_EXT_TEX()--------------------设置TEX为1 eor r1, r1, #L_PTE_DIRTY
tst r1, #L_PTE_RDONLY | L_PTE_DIRTY
orrne r3, r3, #PTE_EXT_APX-----------------------设置AP[2] tst r1, #L_PTE_USER
orrne r3, r3, #PTE_EXT_AP1-----------------------设置AP[1] tst r1, #L_PTE_XN
orrne r3, r3, #PTE_EXT_XN------------------------设置XN位 tst r1, #L_PTE_YOUNG
tstne r1, #L_PTE_VALID
eorne r1, r1, #L_PTE_NONE
tstne r1, #L_PTE_NONE
moveq r3, # ARM( str r3, [r0, #]! )---------------------并没有写入r0,而是写入r0+2048Bytes的偏移。
THUMB( add r0, r0, # )
THUMB( str r3, [r0] )
ALT_SMP(W(nop))
ALT_UP (mcr p15, , r0, c7, c10, ) @ flush_pte
#endif
bx lr
ENDPROC(cpu_v7_set_pte_ext)

Linux内存管理 (2)页表的映射过程的更多相关文章

  1. Linux内存管理 (4)分配物理页面

    专题:Linux内存管理专题 关键词:分配掩码.伙伴系统.水位(watermark).空闲伙伴块合并. 我们知道Linux内存管理是以页为单位进行的,对内存的管理是通过伙伴系统进行. 从Linux内存 ...

  2. 启动期间的内存管理之初始化过程概述----Linux内存管理(九)

    在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检 ...

  3. Linux内存管理 (12)反向映射RMAP

    专题:Linux内存管理专题 关键词:RMAP.VMA.AV.AVC. 所谓反向映射是相对于从虚拟地址到物理地址的映射,反向映射是从物理页面到虚拟地址空间VMA的反向映射. RMAP能否实现的基础是通 ...

  4. linux内存管理

    一.Linux 进程在内存中的数据结构 一个可执行程序在存储(没有调入内存)时分为代码段,数据段,未初始化数据段三部分:    1) 代码段:存放CPU执行的机器指令.通常代码区是共享的,即其它执行程 ...

  5. Linux内存管理原理

    本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址又叫线性地址.linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻 ...

  6. Linux内存管理原理【转】

    转自:http://www.cnblogs.com/zhaoyl/p/3695517.html 本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址 ...

  7. Windows内存管理和linux内存管理

    windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...

  8. Linux内存管理专题

    Linux的内存管理涉及到的内容非常庞杂,而且与内核的方方面面耦合在一起,想要理解透彻非常困难. 在开始学习之前进行了一些准备工作<如何展开Linux Memory Management学习?& ...

  9. 伙伴系统之避免碎片--Linux内存管理(十六)

    1 前景提要 1.1 碎片化问题 分页与分段 页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位,它含有一组意义相对完整的信 ...

随机推荐

  1. 基于open62541的opc ua 服务器开发实现(1)

    关于opcua的介绍这里就不多说了,相信大家大都有了一些了解,open62541是一个开源C(C99)的opc-ua实现,开源代码可在官网或github上下载. 话不多说,首先搭建一个opcua服务器 ...

  2. .Net语言 APP开发平台——Smobiler学习日志:如何设置页面的title

    1.修改Mobile Form的TitleText的属性 输入需要显示标题,如图1: 2.修改Mobile Form的TitleStyle属性 其中包括Image属性(窗体图标).BackColor属 ...

  3. 第23章 Windows身份验证 - Identity Server 4 中文文档(v1.0.0)

    在支持的平台上,您可以使用IdentityServer使用Windows身份验证对用户进行身份验证(例如,针对Active Directory).当前使用以下命令托管IdentityServer时,W ...

  4. php-fpm 的优化

    pid = /usr/local/php/var/run/php-fpm.pid error_log = /usr/local/php/var/log/php-fpm.log log_level = ...

  5. [MySQL] 索引中的b树索引

    1.索引如果没有特别指明类型,一般是说b树索引,b树索引使用b树数据结构存储数据,实际上很多存储引擎使用的是b+树,每一个叶子节点都包含指向下一个叶子节点的指针,从而方便叶子节点的范围遍历 2.底层的 ...

  6. Java开发笔记(三十六)字符串的常用方法

    不管是给字符串赋值,还是对字符串格式化,都属于往字符串填充内容,一旦内容填充完毕,则需开展进一步的处理.譬如一段Word文本,常见的加工操作就有查找.替换.追加.截取等等,按照字符串的处理结果异同,可 ...

  7. 数据结构(java版)学习笔记(序章)

    程序=数据结构+算法 序章做一个简单的思维导图,方便理解数据结构这门课的大纲,接下来我们将是按照线性表,栈,队列,串,树和图的顺序依次往下学.

  8. 一文读懂HTTP/2及HTTP/3特性

    摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...

  9. Python中文词频统计

    以下是关于小说的中文词频统计 这里有三个文件,分别为novel.txt.punctuation.txt.meaningless.txt. 这三个是小说文本.特殊符号和无意义词 Python代码统计词频 ...

  10. Android--解决图片保存到相册显示1970年1月1日 8:00的问题

    import android.content.Context; import android.content.Intent; import android.database.Cursor; impor ...