Linux的进程空间管理

news2024/11/27 0:20:05

用户态和内核态的划分

进程的虚拟地址空间,其实就是站在项目组的角度来看内存,所以我们就从task_struct出发来看。这里面有一个struct mm_struct结构来管理内存

struct mm_struct *mm;

在struct mm_struct里面,有这样一个成员变量:

unsigned long task_size; /* size of task vm space */

整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间,那这两部分的分界线在哪里呢?这就要task_size来定义。

对于32位的系统,内核里面是这样定义TASK_SIZE的:

#ifdef CONFIG_X86_32
/*
 * User space process size: 3GB (default).
 */
#define TASK_SIZE PAGE_OFFSET
#define TASK_SIZE_MAX TASK_SIZE
/*
config PAGE_OFFSET
     hex
     default 0xC0000000
     depends on X86_32
*/
#else
/*
 * User space process size. 47bits minus one guard page.
*/
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
                        IA32_PAGE_OFFSET : TASK_SIZE_MAX)
......

 当执行一个新的进程的时候,会做以下的设置:

current->mm->task_size = TASK_SIZE;

对于32位系统,最大能够寻址2^32=4G,其中用户态虚拟地址空间是3G,内核态是1G。

对于64位系统,虚拟地址只使用了48位。就像代码里面写的一样,1左移了47位,就相当于48 位地址空间一半的位置,0x0000800000000000,然后减去一个页,就是 0x00007FFFFFFFF000,共128T。同样,内核空间也是128T。内核空间和用户空间之间隔着很大的空隙,以此来进行隔离。

用户态布局 

用户态虚拟空间里面有几类数据,例如代码、全局变量、堆、栈、内存映射区等。 在struct mm_struct里面,有下面这些变量定义了这些区域的统计信息和位置。

unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end

其中,total_vm是总共映射的页的数目。当内存吃紧的时候,有些页可以换出到硬盘上,有的页因为比较重要,不能换出。locked_vm就是被锁定不能换出,pinned_vm是不能换出也不能移动

data_vm是存放数据的页的数目,exec_vm是存放可执行文件的页的数目,stack_vm是栈所占的页的数目

start_code和end_code表示可执行代码的开始和结束位置,start_data和end_data表示已初始化数据的开始位置和结束位置

start_brk是堆的起始位置,brk是堆当前的结束位置,malloc申请一小块内存的话,就是通过改变brk位置实现的。start_stack是栈的起始位置,栈的结束位置在寄存器的栈顶指针中

arg_start和arg_end是参数列表的位置, env_start和env_end是环境变量的位置。它们都位于栈中最高地址的地方。

mmap_base表示虚拟地址空间中用于内存映射的起始地址。一般情况下,这个空间是从高地址到低地址增长的。malloc申请一大块内存的时候,就是通过mmap在这里映射一块区域到物理内存。加载动态链接库so文件,也是在这个区域里面,映射一块区域到so文件。

 除了位置信息之外,struct mm_struct里面还专门有一个结构vm_area_struct,来描述这些区域的属性

struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;

这里面一个是单链表,用于将这些区域串起来。另外还有一个红黑树,它的好处就是查找和修改都很快。这里用红黑树,就是为了快速查找一个内存区域,并在需要改变的时候,能够快速修改。

struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
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;
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;

vm_start和vm_end指定了该区域在用户空间中的起始和结束地址。vm_next和vm_prev将这个区域串在链表上。vm_rb将这个区域放在红黑树上。vm_ops里面是对这个内存区域可以做的操作的定义

虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射, anon_vma中,anoy就是anonymous,匿名的意思,映射到文件就需要有vm_file指定被映射的文件。

那么vm_area_struct(描述区域的属性)怎么和上面说到的用户态内存的各个区域关联起来的呢?这个事情是在load_elf_binary里面实现的(加载内核的是它,启动第一个用户态进程init的是它,fork完了以后,调用exec运行一个二进制程序的也是它。)

当exec运行一个二进制程序的时候,除了解析ELF的格式之外,另外一个重要的事情就是建立内 存映射

static int load_elf_binary(struct linux_binprm *bprm)
{
......
 setup_new_exec(bprm);
......
 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
 executable_stack);
......
 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
......
 retval = set_brk(elf_bss, elf_brk, bss_prot);
......
 elf_entry = load_elf_interp(&loc->interp_elf_ex,
 interpreter,
 &interp_map_addr,
 load_bias, interp_elf_phdata);
......
 current->mm->end_code = end_code;
 current->mm->start_code = start_code;
 current->mm->start_data = start_data;
 current->mm->end_data = end_data;
 current->mm->start_stack = bprm->p;
......
}

load_elf_binary会完成以下的事情:

调用setup_new_exec,设置内存映射区mmap_base;

调用setup_arg_pages,设置栈的vm_area_struct,这里面设置了mm->arg_start是指向栈底的,current->mm->start_stack就是栈底;

elf_map会将ELF文件中的代码部分映射到内存中来;

set_brk设置了堆的vm_area_struct,这里面设置了current->mm->start_brk = current- >mm->brk,也即堆里面还是空的;

load_elf_interp将依赖的so映射到内存中的内存映射区域

最终会形成下面的内存映射图:

映射完成后,什么情况下会修改呢?

第一种情况是函数的调用,涉及函数栈的改变,主要是改变栈顶指针

第二种情况是通过malloc申请一个堆内的空间,当然底层要么执行brk,要么执行mmap。这里我们重点看下brk的做法:

brk是怎么做的?

brk系统调用实现的入口是sys_brk函数,就像下面代码定义的一样:

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
    unsigned long retval;
    unsigned long newbrk, oldbrk;
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *next;
......
    newbrk = PAGE_ALIGN(brk);
    oldbrk = PAGE_ALIGN(mm->brk);
    if (oldbrk == newbrk)
        goto set_brk;
/* Always allow shrinking brk. */
    if (brk <= mm->brk) {
        if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf))
            goto set_brk;
        goto out;
}
/* Check against existing mmap mappings. */
    next = find_vma(mm, oldbrk);
    if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
        goto out;
/* Ok, looks good - let it rip. */
    if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0)
        goto out;
set_brk:
    mm->brk = brk;
......
    return brk;
out:
    retval = mm->brk;
    return retval

前面说过,堆是从低地址向高地址增加的,sys_brk函数的参数brk是新的堆顶位置,而当前的mm->brk是原来堆顶的位置

首先要做的第一个事情,将原来的堆顶和现在的堆顶,都按照页对齐地址,然后比较大小。如果 两者相同,说明这次增加的堆的量很小还在一个页里面不需要另行分配页,直接跳到 set_brk那里,设置mm->brk为新的brk就可以了。

如果发现新旧堆顶不在一个页里面,麻烦了,这下要跨页了。如果发现新堆顶小于旧堆顶,这说 明不是新分配内存了,而是释放内存了,释放的还不小,至少释放了一页,于是调用 do_munmap将这一页的内存映射去掉

如果堆将要扩大,就要调用find_vma。如果打开这个函数,看到的是对红黑树的查找,找到的是原堆顶所在的vm_area_struct的下一个vm_area_struct,看当前的堆顶和下一个 vm_area_struct之间还能不能分配一个完整的页。如果不能,没办法只好直接退出返回,内存空间都被占满了。 

如果还有空间,就调用do_brk进一步分配堆空间,从旧堆顶开始,分配计算出的新旧堆顶之间的页数:

static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf)
{
    return do_brk_flags(addr, len, 0, uf);
}
static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    unsigned long len;
    struct rb_node **rb_link, *rb_parent;
    pgoff_t pgoff = addr >> PAGE_SHIFT;
    int error;
    len = PAGE_ALIGN(request);
......
    find_vma_links(mm, addr, addr + len, &prev, &rb_link,
                                 &rb_parent);
......
    vma = vma_merge(mm, prev, addr, addr + len, flags,
    NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        goto out;
......
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    INIT_LIST_HEAD(&vma->anon_vma_chain);
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_pgoff = pgoff;
    vma->vm_flags = flags;
    vma->vm_page_prot = vm_get_page_prot(flags);
    vma_link(mm, vma, prev, rb_link, rb_parent);
out:
    perf_event_mmap(vma);
    mm->total_vm += len >> PAGE_SHIFT;
    mm->data_vm += len >> PAGE_SHIFT;
    if (flags & VM_LOCKED)
        mm->locked_vm += (len >> PAGE_SHIFT);
    vma->vm_flags |= VM_SOFTDIRTY;
    return 0;

在do_brk中,调用find_vma_links找到将来的vm_area_struct节点在红黑树的位置,找到它的父节点、前序节点。接下来调用vma_merge,看这个新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,则不用创建新的vm_area_struct了,直接跳到out,更新统计值即可;如果不能合并,则创建新的vm_area_struct,既加到anon_vma_chain链表中,也加到红黑树中

内核态的布局 

 内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用进入到内核之后,看到的虚拟 地址空间都是一样的

这里强调一下,千万别以为到了内核里面,咱们就会直接使用物理内存地址了,想当然地认为下 面讨论的都是物理内存地址,不是的,这里讨论的还是虚拟内存地址,但是由于内核总是涉及管 理物理内存,因而总是隐隐约约发生关系,所以这里必须思路清晰,分清楚物理内存地址和虚拟 内存地址。

在内核态,32位和64位的布局差别比较大,主要是因为32位内核态空间太小了。

32位的布局如下:

 32位的内核态虚拟地址空间一共就1G,占绝大部分的前896M,我们称为直接映射区

所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚 拟内存地址减去3G,就得到物理内存的位置。 

 在内核里面,有两个宏:

        __pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址

        __va(paddr) 则计算出对应于物理地址 paddr 的虚拟地址

 #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
 #define __pa(x) __phys_addr((unsigned long)(x))
 #define __phys_addr(x) __phys_addr_nodebug(x)
 #define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)

这里虚拟地址和物理地址发生了关联关系,在物理内存的开始的896M的空间,会被直接映射到3G至3G+896M的虚拟地址,这样容易给你一种感觉,是这些内存访问起来和物理内存差不多,别这样想,在大部分情况下,对于这一段内存的访问,在内核中,还是会使用虚拟地址的,并且将来也会为这一段空间建设页表,对这段地址的访问也会走上一节我们讲的分页 地址的流程,只不过页表里面比较简单,是直接的一一对应而已。

这896M还需要仔细分解。在系统启动的时候,物理内存的前1M已经被占用了,从1M开始加载内核代码段,然后就是内核的全局变量、BSS等,也是ELF里面涵盖的。这样内核的代码段,全局变量,BSS也就会被映射到3G后的虚拟地址空间里面。

在内核运行的过程中,如果碰到系统调用创建进程,会创建task_struct这样的实例,内核的进程管理代码会将实例创建在3G至3G+896M的虚拟空间中,当然也会被放在物理内存里面的前896M里面,相应的页表也会被创建

在内核运行的过程中,会涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在3G至 3G+896M的虚拟空间中,当然也就会被放在物理内存里面的前896M里面,相应的页表也会被创建。

896M这个值在内核中被定义为high_memory,在此之上常称为“高端内存”。这是个很笼统的说法。这里仍然需要辨析一下,高端内存是物理内存的概念。它仅仅是内核中的内存管理模块看待物理内存的时候的概念。在内核中,除了内存管理模块直接操作物理地址之外,内核的其他模块,仍然要操作虚拟地址,而虚拟地址是需要内存管理模块分配和映射好的。

可以将剩下的虚拟内存地址分成下面这几个部分:

在896M到VMALLOC_START之间有8M的空间。

VMALLOC_START到VMALLOC_END之间称为内核动态映射空间,也即内核想像用户态进程一样malloc申请内存,在内核里面可以使用vmalloc。假设物理内存里面,896M到1.5G之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核vmalloc的时候,只能从分配物理内存1.5G开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面。

PKMAP_BASE到FIXADDR_START的空间称为持久内核映射。使用alloc_pages()函数的时候,在物理内存的高端内存得到struct page结构(物理页面的描述结构),可以调用kmap将其在映射到这个区

FIXADDR_START到FIXADDR_TOP(0xFFFF F000)的空间,称为固定映射区域,主要用于满足特殊需求

在最后一个区域可以通过kmap_atomic实现临时内核映射。假设用户态的进程要映射一个文件到内存中,先要映射用户态进程空间的一段虚拟地址到物理内存,然后将文件内容写入这个物理内存供用户态进程访问。给用户态进程分配物理内存页可以通过alloc_pages(),分配完毕后,按说将用户态进程虚拟地址和物理内存的映射关系放在用户态进程的页表中,就完事大吉了。这个时候,用户态进程可以通过用户态的虚拟地址,也即0至3G的部分,经过页表映射后访问物理内存,并不需要内核态的虚拟地址里面也划出一块来,映射到这个物理内存页。但是如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过kmap_atomic做一个临时映射,写入物理内存完毕后,再kunmap_atomic来解映射即可。

64位的布局如下: 

 64位的内核主要包含以下几个部分:

从0xffff800000000000开始就是内核的部分,只不过一开始有8T的空档区域

从__PAGE_OFFSET_BASE(0xffff880000000000)开始的64T的虚拟地址空间是直接映射区域,也就是减去PAGE_OFFSET就是物理地址。虚拟地址和物理地址之间的映射在大部分情况下还是会通过建立页表的方式进行映射

从VMEMMAP_START(0xffffea0000000000)开始的1T空间用于存放物理页面的描述结构 struct page的。

从__START_KERNEL_map(0xffffffff80000000)开始的512M用于存放内核代码段、全局变量、BSS等。这里对应到物理内存开始的位置,减去__START_KERNEL_map就能得到物理内存的地址。这里和直接映射区有点像,但是不矛盾,因为直接映射区之前有8T的空当区域,早就过了内核代码在物理内存中加载的位置。

总结:

我们知道一个进程要运行起来需要以下的内存结构。

用户态:

        代码段、全局变量、BSS

        函数栈

        堆

        内存映射区

内核态:

        内核的代码、全局变量、BSS

        内核数据结构例如task_struct

        内核栈

        内核中动态分配的内存

32位下进程运行状态的对应关系:

 对于64位的对应关系,只是稍有区别:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/86033.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何利用ArcGIS实现电子地图可视化表达?分析空间数据?提升SCI论文的层次?探究环境与生态因子对水体、土壤、大气污染物等影响?

查看原文>>>如何利用ArcGIS探究环境与生态因子对水体、土壤、大气污染物等影响 如何利用ArcGIS实现电子地图可视化表达&#xff1f;如何利用ArcGIS分析空间数据&#xff1f;如何利用ArcGIS提升SCI论文的层次&#xff1f;制图是地理数据展现的直观形式&#xff0c;也是…

ARM存储模型(数据存储、指令存储)

目录 1、ARM数据存储 (1) ARM数据类型 (2) ARM数据存储的方式 2、ARM的指令存储 (1) 指令集的分类 (2) 为什么ARM指令集的PC值与低2位无关&#xff1f; 1、ARM数据存储 (1) ARM数据类型 ARM采用32位架构&#xff0c;即ARM一次可以处理32bit的数据&#xff0c;基本的数据…

Linux--进程间通信(2)--共享内存--1126

1.共享内存的原理 进程A先申请一段内存空间&#xff0c;其页表映射到物理空间&#xff0c;进程B通过A的页表映射&#xff0c;将B的页表也同样映射到同一块物理空间。 释放共享内存 各自修改各自的页表指向并释放共享内存 2.共享内存的建立过程 头文件 comm.hpp #pragma o…

Windows系统dos命令之cmd

目录1. 使用cd命令快速切换到指定的盘符中1.1 参数说明1.2 cd 切换盘符2. 使用cd命令切换到指定的目录中2.1 切换指定目录3. 使用cd命令退回到上一层目录3.1 使用命令 ”cd ..“4. 使用cd命令直接退回到当前根目录下4.1 使用命令cd \1. 使用cd命令快速切换到指定的盘符中 cd 是…

JUC并发编程第十一篇,Java对象的内存布局

JUC并发编程第十一篇&#xff0c;Java对象的内存布局一、对象在堆内存中的存储布局1、对象头对象标记Mark Word类元信息&#xff08;类型指针&#xff09;2、实例数据3、对齐填充二、对象标记&#xff08;MarkWord&#xff09;布局与验证代码验证&#xff08;JOL&#xff09;一…

重点| 系统集成项目管理工程师考前50个知识点(4)

本文章总结了系统集成项目管理工程师考试背记50个知识点&#xff01;&#xff01;&#xff01; 帮助大家更好的复习&#xff0c;希望能对大家有所帮助 比较长&#xff0c;放了部分&#xff0c;需要可私信&#xff01;&#xff01; 26、编制范围管理计划的输出&#xff1a; 范…

Flink Window Function

窗口函数定义了要对窗口中收集的数据做的计算操作&#xff0c;根据处理的方式可以分为两类&#xff1a;增量聚合函数和全窗口函数。 文章目录1.增量聚合函数1.1 ReduceFunction1.2 AggregateFunction2.全窗口函数2.1 WindowFunction2.2 ProcessWindowFunction3.增量聚合和全窗口…

【MySQL】用户管理

文章目录用户管理用户信息创建用户删除用户修改用户密码用户权限用户赋权回收权限用户管理 如果我们只能使用root用户,这样存在安全隐患,因为root可以访问所有的数据库和表,这时,就需要使用MySQL的用户管理,从而限制某个特定的用户只能访问特定的数据库和表,并且对其权限作出一…

【51-订单模块-资源整合-整合SpringSession-订单中心核心逻辑-认证拦截-订单提交-订单生成逻辑-接口幂等性处理-接口幂等性解决方案】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了&#xff0c;请点击这里&#xff01;】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

若依上面添加layui组件

1.因为若依本身有layui&#xff0c;所以不需要在引layui的css与js 2.下面是需要添加的 <div className"layui-btn-container" style"margin-top: 30px;"><button data-method"setTop" οnclick"sjhdekjshd()">多窗口模式…

基于vite搭建的vue3项目中如何引用环境变量

目录 回顾一下vue-cli2/cli3中环境变量使用方法 环境变量定义&#xff1a; 环境变量使用&#xff1a; vite中环境变量使用方法 环境变量定义&#xff1a; 环境变量使用&#xff1a; 回顾一下vue-cli2/cli3中环境变量使用方法 在vue-cli2/cli3中使用环境变量时 环境变量定…

Python Flask 路由配置

有关更多的Python 开发内容,可访问:《 Python Flask开发指南》 Flask中通过使用route装饰器实现路由访问功能,其路由匹配URL规则基于Werkzeug的路由模块。该模块基于Apache及更早的HTTP服务器主张,希望保证优雅且唯一的URL。其使用格式如下: from flask import Flask app …

突然 Java 倒下了......

TIOBE 公布了 2022 年 12 月的编程语言排行榜。 Java 首次跌出前 3 名。除此之外&#xff0c;Kotlin 和 Julia 也越来越接近 Top 20。 TIOBE 将于下个月揭晓其 2022 年度编程语言&#xff0c;目前共有 3 个候选者&#xff1a;Python、C 和 C。TIOBE CEO Paul Jansen指出&#…

如何实现工具无关化?——关于自动化测试脚本的设计

问题的提出 最近几年来&#xff0c;我的自动化测试工具之旅大致是这样的&#xff0c;最早用的是QTP&#xff0c;然后是RFT&#xff08;IBM的功能测试自动化产品&#xff09;&#xff0c;之后也经历了Selenium&#xff0c;Watir等。再后来还是一些商业工具&#xff0c;主要是偏…

KubeClipper 1.3.1 正式发布

2022 年 12 月 12 日&#xff0c;KubeClipper 1.3.1 版本正式发布&#xff01; 开源大事记 2022 年 08 月&#xff0c; KubeClipper 项目正式开源到 https://github.com/KubeClipper 项目。 2022 年 08 月&#xff0c;在由 OpenInfra 基金会举办的 2022 OpenInfra Days China…

nacos--基础--4.1--集成--SpringBoot--配置管理、服务发现、服务注册

nacos–基础–4.1–集成–SpringBoot–配置管理、服务发现、服务注册 代码位置 https://gitee.com/DanShenGuiZu/learnDemo/tree/master/nacos-learn/nacos-spring-boot1、介绍 主要面向 SpringBoot 的使用者通过2个实例&#xff0c;来介绍nacos和SpringBoot的集成 配置管理服…

【案例教程】Python气象海洋数据可视化到常见数据分析方法(折线图、柱状图、errorbar图、流场矢量、散点图、风玫瑰图、流场矢量、填色及等值线+地图)

【查看原文】Python在气象与海洋中的实践技术应用 Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;能够在不同操作系统和平台使用&#xff0c;简洁的语法和解释性语言使其成为理想的脚本语言。除了标准库&#xff0c;还有丰富的第三方库&#xf…

点线面数据处理(wkt,geojson互转)

点线面数据点&#xff1a;[103.8810, 31.0896] 线&#xff1a;[[103.7767, 30.8424],[104.2546, 30.8150],[104.3068, 30.4552]] 面&#xff1a;[[[103.8810, 31.0896],[104.0129, 30.8891],[103.7520, 30.8809],[103.8810, 31.0896]]] wkt数据点&#xff1a;POINT(103.365926 …

对Dueling DQN理论的深度分析。

强化学习中Agent与环境的交互过程是由马尔可夫决策过程(Markov Decision Process, MDP)描述的。MDP对环境做了一个假设&#xff0c;称作马尔可夫性质&#xff0c;即下一时刻的状态只由上一时刻的状态和动作决定。 马尔可夫性质决定了值函数(状态值与动作值函数)可以写成递归的形…

【项目总结】医疗化验单的OCR识别

项目总结 医疗化验单OCR 文章目录项目总结前言一、项目要求二、解决思路1.模型1.扶正2.裁剪3.pipeline三、总结前言 课题组项目的总结。 一、项目要求 课题组和广州的一家药企有合作&#xff0c;甲方要求把一张医疗化验单内的表格内容整体识别出来&#xff0c;特别是化验的数…