Linux 文件系统解析(三)cache

Linux文件系统中使用了大量cache,用于提升IO性能,本篇来梳理一下这些与文件系统相关的cache,它们在内存中是如何组织管理的,它们是如何加速文件系统操作的。

Dentry Cache

dentry用于描述系统目录树中的一个节点,磁盘文件系统中通常没有相关结构,dentry只存在于内存之中,它的主要作用是提供了一种快速的通过路径名查找到对应文件的方法。

组织方式

dentry中用于组织管理的数据结构如下:

struct dentry {
    ...
	struct hlist_bl_node d_hash;	/* lookup hash list */    //dentry_hashtable全局hash表中的节点
	struct qstr d_name;    //根据其->hash来计算该dentry在dentry_hashtable中的索引
    ...
	union {
		struct list_head d_lru;		/* LRU list */    //其d_sb->s_dentry_lru中的节点
		wait_queue_head_t *d_wait;	/* in-lookup ones only */
	};
	struct list_head d_child;	/* child of parent list */   //其d_parent->d_subdirs中的节点
	struct list_head d_subdirs;	/* our children */    //子dentry链表
    ...
	union {
		struct hlist_node d_alias;	/* inode alias list */    //在其对应inode->i_dentry中的节点(一个inode可能有多个硬连接,在inode中需要用一个链表管理这些dentry)
		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
	 	struct rcu_head d_rcu;
	} d_u;
}

dentry结构体由struct kmem_cache *dentry_cache 分配,dentry_cache 在内核初始化流程中 dcache_init() 函数里初始化。同时该函数中还会初始化管理dentry的全局hash表dentry_hashtable。

所有dentry总是处于三种状态:

  • unused,未被内核使用,引用计数d_lockref为0,d_inode非空。
  • inuse,正在被使用,引用计数d_lockref>0。
  • negative,相应的磁盘inode已经被删除,d_inode为空。

dentry结构通常在路径查找流程中被创建,主要有以下四种管理方式:

  • dentry_hashtable:以dentry->d_name.hash作为索引在dentry全局hash表中管理,用于路径查找。
  • superblock->s_dentry_lru:管理所有unused和negative的dentry,当需要回收内存时,从中选取“最少使用的”dentry回收。
  • d_subdirs:dentry结构中记录其子dentry,用于dir管理。
  • inode->i_dentry:由于一个inode可能有多个硬链接,这些dentry称为alias,使用双向链表管理,每个inuse的dentry加入其inode->i_dentry链表。

其中用的最多的是dentry_hashtable,每次open文件时,都要反复访问它来查找对应的文件。

path-lookup流程

path-lookup(Documentation/filesystems/path-lookup.txt)是根据一个路径名,逐级查找dentry,最终找到目标文件dentry的过程。既然有了dentry cache,基本策略就是先从cache里找,如果找不到再通过磁盘数据生成dentry,直到得到最终文件的dentry,而内核设计了复杂的流程用于提升查找性能。

由于dentry在系统中可能被并发的访问及修改,为了保证查找数据的正确性并兼顾性能,内核设计了两种查找方式。

  • ref-walk,传统的cache访问方式,使用自旋锁以及引用计数来保证数据正确性,这使得查找路径中的每个dentry都会有加锁的开销以及可能发生阻塞,造成性能下降。
  • rcu-walk,基于顺序锁的查询方式,在查询过程中不用加锁,十分高效,若rcu-walk查询失败再尝试切换到ref-walk模式继续后面的查询。

这两种方式都是从dentry_hashtable中查找(lookup_fast),若dentry_hashtable中无此dentry,再通过inode->i_op->lookup查找(lookup_slow)。path-lookup整体流程如下:

  • 以rcu-walk模式逐级查找dentry,并校验正确性。
  • 若rcu-walk出现校验失败,从头开始以ref-walk模式查找。
  • 若rcu-walk出现查找失败,调用unlazy_walk尝试drop-rcu,校验顺序锁,对最后一个可用dentry增加引用计数,若unlazy_walk成功以ref-walk模式继续查找,若不成功从头开始以ref-walk模式查找。
  • 若ref-walk查找失败,执行lookup_slow,通过inode->i_op->lookup查找。

回收流程

当dentry不在被使用,即其引用计数d_lockref变为0,或者其对应的inode被删除时,将该dentry加入superblcok的s_dentry_lru。当需要回收内存时,由prune_dcache_sb(),回收superblock中使用较少的dentry。

 

Inode Cache

inode描述文件系统中的一个文件,通常有一个或多个dentry与之对应。dentry已经完整描述了系统的目录树,并且提供了路径查找的方法,inode就不需要再处理复杂的相互关系,其管理方式也相对简单一些。

组织方式

inode中用于组织管理的数据结构如下:

struct inode {
    ...
	/* Stat data, not accessed from path walking */
	unsigned long		i_ino;    //与其i_sb一起计算hash作为在inode_hashtable中的索引
    ...
	struct hlist_node	i_hash;    //inode_hashtable全局hash表中的节点
    ...
	struct list_head	i_lru;		/* inode LRU list */    //其i_sb->s_inode_lru中的节点
    ...
}

于dentry类似,内核初始化流程中 inode_init() 函数里初始化struct kmem_cache *inode_cachep 可用于inode分配。同时该函数中还会初始化管理inode的全局hash表inode_hashtable。但通常inode结构会内嵌在文件系统私有inode结构中以实现关联,文件系统驱动通常会实现super_operations->alloc_inode()用于分配inode,此时就不会从inode_cachep中分配了。

inode结构主要有两种管理方式:

  • inode_hashtable:以superblock和i_ino作为索引在inode全局hash表中管理,主要用于通过i_ino查找inode。
  • superblock->s_inode_lru:与s_dentry_lru类似,管理不再使用的inode,当需要回收内存是,从中选取“最少使用的”inode回收。

回收流程

与dentry类似,当需要回收内存时,由prune_icache_sb(),回收superblock中使用较少的inode。

 

Page Cache

page cache是用于缓存磁盘上数据的内存页,是文件系统cache中占比最大的部分,也是对系统文件系统读写性能提升最大的部分,内核围绕page cache设计了复杂的机制用于加速文件系统的操作。 

数据结构

address_space

struct address_space {
	struct inode		*host;    //该段cache的拥有者,可以是inode或者block device
	struct xarray		i_pages;    //page列表(旧版本内核使用radix tree管理page)
    gfp_t			gfp_mask;    //申请内存时使用的flag
    ...
	struct rb_root_cached	i_mmap;    //mmap使用的vma rbtree根节点
	struct rw_semaphore	i_mmap_rwsem;
	unsigned long		nrpages;        //包含多少个page
    ...
	const struct address_space_operations *a_ops;    //address_space方法集合
    ...
}

之前介绍过这个结构,其中包含了一个pagecache列表,以及所映射的对象,这个对象可以是一个inode或者是块设备,这里主要讨论映射inode的情况。address_space内嵌在inode结构中(inode.idata),为方便使用inode初始化时使用一个指针指向它(inode.i_mapping)。a_ops方法集合提供了对page的操作方法,需要文件系统驱动实现。总的来说,address_space结构将一个inode与它在内存中的pagecache关联起来,并提供了用于操作这些page的接口,他包含了对一个inode内容进行读写需要的全部信息。

page

struct page {
	unsigned long flags;		/* Atomic flags, some possibly    //记录page的属性
					 * updated asynchronously */
    ...
	union {
		struct {	/* Page cache and anonymous pages */
            ...
			struct list_head lru;        //在LRU链表中的节点
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping;    //指向所映射的结构(不一定是address_space)
			pgoff_t index;		/* Our offset within mapping. */    //在address_space中的偏移
            ...
		};
    ...
#ifdef CONFIG_MEMCG
	struct mem_cgroup *mem_cgroup;    //若使用了cgroup,该page也会加入cgroup维护的LRU中管理
#endif
    ...
}

Linux以页作为内存管理的最小单元,page结构用于描述内存中的一个物理页面,通常是4KB大小,其中包含了大量的信息。内存根据不同用途可分为多种类型,比如pagecache,进程匿名页,slab使用的内存池,dma等等,page结构中为了节省空间使用了大量union,这里主要看一下与pagecache相关的数据。pagecache与匿名内存使用同样结构:

  • lru:一个list_head用于加入LRU链表,
  • mapping:这里的mapping有可能指向两种结构,当page是匿名页时mapping指向anon_vma,其第一个bit(PAGE_MAPPING_ANON)为1,当page是pagecache时mapping指向address_space,其第一个bit(PAGE_MAPPING_ANON)为0。
  • index:当mapping指向address_space时,代码其在address_space.i_pages中的偏移。

pagevec

struct pagevec {
	unsigned char nr;    //缓存的page个数
	bool percpu_pvec_drained;
	struct page *pages[PAGEVEC_SIZE];    //page list
};

将page加入LRU需要获取锁,并发频繁地调用会影响性能。内存子系统为每个CPU分配一个pagevec结构,用于将这些要加入LRU的page缓存起来,等积攒到一定数量再统一加入LRU,以提升性能。

lruvec

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,    //匿名页inactive
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,    //匿名页activ
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,    //pagecache incactive
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,    //pagecache active
	LRU_UNEVICTABLE,    //不可回收
	NR_LRU_LISTS
};
struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];    //5个链表
	struct zone_reclaim_stat	reclaim_stat;
	/* Evictions & activations on the inactive file list */
	atomic_long_t			inactive_age;
	/* Refaults at the time of last reclaim cycle */
	unsigned long			refaults;
	/* Various lruvec state flags (enum lruvec_flags) */
	unsigned long			flags;
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif
};

每个内存node用5个LRU链表管理内存页,lruvec用于描述这些链表。内核编译时通常会默认打开CONFIG_MEMCG,此时系统中每个cgroup都会使用一个lruvec结构维护一组LRU,用于实现基于cgroup的内存管理。

组织方式

对文件进行读写时会先通过find_get_page()/find_get_entry()从address_space中根据偏移查找是否已经有缓存的page,若找不到就调用_page_cache_alloc()函数分配一个page,分配成功后由add_to_page_cache_lru()将page加入addres_space和LRU链表。

所以pagecache中的page主要有两种管理方式:

  • address_space->i_pages:page在所属的address_space中以xarry管理,page->index记录在其中的偏移,用于对文件内容的映射。
  • lruvec->lists:每个page加入LRU_ACTIVE_FILE 或LRU_INACTIVE_FILE  LRU链表,用于内存回收。

bufferIO 读写流程

pagecache主要在bufferIO流程中使用,这是Linux中最常见的IO模式,bufferIO利用pagecache作为缓存,降低读写操作对实际磁盘的访问频次以提高性能。结合前两篇内容,看一下ext4文件系统下bufferIO的流程。

read 流程图如下:

read的大致策略是,先从address_space->i_pages里找是否已有缓存的数据,若找到则直接拷贝给用户进程无需再访问磁盘,若找不到则扩充address_space->i_pages,将磁盘数据读到pagecache里,再拷贝给用户进程,这样后续如果再读同一page就不用访问磁盘。这里只列出大致流程,其具体的读取逻辑还是比较复杂的,涉及到预读的概念,后面再介绍。

write 流程图如下:

write的大致策略是,先建立文件要写的数据段在pagecache中的映射,将数据写入pagecahce后直接放回,等待后续回写被触发时在将pagecache数据写入磁盘。这样在频繁进行写操作时降低了实际操作磁盘的频次。

预读

当应用读一个文件时,发现要读的数据已经在pagecache中,就不用再去访问磁盘,可以极大提升读数据性能。为了更好的利用这一机制,内核设计了预读,通过预测算法预先将一部分即将请求的数据读到pagecache,将多次小IO转换为一次大IO,降低磁盘操作频次。

预测算法是预读策略的核心,使得读取逻辑变得十分复杂,来看下它是如何实现的。

file_ra_state结构内嵌与file结构中,用于记录一个fd最近一次的预读状态,

struct file_ra_state {
	pgoff_t start;			/* where readahead started */    //预读的起始page偏移
	unsigned int size;		/* # of readahead pages */    //预读窗口大小
	unsigned int async_size;	/* do asynchronous readahead when    
					   there are only # of pages ahead */    //用于计算PG_readahead标记
	unsigned int ra_pages;		/* Maximum readahead window */    //最大预读page数量,由/sys/block/*/queue/read_ahead_kb 设置
	unsigned int mmap_miss;		/* Cache miss stat for mmap accesses */    //mmap用到的miss计数
	loff_t prev_pos;		/* Cache last read() position */    //前一次读的数据偏移
}

内核注释中描述了readahead的设计思路:

 *
 *                        |<----- async_size ---------|
 *     |------------------- size -------------------->|
 *     |==================#===========================|
 *     ^start             ^page marked with PG_readahead
 *
 * To overlap application thinking time and disk I/O time, we do
 * `readahead pipelining': Do not wait until the application consumed all
 * readahead pages and stalled on the missing page at readahead_index;
 * Instead, submit an asynchronous readahead I/O as soon as there are
 * only async_size pages left in the readahead window. Normally async_size
 * will be equal to size, for maximum pipelining.
 *

预读策略使用了流水线的设计方式,当在pagecache找不到请求页面时,启动同步预读,读取一批数据并标记一个page为PG_readahead。随着应用连续读取数据,预读的page慢慢消耗,此时内核不会等到应用消耗完所有预读的page再启动新一次预读,当应用读取到PG_readahead标志的page时,就启动异步预读,读取下一批数据,将预读窗口后移。

再来看下相关代码逻辑:

page_cache_sync_readahead(),同步预读。判断该文件是否支持预读,是否有IO拥塞,然后调用ondemand_readahead。

page_cache_async_readahead(),异步预读。与同步类似,主要区别在于调用ondemand_readahead时设置hit_readahead_marker参数为true

ondemand_readahead(),基于file_ra_state实现预测算法。确定最终读取的起点与数量,调用__do_page_cache_readahead()读取page。

策略如下:

  • 读文件第一个page,一次读数据超过read_ahead_kb,之前的顺序读被中断且探测到新的顺序读,在这三种情况下初始化file_ra_state 开始一轮新的预读。
  • 探测到有持续连续读,逐步增大预读page数,将预读window后移。
  • 其他情况判定为随机读,不改变file_ra_state状态,仅读取请求大小的page。
static unsigned long
ondemand_readahead(struct address_space *mapping,
		   struct file_ra_state *ra, struct file *filp,
		   bool hit_readahead_marker, pgoff_t offset,
		   unsigned long req_size)
{
	struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
	unsigned long max_pages = ra->ra_pages;
	unsigned long add_pages;
	pgoff_t prev_offset;

	/*
	 * If the request exceeds the readahead window, allow the read to
	 * be up to the optimal hardware IO size
	 */
    /*
     * 确定预读的最大page数,默认设置为read_ahead_kb相应page,若请求数据大于read_ahead_kb,        
     * 尽量使一次预读不超过硬件最大IO大小(通常在/sys/block/*/queue/max_sectors_kb设置)。
     */
	if (req_size > max_pages && bdi->io_pages > max_pages)
		max_pages = min(req_size, bdi->io_pages);

	/*
	 * start of file
	 */
	if (!offset)    //读一个文件第一页时,初始化fd的file_ra_state结构
		goto initial_readahead;

	/*
	 * It's the expected callback offset, assume sequential access.
	 * Ramp up sizes, and push forward the readahead window.
	 */
    //读取预读window末尾的下一个page,判定为顺序读
	if ((offset == (ra->start + ra->size - ra->async_size) ||
	     offset == (ra->start + ra->size))) {
		ra->start += ra->size;
		ra->size = get_next_ra_size(ra, max_pages);    //逐步增大预读page数
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * Hit a marked page without valid readahead state.
	 * E.g. interleaved reads.
	 * Query the pagecache for async_size, which normally equals to
	 * readahead size. Ramp it up and use it as the new readahead size.
	 */
	if (hit_readahead_marker) {    //读取到PG_readahead的page时启动异步预读
		pgoff_t start;

		rcu_read_lock();
        //从offset开始,找下一个未在cache中的page作为下一次预读window的起始page
		start = page_cache_next_miss(mapping, offset + 1, max_pages);
		rcu_read_unlock();

		if (!start || start - offset > max_pages)
			return 0;

		ra->start = start;
		ra->size = start - offset;	/* old async_size */
		ra->size += req_size;
		ra->size = get_next_ra_size(ra, max_pages);    //逐步增大预读page数
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * oversize read
	 */
    //一次性读取大量page,直接开始新一轮预读
	if (req_size > max_pages)
		goto initial_readahead;

	/*
	 * sequential cache miss
	 * trivial case: (offset - prev_offset) == 1
	 * unaligned reads: (offset - prev_offset) == 0
	 */
    //探测到有新的顺序读,开始新一轮预读
	prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
	if (offset - prev_offset <= 1UL)
		goto initial_readahead;

	/*
	 * Query the page cache and look for the traces(cached history pages)
	 * that a sequential stream would leave behind.
	 */
    //根据offset之前的page在cache里的情况判断是否是顺序读
	if (try_context_readahead(mapping, ra, offset, req_size, max_pages))
		goto readit;

	/*
	 * standalone, small random read
	 * Read as is, and do not pollute the readahead state.
	 */
    //判断为随机读,仅读取请求大小的page,不改变file_ra_state和PG_readahead状态
	return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);

initial_readahead:
    //初始化file_ra_state,根据请求大小设置预读大小及PG_readahead的位置
	ra->start = offset;
	ra->size = get_init_ra_size(req_size, max_pages);
	ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;

readit:
	/*
	 * Will this read hit the readahead marker made by itself?
	 * If so, trigger the readahead marker hit now, and merge
	 * the resulted next readahead window into the current one.
	 * Take care of maximum IO pages as above.
	 */
	if (offset == ra->start && ra->size == ra->async_size) {
		add_pages = get_next_ra_size(ra, max_pages);
		if (ra->size + add_pages <= max_pages) {
			ra->async_size = add_pages;
			ra->size += add_pages;
		} else {
			ra->size = max_pages;
			ra->async_size = max_pages >> 1;
		}
	}
    //根据file_ra_state调用__do_page_cache_readahead()读取相应page,设置(start+size-async_size)的page为PG_readahead
	return ra_submit(ra, mapping, filp);
}

__do_page_cache_readahead(),从磁盘读取一段连续page。在address_space中分配page,读取磁盘相应数据,标记PG_readahead page,并将读取的page加入LRU链表。

generic_file_buffered_read(),从文件中读取一段连续数据。在address_space中逐页查找数据对应的page,若查找不到调用page_cache_sync_readahead()进行同步预读,若查找到的page是PG_readahead调用page_cache_async_readahead()进行异步预读,找到page后校验page状态,拷贝数据。

总的来说,预读是通过探测应用的顺序读行为,预先读取数据,其探测算法的准确性对优化效果至关重要。其算法对顺序读的判定比较宽松,一些小范围的随机不会中断预读,使得如多线程对同一文件的交替顺序读的场景也会受到预读的优化,但这通常也是造成IO放大的原因。

回收流程

pagecache的回收会用LRU链表,与其他cache的回收策略类似,根据LRU算法将“最少使用”的page放在链表尾部,当触发pagecache回收时从尾部进行回收。

 

总结

本篇主要介绍了文件系统中主要用到的三种cache结构,它们的用途,组织方式,回收策略,以及内核围绕这些cache设计的机制。涉及到的流程及代码繁多,难免有些发散,部分流程也没有深入分析,比如内存回收,pagecache与swap的关联,LRU算法等等,待后续有机会做专题研究。

在讨论pagecache时,描述了bufferIO的流程,这是Linux中最常用的IO模式,它通过pagecache对读写操作加速,但同时也带来了问题。比如在读数据时需要一次额外的内存拷贝,写数据时应用不知道何时数据才能落盘,除非显式调用sync,为了让应用程序对IO操作有更多的控制,除了bufferIO外内核还提供了多种IO模式,最后一篇来梳理下这部分,文件系统为应用层提供了哪些IO模式。

 

参考资料:

https://www.ibm.com/developerworks/cn/linux/l-cache/

http://blog.chinaunix.net/uid-20522771-id-4419703.html

http://www.ilinuxkernel.com/files/Linux.Kernel.Cache.pdf

https://blog.csdn.net/zhoutaopower/article/details/93429620

Documentation/filesystems/path-lookup.txt

Documentation/filesystems/vfs.txt

热门文章

暂无图片
编程学习 ·

springcloud config 配置访问

springcloud http请求地址和资源文件映射如下: / { 应用名 } / { 环境名 } [ / { 分支名 } ] / { 应用名 } - { 环境名 }.yml / { 应用名 } - { 环境名 }.properties / { 分支名 } / { 应用名 } - { 环境名 }.yml / { 分支名 } / { 应用名 } - { 环境名 }.properties label 分支…
暂无图片
编程学习 ·

Android 模拟器联网

先配置好adb环境变量,打开模拟器,cmd中输入adb shell 查看是否adb配置完成,exit退出adb模式,adb root将模拟器root,然后adb shell,可以直接setprop net.eth0.dns1 192.168.1.1 后面为自己ip地址
暂无图片
编程学习 ·

7-9 1.2.5 双重回文数 (70分)

如果一个数从左往右读和从右往左读都是一样,那么这个数就叫做“回文数”.例如,12321 就是一个回文数,而 77778 就不是. 当然,回文数的首和尾都应是非零的,因此 0220 就不是回文数. 事实上,有一些数(如 21),在十进制时不是回文数,但在其它进制(如二进制时为 10101)时就是 回…
暂无图片
编程学习 ·

记一次spark-submi 提交python脚本 遇到的问题

一、通过spark-submit 提交报错如下 yarn运行模式spark用的版本是2.4.0是支持pandas_udf的,而且通过pyspark的shell命令行一条条执行 都是没有问题的 但是将代码作为文件用spark submit提交就报这个错误 二、解决办法: @pandas_udf(returnType=“string”, PandasUDFType.…
暂无图片
编程学习 ·

虚拟机VMware安装学习过程中遇到的几个问题

1.在安装VMware的时候刚开始因为版本不足的原因,电脑显示 Failed to initialize ploicy for cpu 后来我把它复制到百度上发现是我电脑版本过高的原因,于是又下载了VMware15.5.1版本 又查找了它的破解版。2.在安装的过程中还出现过屏幕就一个-,然后什么都不出现,于是查找资…
暂无图片
编程学习 ·

画图-身份证

画图函数:base_dir = f{main.BASE_DIR}/quality_management_logic/dataCenter/self.draw.text((55, self.height * 0.31), self.personName, (0, 0, 0),font=ImageFont.truetype(os.path.join(base_dir, msyh.ttc),24)) #personname应用: # -*- coding: utf-8 -*- # import …
暂无图片
编程学习 ·

计算机网络基础,看完不怕面试

前言 计算机网络学习的核心内容就是网络协议的学习。网络协议是为计算机网络中进行数据交换而建立的规则、标准或者说是约定的集合。因为不同用户的数据终端可能采取的字符集是不同的,两者需要进行通信,必须要在一定的标准上进行。一个很形象地比喻就是我们的语言,我们大天朝…
暂无图片
编程学习 ·

Spring MVC的运行原理(简答)

a)(浏览器)用户发送请求 b)前端控制器(dispatcherServlet)接收(他会委托其他模块进行真正的业务和数据处理 ) c)向handlermapping发送url查找相应的方法(handlermapping中储存的是url 和方法的键值对) d)返回处理器(“地址”)。 e)前端控制器发送处理器“地址”给处理…
暂无图片
编程学习 ·

2.4-7、背包问题

7、背包问题 【问题描述】 简单的背包问题。设有一个背包,可以放入的重量为s。现有n件物品,重量分别为w1,w2…,wn,(1≤i≤n)均为正整数,从n件物品中挑选若干件,使得放入背包的重量之和正好为s。找到一组解即可。 【输入格式】 第一行是物品总件数和背包的载重量,第二行…
暂无图片
编程学习 ·

OA、ERP、BPM 各自的功能和特点是什么?怎么配合使用?

​OA是Office Automation的简称,译为办公自动化,常用于企业内部事务管理。OA具有的几个功能:信息存储、数据管理、数据共享。因此,它的使用场景常分布在日常办公中,比如:公文管理、沟通工具、文档管理、项目管理、任务管理、会议管理、通讯录、工作便签、问卷调查、常用工…
暂无图片
编程学习 ·

操作URL的黑科技

处理URL的query的接口:URLSearchParams// 处理URL的query的接口:URLSearchParams // 简单使用 let url = ?wd=胡歌&love=fx&year=2020; let searchParams = new URLSearchParams(url); for (let p of searchParams) {console.log(p); } // ["wd", "胡…
暂无图片
编程学习 ·

机器学习 | 优秀Tensorflow开源项目汇总(上)

1、Open_model_zoo预先训练的深度学习模型和样本(高质量且快速)https://github.com/opencv/open_model_zoo2、Deep Learning In Productionhttps://github.com/ahkarami/Deep-Learning-in-Production3、AndroidtensorflowmachinelearningexampleAndroid TensorFlow机器学习示…
暂无图片
编程学习 ·

网络安全技术及应用第3版 主编贾铁军等——教材习题 期末重点 复习题 知识提炼 (第5章 密码与加密技术)

参考教材:网络安全技术及应用 第3版 主编贾铁军等 第5章 密码与加密技术选择题填空题简答题计算题1)凯撒密码:(将字母表示一个循环的表,再进行移位)2)单字母替换密码3)矩阵转置技术4)RSA算法论述题 选择题 张三给李四发信息,为了实现数据保密,加密秘钥是()————李…
暂无图片
编程学习 ·

python3安装OpenCV3出现:ImportError: numpy.core.multiarray failed to import

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是…
暂无图片
编程学习 ·

线性代数知识课笔记1

笔记内容摘自 猴博士爱讲课@B站 https://www.bilibili.com/video/BV1hs411e7X8?p=4行列式 行列式的计算 行列式分为2阶、3阶、4阶……n阶等,其中2阶的计算方法为: ∣1326∣ \begin{vmatrix}1&3\\2&6\end{vmatrix} ∣∣∣∣​12​36​∣∣∣∣​ 计算方法为对角线相乘…
暂无图片
编程学习 ·

Python电影票房数据可视化分析基础实践

数据可视化一直是很多数据分析或者是建模挖掘任务里面经常会用到的一项功能,今天我们基于某电影网站中公开发布的电影票房数据进行一些基础的数据可视化分析实践,下面是部分的数据样例:叶问.,20160304,33151,2193,196.9万,33.96%,46 捉妖记,20150718,17860,995,192.71万,64.…
暂无图片
编程学习 ·

【python】三方包安装教程以requests包安装为例【通用教程】

【python】三方包安装教程以requests包安装为例【通用教程】一、在线安装二、本地安装1、包下载地址:2、官网搜索三方包名称3、选择相应的包。4、到对应的下载界面下载对应版本的安装包。5、然后在编辑器里面输入相应安装命令前言:此教程适用于requests包安装,也适用于其他三…